rfc:nullable-not-nullable-cast-operator

PHP RFC: Nullable and Non-Nullable Cast Operators

Introduction

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:

  • Nullable cast (?type): Allows null to pass through unchanged; non-null values are validated and coerced using weak mode rules
  • Non-null cast (!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.

Current Behavior

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.

Proposal

This RFC introduces two new cast operator variants:

Nullable Cast: (?type)

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

Non-Null Cast: (!type)

The non-null cast throws a TypeError in two scenarios:

  • If the value is null: Rejected because null is not allowed
  • If weak mode validation fails: Rejected for malformed values (e.g., "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
?>

Type Coercion Semantics

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.

Mental Model

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.

Exceptions: array and object

For array and object types, the behavior differs from the function parameter analogy:

  • Function parameters with array or object types in weak mode do not perform type conversion - they only accept values that are already of the correct type
  • The new cast operators (!array), (?array), (!object), and (?object) do perform type conversion (like traditional casts)

In other words, for these types: conversion happens like traditional casts + null handling follows the nullable/non-null semantics.

Key Difference from Traditional Casts

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

Validation Behavior

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.

Float to Int Conversion

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;