rfc:deprecate-fuzzy-casts

PHP RFC: Deprecate Fuzzy Type Casts and Allow Stringable in Strict Mode

Summary

This RFC seeks to address two differences in scalar conversion behavior:

  • Explicit casts can perform lossy conversions (“fuzzy casts”), silently discarding part of the input.
  • In strict typing mode (declare(strict_types=1)), string type declarations reject Stringable values even though they have a well-defined string representation.

This RFC changes both behaviors:

  • PHP 8.6: emit E_DEPRECATED for fuzzy casts; accept Stringable for string parameters in strict mode.
  • PHP 9.0: throw TypeError for fuzzy casts.

Introduction

PHP's type conversion system contains differences between explicit casts and scalar type checks. This RFC addresses two issues:

Issue 1: Fuzzy Casts

PHP's explicit cast operators can discard part of the input when it cannot be fully represented in the target type:

<?php
(int) "123abc";      // 123 (discards "abc")
(float) "12.34foo";  // 12.34 (discards "foo")
(object) 42;         // stdClass { scalar: 42 }
?>

Issue 2: Stringable Objects in Strict Mode

Strict mode rejects Stringable objects for string parameters, despite Stringable being an explicit contract for safe string conversion:

<?php
declare(strict_types=1);
 
function process(string $value): void { }
 
$obj = new class implements Stringable {
    public function __toString(): string { return "hello"; }
};
 
process($obj);  // TypeError (works in non-strict mode)
?>

Terminology

For this RFC:

  • A numeric string is a string accepted by PHP's scalar type coercion for numeric type declarations in the default (coercive) typing mode. Examples include "123", " 42 ", and "1e3".
  • A fuzzy cast is an explicit cast (or cast-like function such as intval()) that succeeds today by discarding information or by performing a conversion that is rejected by the engine's scalar type checks.

Motivation

Fuzzy Casts

Since PHP 7.0, scalar type declarations in the default typing mode reject partial string conversions (process("123abc") throws TypeError), yet explicit casts can truncate ((int) "123abc" returns 123). This difference has several effects:

  • Data loss: Values like "19.99corrupted" can be accepted as 19 or 19.99
  • Different acceptance rules: Scalar type declarations and explicit casts can accept different inputs

Additionally, strict typing mode is widely enabled by modern coding styles. In practice this can lead to code using explicit casts simply to satisfy type declarations. When those casts are fuzzier than the engine's type checks, “too strict” code can become “too lax”.

Making explicit casts follow the same acceptance rules as scalar type checks reduces the incentive to “cast to make it pass” and encourages either valid coercion or explicit parsing. This reduces the number of conversion rules that need to be remembered and reduces reliance on explicit casts to satisfy scalar type declarations.

Stringable Objects in Strict Mode

In strict typing mode, passing a Stringable object to a string parameter throws TypeError. In the default typing mode, the same call is accepted.

This results in patterns such as:

  • using unions like string|Stringable and casting manually, or
  • requiring callers to cast with (string) $value.

This RFC proposes accepting Stringable in strict mode for string parameters, as part of reducing special cases in conversion behavior.

