Skip to content

[RFC] Add #[\DelayedTargetValidation] attribute #18817

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Aug 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ PHP 8.5 UPGRADE NOTES
. Applying #[\Attribute] to an abstract class, enum, interface, or trait triggers
an error during compilation. Previously, the attribute could be added, but when
ReflectionAttribute::newInstance() was called an error would be thrown.
The error can be delayed from compilation to runtime using the new
#[\DelayedTargetValidation] attribute.

- DOM:
. Cloning a DOMNamedNodeMap, DOMNodeList, Dom\NamedNodeMap, Dom\NodeList,
Expand Down Expand Up @@ -184,6 +186,11 @@ PHP 8.5 UPGRADE NOTES
RFC: https://wiki.php.net/rfc/final_promotion
. #[\Override] can now be applied to properties.
RFC: https://wiki.php.net/rfc/override_properties
. The #[\DelayedTargetValidation] attribute can be used to suppress
compile-time errors from core (or extension) attributes that are used on
invalid targets. These errors are instead reported at runtime if and when
ReflectionAttribute::newInstance() is called.
RFC: https://wiki.php.net/rfc/delayedtargetvalidation_attribute

- Curl:
. Added support for share handles that are persisted across multiple PHP
Expand Down Expand Up @@ -528,6 +535,11 @@ PHP 8.5 UPGRADE NOTES
hooks are final, and whether the property is virtual. This also affects
the output of ReflectionClass::__toString() when a class contains hooked
properties.
. ReflectionAttribute::newInstance() can now throw errors for internal
attributes if the attribute was applied on an invalid target and the
error was delayed from compile-time to runtime via the
#[\DelayedTargetValidation] attribute.
RFC: https://wiki.php.net/rfc/delayedtargetvalidation_attribute

- Session:
. session_start is stricter in regard to the option argument.
Expand Down Expand Up @@ -648,6 +660,8 @@ PHP 8.5 UPGRADE NOTES
- Core:
. NoDiscard attribute was added.
RFC: https://wiki.php.net/rfc/marking_return_value_as_important
. DelayedTargetValidation attribute was added.
RFC: https://wiki.php.net/rfc/delayedtargetvalidation_attribute

- Curl:
. CurlSharePersistentHandle representing a share handle that is persisted
Expand Down
6 changes: 6 additions & 0 deletions UPGRADING.INTERNALS
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ PHP 8.5 INTERNALS UPGRADE NOTES
extra layer of indirection can be removed. In other cases a zval can
be heap-allocated and stored in the pointer as a minimal change to keep
compatibility.
. The validator callbacks for internal attribute now return `zend_string *`
rather than `void`; instead of emitting an error when an attribute is
applied incorrectly, the error message should be returned as a zend_string
pointer. If the error will be delayed until runtime, it is stored in the
new `validation_error` field of the `zend_attribute` struct.
RFC: https://wiki.php.net/rfc/delayedtargetvalidation_attribute

- Hash
. Hash functions now use proper hash_spec_result enum for return values
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
--TEST--
#[\DelayedTargetValidation] has errors at runtime
--FILE--
<?php

#[DelayedTargetValidation]
#[NoDiscard]
class Demo {

#[DelayedTargetValidation]
#[Attribute]
public const FOO = 'BAR';

#[DelayedTargetValidation]
#[Attribute]
public string $v1;

public string $v2 {
#[DelayedTargetValidation]
#[Attribute]
get => $this->v2;
#[DelayedTargetValidation]
#[Attribute]
set => $value;
}

#[DelayedTargetValidation]
#[Attribute]
public function __construct(
#[DelayedTargetValidation]
#[Attribute]
public string $v3
) {
$this->v1 = $v3;
echo __METHOD__ . "\n";
}
}

#[DelayedTargetValidation]
#[Attribute]
function demoFn() {
echo __FUNCTION__ . "\n";
}

#[DelayedTargetValidation]
#[Attribute]
const EXAMPLE = true;

