rfc:deprecate-fuzzy-casts

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:

  1. 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
  2. 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:

<?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_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.

Accept type conversion consistency improvements (fuzzy cast deprecation + Stringable in strict mode) for PHP 8.6/9.0?
Real name Yes No Abstain
Final result: 0 0 0
This poll has been closed.

Vote will start on [TBD] and end on [TBD].

References

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