PHP RFC: Improve Type Conversion Inconsistencies
- Version: 1.0
- Date: 2025-10-28
- Author: Alexandre Daubois alexandredaubois@php.net, Nicolas Grekas nicolasgrekas@php.net
- 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:
<?php // Partial string conversions - only a portion of the string is used (int) "123abc"; // Returns 123 (discards "abc") (float) "12.34foo"; // Returns 12.34 (discards "foo") // Scalar to object casts - creates arbitrary structure (object) 42; // Returns stdClass Object ( [scalar] => 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:
<?php // Non-strict mode function process(string $value): void { } $obj = new class implements Stringable { public function __toString(): string { return "hello"; } }; process($obj); // Works // Strict mode declare(strict_types=1); process($obj); // TypeError: Argument must be of type string, object given ?>
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:
<?php // Function parameter - rejects fuzzy conversions function process(int $value): void { } process("123abc"); // TypeError // Explicit cast - silently truncates $result = (int) "123abc"; // 123 (no error) ?>
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):
<?php // PHP 8.6 behavior // Valid conversions - no change (int) "123"; // 123 (no notice) (int) " 42 "; // 42 (whitespace is acceptable) (float) "12.5"; // 12.5 (no notice) (float) "1e3"; // 1000.0 (scientific notation is acceptable) // Fuzzy casts - emit E_DEPRECATED (int) "123abc"; // Deprecated: Partial string conversion from "123abc" to int // Returns: 123 (float) "12.5foo"; // Deprecated: Partial string conversion from "12.5foo" to float // Returns: 12.5 // Scalar to object casts - emit E_DEPRECATED (fuzzy cast) (object) 42; // Deprecated: Conversion from int to object is deprecated // Returns: stdClass Object ( [scalar] => 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:
<?php // PHP 9.0 behavior // Valid conversions - no change (int) "123"; // 123 (float) "12.5"; // 12.5 // Fuzzy casts - throw TypeError (int) "123abc"; // TypeError: Cannot convert string "123abc" to int: trailing data (float) "12.5foo"; // TypeError: Cannot convert string "12.5foo" to float: trailing data (object) 42; // TypeError: Cannot convert int to object (object) "hello"; // TypeError: Cannot convert string to object ?>
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:
<?php declare(strict_types=1); function greet(string $name): string { return "Hello, $name!"; } $user = new class implements Stringable { public function __toString(): string { return "John"; } }; // PHP 8.x: TypeError // PHP 9.0: Works - returns "Hello, John!" greet($user); ?>
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, orbool) is cast to(object), creating astdClasswith an arbitraryscalarproperty
| 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:
<?php // Instead of this: $enabled = (bool) $value; // Prefer explicit comparison: $enabled = $value !== "" && $value !== "0" && $value !== false && $value !== null; // Or for boolean strings: $enabled = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); ?>
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:
<?php // Database/API responses with trailing whitespace or corruption $age = (int) $row['age']; // If $row['age'] is "25\0" or "25 years" // User input with mixed content $quantity = (int) $_POST['quantity']; // If input is "10 items" // Configuration values with units $timeout = (int) $config['timeout']; // If config is "30s" or "30 seconds" // Scalar to object wrapping $wrapped = (object) $someScalar; // Creates stdClass with 'scalar' property ?>
Migration Path
Developers have several options to migrate away from fuzzy casts:
Option 1: Validate before casting
<?php // Explicit validation using is_numeric() if (!is_numeric($value)) { throw new ValueError("Invalid numeric value: $value"); } $result = (int) $value; ?>
Option 2: Use filter functions
<?php // Validate and convert $result = filter_var($value, FILTER_VALIDATE_INT); if ($result === false) { throw new ValueError("Invalid integer: $value"); } // Sanitize before converting (explicit intent) $sanitized = filter_var($value, FILTER_SANITIZE_NUMBER_INT); $result = (int) $sanitized; ?>
Option 3: For scalar to object casts
Create a proper object structure or use an array:
<?php // Instead of: $obj = (object) 42; // Option A: Create proper object structure $obj = new stdClass(); $obj->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_DEPRECATEDfor fuzzy casts (Stringable behavior unchanged) - PHP 9.0: Throw
TypeErrorfor 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.
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]