$cases = [
new ReflectionClass('Demo'),
new ReflectionClassConstant('Demo', 'FOO'),
new ReflectionProperty('Demo', 'v1'),
new ReflectionProperty('Demo', 'v2')->getHook(PropertyHookType::Get),
new ReflectionProperty('Demo', 'v2')->getHook(PropertyHookType::Set),
new ReflectionMethod('Demo', '__construct'),
new ReflectionParameter([ 'Demo', '__construct' ], 'v3'),
new ReflectionProperty('Demo', 'v3'),
new ReflectionFunction('demoFn'),
new ReflectionConstant('EXAMPLE'),
];
foreach ($cases as $r) {
echo str_repeat("*", 20) . "\n";
echo $r . "\n";
$attributes = $r->getAttributes();
var_dump($attributes);
try {
$attributes[1]->newInstance();
} catch (Error $e) {
echo get_class($e) . ": " . $e->getMessage() . "\n";
}
}

?>
--EXPECTF--
********************
Class [ <user> <iterateable> class Demo ] {
@@ %s %d-%d

- Constants [1] {
Constant [ public string FOO ] { BAR }
}

- Static properties [0] {
}

- Static methods [0] {
}

- Properties [3] {
Property [ public string $v1 ]
Property [ public string $v2 { get; set; } ]
Property [ public string $v3 ]
}

- Methods [1] {
Method [ <user, ctor> public method __construct ] {
@@ %s %d - %d

- Parameters [1] {
Parameter #0 [ <required> string $v3 ]
}
}
}
}

array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "NoDiscard"
}
}
Error: Attribute "NoDiscard" cannot target class (allowed targets: function, method)
********************
Constant [ public string FOO ] { BAR }

array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "Attribute"
}
}
Error: Attribute "Attribute" cannot target class constant (allowed targets: class)
********************
Property [ public string $v1 ]

array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "Attribute"
}
}
Error: Attribute "Attribute" cannot target property (allowed targets: class)
********************
Method [ <user> public method $v2::get ] {
@@ %s %d - %d

- Parameters [0] {
}
- Return [ string ]
}

array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "Attribute"
}
}
Error: Attribute "Attribute" cannot target method (allowed targets: class)
********************
Method [ <user> public method $v2::set ] {
@@ %s %d - %d

- Parameters [1] {
Parameter #0 [ <required> string $value ]
}
- Return [ void ]
}

array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "Attribute"
}
}
Error: Attribute "Attribute" cannot target method (allowed targets: class)
********************
Method [ <user, ctor> public method __construct ] {
@@ %s %d - %d

- Parameters [1] {
Parameter #0 [ <required> string $v3 ]
}
}

array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "Attribute"
}
}
Error: Attribute "Attribute" cannot target method (allowed targets: class)
********************
Parameter #0 [ <required> string $v3 ]
array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "Attribute"
}
}
Error: Attribute "Attribute" cannot target parameter (allowed targets: class)
********************
Property [ public string $v3 ]

array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "Attribute"
}
}
Error: Attribute "Attribute" cannot target property (allowed targets: class)
********************
Function [ <user> function demoFn ] {
@@ %s %d - %d
}

array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "Attribute"
}
}
Error: Attribute "Attribute" cannot target function (allowed targets: class)
********************
Constant [ bool EXAMPLE ] { 1 }

array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "Attribute"
}
}
Error: Attribute "Attribute" cannot target constant (allowed targets: class)
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
--TEST--
#[\DelayedTargetValidation] prevents target errors at compile time
--FILE--
<?php

#[DelayedTargetValidation]
#[NoDiscard]
class Demo {

#[DelayedTargetValidation]
#[Attribute]
public const FOO = 'BAR';

#[DelayedTargetValidation]
#[Attribute]
public string $v1;

public string $v2 {
#[DelayedTargetValidation]
#[Attribute]
get => $this->v2;
#[DelayedTargetValidation]
#[Attribute]
set => $value;
}

#[DelayedTargetValidation]
#[Attribute]
public function __construct(
#[DelayedTargetValidation]
#[Attribute]
public string $v3
) {
$this->v1 = $v3;
echo __METHOD__ . "\n";
}
}

#[DelayedTargetValidation]
#[Attribute]
function demoFn() {
echo __FUNCTION__ . "\n";
}

$o = new Demo( "foo" );
demoFn();

#[DelayedTargetValidation]
#[Attribute]
const EXAMPLE = true;

?>
--EXPECT--
Demo::__construct
demoFn
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

#[DelayedTargetValidation]
#[AllowDynamicProperties]
trait DemoTrait {}
Loading