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 two particularly problematic categories of casts:
1. 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" ) ?>
2. Null casts: conversions where null is coerced to type-specific default values, masking the absence of data:
<?php (int) null; // Returns 0 (null becomes zero) (string) null; // Returns "" (null becomes empty string) (bool) null; // Returns false (null becomes false) (array) null; // Returns [] (null becomes empty array) (object) null; // Returns stdClass object (null becomes empty object) ?>
This RFC proposes to deprecate both fuzzy casts and null 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 null conversions. Well-formed numeric strings (e.g., "123", "12.5") will continue to work as expected.
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:
declare(strict_types=1)TypeError exceptions for invalid type conversionsDespite these improvements, explicit cast operators have retained their permissive legacy behavior, creating a significant inconsistency in the language.
Consider these examples:
<?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") ?>
<?php // Function parameter type checking - rejects null function calculate(int $count): void { var_dump($count); } calculate(null); // TypeError: Cannot pass null to non-nullable parameter // Explicit cast - converts null to 0 $result = (int) null; // 0 (no error, null becomes zero) ?>
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).
Both fuzzy casts and null casts can introduce security vulnerabilities and correctness issues:
Fuzzy cast 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) ?>
Null cast issues:
<?php // Ambiguity: is 0 a valid value or was it null? $age = (int) $row['age']; // If $row['age'] is null, becomes 0 if ($age === 0) { // Is the user a newborn, or was age missing? } // Logic error: null becomes falsy $hasOptedIn = (bool) $userData['newsletter']; // null becomes false // User never answered, but we treat it as "no" // Data loss: null becomes empty string $middleName = (string) $person['middle_name']; // null becomes "" // Cannot distinguish between empty string and missing value ?>
In all these cases, invalid or missing input is silently accepted instead of failing fast, making bugs harder to detect and fix.
When developers explicitly cast a value, they typically expect one of two behaviors:
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.
PHP's explicit scalar cast operators currently exhibit two problematic behaviors:
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 (no numeric prefix) (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.
All cast operators convert null to type-specific default values:
<?php // Scalar conversions (int) null; // 0 ⚠️ (null becomes zero) (float) null; // 0.0 ⚠️ (null becomes zero) (string) null; // "" ⚠️ (null becomes empty string) (bool) null; // false ⚠️ (null becomes false) // Compound type conversions (array) null; // [] ⚠️ (null becomes empty array) (object) null; // object(stdClass)#1 (0) {} ⚠️ (null becomes empty object) ?>
This behavior makes it impossible to distinguish between a legitimate value and the absence of a value:
<?php $age = (int) $row['age']; // If null, becomes 0 // Problem: 0 could mean "newborn" or "missing data" $name = (string) $row['name']; // If null, becomes "" // Problem: "" could mean "no name" or "missing data" $tags = (array) $row['tags']; // If null, becomes [] // Problem: [] could mean "no tags" or "missing data" ?>
In contrast, typed function parameters in non-strict mode reject both malformed strings and null:
<?php function test(int $i): int { return $i; } // Fuzzy conversions - rejected test("123"); // 123 (valid numeric string) test("123abc"); // TypeError: Cannot convert string to int // Null conversions - rejected test(null); // TypeError: Cannot pass null to non-nullable parameter ?>
This creates a behavioral inconsistency between explicit casts and function parameter type checking.
This RFC proposes a two-phase deprecation of both fuzzy scalar casts and null casts:
In PHP 8.6, both fuzzy casts and null casts will emit an E_DEPRECATED notice:
Fuzzy casts - when a string value can only be partially converted to the target scalar type, or when a scalar is cast to (object):
Null casts - when null is cast to any type:
<?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" ) // Null casts - emit E_DEPRECATED (int) null; // Deprecated: Implicit conversion from null to int is deprecated // Returns: 0 (string) null; // Deprecated: Implicit conversion from null to string is deprecated // Returns: "" (array) null; // Deprecated: Implicit conversion from null to array is deprecated // Returns: [] ?>
The deprecation notice will clearly indicate:
In PHP 9.0, both fuzzy casts and null 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 // Null casts - throw TypeError (int) null; // TypeError: Cannot convert null to int (string) null; // TypeError: Cannot convert null to string (bool) null; // TypeError: Cannot convert null to bool (array) null; // TypeError: Cannot convert null to array (object) null; // TypeError: Cannot convert null 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 and null values since PHP 7.0. This RFC simply applies the same proven, consistent behavior to explicit cast operators.
A fuzzy cast occurs when:
int, float, string, or bool) is cast to (object), creating a stdClass with an arbitrary scalar propertyExamples 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) ?>
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); ?>
A null cast occurs when:
nullExamples of null casts (affected by this RFC):
<?php (int) null; // ✗ Null cast to int (float) null; // ✗ Null cast to float (string) null; // ✗ Null cast to string (bool) null; // ✗ Null cast to bool (array) null; // ✗ Null cast to array (object) null; // ✗ Null cast to object ?>
All null casts are deprecated because they mask the absence of data and create ambiguity.
Migration strategies for null handling:
<?php // Option 1: Use null coalescing operator $age = $row['age'] ?? 0; // Explicit default value // Option 2: Explicit null check if ($value !== null) { $result = (int) $value; } else { // Handle null case explicitly } // Option 3: Use nullable types in function signatures function process(?int $value): void { if ($value === null) { // Handle null explicitly } else { // Process non-null value } } ?>
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 | 0 | 0 |
| null | (int) null | 0 | E_DEPRECATED + 0 | TypeError |
| null | (string) null | “” | E_DEPRECATED + “” | TypeError |
| null | (bool) null | false | E_DEPRECATED + false | TypeError |
| null | (array) null | [] | E_DEPRECATED + [] | TypeError |
| null | (object) null | empty stdClass | E_DEPRECATED + empty stdClass | 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) |
This RFC introduces backward incompatible changes that will affect existing code relying on fuzzy cast or null cast behavior.
Code that currently relies on fuzzy casts or null casts will need to be updated. Common patterns that will be affected include:
Fuzzy cast patterns:
<?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 ?>
Null cast patterns:
<?php // Database columns that may be NULL $age = (int) $row['age']; // If $row['age'] is null, becomes 0 // Optional form fields $middleName = (string) ($_POST['middle_name'] ?? null); // null becomes "" // API responses with optional fields $count = (int) $response['count']; // If 'count' is null, becomes 0 // Configuration with optional values $maxRetries = (int) $config['max_retries']; // null becomes 0 ?>
Developers have several options to migrate away from fuzzy casts and null casts:
For 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; ?>
For null casts:
Option 1: Use null coalescing operator
<?php // Provide explicit default value $age = $row['age'] ?? 0; // 0 is the intended default $name = $row['name'] ?? ''; // Empty string is the intended default $tags = $row['tags'] ?? []; // Empty array is the intended default ?>
Option 2: Explicit null check
<?php if ($value !== null) { $result = (int) $value; } else { // Handle null case explicitly throw new ValueError("Value cannot be null"); // Or provide a default: // $result = 0; } ?>
Option 3: Use nullable types
<?php // Accept and preserve null in type system function process(?int $value): void { if ($value === null) { // Handle null explicitly } else { // Process non-null value } } // Or at variable level $age = $row['age']; // Keep as ?int, don't cast if ($age !== null) { // Work with non-null int } ?>
For scalar to object casts (fuzzy cast):
Option 1: Create a proper object structure
<?php // Instead of: $obj = (object) 42; // Use: $obj = new stdClass(); $obj->value = 42; // Or create a proper class: class ValueWrapper { public function __construct(public mixed $value) {} } $obj = new ValueWrapper(42); ?>
Option 2: Use an array if you need a key-value structure
<?php // Instead of: $obj = (object) "hello"; // Use: $data = ['value' => 'hello']; // Or if object is required: $obj = (object) ['value' => 'hello']; ?>
Option 3: Question whether object wrapping is necessary
<?php // Often, the scalar value can be used directly // Instead of: $obj = (object) $count; processObject($obj); // Consider: processValue($count); ?>
To help developers identify fuzzy casts and null casts in their codebase:
(int), (float), (string), (object) applied to variables that may be null or contain malformed data
We chose TypeError for consistency with:
TypeError for invalid conversions)TypeErrorThis RFC focuses specifically on fuzzy casts (including partial string conversions and scalar to object casts) and null casts because:
Fuzzy casts silently discard data or create arbitrary structures:
scalar property that has no meaningful semantic valueNull casts make it impossible to distinguish between legitimate values (0, “”, [], false) and missing data (null).
Other cast behaviors (e.g., (int) "abc" returning 0) are not addressed by this RFC because:
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.
<?php // Current behavior - null becomes 0, creating ambiguity $row = ['age' => null]; $age = (int) $row['age']; // 0 (is this a newborn or missing data?) if ($age === 0) { echo "Baby!"; // Wrong! This is actually missing data } // PHP 8.6 - deprecation notice $age = (int) $row['age']; // Deprecated: Implicit conversion from null to int is deprecated // PHP 9.0 - explicit error $age = (int) $row['age']; // TypeError: Cannot convert null to int // Recommended migration - explicit null handling $age = $row['age'] ?? throw new RuntimeException("Age is required"); // Or with default: $age = $row['age'] ?? 0; // Explicitly choosing 0 as default ?>
<?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']}"); } ?>
<?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']}"); } ?>
<?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 ?>
E_DEPRECATED for fuzzy casts and null castsTypeError for fuzzy casts and null castsThese are 2 simple yes/no vote requiring a 2/3 majority.
Vote will start on [TBD] and end on [TBD].