rfc:deprecate-fuzzy-casts

PHP RFC: Deprecate Fuzzy Scalar Casts

Introduction

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. This behavior, inherited from early PHP versions, creates inconsistencies between explicit casts and function parameter type checking, and can mask bugs by silently converting problematic values.

This RFC addresses a particularly problematic category of casts:

Fuzzy casts: 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" )
?>

This RFC proposes to deprecate fuzzy casts in PHP 8.6 and make them throw <php>TypeError</php> in PHP 9.0, aligning explicit cast behavior with the existing validation rules already used for typed function parameters in non-strict mode.

Important: 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.

Background and Motivation

Historical Context

PHP's permissive type casting behavior dates back to PHP 3/4, when the language prioritized ease of use and automatic type juggling. At that time, strict type checking was not a core language feature.

However, since PHP 7.0, the language has evolved to support:

  • Type declarations for function parameters and return values
  • Strict type checking via declare(strict_types=1)
  • Clear TypeError exceptions for invalid type conversions

Despite these improvements, explicit cast operators have retained their permissive legacy behavior, creating a significant inconsistency in the language.

The Inconsistency Problem

Consider this example:

<?php
// Function parameter type checking - rejects fuzzy conversions
function process(int $value): void {
    var_dump($value);
}
 
process("123abc");  // TypeError: Cannot convert string to int
 
// Explicit cast - permissive truncation
$result = (int) "123abc";  // 123 (no error, silently truncates "abc")
?>

This is inconsistent and confusing. Developers expect similar operations to have similar validation rules, but explicit casts are far more permissive than function parameter type checking (even in non-strict mode).

Security and Correctness Issues

Fuzzy casts can introduce security vulnerabilities and correctness issues:

<?php
// Security risk: silently truncating user input
$limit = (int) $_GET['limit'];  // "100 OR 1=1" becomes 100
 
// Correctness issue: masking data corruption
$price = (float) $dbRow['price'];  // "19.99corrupted" becomes 19.99
 
// Logic error: unexpected boolean coercion
$enabled = (bool) $config['enabled'];  // "0 disabled" becomes true (non-empty string)
?>

In these cases, invalid input is silently accepted instead of failing fast, making bugs harder to detect and fix.

Developer Expectations

When developers explicitly cast a value, they typically expect one of two behaviors:

  1. Successful conversion: The value can be fully interpreted as the target type
  2. Failure: The value cannot be converted, and an error is raised

The current behavior provides a third, unexpected outcome: partial conversion with silent data loss. This violates the principle of least surprise and makes code harder to reason about.

Current Behavior

PHP's explicit scalar cast operators currently exhibit problematic fuzzy conversion behavior:

Fuzzy Conversions

Fuzzy conversions parse as much of the input string as possible and discard the rest:

Integer Casts:

<?php
(int) "123";       // 123 (valid)
(int) "123abc";    // 123 ⚠️ (fuzzy cast - discards "abc")
(int) "  42  ";    // 42 (whitespace is trimmed)
(int) "abc123";    // 0 ⚠️ (fuzzy cast - discards "123")
(int) "0x1F";      // 0 (hex not supported in string parsing)
?>

Float Casts:

<?php
(float) "12.5";      // 12.5 (valid)
(float) "12.5foo";   // 12.5 ⚠️ (fuzzy cast - discards "foo")
(float) "  3.14  ";  // 3.14 (whitespace is trimmed)
(float) "1e3";       // 1000.0 (scientific notation supported)
(float) "1e3extra";  // 1000.0 ⚠️ (fuzzy cast - discards "extra")
?>

Boolean Casts:

Boolean casts are less affected by fuzzy conversion because they rely on truthiness rules rather than parsing:

<?php
(bool) "1";          // true (non-empty string)
(bool) "0";          // false (string "0" is falsy)
(bool) "false";      // true ⚠️ (non-empty string, even if content is "false")
(bool) "";           // false (empty string)
?>

While boolean casts don't perform partial parsing, they still exhibit counterintuitive behavior where "0" is falsy but "false" is truthy.

Comparison with Function Parameters

In contrast, typed function parameters in non-strict mode reject malformed strings:

<?php
function test(int $i): int { return $i; }
 
// Valid conversions - accepted
test("123");      // 123 (valid numeric string)
 
// Fuzzy conversions - rejected
test("123abc");   // TypeError: Cannot convert string to int
?>

This creates a behavioral inconsistency between explicit casts and function parameter type checking.

Proposal

This RFC proposes a two-phase deprecation of 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.

Note: These are not new validation rules. Function parameters in non-strict mode have rejected fuzzy conversions since PHP 7.0. This RFC simply applies the same proven, consistent behavior to explicit cast operators.

Scope and Affected Cases

What is Considered a Fuzzy Cast?

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

Examples of fuzzy casts (affected by this RFC):

<?php
// Partial string conversions
(int) "123abc";         // ✗ Fuzzy cast
(int) "42.5extra";      // ✗ Fuzzy cast
(float) "12.34foo";     // ✗ Fuzzy cast
(float) "1e3garbage";   // ✗ Fuzzy cast
 
// Scalar to object casts
(object) 42;            // ✗ Fuzzy cast
(object) 3.14;          // ✗ Fuzzy cast
(object) "hello";       // ✗ Fuzzy cast
(object) true;          // ✗ Fuzzy cast
?>

Examples of valid casts (NOT affected):

