Skip to content

Commit 5b1bbee

Browse files
Only run validators once, remember the error
1 parent 6a1fd78 commit 5b1bbee

File tree

9 files changed

+94
-53
lines changed

9 files changed

+94
-53
lines changed

UPGRADING.INTERNALS

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,11 @@ PHP 8.5 INTERNALS UPGRADE NOTES
2828
extra layer of indirection can be removed. In other cases a zval can
2929
be heap-allocated and stored in the pointer as a minimal change to keep
3030
compatibility.
31-
* The validator callbacks for internal attribute now return `zend_string *`
31+
. The validator callbacks for internal attribute now return `zend_string *`
3232
rather than `void`; instead of emitting an error when an attribute is
3333
applied incorrectly, the error message should be returned as a zend_string
34-
pointer.
35-
. The `target` parameter for validator callbacks for internal attributes
36-
can now include in the bitmask the new flag
37-
ZEND_ATTRIBUTE_DELAYED_TARGET_VALIDATION. The flag indicates that
38-
validation is being performed at runtime and that other than returning an
39-
error message, the validator should not do anything, most notably including
40-
modification of any class properties.
34+
pointer. If the error will be delayed until runtime, it is stored in the
35+
new `validation_error` field of the `zend_attribute` struct.
4136
RFC: https://wiki.php.net/rfc/delayedtargetvalidation_attribute
4237

4338
- Hash
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
#[DelayedTargetValidation]
4+
#[AllowDynamicProperties]
5+
trait DemoTrait {}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
--TEST--
2+
#[\DelayedTargetValidation] with preloaded attribute with errors
3+
--INI--
4+
opcache.enable=1
5+
opcache.enable_cli=1
6+
opcache.preload={PWD}/opcache_validator_errors.inc
7+
--EXTENSIONS--
8+
opcache
9+
--SKIPIF--
10+
<?php
11+
if (PHP_OS_FAMILY == 'Windows') die('skip Preloading is not supported on Windows');
12+
?>
13+
--FILE--
14+
<?php
15+
$r = new ReflectionClass('DemoTrait');
16+
echo $r . "\n";
17+
$attributes = $r->getAttributes();
18+
var_dump($attributes);
19+
try {
20+
$attributes[1]->newInstance();
21+
} catch (Error $e) {
22+
echo get_class($e) . ": " . $e->getMessage() . "\n";
23+
}
24+
25+
?>
26+
--EXPECTF--
27+
Trait [ <user> trait DemoTrait ] {
28+
@@ %s %d-%d
29+
30+
- Constants [0] {
31+
}
32+
33+
- Static properties [0] {
34+
}
35+
36+
- Static methods [0] {
37+
}
38+
39+
- Properties [0] {
40+
}
41+
42+
- Methods [0] {
43+
}
44+
}
45+
46+
array(2) {
47+
[0]=>
48+
object(ReflectionAttribute)#%d (1) {
49+
["name"]=>
50+
string(23) "DelayedTargetValidation"
51+
}
52+
[1]=>
53+
object(ReflectionAttribute)#%d (1) {
54+
["name"]=>
55+
string(22) "AllowDynamicProperties"
56+
}
57+
}
58+
Error: Cannot apply #[\AllowDynamicProperties] to trait DemoTrait

