Hi everyone,
Nicolas Grekas and I would like to propose this new RFC to the
discussion. This proposes to introduce two new cast operators to the
language: (?type) and (!type).
Please find the details here:
https://wiki.php.net/rfc/nullable-not-nullable-cast-operator
Thanks!
— Alexandre Daubois
Hi everyone,
Nicolas Grekas and I would like to propose this new RFC to the
discussion. This proposes to introduce two new cast operators to the
language:(?type)and(!type).Please find the details here:
https://wiki.php.net/rfc/nullable-not-nullable-cast-operatorThanks!
— Alexandre Daubois
My question not covered in the RFC: is this (https://3v4l.org/iF8V9) notice still emitted with this cast?
— Rob
Hi Rob,
My question not covered in the RFC: is this (https://3v4l.org/iF8V9) notice still emitted with this cast?
This is an interesting case, thank you. I asked Gina what is the plan
for this deprecation in PHP 9.0 as it is not explained in the related
RFC. That said, we would be in favor of throwing immediately. I'll
update the test covering exactly this case in the PR once we have all
the elements of answer.
— Alexandre Daubois
On Fri, Oct 24, 2025 at 9:09 AM Alexandre Daubois <
alex.daubois+php@gmail.com> wrote:
Hi everyone,
Nicolas Grekas and I would like to propose this new RFC to the
discussion. This proposes to introduce two new cast operators to the
language:(?type)and(!type).Please find the details here:
https://wiki.php.net/rfc/nullable-not-nullable-cast-operatorThanks!
— Alexandre Daubois
This looks useful to me
- In the RFC the "Behavior Comparison Table" the colors make it hard to
read what's listed in the table, could this be adjusted? - We still end up having null coalescing operators (or isset) for arrays,
would something like this work?
// suggested through RFC
$quantity = ((?int) ($_POST['quantity'] ?? null)) ?? 0;
// the underlying logic would be
$quantity = $_POST['quantity'] ?? null;
if ($quantity !== null && ! is_valid_int_value($quantity)) {
throw new TypeError();
}
$quantity = (int) $quantity;
// would be nice if it incorporated an "isset check" as it's effectively
treated as a null value
$quantity = ((?int) $_POST['quantity']) ?? 0;
Hi Lynn,
- In the RFC the "Behavior Comparison Table" the colors make it hard to read what's listed in the table, could this be adjusted?
Unfortunately this is the default style rendered by the PHP website.
But I can see if plain text could help readability. Let me check.
// would be nice if it incorporated an "isset check" as it's effectively treated as a null value
$quantity = ((?int) $_POST['quantity']) ?? 0;
I understand that it would probably help, but we prefer to keep this
proposition consistent with the current coercion rules of the core and
avoid adding more special rules to the numerous existing ones.
— Alexandre Daubois
Hi everyone,
Nicolas Grekas and I would like to propose this new RFC to the
discussion. This proposes to introduce two new cast operators to the
language:(?type)and(!type).Please find the details here:
https://wiki.php.net/rfc/nullable-not-nullable-cast-operator
Hi both, and thanks for the RFC.
PHP definitely needs a better range of cast operators, and I've been thinking for a while about what that should look like. My main concern with this proposal is that it picks a few specific options and uses syntax in a way that's not easy to extend.
What I mean by that is that there are, roughly, three things that a user might want to specify:
- What output types are desired?
- What input values are considered castable?
- What should happen for invalid values?
For (1), our current casts allow base scalar types + array. I can definitely see value in allowing nullable types there, but also other complex types, e.g. (string|Widget)$foo could be equivalent to $foo instanceof Widget ? $foo : (string)$foo
For (2) and (3), you could say our current casts accept anything, but you could argue that they replace invalid values with a fixed "empty" value from the target type.
More importantly, there are a few different things you might want to happen:
- throw an exception, because you expect the cast to succeed, and want to abort if it doesn't
- fill with null, or with some default value from the target type, e.g. because you're sanitising untrusted data and want to proceed with the best you can get
- detect that it would fail, e.g. because you're validating data.
Throwing exceptions is generally a poor choice for validation or sanitisation, because you have to either make the user fix one mistake at a time, or add a separate try-catch around every check.
The current RFC adds support for nullable types (1), but only if you also want a different set of validation rules (2) and a different behaviour if those rules aren't met (3). That's a useful combination, but it's not the only useful combination. Using the (?type) syntax for that combination makes it hard to add other combinations in future.
Rowan Tommins
[IMSoP]
Hi Rowan,
PHP definitely needs a better range of cast operators, and I've been thinking for a while about what that should look like. My main concern with this proposal is that it picks a few specific options and uses syntax in a way that's not easy to extend.
Thank you for your interest! I get your concerns about the extension
of such syntax, but we don't feel that there would be a particular
need of extending this syntax. The syntax does not bring something
really new: the behavior described in the RFC already exists in PHP —
we are just making it available at the cast level with explicit null
handling.
What I mean by that is that there are, roughly, three things that a user might want to specify:
- What output types are desired?
- What input values are considered castable?
- What should happen for invalid values?
For (1), our current casts allow base scalar types + array. I can definitely see value in allowing nullable types there, but also other complex types, e.g. (string|Widget)$foo could be equivalent to $foo instanceof Widget ? $foo : (string)$foo
For (2) and (3), you could say our current casts accept anything, but you could argue that they replace invalid values with a fixed "empty" value from the target type.
More importantly, there are a few different things you might want to happen:
- throw an exception, because you expect the cast to succeed, and want to abort if it doesn't
- fill with null, or with some default value from the target type, e.g. because you're sanitising untrusted data and want to proceed with the best you can get
- detect that it would fail, e.g. because you're validating data.
Throwing exceptions is generally a poor choice for validation or sanitisation, because you have to either make the user fix one mistake at a time, or add a separate try-catch around every check.
The current RFC adds support for nullable types (1), but only if you also want a different set of validation rules (2) and a different behaviour if those rules aren't met (3). That's a useful combination, but it's not the only useful combination. Using the (?type) syntax for that combination makes it hard to add other combinations in future.
I'm sorry, I think I don't get what you mean. It looks really complex
for something that we would like to keep straightforward. This RFC
doesn't really introduce *new * behavior: these rules already exist in
the engine. Casting with these new operators is indeed stricter than
existing ones and it may look it introduces brand new rules, but it
actually isn't. This strictness comes from implicit casts already done
at function calls.
— Alexandre Daubois
Hi Alexandre,
Thank you for your interest! I get your concerns about the extension
of such syntax, but we don't feel that there would be a particular
need of extending this syntax. The syntax does not bring something
really new: the behavior described in the RFC already exists in PHP —
we are just making it available at the cast level with explicit null
handling.
I definitely agree that reusing the behaviour from the existing scalar
parameter/return checks makes a lot of sense. I also agree that allowing
nullable types as the target of the cast is a useful feature.
What I'm not convinced by is that those two features should be combined
into one:
$foo = '123abc';
$a = (int)$foo; // OK
$b = (?int)$foo; // not OK!?
It looks like all I've changed is the type I'm casting to, but suddenly
I get different behaviour for values that are nothing to do with that
change.
Remember that in general contexts, ?int is just short-hand for int|null,
and one of the future expansions I can see is adding other unions as
cast targets:
(int)$foo;
(?int)$foo;
(int|null)$foo;
(int|float)$foo;
( int | (Countable&SomethingElse) )$foo;
There's no obvious reason for the first one to accept values that the
others reject.
The most obvious thing to me is to split the proposal into two, and let
people choose how to combine them:
// existing casts
(int)'123abc'; // 123, using existing cast rules
(int)null; // 0, using existing fallback-to-zero
// nullable types, existing rules
(?int)'123abc'; // 123
(?int)null; // null
// "failable cast" mode, indicated by !type
(!int)'123abc'; // TypeError
(!int)null; // TypeError
// nullable type, in failable mode
(!?int)'123abc'; // TypeError
(!?int)null; // null
"!?" looks a bit ugly, but I'm not convinced by a leading "!" for the
new mode anyway, as I'll get into below...
I'm sorry, I think I don't get what you mean. It looks really complex
for something that we would like to keep straightforward.
I agree that it's complex, but the complexity isn't something I'm
proposing, it's something I'm observing: there are lots of different
things people want out of cast operators, and we have to decide which
ones we're catering for.
As a specific example, last time nullable casts were discussed, some
people were hoping for a way to say "cast this to int if valid, and
default to null if invalid". In your current proposal, the best they
would get is this:
try { $foo = (!int)$_GET['foo']; } catch ( TypeError ) { $foo = null; }
The language is also missing a clean way to get a boolean for whether a
particular cast/coercion will succeed - in other words, to validate
input against the same rules as the cast. As I mentioned in my last
e-mail, exception handling as we currently have it is not well suited
for this task either:
try {
(!int)$_GET['foo'];
} catch ( TypeError ) {
$validationErrors[] = 'The value for "foo" must be a valid integer';
}
That's a lot of boilerplate, and a lot of run-time overhead, for what
could be a simple if statement.
Now, maybe we just say that there are more modes added in future; but if
we go with punctuation for modes, we're going to end up with symbol salad:
// target type is "?int"; "!" indicates not to allow values like
'123abc'; "@" indicates null-on-error
$foo = (@!?int)$_GET['foo'];
// target type is "?int"; "!" for the validation rules; "~" to return a
boolean of whether it would succeed
if ( (~!?int)$_GET['foo'] ) {
$validationErrors[] = 'The value for "foo" must be a valid integer';
}
That's what I meant by being "easy to extend" - not that your proposal
isn't useful, but that other people might want to propose other things
later, and we might want a framework to fit them in.
What exactly that should look like I'm not sure, and that's why I
haven't shared a full proposal. The main ideas I've been playing with
are "SQL style" pseudo-functions with keywords...
$foo = must_cast($_GET['foo'] as int); // throw on error
$foo = try_cast($_GET['foo'] as int default -1); // fill with
default on error
if ( ! can_cast($_GET['foo'] as int) ) { ... } // test with same rules
// all available for any type, including nullables:
$foo = must_cast($_GET['foo'] as ?int);
$foo = try_cast($_GET['foo'] as ?int default null);
if ( ! can_cast($_GET['foo'] as ?int) ) { ... }
...or "C++ style" generic/template syntax
$foo = must_cast<int>($_GET['foo']);
$foo = try_cast<int>($_GET['foo'], -1);
if ( ! can_cast<int>($_GET['foo']) ) { ... }
$foo = must_cast<?int>($_GET['foo']);
$foo = try_cast<?int>($_GET['foo'], null);
if ( ! can_cast<?int>($_GET['foo']) ) { ... }
Neither of those looks anything like the current casts, but maybe that's
a good thing - we can consider the current loose rules as a legacy feature.
There are obviously plenty of variations, including combining must_cast
and try_cast into one keyword, with the behaviour depending on whether a
default was provided.
To re-iterate: I'm not saying that your proposal needs to add all
possible variations; but concise syntax is a scarce resource, so we
should think carefully before adding something we can't re-use for
future scope.
Regards,
--
Rowan Tommins
[IMSoP]
On Mon, Oct 27, 2025, 09:55 Rowan Tommins [IMSoP] imsop.php@rwec.co.uk
wrote:
Hi Alexandre,
Thank you for your interest! I get your concerns about the extension
of such syntax, but we don't feel that there would be a particular
need of extending this syntax. The syntax does not bring something
really new: the behavior described in the RFC already exists in PHP —
we are just making it available at the cast level with explicit null
handling.I definitely agree that reusing the behaviour from the existing scalar
parameter/return checks makes a lot of sense. I also agree that allowing
nullable types as the target of the cast is a useful feature.What I'm not convinced by is that those two features should be combined
into one:$foo = '123abc';
Why not create a separate RFC to deprecate and remove(9.0) this and others
strange cast behaviors.
Then after it finishes decide how to processed with this one?
There no need to rush, php 8.6 will be released only next year
Hi,
Why not create a separate RFC to deprecate and remove(9.0) this and others strange cast behaviors.
Then after it finishes decide how to processed with this one?
We did some research with Nicolas about deprecating casting null
instead of proposing a new (!type) operator. The idea did not make
it, and we explained why in this new section:
https://wiki.php.net/rfc/nullable-not-nullable-cast-operator#alternative_considereddeprecating_null_casting
As for other strange (or "fuzzy") cast behaviors, we agree the current
situation is not ideal. It is inconsistent with the rules of typed
function parameters. We added the "Future scope" section to the RFC in
this regard: https://wiki.php.net/rfc/nullable-not-nullable-cast-operator#future_scope.
This is something we will investigate in the near future.
This is fortunately not a blocker to this RFC, as this would address
an orthogonal problem. As you said, we have plenty of time before 8.6!
— Alexandre Daubois
Hi
Thank you for the RFC. I agree that having functionality to do type
conversions in a safer and/or more succinct way is useful. However
Am 2025-10-31 10:05, schrieb Alexandre Daubois:
This is fortunately not a blocker to this RFC, as this would address
an orthogonal problem. As you said, we have plenty of time before 8.6!
I share Rowan's concerns about the proposed syntax. And I disagree that
that the proposed Future Scope is not a blocker for this RFC. If we
decide to introduce new syntax for this type of casts, then the “Future
Scope” might also be interested in new syntax.
One big problem with the current syntax, that is hinted at in the
backwards compatibility section is that PHP does not have a generic
“type cast” syntax, but instead requires a separate token for each
possible cast. This means:
-
It has quite a big impact on the ecosystem, since each tool working
with the tokenizer needs to learn about the new tokens. Similarly, they
also need to learn about new AST nodes to even make sense of the code.
In fact the “RFC Impact” section from the suggested RFC template
(https://wiki.php.net/rfc/template#rfc_impact) is completely missing
from your RFC. Every RFC that introduces syntax has a significant impact
on all kinds of tooling and also for users learning the language. -
“no backward compatibility issues:” as an absolute statement is
incorrect, as you acknowledge yourself right below: “The syntax (!type)
is technically correct”. It is true that this is exceedingly unlikely to
affect existing code, but it is wrong to say that there are no BC
issues. The RFC template specifically states “Please include all
breaking changes, no matter how minor they might appear.”
With regard to reusing the type cast syntax, one issue I'm already
having with the existing casts is that I don't have a good intuition
about the precedence of the casting operators and the accepted PER-CS
code style does not help there:
$a = "1";
$b = "2";
$result = (int) $a . $b; // what is $result?
Given that the proposed semantics matches that of function calls, I
share Rowan's suggestion of using “function-call style” casts:
function string_internal(string $input): string { return $input; }
function string(mixed $input, bool $passNull = false): string {
if ($passNull && $input === null) return null;
return string_internal($input);
}
$input = 123;
$str = string($input);
var_dump($str);
In fact this is already valid PHP as of now and would allow polyfilling
the new operators: https://3v4l.org/ldvFb and it nicely circumsteps all
the backwards compatibility and ecosystem impact as well as the
precedence issue. The only issue I'm seeing is possible confusion with
the existing strval() and friends. But perhaps this is not relevant in
practice.
Best regards
Tim Düsterhus
Given that the proposed semantics matches that of function calls, I share Rowan's suggestion of using “function-call style” casts:
function string_internal(string $input): string { return $input; }
function string(mixed $input, bool $passNull = false): string {
if ($passNull && $input === null) return null;return string_internal($input);}
$input = 123;
$str = string($input);
var_dump($str);In fact this is already valid PHP as of now and would allow polyfilling the new operators: https://3v4l.org/ldvFb and it nicely circumsteps all the backwards compatibility and ecosystem impact as well as the precedence issue. The only issue I'm seeing is possible confusion with the existing
strval()and friends. But perhaps this is not relevant in practice.
The ability to polyfill is very tempting, but I think this would be a dead end. There's no natural way to express nullable types, and no way at all to allow arbitrary union types. It would be like having "string_function foo() {}" instead of "function foo(): string {}"
That's why both of my example syntaxes had the type as some sort of parameter. I don't think there's any way around introducing new syntax for that, because you can't "pass a type" to a normal function.
There's also no obvious reason why int($foo) and intval($foo) should do different things, and neither is inherently "better".
The new int() would cover the "assertion" case (where an invalid value is a surprise that should abort processing), and (int) or intval() would cover the "sanitisation" case (where you want to substitute an empty or chosen value for invalid input), but the names don't tell you which is which.
And neither is useful for the "validation" case (where you want to efficiently test user input).
Rowan Tommins
[IMSoP]
We did some research with Nicolas about deprecating casting null
instead of proposing a new(!type)operator. The idea did not make
it, and we explained why in this new section:
https://wiki.php.net/rfc/nullable-not-nullable-cast-operator#alternative_considereddeprecating_null_casting
This highlights something that's been bugging me about this RFC: you've called the "(!type)" syntax a "non-nullable cast", but that's actually the least significant difference from existing casts. The most significant difference is that it can throw a TypeError for an invalid value.
In fact I would argue that existing casts don't "accept null" at all - the behaviour of (int)$foo can be adequately described something like this:
- if $foo is already an int, return that
- if $foo is a float, truncate it to an integer
- if $foo is a numeric string, return the integer equivalent
- if $foo begins with a numeric string, return the integer equivalent of that part
- else, return integer zero
I may have missed some other cases, but there's no explicit rule for null, it's just falling into the "else" clause at the end.
You would get the same answer from a hypothetical "cast($foo as int default 0)", except that maybe we'd like to remove rule #4.
What you are proposing is a new syntax that changes step 5 to "else, throw a TypeError". That might be a useful feature in some cases, but it's nothing to do with the title of the RFC.
Rowan Tommins
[IMSoP]
Hi Rowan,
What you are proposing is a new syntax that changes step 5 to "else, throw a TypeError". That might be a useful feature in some cases, but it's nothing to do with the title of the RFC.
What we propose is to align these new operators to already existing
rules applied to function arguments. This is, indeed, stricter than
current cast operators. But I wouldn't say it has nothing to do with
the title of the RFC.
Maybe it's not perfectly accurate. If the naming is a problem and
should be changed, I'd be happy to hear suggestions and update
accordingly with a better name.
— Alexandre Daubois
What we propose is to align these new operators to already existing
rules applied to function arguments. This is, indeed, stricter than
current cast operators. But I wouldn't say it hasnothing to do with
the title of the RFC.
Maybe it's not perfectly accurate. If the naming is a problem and
should be changed, I'd be happy to hear suggestions and update
accordingly with a better name.
Let's imagine how someone would describe these two expressions if this
RFC passed:
(int)$foo
(!int)$foo
Here are the differences I see:
- The first casts any non-numeric input to zero; the second throws
TypeError. - Certain values, such as "123foo" are considered numeric by one, but
not by the other.
That's it. There's no need to mention null specifically, it's just one
of many non-numeric values.
In fact, people might forget to mention point 2, because the difference
between "always returns an integer" and "may cause your entire program
to exit if fed unvalidated input" is far more important.
So, if you want a name, I suggest something to do with "throw" or "error".
But the problem I see with the RFC is deeper than that: there's no
actual discussion of why throwing a TypeError is better, or when
this completely new form of cast would be used.
As I've tried to argue in previous messages, exception-handling is not
convenient for tasks like handling user input. It is maybe useful for
deeper code, where you expect the value to already be validated. So I
think we should be looking at more than one type of new cast; or at
least a choice of syntax that anticipates that.
As for the other part of the RFC, looking at these two expressions:
(int)$foo
(?int)$foo
I would expect the only differences to be to do with null input, and
maybe null output.
But in the proposal, using the new operator will crash my application
for values where the other returned zero, and even for values where the
other returned non-zero integers.
If that part of the proposal goes to a vote as currently written, I will
vote No.
--
Rowan Tommins
[IMSoP]
Please find the details here:
https://wiki.php.net/rfc/nullable-not-nullable-cast-operator
I see two issues here, and I'm not sure how I feel about them.
-
The new operators not only change how
NULLis handled, but also add
validation rules. So, their name should be changed, maybe "nullable
validating cast". I guess "strict" word would not work as they aren't
really strict. -
They are supposed to have behavior in line with function arguments,
but they don't react to strict_types.
--
Aleksander Machniak
Kolab Groupware Developer [https://kolab.org]
Roundcube Webmail Developer [https://roundcube.net]
PGP: 19359DC1 # Blog: https://kolabian.wordpress.com
Please find the details here:
https://wiki.php.net/rfc/nullable-not-nullable-cast-operator
I see two issues here, and I'm not sure how I feel about them.
The new operators not only change how
NULLis handled, but also add
validation rules. So, their name should be changed, maybe "nullable
validating cast". I guess "strict" word would not work as they aren't
really strict.They are supposed to have behavior in line with function arguments,
but they don't react to strict_types.
The RFC says: "using weak mode function parameter coercion (stricter than traditional casts)." (emphasis not mine) If it were to act like strict-mode arguments, it’d be more like a type-check than a cast. The entire point of a cast is to "turn one type into another" and strict mode doesn’t do that at all. Parameter coercion in non-strict mode (I hesitate to call it "weak mode" because in some ways it’s stronger than strict mode) is a well-documented behaviour, and if you’re used to working with it, this is a welcome addition, IMHO.
— Rob