Skip to content

Commit fd3ae60

Browse files
Add #[\DelayedTargetValidation] attribute
1 parent e118147 commit fd3ae60

File tree

9 files changed

+380
-41
lines changed

9 files changed

+380
-41
lines changed
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
--TEST--
2+
#[\DelayedTargetValidation] prevents target errors at compile time
3+
--FILE--
4+
<?php
5+
6+
#[DelayedTargetValidation]
7+
#[NoDiscard]
8+
class Demo {
9+
10+
#[DelayedTargetValidation]
11+
#[Attribute]
12+
public const FOO = 'BAR';
13+
14+
#[DelayedTargetValidation]
15+
#[Attribute]
16+
public string $v;
17+
18+
#[DelayedTargetValidation]
19+
#[Attribute]
20+
public function __construct(
21+
#[DelayedTargetValidation]
22+
#[Attribute]
23+
public string $v2
24+
) {
25+
$this->v = $v2;
26+
echo __METHOD__ . "\n";
27+
}
28+
}
29+
30+
#[DelayedTargetValidation]
31+
#[Attribute]
32+
function demoFn() {
33+
echo __FUNCTION__ . "\n";
34+
}
35+
36+
#[DelayedTargetValidation]
37+
#[Attribute]
38+
const EXAMPLE = true;
39+
40+
$cases = [
41+
new ReflectionClass('Demo'),
42+
new ReflectionClassConstant('Demo', 'FOO'),
43+
new ReflectionProperty('Demo', 'v'),
44+
new ReflectionMethod('Demo', '__construct'),
45+
new ReflectionParameter([ 'Demo', '__construct' ], 'v2'),
46+
new ReflectionProperty('Demo', 'v2'),
47+
new ReflectionFunction('demoFn'),
48+
new ReflectionConstant('EXAMPLE'),
49+
];
50+
foreach ($cases as $r) {
51+
echo str_repeat("*", 20) . "\n";
52+
echo $r . "\n";
53+
$attributes = $r->getAttributes();
54+
var_dump($attributes);
55+
try {
56+
$attributes[1]->newInstance();
57+
} catch (Error $e) {
58+
echo get_class($e) . ": " . $e->getMessage() . "\n";
59+
}
60+
}
61+
62+
?>
63+
--EXPECTF--
64+
********************
65+
Class [ <user> class Demo ] {
66+
@@ %s %d-%d
67+
68+
- Constants [1] {
69+
Constant [ public string FOO ] { BAR }
70+
}
71+
72+
- Static properties [0] {
73+
}
74+
75+
- Static methods [0] {
76+
}
77+
78+
- Properties [2] {
79+
Property [ public string $v ]
80+
Property [ public string $v2 ]
81+
}
82+
83+
- Methods [1] {
84+
Method [ <user, ctor> public method __construct ] {
85+
@@ %s %d - %d
86+
87+
- Parameters [1] {
88+
Parameter #0 [ <required> string $v2 ]
89+
}
90+
}
91+
}
92+
}
93+
94+
array(2) {
95+
[0]=>
96+
object(ReflectionAttribute)#%d (1) {
97+
["name"]=>
98+
string(23) "DelayedTargetValidation"
99+
}
100+
[1]=>
101+
object(ReflectionAttribute)#%d (1) {
102+
["name"]=>
103+
string(9) "NoDiscard"
104+
}
105+
}
106+
Error: Attribute "NoDiscard" cannot target class (allowed targets: function, method)
107+
********************
108+
Constant [ public string FOO ] { BAR }
109+
110+
array(2) {
111+
[0]=>
112+
object(ReflectionAttribute)#%d (1) {
113+
["name"]=>
114+
string(23) "DelayedTargetValidation"
115+
}
116+
[1]=>
117+
object(ReflectionAttribute)#%d (1) {
118+
["name"]=>
119+
string(9) "Attribute"
120+
}
121+
}
122+
Error: Attribute "Attribute" cannot target class constant (allowed targets: class)
123+
********************
124+
Property [ public string $v ]
125+
126+
array(2) {
127+
[0]=>
128+
object(ReflectionAttribute)#%d (1) {
129+
["name"]=>
130+
string(23) "DelayedTargetValidation"
131+
}
132+
[1]=>
133+
object(ReflectionAttribute)#%d (1) {
134+
["name"]=>
135+
string(9) "Attribute"
136+
}
137+
}
138+
Error: Attribute "Attribute" cannot target property (allowed targets: class)
139+
********************
140+
Method [ <user, ctor> public method __construct ] {
141+
@@ %s %d - %d
142+
143+
- Parameters [1] {
144+
Parameter #0 [ <required> string $v2 ]
145+
}
146+
}
147+
148+
array(2) {
149+
[0]=>
150+
object(ReflectionAttribute)#%d (1) {
151+
["name"]=>
152+
string(23) "DelayedTargetValidation"
153+
}
154+
[1]=>
155+
object(ReflectionAttribute)#%d (1) {
156+
["name"]=>
157+
string(9) "Attribute"
158+
}
159+
}
160+
Error: Attribute "Attribute" cannot target method (allowed targets: class)
161+
********************
162+
Parameter #0 [ <required> string $v2 ]
163+
array(2) {
164+
[0]=>
165+
object(ReflectionAttribute)#%d (1) {
166+
["name"]=>
167+
string(23) "DelayedTargetValidation"
168+
}
169+
[1]=>
170+
object(ReflectionAttribute)#%d (1) {
171+
["name"]=>
172+
string(9) "Attribute"
173+
}
174+
}
175+
Error: Attribute "Attribute" cannot target parameter (allowed targets: class)
176+
********************
177+
Property [ public string $v2 ]
178+
179+
array(2) {
180+
[0]=>
181+
object(ReflectionAttribute)#%d (1) {
182+
["name"]=>
183+
string(23) "DelayedTargetValidation"
184+
}
185+
[1]=>
186+
object(ReflectionAttribute)#%d (1) {
187+
["name"]=>
188+
string(9) "Attribute"
189+
}
190+
}
191+
Error: Attribute "Attribute" cannot target property (allowed targets: class)
192+
********************
193+
Function [ <user> function demoFn ] {
194+
@@ %s %d - %d
195+
}
196+
197+
array(2) {
198+
[0]=>
199+
object(ReflectionAttribute)#%d (1) {
200+
["name"]=>
201+
string(23) "DelayedTargetValidation"
202+
}
203+
[1]=>
204+
object(ReflectionAttribute)#%d (1) {
205+
["name"]=>
206+
string(9) "Attribute"
207+
}
208+
}
209+
Error: Attribute "Attribute" cannot target function (allowed targets: class)
210+
********************
211+
Constant [ bool EXAMPLE ] { 1 }
212+
213+
array(2) {
214+
[0]=>
215+
object(ReflectionAttribute)#%d (1) {
216+
["name"]=>
217+
string(23) "DelayedTargetValidation"
218+
}
219+
[1]=>
220+
object(ReflectionAttribute)#%d (1) {
221+
["name"]=>
222+
string(9) "Attribute"
223+
}
224+
}
225+
Error: Attribute "Attribute" cannot target constant (allowed targets: class)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
--TEST--
2+
#[\DelayedTargetValidation] prevents target errors at compile time
3+
--FILE--
4+
<?php
5+
6+
#[DelayedTargetValidation]
7+
#[NoDiscard]
8+
class Demo {
9+
10+
#[DelayedTargetValidation]
11+
#[Attribute]
12+
public const FOO = 'BAR';
13+
14+
#[DelayedTargetValidation]
15+
#[Attribute]
16+
public string $v;
17+
18+
#[DelayedTargetValidation]
19+
#[Attribute]
20+
public function __construct(
21+
#[DelayedTargetValidation]
22+
#[Attribute]
23+
public string $v2
24+
) {
25+
$this->v = $v2;
26+
echo __METHOD__ . "\n";
27+
}
28+
}
29+
30+
#[DelayedTargetValidation]
31+
#[Attribute]
32+
function demoFn() {
33+
echo __FUNCTION__ . "\n";
34+
}
35+
36+
$o = new Demo( "foo" );
37+
demoFn();
38+
39+
#[DelayedTargetValidation]
40+
#[Attribute]
41+
const EXAMPLE = true;
42+
43+
?>
44+
--EXPECT--
45+
Demo::__construct
46+
demoFn
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
--TEST--
2+
#[\DelayedTargetValidation] does not prevent repetition errors
3+
--FILE--
4+
<?php
5+
6+
#[DelayedTargetValidation]
7+
#[NoDiscard]
8+
#[NoDiscard]
9+
class Demo {}
10+
11+
?>
12+
--EXPECTF--
13+
Fatal error: Attribute "NoDiscard" must not be repeated in %s on line %d

