Skip to content

Commit 780a828

Browse files
iluuu1994nikic
andauthored
[RFC] Property hooks (#13455)
RFC: https://wiki.php.net/rfc/property-hooks Co-authored-by: Nikita Popov <[email protected]>
1 parent 09d61b6 commit 780a828

File tree

240 files changed

+9470
-1050
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

240 files changed

+9470
-1050
lines changed

UPGRADING

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ PHP 8.4 UPGRADE NOTES
215215
RFC: https://wiki.php.net/rfc/new_without_parentheses
216216
. Added the #[\Deprecated] attribute.
217217
RFC: https://wiki.php.net/rfc/deprecated_attribute
218+
. Implemented property hooks.
219+
RFC: https://wiki.php.net/rfc/property-hooks
218220

219221
- Curl:
220222
. curl_version() returns an additional feature_list value, which is an

Zend/Optimizer/compact_literals.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,9 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
197197
LITERAL_INFO(opline->op2.constant, 2);
198198
}
199199
break;
200+
case ZEND_INIT_PARENT_PROPERTY_HOOK_CALL:
201+
LITERAL_INFO(opline->op1.constant, 1);
202+
break;
200203
case ZEND_CATCH:
201204
LITERAL_INFO(opline->op1.constant, 2);
202205
break;

Zend/Optimizer/optimize_func_calls.c

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ static void zend_delete_call_instructions(zend_op_array *op_array, zend_op *opli
4848
case ZEND_INIT_STATIC_METHOD_CALL:
4949
case ZEND_INIT_METHOD_CALL:
5050
case ZEND_INIT_FCALL:
51+
case ZEND_INIT_PARENT_PROPERTY_HOOK_CALL:
5152
if (call == 0) {
5253
MAKE_NOP(opline);
5354
return;
@@ -169,12 +170,15 @@ void zend_optimize_func_calls(zend_op_array *op_array, zend_optimizer_ctx *ctx)
169170
case ZEND_INIT_METHOD_CALL:
170171
case ZEND_INIT_FCALL:
171172
case ZEND_NEW:
173+
case ZEND_INIT_PARENT_PROPERTY_HOOK_CALL:
172174
/* The argument passing optimizations are valid for prototypes as well,
173175
* as inheritance cannot change between ref <-> non-ref arguments. */
174176
call_stack[call].func = zend_optimizer_get_called_func(
175177
ctx->script, op_array, opline, &call_stack[call].is_prototype);
176178
call_stack[call].try_inline =
177-
!call_stack[call].is_prototype && opline->opcode != ZEND_NEW;
179+
!call_stack[call].is_prototype
180+
&& opline->opcode != ZEND_NEW
181+
&& opline->opcode != ZEND_INIT_PARENT_PROPERTY_HOOK_CALL;
178182
ZEND_FALLTHROUGH;
179183
case ZEND_INIT_DYNAMIC_CALL:
180184
case ZEND_INIT_USER_CALL:
@@ -212,6 +216,7 @@ void zend_optimize_func_calls(zend_op_array *op_array, zend_optimizer_ctx *ctx)
212216
}
213217
} else if (fcall->opcode == ZEND_INIT_STATIC_METHOD_CALL
214218
|| fcall->opcode == ZEND_INIT_METHOD_CALL
219+
|| fcall->opcode == ZEND_INIT_PARENT_PROPERTY_HOOK_CALL
215220
|| fcall->opcode == ZEND_NEW) {
216221
/* We don't have specialized opcodes for this, do nothing */
217222
} else {

Zend/Optimizer/zend_call_graph.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ ZEND_API void zend_analyze_calls(zend_arena **arena, zend_script *script, uint32
6161
case ZEND_INIT_FCALL:
6262
case ZEND_INIT_METHOD_CALL:
6363
case ZEND_INIT_STATIC_METHOD_CALL:
64+
case ZEND_INIT_PARENT_PROPERTY_HOOK_CALL:
6465
call_stack[call] = call_info;
6566
func = zend_optimizer_get_called_func(
6667
script, op_array, opline, &is_prototype);

Zend/Optimizer/zend_optimizer.c

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,28 @@ zend_function *zend_optimizer_get_called_func(
974974
}
975975
}
976976
break;
977+
case ZEND_INIT_PARENT_PROPERTY_HOOK_CALL: {
978+
zend_class_entry *scope = op_array->scope;
979+
ZEND_ASSERT(scope != NULL);
980+
if ((scope->ce_flags & ZEND_ACC_LINKED) && scope->parent) {
981+
zend_class_entry *parent_scope = scope->parent;
982+
zend_string *prop_name = Z_STR_P(CRT_CONSTANT(opline->op1));
983+
zend_property_hook_kind hook_kind = opline->op2.num;
984+
zend_property_info *prop_info = zend_get_property_info(parent_scope, prop_name, /* silent */ true);
985+
986+
if (prop_info
987+
&& prop_info != ZEND_WRONG_PROPERTY_INFO
988+
&& !(prop_info->flags & ZEND_ACC_PRIVATE)
989+
&& prop_info->hooks) {
990+
zend_function *fbc = prop_info->hooks[hook_kind];
991+
if (fbc) {
992+
*is_prototype = false;
993+
return fbc;
994+
}
995+
}
996+
}
997+
break;
998+
}
977999
case ZEND_NEW:
9781000
{
9791001
zend_class_entry *ce = zend_optimizer_get_class_entry_from_op1(
@@ -1531,6 +1553,19 @@ void zend_foreach_op_array(zend_script *script, zend_op_array_func_t func, void
15311553
zend_foreach_op_array_helper(op_array, func, context);
15321554
}
15331555
} ZEND_HASH_FOREACH_END();
1556+
1557+
zend_property_info *property;
1558+
ZEND_HASH_MAP_FOREACH_PTR(&ce->properties_info, property) {
1559+
zend_function **hooks = property->hooks;
1560+
if (property->ce == ce && property->hooks) {
1561+
for (uint32_t i = 0; i < ZEND_PROPERTY_HOOK_COUNT; i++) {
1562+
zend_function *hook = hooks[i];
1563+
if (hook && hook->common.scope == ce) {
1564+
zend_foreach_op_array_helper(&hooks[i]->op_array, func, context);
1565+
}
1566+
}
1567+
}
1568+
} ZEND_HASH_FOREACH_END();
15341569
} ZEND_HASH_FOREACH_END();
15351570
}
15361571