Zend/zend_attributes.c

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,6 @@ static zend_string *validate_allow_dynamic_properties(
8989
smart_str_append_printf(&str, msg, ZSTR_VAL(scope->name));
9090
return smart_str_extract(&str);
9191
}
92-
if (target & ZEND_ATTRIBUTE_DELAYED_TARGET_VALIDATION) {
93-
// Should have passed the first time
94-
ZEND_ASSERT((scope->ce_flags & ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES) != 0);
95-
return NULL;
96-
}
9792
scope->ce_flags |= ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES;
9893
return NULL;
9994
}
@@ -221,23 +216,15 @@ ZEND_METHOD(Deprecated, __construct)
221216
static zend_string *validate_nodiscard(
222217
zend_attribute *attr, uint32_t target, zend_class_entry *scope)
223218
{
224-
/* There isn't an easy way to identify the *method* that the attribute is
225-
* applied to in a manner that works during both compilation (normal
226-
* validation) and runtime (delayed validation). So, handle them separately. */
227-
if (CG(in_compilation)) {
228-
ZEND_ASSERT((target & ZEND_ATTRIBUTE_DELAYED_TARGET_VALIDATION) == 0);
229-
zend_op_array *op_array = CG(active_op_array);
230-
const zend_string *prop_info_name = CG(context).active_property_info_name;
231-
if (prop_info_name == NULL) {
232-
op_array->fn_flags |= ZEND_ACC_NODISCARD;
233-
return NULL;
234-
}
235-
// Applied to a hook, either throw or ignore
236-
return ZSTR_INIT_LITERAL("#[\\NoDiscard] is not supported for property hooks", 0);
219+
ZEND_ASSERT(CG(in_compilation));
220+
zend_op_array *op_array = CG(active_op_array);
221+
const zend_string *prop_info_name = CG(context).active_property_info_name;
222+
if (prop_info_name == NULL) {
223+
op_array->fn_flags |= ZEND_ACC_NODISCARD;
224+
return NULL;
237225
}
238-
/* At runtime, no way to identify the target method; Reflection will handle
239-
* throwing the error if needed. */
240-
return NULL;
226+
// Applied to a hook
227+
return ZSTR_INIT_LITERAL("#[\\NoDiscard] is not supported for property hooks", 0);
241228
}
242229

243230
ZEND_METHOD(NoDiscard, __construct)
@@ -467,6 +454,9 @@ static void attr_free(zval *v)
467454

468455
zend_string_release(attr->name);
469456
zend_string_release(attr->lcname);
457+
if (attr->validation_error != NULL) {
458+
zend_string_release(attr->validation_error);
459+
}
470460

471461
for (uint32_t i = 0; i < attr->argc; i++) {
472462
if (attr->args[i].name) {
@@ -499,6 +489,7 @@ ZEND_API zend_attribute *zend_add_attribute(HashTable **attributes, zend_string
499489
}
500490

501491
attr->lcname = zend_string_tolower_ex(attr->name, persistent);
492+
attr->validation_error = NULL;
502493
attr->flags = flags;
503494
attr->lineno = lineno;
504495
attr->offset = offset;

Zend/zend_attributes.h

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,6 @@
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-
* being run at runtime; must not conflict with any of the real flags above. */
39-
#define ZEND_ATTRIBUTE_DELAYED_TARGET_VALIDATION (1<<8)
40-
4137
/* Flags for zend_attribute.flags */
4238
#define ZEND_ATTRIBUTE_PERSISTENT (1<<0)
4339
#define ZEND_ATTRIBUTE_STRICT_TYPES (1<<1)
@@ -64,6 +60,9 @@ typedef struct {
6460
typedef struct _zend_attribute {
6561
zend_string *name;
6662
zend_string *lcname;
63+
/* Only non-null for internal attributes with validation errors that are
64+
* delayed until runtime via #[\DelayedTargetValidation] */
65+
zend_string *validation_error;
6766
uint32_t flags;
6867
uint32_t lineno;
6968
/* Parameter offsets start at 1, everything else uses 0. */

Zend/zend_compile.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7606,8 +7606,10 @@ static void zend_compile_attributes(
76067606
if (error != NULL) {
76077607
if (delayed_target_validation == false) {
76087608
zend_error_noreturn(E_COMPILE_ERROR, "%s", ZSTR_VAL(error));
7609+
zend_string_efree(error);
7610+
} else {
7611+
attr->validation_error = error;
76097612
}
7610-
zend_string_efree(error);
76117613
}
76127614
}
76137615
} ZEND_HASH_FOREACH_END();

ext/opcache/zend_file_cache.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,7 @@ static void zend_file_cache_serialize_attribute(zval *zv,
461461

462462
SERIALIZE_STR(attr->name);
463463
SERIALIZE_STR(attr->lcname);
464+
SERIALIZE_STR(attr->validation_error);
464465

465466
for (i = 0; i < attr->argc; i++) {
466467
SERIALIZE_STR(attr->args[i].name);
@@ -1352,6 +1353,7 @@ static void zend_file_cache_unserialize_attribute(zval *zv, zend_persistent_scri
13521353

13531354
UNSERIALIZE_STR(attr->name);
13541355
UNSERIALIZE_STR(attr->lcname);
1356+
UNSERIALIZE_STR(attr->validation_error);
13551357

13561358
for (i = 0; i < attr->argc; i++) {
13571359
UNSERIALIZE_STR(attr->args[i].name);

ext/opcache/zend_persist_calc.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ static void zend_persist_attributes_calc(HashTable *attributes)
181181
ADD_SIZE(ZEND_ATTRIBUTE_SIZE(attr->argc));
182182
ADD_INTERNED_STRING(attr->name);
183183
ADD_INTERNED_STRING(attr->lcname);
184+
if (attr->validation_error != NULL) {
185+
ADD_INTERNED_STRING(attr->validation_error);
186+
}
184187

185188
for (i = 0; i < attr->argc; i++) {
186189
if (attr->args[i].name) {

ext/reflection/php_reflection.c

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7373,27 +7373,13 @@ ZEND_METHOD(ReflectionAttribute, newInstance)
73737373

73747374
/* Run the delayed validator function for internal attributes */
73757375
if (delayed_target_validation && ce->type == ZEND_INTERNAL_CLASS) {
7376-
zend_internal_attribute *config = zend_internal_attribute_get(attr->data->lcname);
7377-
if (config != NULL && config->validator != NULL) {
7378-
zend_string *error = config->validator(
7379-
attr->data,
7380-
attr->target | ZEND_ATTRIBUTE_DELAYED_TARGET_VALIDATION,
7381-
attr->scope
7382-
);
7383-
if (error != NULL) {
7384-
zend_throw_exception(zend_ce_error, ZSTR_VAL(error), 0);
7385-
zend_string_efree(error);
7386-
RETURN_THROWS();
7387-
}
7388-
}
7389-
/* For #[NoDiscard], the attribute does not work on property hooks, but
7390-
* at runtime the validator has no way to access the method that an
7391-
* attribute is applied to, attr->scope is just the overall class entry
7392-
* that the method is a part of. */
7393-
if (ce == zend_ce_nodiscard && attr->on_property_hook) {
7394-
zend_throw_error(NULL, "#[\\NoDiscard] is not supported for property hooks");
7376+
zend_string *error = attr->data->validation_error;
7377+
if (error != NULL) {
7378+
zend_throw_exception(zend_ce_error, ZSTR_VAL(error), 0);
73957379
RETURN_THROWS();
73967380
}
7381+
} else {
7382+
ZEND_ASSERT(attr->data->validation_error == NULL);
73977383
}
73987384

73997385
/* Repetition validation is done even if #[DelayedTargetValidation] is used

0 commit comments

Comments
 (0)