====== PHP RFC: Improve Type Conversion Inconsistencies ====== * Version: 1.0 * Date: 2025-10-28 * Author: Alexandre Daubois , Nicolas Grekas * Status: Under discussion * Target Version: PHP 8.6 (deprecation), PHP 9.0 (TypeError and Stringable support) * Implementation: TBD ===== Introduction ===== PHP's type conversion system contains inconsistencies that make behavior unpredictable across different mechanisms. This RFC addresses two related issues: **Issue 1: Fuzzy Casts** PHP's explicit type casting operators ((int), (float), (bool), (string), (array), (object)) have historically been maximally permissive, performing lossy conversions when the input value cannot be fully represented in the target type. Fuzzy casts are conversions where information is lost or transformed in unexpected ways: 42 ) (object) "hello"; // Returns stdClass Object ( [scalar] => "hello" ) ?> **Issue 2: Stringable Objects in Strict Mode** Another inconsistency exists with Stringable objects and string parameters in strict mode: The Stringable interface was introduced in PHP 8.0 to explicitly declare that an object can be safely converted to a string. Strict mode rejecting this explicit contract is inconsistent with its purpose. **Proposed Changes** This RFC proposes: 1. Deprecate fuzzy casts in PHP 8.6 and make them throw TypeError in PHP 9.0, aligning explicit cast behavior with the validation rules already used for typed function parameters in non-strict mode 2. Allow Stringable objects for string parameters in strict mode starting in PHP 9.0, respecting the explicit conversion contract **Important:** For fuzzy casts, this RFC specifically targets **partial string conversions** and **scalar-to-object casts**. Well-formed numeric strings (e.g., "123", "12.5") will continue to work as expected. ===== Motivation ===== ==== Fuzzy Casts ==== Since PHP 7.0, the language has supported type declarations and TypeError exceptions, yet explicit cast operators have retained their legacy permissive behavior from PHP 3/4. This creates a significant inconsistency: This inconsistency is problematic because: * **Silent data loss**: Corrupted values like "19.99corrupted" are silently accepted, masking bugs * **Unexpected behavior**: Developers expect either full conversion or failure, not partial conversion * **Inconsistent validation**: Function parameters and casts should follow the same rules ==== Stringable Objects in Strict Mode ==== The Stringable interface provides an explicit contract for safe string conversion. However, strict mode rejects these explicitly declared safe conversions (see Introduction for example). This is inconsistent because: * The explicit contracts are ignored * Strict mode is misaligned as it should reject *unsafe* conversions, not explicitly declared safe ones * Non-strict mode works, this creates confusion ===== Proposal ===== This RFC proposes two changes to improve type conversion consistency: ==== Fuzzy Scalar Casts ==== === Phase 1: PHP 8.6 - Deprecation Notice === In PHP 8.6, fuzzy casts will emit an E_DEPRECATED notice when a string value can only be **partially** converted to the target scalar type, or when a scalar is cast to (object): 42 ) (object) "hello"; // Deprecated: Conversion from string to object is deprecated // Returns: stdClass Object ( [scalar] => "hello" ) ?> The deprecation notice will clearly indicate the original value, the target type, and the nature of the problematic conversion, and that these conversions will become errors in PHP 9.0. === Phase 2: PHP 9.0 - TypeError === In PHP 9.0, fuzzy casts will throw a TypeError instead of performing conversion: This aligns explicit cast behavior with the validation rules already used for typed function parameters in non-strict mode. ==== Stringable Objects in Strict Mode ==== === PHP 8.6 === No changes to Stringable behavior. Strict mode continues to throw TypeError when Stringable objects are passed to string parameters. === PHP 9.0 === Strict mode will accept Stringable objects for string parameters, converting them via __toString() just like in non-strict mode: ===== Scope and Affected Cases ===== **Important:** Apart from Stringable, the proposed behavior aligns with the **existing validation rules** used by typed function parameters in **non-strict mode**. This RFC does not introduce new rules—it applies the same well-established rules that have been used for function parameters since PHP 7.0. A fuzzy cast occurs when: - **Partial string conversions**: The input is a **string** containing a valid numeric prefix **followed by non-numeric/non-whitespace characters**, and the cast operator uses only the valid prefix and discards the trailing data - **Scalar to object casts**: A scalar value (int, float, string, or bool) is cast to (object), creating a stdClass with an arbitrary scalar property ^ Input Type ^ Cast ^ Current Behavior ^ PHP 8.6 ^ PHP 9.0 ^ | String (fully numeric) | (int) "123" | 123 | 123 | 123 | | String (whitespace + numeric) | (int) " 42 " | 42 | 42 | 42 | | String (fuzzy) | (int) "123abc" | 123 | E_DEPRECATED + 123 | TypeError | | String (fuzzy) | (float) "12.5foo" | 12.5 | E_DEPRECATED + 12.5 | TypeError | | String (non-numeric) | (int) "abc" | 0 | E_DEPRECATED + 0 | TypeError | | Scalar (fuzzy) | (object) 42 | stdClass with scalar property | E_DEPRECATED + stdClass | TypeError | | Scalar (fuzzy) | (object) "hello" | stdClass with scalar property | E_DEPRECATED + stdClass | TypeError | | Array | (object) ['a' => 1] | stdClass with properties | stdClass (not affected) | stdClass (not affected) | | Non-string | (int) 12.5 | 12 | 12 (E_DEPRECATED for precision loss) | 12 (may throw in future) | | Boolean string | (bool) "false" | true | true (not affected) | true (not affected) | | Stringable (non-strict) | function(string $s) with Stringable | Accepts (converts via __toString) | Accepts (converts) | Accepts (converts) | | Stringable (strict) | function(string $s) with Stringable in strict mode | TypeError | TypeError | Accepts (converts via __toString) | **Note on boolean casts:** Boolean casts are not affected by this RFC. They use truthiness rules rather than numeric parsing, so they evaluate the entire value rather than performing partial conversion. Developers are encouraged to use explicit comparisons for clarity: ===== Backward Compatibility Impact ===== This RFC introduces **backward incompatible changes** that will affect existing code relying on fuzzy cast behavior and implicit use of Stringable for string parameters in strict mode. ==== Fuzzy Casts ==== === Potential Impact === Code that currently relies on fuzzy casts will need to be updated. Common patterns that will be affected include: === Migration Path === Developers have several options to migrate away from fuzzy casts: **Option 1: Validate before casting** **Option 2: Use filter functions** **Option 3: For scalar to object casts** Create a proper object structure or use an array: value = 42; // Option B: Or create a proper class class ValueWrapper { public function __construct(public mixed $value) {} } $obj = new ValueWrapper(42); // Option C: Use an array if you need a key-value structure $data = ['value' => 42]; // Or if object is required: $obj = (object) ['value' => 42]; // Option D: Question whether object wrapping is necessary // Often, the scalar value can be used directly processValue(42); // Instead of processObject((object) 42) ?> === Detection and Tooling === To help developers identify fuzzy casts in their codebase: * **PHP 8.6**: The deprecation notices will appear in error logs, making it easy to find affected code * **Static analysis tools**: Tools like PHPStan and Psalm can be updated to detect potential fuzzy casts * **Code review**: Search for cast patterns like (int), (float), (object) applied to variables that may contain malformed data ==== Stringable Objects in Strict Mode ==== When using strict mode, code relying on the fact that providing a Stringable to a string parameter without the (string) explicit cast will throw won't be working as expected. ===== Rationale ===== Both changes in this RFC address the same fundamental issue: **PHP's type conversion behavior is inconsistent across different mechanisms**. **Fuzzy casts** are more permissive than function parameter validation, silently accepting malformed data that would be rejected as function arguments. This violates the principle of least surprise and can mask bugs. **Stringable in strict mode** demonstrates the opposite problem: strict mode rejects explicitly declared safe conversions. The Stringable interface exists specifically to declare that an object can be safely converted to a string, yet strict mode ignores this contract. Together, these changes move PHP toward **consistent, predictable type conversion**. This RFC uses TypeError for consistency with function parameter type checking, which already uses TypeError for invalid conversions. ===== Proposed PHP Version ===== * **PHP 8.6**: Emit E_DEPRECATED for fuzzy casts (Stringable behavior unchanged) * **PHP 9.0**: Throw TypeError for fuzzy casts, accept Stringable objects in strict mode for string parameters ===== RFC Impact ===== ==== To the Ecosystem ==== A first patch has been created to php-src to test the new rules against the Symfony codebase. Only a few dozens of deprecations were raised across the whole codebase of components, built-in bundles and bridges. Symfony doesn't use strict types in its codebase, so no impact is expected from the Stringable change in this project. ===== Voting Choices ===== This is a simple yes/no vote requiring a 2/3 majority. * Yes * No * Abstain Vote will start on [TBD] and end on [TBD]. ===== References ===== * RFC: Null Coercion Consistency: https://wiki.php.net/rfc/null_coercion_consistency * PHP Type Juggling Documentation: https://www.php.net/manual/en/language.types.type-juggling.php * PHP Type Declarations: https://www.php.net/manual/en/language.types.declarations.php * PHP filter_var() Documentation: https://www.php.net/manual/en/function.filter-var.php * RFC: Deprecate implicit float to int conversions: https://wiki.php.net/rfc/implicit-float-int-deprecate * Discussion thread: [TBD]