<?php
(int) "123";            // ✓ Fully numeric string
(int) "  42  ";         // ✓ Whitespace is acceptable (trimmed)
(int) "  42\n";         // ✓ Whitespace/newlines are acceptable
(float) "12.5";         // ✓ Fully numeric string
(float) "1e3";          // ✓ Scientific notation
(float) "  3.14  ";     // ✓ Whitespace is acceptable
 
// Non-string inputs - not affected (except object casts)
(int) 123.0;            // ✓ Float to int conversion
(int) true;             // ✓ Boolean to int conversion
(float) 42;             // ✓ Int to float conversion
 
// Array to object - not affected
(object) ['name' => 'John'];  // ✓ Array to object (keys become properties)
?>

What About Boolean Casts?

Boolean casts do not perform partial string parsing in the same way as numeric casts. They rely on PHP's truthiness rules:

<?php
(bool) "0";       // false (string "0" is falsy)
(bool) "false";   // true (non-empty string)
(bool) "";        // false (empty string)
?>

While this behavior is counterintuitive (e.g., "false" being truthy), it is not a fuzzy cast in the sense defined by this RFC. Boolean casts evaluate the entire string value according to truthiness rules rather than parsing a numeric prefix.

This RFC does not change boolean cast behavior. However, 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);
?>

Summary Table

Important: The proposed behavior for explicit casts aligns with the existing validation rules used by typed function parameters in non-strict mode (without declare(strict_types=1)). This RFC does not introduce new type validation rules—it applies the same well-established rules that have been used for function parameters since PHP 7.0.

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)

Backward Compatibility Impact

This RFC introduces backward incompatible changes that will affect existing code relying on fuzzy cast behavior.

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

Rationale

Why TypeError Instead of ValueError?

We chose TypeError for consistency with:

  • Function parameter type checking (which throws TypeError for invalid conversions)
  • PHP's general convention that type-related validation failures throw TypeError

Why These Specific Cases?

This RFC focuses specifically on fuzzy casts (including partial string conversions and scalar to object casts) because:

  • These are cases where explicit casts diverge from function parameter validation in non-strict mode
  • They represent problematic cases of silent data loss
  • They are likely to mask bugs and cause subtle errors
  • Function parameters already reject these conversions, providing a proven precedent

Fuzzy casts silently discard data or create arbitrary structures:

  • Partial string conversions discard trailing data, making malformed input appear valid
  • Scalar to object casts create objects with an arbitrary scalar property that has no meaningful semantic value

Why Encourage filter_var()?

The filter_var() family of functions provides explicit intent:

<?php
// Intent: Validate that the value is an integer
$value = filter_var($input, FILTER_VALIDATE_INT);
 
// Intent: Sanitize by extracting numeric characters, then convert
$value = (int) filter_var($input, FILTER_SANITIZE_NUMBER_INT);
?>

This makes code more readable, maintainable, and less error-prone than relying on implicit fuzzy cast behavior.

Examples and Use Cases

Example 1: Database Column with Corruption

<?php
// Current behavior - silently accepts corrupted data
$row = ['age' => '25corrupted'];
$age = (int) $row['age'];  // 25 (corrupted data silently discarded)
 
// PHP 8.6 - deprecation notice
$age = (int) $row['age'];
// Deprecated: Partial string conversion from "25corrupted" to int
 
// PHP 9.0 - explicit error
$age = (int) $row['age'];
// TypeError: Cannot convert string "25corrupted" to int: trailing data
 
// Recommended migration
$age = filter_var($row['age'], FILTER_VALIDATE_INT);
if ($age === false) {
    throw new RuntimeException("Corrupted age value in database: {$row['age']}");
}
?>

Example 2: API Response with Units

<?php
// Current behavior - silently strips units
$response = ['timeout' => '30s'];
$timeout = (int) $response['timeout'];  // 30 (loses the "s" unit)
 
// PHP 9.0 - explicit error
$timeout = (int) $response['timeout'];
// TypeError: Cannot convert string "30s" to int: trailing data
 
// Recommended migration - parse units explicitly
if (preg_match('/^(\d+)\s*s(?:econds)?$/i', $response['timeout'], $matches)) {
    $timeout = (int) $matches[1];
} else {
    throw new ValueError("Invalid timeout format: {$response['timeout']}");
}
?>

Example 3: Configuration with Sanitization

<?php
// Current behavior - fuzzy cast extracts number
$config = ['max_retries' => '3 times'];
$maxRetries = (int) $config['max_retries'];  // 3
 
// PHP 9.0 - explicit error
$maxRetries = (int) $config['max_retries'];
// TypeError: Cannot convert string "3 times" to int: trailing data
 
// Migration option 1: Sanitize explicitly (shows intent)
$sanitized = filter_var($config['max_retries'], FILTER_SANITIZE_NUMBER_INT);
$maxRetries = (int) $sanitized;  // Explicitly extracting number
 
// Migration option 2: Fix the configuration source
// (Preferred - configuration should store clean values)
$config = ['max_retries' => 3];  // or "3"
$maxRetries = (int) $config['max_retries'];  // Clean conversion
?>

Proposed PHP Version

  • PHP 8.6: Emit E_DEPRECATED for fuzzy casts
  • PHP 9.0: Throw TypeError for fuzzy casts

Voting Choices

This is a simple yes/no vote requiring a 2/3 majority.

Accept deprecation of fuzzy scalar casts 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