PHP's type casting operators currently almost always convert values to the target type even if it cannot be represented from the original value.
For example null is evaluated in the following way: (int) null evaluates to 0, (string) null evaluates to "", and (bool) null evaluates to false. While this behavior is consistent with PHP's loose typing philosophy, it can mask bugs when null should be treated as a distinct absence of value.
The “almost always” caveat is because non-Stringable objects when being cast to string with (string) will throw a TypeError instead of returning an empty string.
This RFC proposes two new cast operator variants that provide explicit control over null handling and stricter validation than traditional casts:
(?type): Allows null to pass through unchanged; non-null values are validated and coerced using weak mode rules(!type): Throws TypeError if the value is null or if it fails weak mode validation; valid non-null values are coerced to the target type
Unlike traditional casts that silently truncate malformed values (e.g., (int) "123aze" returns 123), these operators reject invalid input while still allowing safe conversions (e.g., (!int) "123" works, but (!int) "123aze" throws).
Important: These operators do not introduce new coercion rules. They reuse PHP's existing weak mode type juggling rules that are already used for function parameter type checking (without strict_types). The only additions are explicit null handling semantics.
These operators complement PHP's existing nullable type declarations (?int, ?string, etc.) by providing the same semantics at the cast level, with the added benefit of input validation.
Currently, PHP casts coerce null to type-specific default values:
<?php var_dump((int) null); // int(0) var_dump((float) null); // float(0) var_dump((string) null); // string(0) "" var_dump((bool) null); // bool(false) var_dump((array) null); // array(0) {} var_dump((object) null); // object(stdClass)#1 (0) {} ?>
This behavior applies regardless of the strict_types declaration:
<?php declare(strict_types=1); var_dump((int) null); // Still int(0) ?>
The strict_types directive only affects function parameter and return type checking, not explicit casts.
This RFC introduces two new cast operator variants:
The nullable cast allows null to pass through without coercion. If the value is not null, it is coerced to the specified type using weak mode function parameter coercion (stricter than traditional casts).
<?php (?int) $value // null -> null, others -> int (?float) $value // null -> null, others -> float (?string) $value // null -> null, others -> string (?bool) $value // null -> null, others -> bool (?array) $value // null -> null, others -> array (?object) $value // null -> null, others -> object ?>
The non-null cast throws a TypeError in two scenarios:
"123aze" cannot be converted to int)Valid non-null values are coerced to the specified type using weak mode function parameter coercion (stricter than traditional casts).
<?php (!int) $value // null -> TypeError, "123" -> 123, "123aze" -> TypeError (!float) $value // null -> TypeError, "12.5" -> 12.5, "abc" -> TypeError (!string) $value // null -> TypeError, 123 -> "123", non-Stringable object -> TypeError (!bool) $value // null -> TypeError, valid values -> bool (!array) $value // null -> TypeError, valid values -> array (!object) $value // null -> TypeError, valide values -> object ?>
The nullable and non-null cast operators use weak mode function parameter coercion rules, not the permissive behavior of traditional explicit casts.
This is not a new coercion system. These operators simply apply PHP's existing type juggling rules that have been used for function parameters since PHP 7.0. The behavior described below already exists in PHP - we are just making it available at the cast level with explicit null handling.
For scalar types (int, float, string, bool), these operators behave exactly as if passing a value through a typed function parameter in weak mode (without strict_types):
<?php // (!type) $value is equivalent to calling a function // with typed parameter in weak mode: function cast(type $v): type { return $v; } $result = cast($value); // weak mode behavior // (?type) $value is equivalent to: function cast(?type $v): ?type { return $v; } $result = cast($value); // weak mode behavior ?>
This is not just a pedagogical analogy - it reflects the actual implementation. The operators internally use the same coercion logic that PHP already uses for weak mode function parameters. This means the new operators always use weak mode validation rules, even when strict_types=1 is declared (see the “Interaction with strict_types” section for details).
Important: Weak mode validation can still throw TypeError. Since these operators use the same logic as function parameters, both the null handling AND the validation behavior match typed parameters - which means TypeError is thrown for both null (when non-nullable) and invalid values that cannot be coerced.
For array and object types, the behavior differs from the function parameter analogy:
array or object types in weak mode do not perform type conversion - they only accept values that are already of the correct typeIn other words, for these types: conversion happens like traditional casts + null handling follows the nullable/non-null semantics.
Traditional casts like (int) are maximally permissive and perform lossy conversions. The new operators use PHP's existing weak mode validation rules (the same ones used for function parameters), which are stricter:
<?php // Traditional cast - silently truncates (int) "123aze"; // 123 (no error, data loss) // Function parameter in weak mode - validates input function test(int $i): int { return $i; } test("123aze"); // TypeError: Cannot convert string to int // New non-null cast - uses the same validation as function parameters (!int) "123aze"; // TypeError: Cannot convert string to int ?>
The new operators validate inputs according to weak mode type juggling rules:
<?php // Valid numeric strings - accepted (!int) "123"; // 123 (!float) "12.5"; // 12.5 // Malformed numeric strings - rejected (!int) "123aze"; // TypeError (!int) "abc"; // TypeError // null handling (!int) null; // TypeError (?int) null; // null // Nullable cast still validates non-null values (?int) "123"; // 123 (?int) "123aze"; // TypeError (validates when non-null) ?>
This behavior ensures that the new cast operators are stricter and safer than traditional casts while remaining more convenient than manual validation.
For the new nullable and non-null cast operators, float to int conversions that lose precision throw a TypeError instead of emitting an E_DEPRECATED notice:
<?php // Traditional cast - emits E_DEPRECATED but succeeds (int) 78.9; // Deprecated: Implicit conversion from float 78.9 to int loses precision // Returns: 78 // New operators - throw TypeError immediately (!int) 78.9; // TypeError: Cannot cast float 78.9 to int (precision loss) (?int) 78.9; // TypeError: Cannot cast float 78.9 to int (precision loss) // Safe conversions (no precision loss) work normally (!int) 5.0; // 5 (?int) 5.0; // 5 ?>
Rationale: In PHP 8.1, implicit conversions from float to int that lose precision emit an E_DEPRECATED notice, and this deprecation will become an error in PHP 9.0. Since the new cast operators are designed to provide stricter, safer type conversions than traditional casts, they adopt the PHP 9.0 behavior immediately rather than emitting a deprecation that will soon become an error.
This means:
(int): Emits E_DEPRECATED (current PHP 8.x behavior)(!int) and (?int): Throw TypeError immediately (PHP 9.0+ behavior)The new cast operators have different behavior for objects depending on the target type:
For int, float, and bool casts:
All objects are strictly rejected with a TypeError:
<?php class SimpleObject {} $obj = new SimpleObject(); (!int) $obj; // TypeError: Cannot cast SimpleObject to int (!float) $obj; // TypeError: Cannot cast SimpleObject to float (!bool) $obj; // TypeError: Cannot cast SimpleObject to bool (?int) $obj; // TypeError: Cannot cast SimpleObject to int (?float) $obj; // TypeError: Cannot cast SimpleObject to float (?bool) $obj; // TypeError: Cannot cast SimpleObject to bool ?>
This is stricter than traditional casts, which emit a warning but return a value:
<?php // Traditional casts - warning but returns value (int) $obj; // Warning: "Object could not be converted to int" + returns 1 (float) $obj; // Warning: "Object could not be converted to float" + returns 1.0 (bool) $obj; // Returns true (no warning) // New operators - strict rejection (!int) $obj; // TypeError (!float) $obj; // TypeError (!bool) $obj; // TypeError ?>
For string casts:
Objects with __toString() are accepted, while non-stringable objects are rejected:
<?php class StringableObject { public function __toString(): string { return "I am stringable"; } } class NonStringableObject {} $stringable = new StringableObject(); $nonStringable = new NonStringableObject(); (!string) $stringable; // "I am stringable" (?string) $stringable; // "I am stringable" (!string) $nonStringable; // TypeError: Cannot cast NonStringableObject to string (?string) $nonStringable; // TypeError: Cannot cast NonStringableObject to string ?>
This behavior is consistent with traditional string casts (which also call __toString()), but throws TypeError for non-stringable objects.
<?php // Database value that might be NULL $age = (?int) $row['age']; // null stays null, "25" becomes 25 // Optional query parameter $limit = (?int) ($_GET['limit'] ?? null); // null if not set ?>
<?php // Required field that must not be null $userId = (!int) $data['user_id']; // Throws if null or malformed // API response validation $response = json_decode($json, true); $id = (!int) $response['id']; // Throws if null or malformed ?>
<?php class Foo { public $bar = "baz"; } var_dump((!object) new Foo()); // object(Foo)#1 (1) { // ["bar"]=> // string(3) "baz" // } var_dump((!object) ["key" => "value"]); // object(stdClass)#1 (1) { // ["key"]=> // string(5) "value" // } try { var_dump((!object) null); } catch (TypeError $e) { echo "TypeError: " . $e->getMessage() . "\n"; // TypeError: Cannot cast null to object } var_dump((?object) null); // null var_dump((?object) ["key" => "value"]); // object(stdClass)#1 (...) ?>
<?php // ===== NULL HANDLING ===== $value = null; $result = (int) $value; // 0 - may hide a bug // Nullable cast - null stays null $result = (?int) $value; // null - preserves absence // Non-null cast - null throws $result = (!int) $value; // TypeError - fails fast // ===== MALFORMED STRING HANDLING ===== // Traditional cast - silently truncates (lossy) $value = "123aze"; $result = (int) $value; // 123 - data loss, no error // New operators - reject invalid input (safe) $result = (!int) $value; // TypeError - cannot convert $result = (?int) $value; // TypeError - validates even when nullable // ===== VALID STRING HANDLING ===== // All three work the same for valid numeric strings $value = "123"; $result = (int) $value; // 123 $result = (!int) $value; // 123 $result = (?int) $value; // 123 ?>
The key difference: traditional casts are maximally permissive (silent data loss), while the new operators use weak mode validation (reject malformed input).
String and Scalar Values:
| Input Value |  (int)  |  (!int)  |  (?int)  |  Function param int (weak mode)  | 
	