Zend/tests/alternative_offset_syntax_compile_error_in_const_expr.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ const FOO_COMPILE_ERROR = "BAR"{0};
66
var_dump(FOO_COMPILE_ERROR);
77
?>
88
--EXPECTF--
9-
Fatal error: Array and string offset access syntax with curly braces is no longer supported in %s on line 2
9+
Parse error: syntax error, unexpected token "{", expecting "," or ";" in %s on line %d

Zend/tests/alternative_offset_syntax_compile_error_outside_const_expr.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ $foo = 'BAR';
66
var_dump($foo{0});
77
?>
88
--EXPECTF--
9-
Fatal error: Array and string offset access syntax with curly braces is no longer supported in %s on line 3
9+
Parse error: syntax error, unexpected token "{", expecting ")" in %s on line %d

Zend/tests/alternative_offset_syntax_in_encaps_string.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ Alternative offset syntax should emit only E_COMPILE_ERROR in string interpolati
33
--FILE--
44
<?php "{$g{'h'}}"; ?>
55
--EXPECTF--
6-
Fatal error: Array and string offset access syntax with curly braces is no longer supported in %s on line 1
6+
Parse error: syntax error, unexpected token "{", expecting "->" or "?->" or "[" in %s on line %d

Zend/tests/errmsg_037.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ class test {
1010
echo "Done\n";
1111
?>
1212
--EXPECTF--
13-
Fatal error: Cannot use the abstract modifier on a property in %s on line %d
13+
Fatal error: Only hooked properties may be declared abstract in %s on line %d

Zend/tests/errmsg_038.phpt

Lines changed: 0 additions & 13 deletions
This file was deleted.

Zend/tests/new_without_parentheses/unset_new.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ unset(new ArrayObject());
77

88
?>
99
--EXPECTF--
10-
Parse error: syntax error, unexpected token ")", expecting "->" or "?->" or "{" or "[" in %s on line %d
10+
Parse error: syntax error, unexpected token ")", expecting "->" or "?->" or "[" in %s on line %d
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
--TEST--
2+
Abstract hooks compile successfully
3+
--FILE--
4+
<?php
5+
6+
abstract class A {
7+
public abstract $prop {
8+
get;
9+
set {}
10+
}
11+
}
12+
13+
class B extends A {
14+
public $prop {
15+
get {}
16+
}
17+
}
18+
19+
?>
20+
===DONE===
21+
--EXPECT--
22+
===DONE===
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
--TEST--
2+
Abstract hooks in non-abstract class gives an error
3+
--FILE--
4+
<?php
5+
6+
class Test {
7+
public abstract $prop {
8+
get;
9+
set {}
10+
}
11+
}
12+
13+
?>
14+
--EXPECTF--
15+
Fatal error: Class Test contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (Test::$prop::get) in %s on line %d
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
--TEST--
2+
Abstract hooks that are not implemented throw an error
3+
--FILE--
4+
<?php
5+
6+
abstract class A {
7+
public abstract $prop {
8+
get;
9+
set {}
10+
}
11+
}
12+
13+
class B extends A {}
14+
15+
?>
16+
--EXPECTF--
17+
Fatal error: Class B contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (A::$prop::get) in %s on line %d
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
--TEST--
2+
Explicit hooked property satisfies abstract property
3+
--FILE--
4+
<?php
5+
6+
abstract class A {
7+
abstract public $prop { get; set; }
8+
}
9+
10+
class B extends A {
11+
public $prop { get {} set {} }
12+
}
13+
14+
?>
15+
===DONE===
16+
--EXPECT--
17+
===DONE===
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
--TEST--
2+
Abstract property not implemented throws an error
3+
--FILE--
4+
<?php
5+
6+
class A {
7+
abstract public $prop { get; set; }
8+
}
9+
10+
class B extends A {}
11+
12+
?>
13+
--EXPECTF--
14+
Fatal error: Class A contains 2 abstract methods and must therefore be declared abstract or implement the remaining methods (A::$prop::get, A::$prop::set) in %s on line %d
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
--TEST--
2+
Plain property satisfies abstract property
3+
--FILE--
4+
<?php
5+
6+
abstract class A {
7+
abstract public $prop { get; set; }
8+
}
9+
10+
class B extends A {
11+
public $prop;
12+
}
13+
14+
?>
15+
===DONE===
16+
--EXPECT--
17+
===DONE===
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
--TEST--
2+
Abstract property without hook is illegal
3+
--FILE--
4+
<?php
5+
6+
class C {
7+
abstract public $prop;
8+
}
9+
10+
?>
11+
--EXPECTF--
12+
Fatal error: Only hooked properties may be declared abstract in %s on line %d
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
--TEST--
2+
Array offset on ArrayAccess object in virtual property is allowed
3+
--FILE--
4+
<?php
5+
6+
class Collection implements ArrayAccess {
7+
public function offsetExists(mixed $offset): bool {
8+
echo __METHOD__ . "\n";
9+
return true;
10+
}
11+
12+
public function offsetGet(mixed $offset): mixed {
13+
echo __METHOD__ . "\n";
14+
return true;
15+
}
16+
17+
public function offsetSet(mixed $offset, mixed $value): void {
18+
echo __METHOD__ . "\n";
19+
}
20+
21+
public function offsetUnset(mixed $offset): void {
22+
echo __METHOD__ . "\n";
23+
}
24+
}
25+
26+
class C {
27+
public function __construct(
28+
public Collection $collection = new Collection(),
29+
) {}
30+
public $prop {
31+
get => $this->collection;
32+
}
33+
}
34+
35+
$c = new C();
36+
var_dump($c->prop['foo']);
37+
var_dump($c->prop[] = 'foo');
38+
var_dump(isset($c->prop['foo']));
39+
unset($c->prop['foo']);
40+
41+
?>
42+
--EXPECT--
43+
Collection::offsetGet
44+
bool(true)
45+
Collection::offsetSet
46+
string(3) "foo"
47+
Collection::offsetExists
48+
bool(true)
49+
Collection::offsetUnset
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
--TEST--
2+
Hook AST printing
3+
--FILE--
4+
<?php
5+
6+
try {
7+
assert(false && new class {
8+
public $prop1 { get; set; }
9+
public $prop2 {
10+
get {
11+
return parent::$prop1::get();
12+
}
13+
final set {
14+
echo 'Foo';
15+
$this->prop1 = 42;
16+
}
17+
}
18+
public $prop3 = 1 {
19+
get => 42;
20+
}
21+
});
22+
} catch (Error $e) {
23+
echo $e->getMessage(), "\n";
24+
}
25+
26+
?>
27+
--EXPECT--
28+
assert(false && new class {
29+
public $prop1 {
30+
get;
31+
set;
32+
}
33+
public $prop2 {
34+
get {
35+
return parent::$prop1::get();
36+
}
37+
final set {
38+
echo 'Foo';
39+
$this->prop1 = 42;
40+
}
41+
}
42+
public $prop3 = 1 {
43+
get => 42;
44+
}
45+
})

0 commit comments

Comments
 (0)