====== PHP RFC: Deprecate Fuzzy Scalar Casts and Null Casts ====== * 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) * Implementation: TBD ===== 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 two particularly problematic categories of casts: **1. Fuzzy casts**: conversions where information is lost or transformed in unexpected ways: 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: This RFC proposes to **deprecate both fuzzy casts and null casts in PHP 8.6** and **make them throw TypeError 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. ===== 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 these examples: **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 ==== Both fuzzy casts and null casts can introduce security vulnerabilities and correctness issues: **Fuzzy cast issues:** **Null cast issues:** In all these cases, **invalid or missing 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: - **Successful conversion**: The value can be fully interpreted as the target type - **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 two problematic behaviors: ==== Fuzzy Conversions ==== Fuzzy conversions parse as much of the input string as possible and discard the rest: **Integer Casts:** **Float Casts:** **Boolean Casts:** Boolean casts are less affected by fuzzy conversion because they rely on truthiness rules rather than parsing: While boolean casts don't perform partial parsing, they still exhibit **counterintuitive behavior** where "0" is falsy but "false" is truthy. ==== Null Conversions ==== All cast operators convert null to type-specific default values: This behavior makes it **impossible to distinguish** between a legitimate value and the absence of a value: ==== Comparison with Function Parameters ==== In contrast, typed function parameters in **non-strict mode** reject both malformed strings and null: This creates a **behavioral inconsistency** between explicit casts and function parameter type checking. ===== Proposal ===== This RFC proposes a **two-phase deprecation** of both fuzzy scalar casts and null casts: ==== Phase 1: PHP 8.6 - Deprecation Notice ==== 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: 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: * For fuzzy casts: The original value, the target type, and the nature of the problematic conversion * For null casts: That the conversion from null to the target type is deprecated * That these conversions will become errors in PHP 9.0 ==== Phase 2: PHP 9.0 - TypeError ==== In PHP 9.0, both fuzzy casts and null 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. **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. ===== Scope and Affected Cases ===== ==== What is Considered a Fuzzy Cast? ==== 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 **Examples of fuzzy casts (affected by this RFC):** **Examples of valid casts (NOT affected):** '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: 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: ==== What is Considered a Null Cast? ==== A null cast occurs when: - The input value is null - Any explicit cast operator is used on it **Examples of null casts (affected by this RFC):** **All null casts are deprecated** because they mask the absence of data and create ambiguity. **Migration strategies for null handling:** ==== 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 | 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) | ===== Backward Compatibility Impact ===== This RFC introduces **backward incompatible changes** that will affect existing code relying on fuzzy cast or null cast behavior. ==== Potential Impact ==== 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:** **Null cast patterns:** ==== Migration Path ==== Developers have several options to migrate away from fuzzy casts and null casts: **For fuzzy casts:** **Option 1: Validate before casting** **Option 2: Use filter functions** **For null casts:** **Option 1: Use null coalescing operator** **Option 2: Explicit null check** **Option 3: Use nullable types** **For scalar to object casts (fuzzy cast):** **Option 1: Create a proper object structure** 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** 'hello']; // Or if object is required: $obj = (object) ['value' => 'hello']; ?> **Option 3: Question whether object wrapping is necessary** ==== Detection and Tooling ==== To help developers identify fuzzy casts and null 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 and null casts * **Code review**: Search for cast patterns like (int), (float), (string), (object) applied to variables that may be null or 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) and **null casts** because: * These are the cases where explicit casts diverge from function parameter validation in non-strict mode * They represent the most problematic cases (silent data loss and ambiguity) * They are the most 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 **Null 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: * They are well-documented and widely understood * They don't involve data loss (the entire string is "consumed") * They don't create ambiguity about data presence * Changing them would require broader discussion and different tradeoffs ==== Why Encourage filter_var()? ==== The filter_var() family of functions provides **explicit intent**: 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 NULL ==== 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 ?> ==== Example 2: Database Column with Corruption ==== '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 3: API Response with Units ==== '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 4: Configuration with Sanitization ==== '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 and null casts * **PHP 9.0**: Throw TypeError for fuzzy casts and null casts ===== Voting Choices ===== These are 2 simple yes/no vote requiring a 2/3 majority. * Yes * No * Abstain * 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]