This also matches the longer-term direction of reducing the gap between typing modes (see the Unify PHP's typing modes gist).

Proposal

Fuzzy Scalar Casts

Phase 1: PHP 8.6 - Deprecation

Fuzzy casts emit E_DEPRECATED:

<?php
// Valid conversions (unchanged)
(int) "123";       // 123
(int) "  42  ";    // 42 (whitespace acceptable)
(float) "1e3";     // 1000.0 (scientific notation acceptable)
 
// Fuzzy casts (deprecated)
(int) "123abc";    // E_DEPRECATED, returns 123
(float) "12.5foo"; // E_DEPRECATED, returns 12.5
(object) 42;       // E_DEPRECATED, returns stdClass { scalar: 42 }
?>

Phase 2: PHP 9.0 - TypeError

Fuzzy casts throw TypeError:

<?php
(int) "123abc";    // TypeError: Cannot convert "123abc" to int
(object) 42;       // TypeError: Cannot convert int to object
?>

This aligns explicit casts with scalar type coercion rules used by the engine in the default (coercive) typing mode.

Stringable Objects in Strict Mode

PHP 8.6

Strict mode will accept Stringable objects for string parameters, converting them via __toString():

<?php
declare(strict_types=1);
 
function greet(string $name): string { return "Hello, $name!"; }
 
$user = new class implements Stringable {
    public function __toString(): string { return "John"; }
};
 
greet($user);  // PHP 8.5: TypeError, PHP 8.6: "Hello, John!"
?>

No deprecation in PHP 8.6; this is a relaxation of strict mode restrictions.

Scope and Affected Cases

The proposed fuzzy cast rules align with existing validation used by scalar type declarations in the default (coercive) typing mode since PHP 7.0.

Input Cast Current PHP 8.6 PHP 9.0
"123" (int) 123 123 123
" 42 " (int) 42 42 42
"123abc" (int) 123 E_DEPRECATED TypeError
"12.5foo" (float) 12.5 E_DEPRECATED TypeError
"abc" (int) 0 E_DEPRECATED TypeError
42 (object) stdClass{scalar:42} E_DEPRECATED TypeError
['a'=>1] (object) stdClass{a:1} Unchanged Unchanged
"false" (bool) true Unchanged Unchanged
Stringable (non-strict mode) String parameter Converts Converts
Stringable (strict mode) String parameter TypeError Converts

Note: Boolean casts use truthiness rules, not numeric parsing, they are unaffected. Float-to-int precision loss ((int) 12.5) is covered by a separate RFC.

Backward Compatibility Impact

Fuzzy Casts

Affected patterns:

<?php
$quantity = (int) $_POST['quantity'];  // Deprecated in 8.6 if not a valid numeric string
$timeout = (int) $config['timeout'];   // Deprecated in 8.6 if not a valid numeric string
$wrapped = (object) $someScalar;       // Deprecated in 8.6 (use array cast instead)
?>

In practice, these patterns will start emitting E_DEPRECATED in PHP 8.6 and will throw TypeError in PHP 9.0.

Migration options:

<?php
// Validate before casting
if (!is_numeric($value)) {
    throw new ValueError("Invalid numeric value: $value");
}
$result = (int) $value;
 
// Or use filter functions
$result = filter_var($value, FILTER_VALIDATE_INT);
 
// If you intentionally accept formats like "10 items" or "30s", parse them explicitly
if (preg_match('/^\s*([+-]?\d+)/', $value, $m)) {
    $result = (int) $m[1];
}
 
// For scalar-to-object, use array cast
$obj = (object) ['value' => 42];  // Instead of (object) 42
?>

Deprecation notices in PHP 8.6 will identify affected code. Static analysis tools (PHPStan, Psalm) can also detect these patterns.

Stringable in Strict Mode

Code that relies on strict mode rejecting Stringable objects will need adjustment. This is expected to be rare.

settype() will follow the same rules as explicit casts: fuzzy conversions will emit deprecation in PHP 8.6 and throw TypeError in PHP 9.0.

intval() and floatval() are thin wrappers around casts and will behave identically.

Proposed PHP Version

  • PHP 8.6: Emit E_DEPRECATED for fuzzy casts; accept Stringable for string parameters in strict mode
  • PHP 9.0: Throw TypeError for fuzzy casts

RFC Impact

Testing against the Symfony codebase raised only a few dozen deprecations across components, bundles, and bridges. Symfony doesn't use strict types, so no impact from the Stringable change.

Vote

Single yes/no vote requiring 2/3 majority.

Deprecate fuzzy casts and allow Stringable in strict mode in PHP 8.6?
Real name Yes No Abstain
Final result: 0 0 0
This poll has been closed.

Voting opens [TBD] and closes [TBD].

References

rfc/deprecate-fuzzy-casts.txt · Last modified: by alexandredaubois