Zend/zend_attributes.c

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ ZEND_API zend_class_entry *zend_ce_sensitive_parameter_value;
3232
ZEND_API zend_class_entry *zend_ce_override;
3333
ZEND_API zend_class_entry *zend_ce_deprecated;
3434
ZEND_API zend_class_entry *zend_ce_nodiscard;
35+
ZEND_API zend_class_entry *zend_ce_delayed_target_validation;
3536

3637
static zend_object_handlers attributes_object_handlers_sensitive_parameter_value;
3738

@@ -72,25 +73,21 @@ uint32_t zend_attribute_attribute_get_flags(zend_attribute *attr, zend_class_ent
7273
static void validate_allow_dynamic_properties(
7374
zend_attribute *attr, uint32_t target, zend_class_entry *scope)
7475
{
76+
const char *msg = NULL;
7577
if (scope->ce_flags & ZEND_ACC_TRAIT) {
76-
zend_error_noreturn(E_ERROR, "Cannot apply #[AllowDynamicProperties] to trait %s",
77-
ZSTR_VAL(scope->name)
78-
);
78+
msg = "Cannot apply #[AllowDynamicProperties] to trait %s";
79+
} else if (scope->ce_flags & ZEND_ACC_INTERFACE) {
80+
msg = "Cannot apply #[AllowDynamicProperties] to interface %s";
81+
} else if (scope->ce_flags & ZEND_ACC_READONLY_CLASS) {
82+
msg = "Cannot apply #[AllowDynamicProperties] to readonly class %s";
83+
} else if (scope->ce_flags & ZEND_ACC_ENUM) {
84+
msg = "Cannot apply #[AllowDynamicProperties] to enum %s";
7985
}
80-
if (scope->ce_flags & ZEND_ACC_INTERFACE) {
81-
zend_error_noreturn(E_ERROR, "Cannot apply #[AllowDynamicProperties] to interface %s",
82-
ZSTR_VAL(scope->name)
83-
);
84-
}
85-
if (scope->ce_flags & ZEND_ACC_READONLY_CLASS) {
86-
zend_error_noreturn(E_ERROR, "Cannot apply #[AllowDynamicProperties] to readonly class %s",
87-
ZSTR_VAL(scope->name)
88-
);
89-
}
90-
if (scope->ce_flags & ZEND_ACC_ENUM) {
91-
zend_error_noreturn(E_ERROR, "Cannot apply #[AllowDynamicProperties] to enum %s",
92-
ZSTR_VAL(scope->name)
93-
);
86+
if (msg != NULL) {
87+
if (target & ZEND_ATTRIBUTE_NO_TARGET_VALIDATION) {
88+
return;
89+
}
90+
zend_error_noreturn(E_ERROR, msg, ZSTR_VAL(scope->name) );
9491
}
9592
scope->ce_flags |= ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES;
9693
}
@@ -487,7 +484,12 @@ ZEND_API zend_internal_attribute *zend_mark_internal_attribute(zend_class_entry
487484
if (zend_string_equals(attr->name, zend_ce_attribute->name)) {
488485
internal_attr = pemalloc(sizeof(zend_internal_attribute), 1);
489486
internal_attr->ce = ce;
490-
internal_attr->flags = Z_LVAL(attr->args[0].value);
487+
if (Z_TYPE(attr->args[0].value) == IS_NULL) {
488+
// Apply default of Attribute::TARGET_ALL
489+
internal_attr->flags = ZEND_ATTRIBUTE_TARGET_ALL;
490+
} else {
491+
internal_attr->flags = Z_LVAL(attr->args[0].value);
492+
}
491493
internal_attr->validator = NULL;
492494

493495
zend_string *lcname = zend_string_tolower_ex(ce->name, 1);
@@ -548,6 +550,9 @@ void zend_register_attribute_ce(void)
548550

549551
zend_ce_nodiscard = register_class_NoDiscard();
550552
attr = zend_mark_internal_attribute(zend_ce_nodiscard);
553+
554+
zend_ce_delayed_target_validation = register_class_DelayedTargetValidation();
555+
attr = zend_mark_internal_attribute(zend_ce_delayed_target_validation);
551556
}
552557

553558
void zend_attributes_shutdown(void)

Zend/zend_attributes.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@
3434
#define ZEND_ATTRIBUTE_IS_REPEATABLE (1<<7)
3535
#define ZEND_ATTRIBUTE_FLAGS ((1<<8) - 1)
3636

37+
/* Not a real flag, just passed to validators when target validation is *
38+
* suppressed; must not conflict with any of the real flags above. */
39+
#define ZEND_ATTRIBUTE_NO_TARGET_VALIDATION (1<<8)
40+
3741
/* Flags for zend_attribute.flags */
3842
#define ZEND_ATTRIBUTE_PERSISTENT (1<<0)
3943
#define ZEND_ATTRIBUTE_STRICT_TYPES (1<<1)
@@ -50,6 +54,7 @@ extern ZEND_API zend_class_entry *zend_ce_sensitive_parameter_value;
5054
extern ZEND_API zend_class_entry *zend_ce_override;
5155
extern ZEND_API zend_class_entry *zend_ce_deprecated;
5256
extern ZEND_API zend_class_entry *zend_ce_nodiscard;
57+
extern ZEND_API zend_class_entry *zend_ce_delayed_target_validation;
5358

5459
typedef struct {
5560
zend_string *name;

Zend/zend_attributes.stub.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,9 @@ final class NoDiscard
9797

9898
public function __construct(?string $message = null) {}
9999
}
100+
101+
/**
102+
* @strict-properties
103+
*/
104+
#[Attribute]
105+
final class DelayedTargetValidation {}

0 commit comments

Comments
 (0)