|---|---|---|---|---|
 "123"  | 123 | 123 | 123 | 123 | 
 "123aze"  | 123 ⚠️ | TypeError | TypeError | TypeError | 
 "abc"  | 0 ⚠️ | TypeError | TypeError | TypeError | 
 78.9  | 78 (E_DEPRECATED) ⚠️ | TypeError | TypeError | 78 (E_DEPRECATED) | 
 5.0  | 5 | 5 | 5 | 5 | 
| null | 0 ⚠️ | TypeError | null | TypeError | 
| 123 | 123 | 123 | 123 | 123 | 
⚠️ = potentially unsafe (silent coercion/data loss)
Objects:
| Input Value |  (int)  |  (!int)  |  (?int)  |  (string)  |  (!string)  |  (?string)  | 
	
|---|---|---|---|---|---|---|
| Simple object | Warning + 1 ⚠️ | TypeError | TypeError | Error | TypeError | TypeError | 
| Stringable object | Warning + 1 ⚠️ | TypeError | TypeError |  Calls __toString()  |  Calls __toString()  |  Calls __toString()  | 
	
⚠️ = potentially unsafe (warning but returns value)
Key Differences:
TypeError instead of Error for non-stringable objects!type and ?type) provide safer and more predictable type coercion by failing fast instead of returning potentially incorrect values
These cast operators always use weak mode coercion rules, regardless of the strict_types setting. This is a design choice to provide consistent, predictable behavior.
Reminder: The weak mode coercion rules used here are not new - they are PHP's existing type juggling rules that have been part of the language since PHP 7.0.
<?php declare(strict_types=1); // Valid numeric strings - coerced in both strict and weak modes (?int) "123"; // 123 (allowed) (!int) "123"; // 123 (allowed) // Malformed strings - rejected in both strict and weak modes (?int) "123aze"; // TypeError (rejected) (!int) "123aze"; // TypeError (rejected) // null handling - consistent in both modes (?int) null; // null (allowed) (!int) null; // TypeError (rejected) ?>
The behavior is uniform because these operators always use weak mode coercion rules, even when strict_types=1 is declared.
This differs from function parameters, which do respect strict_types:
<?php declare(strict_types=1); // Function parameter - respects strict_types (rejects string) function test(int $i): int { return $i; } test("123"); // TypeError in strict mode, allowed in weak mode // New operators - always use weak mode rules (allows valid strings) (!int) "123"; // 123 (allowed regardless of strict_types) (!int) "123aze"; // TypeError (rejected regardless of strict_types) ?>
The new operators provide a middle ground: stricter than traditional casts (validate input), but more permissive than strict mode (allow valid numeric strings).
Traditional casts like (int) are always maximally permissive regardless of strict_types:
<?php declare(strict_types=1); // Traditional cast - always permissive, even with strict_types (int) "123aze"; // 123 (no error, data loss) // New operators - always validate, regardless of strict_types (!int) "123aze"; // TypeError (rejects malformed input) ?>
<?php // Traditional approach - ambiguous $age = (int) $row['age']; // NULL becomes 0, but 0 might be valid // With nullable cast - explicit $age = (?int) $row['age']; // NULL stays null, distinguishable from 0 if ($age === null) { echo "Age not provided"; } else { echo "Age: $age"; } ?>
<?php $json = '{"count": null, "total": 100}'; $data = json_decode($json, true); // Non-null cast for required fields $total = (!int) $data['total']; // 100 // Nullable cast for optional fields $count = (?int) $data['count']; // null $count = $count ?? 0; // Default to 0 if null ?>
<?php // Required field $quantity = (!int) ($_POST['quantity'] ?? null); // Throws TypeError if missing // Optional field $discount = (?float) ($_POST['discount'] ?? null); // null if missing $discount = $discount ?? 0.0; // Apply default ?>
<?php function getConfigInt(string $key): ?int { $value = getenv($key); // Returns string|false if ($value === false) { return null; // Config not set } return (!int) $value; // Validate and convert to int } function requireConfigInt(string $key): int { $value = getenv($key); // Returns string|false if ($value === false) { throw new RuntimeException("Missing config: $key"); } return (!int) $value; // Validate and convert to int } ?>
<?php // Before - verbose null checking $value = $input['value'] ?? null; if ($value !== null) { $value = (int) $value; // Permissive, may silently truncate } // After - concise and validates input $value = (?int) ($input['value'] ?? null); ?>
| Cast Type | Syntax | null Input |  Valid String ("123")  |  Malformed String ("123aze")  | 
	
|---|---|---|---|---|
| Current cast |  (int) $value  | 0 ⚠️ | 123 | 123 ⚠️ (silently truncates) | 
| Nullable cast |  (?int) $value  | null | 123 | TypeError (validates input) | 
| Non-null cast |  (!int) $value  | TypeError | 123 | TypeError (validates input) | 
Key differences:
Both ? and ! operators support all existing cast types:
(?int) / (!int) - Integer cast(?float) / (!float) - Float cast(?string) / (!string) - String cast(?bool) / (!bool) - Boolean cast(?object) / (!object) - Object cast
An alternative approach to introducing the (!type) operator would be to deprecate the current behavior where (int) null, (string) null, etc. convert null to type-specific default values (0, “”, false, etc.).
However, after creating a patch to test this approach and running it against Symfony's test suite on several core components, we found that the resulting code changes were verbose and often felt like “change for the sake of change” rather than genuine improvements. See the experimental changes here: https://github.com/alexandre-daubois/symfony/pull/22
The deprecation approach would force developers to add explicit null checks throughout their codebase:
<?php // Before deprecation $age = (int) $row['age']; // null becomes 0 // After deprecation - verbose boilerplate $age = $row['age'] !== null ? (int) $row['age'] : 0; // Or with ternary $age = $row['age'] === null ? 0 : (int) $row['age']; ?>
This adds significant verbosity without necessarily improving code quality, especially in cases where the current behavior is actually desired. Moreover, it would create a large migration burden for existing codebases that rely on this behavior.
Instead, introducing the new (!type) and (?type) operators provides:
(?int) $value is more concise and readable than $value !== null ? (int) $value : nullThis approach preserves backward compatibility while giving developers the tools they need to write safer code when appropriate.
This RFC could bring a backward incompatibility:
(!type) is currently technically correct and could be interpreted as the negation of a constant named type. This would mean that a constant is defined with the name of a built-in scalar type, which seems highly unlikely.Apart from this case, the new syntax has no other backward compatibility issues:
(?type) is currently parse errorsstrict_types semantics
While this RFC focuses on providing opt-in stricter cast operators, it does not address the existing permissive behavior of traditional casts. In particular, the issue of fuzzy casts - where partial string conversions silently discard data (e.g., (int) "123abc" returns 123) or where scalars are cast to objects with arbitrary structure (e.g., (object) 42) - remains unaddressed.
These fuzzy casts can mask bugs and create subtle errors by accepting malformed input without validation. Future proposals could consider deprecating such fuzzy cast behavior to align traditional casts more closely with the stricter validation rules already enforced by typed function parameters in weak mode.
The (!type) and (?type) operators provide developers with the tools to write safer code today, while leaving the door open for future improvements to PHP's type system.
PHP 8.6
This is a simple yes/no vote requiring a 2/3 majority.
Vote will start on [TBD] and end on [TBD].