[RFC] Property hooks (#13455)

RFC: https://wiki.php.net/rfc/property-hooks

Co-authored-by: Nikita Popov <nikita.ppv@gmail.com>
This commit is contained in:
Ilija Tovilo 2024-07-14 11:55:03 +02:00 committed by GitHub
parent 09d61b6368
commit 780a8280d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
240 changed files with 9469 additions and 1049 deletions

View File

@ -215,6 +215,8 @@ PHP 8.4 UPGRADE NOTES
RFC: https://wiki.php.net/rfc/new_without_parentheses
. Added the #[\Deprecated] attribute.
RFC: https://wiki.php.net/rfc/deprecated_attribute
. Implemented property hooks.
RFC: https://wiki.php.net/rfc/property-hooks
- Curl:
. curl_version() returns an additional feature_list value, which is an

View File

@ -197,6 +197,9 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
LITERAL_INFO(opline->op2.constant, 2);
}
break;
case ZEND_INIT_PARENT_PROPERTY_HOOK_CALL:
LITERAL_INFO(opline->op1.constant, 1);
break;
case ZEND_CATCH:
LITERAL_INFO(opline->op1.constant, 2);
break;

View File

@ -48,6 +48,7 @@ static void zend_delete_call_instructions(zend_op_array *op_array, zend_op *opli
case ZEND_INIT_STATIC_METHOD_CALL:
case ZEND_INIT_METHOD_CALL:
case ZEND_INIT_FCALL:
case ZEND_INIT_PARENT_PROPERTY_HOOK_CALL:
if (call == 0) {
MAKE_NOP(opline);
return;
@ -169,12 +170,15 @@ void zend_optimize_func_calls(zend_op_array *op_array, zend_optimizer_ctx *ctx)
case ZEND_INIT_METHOD_CALL:
case ZEND_INIT_FCALL:
case ZEND_NEW:
case ZEND_INIT_PARENT_PROPERTY_HOOK_CALL:
/* The argument passing optimizations are valid for prototypes as well,
* as inheritance cannot change between ref <-> non-ref arguments. */
call_stack[call].func = zend_optimizer_get_called_func(
ctx->script, op_array, opline, &call_stack[call].is_prototype);
call_stack[call].try_inline =
!call_stack[call].is_prototype && opline->opcode != ZEND_NEW;
!call_stack[call].is_prototype
&& opline->opcode != ZEND_NEW
&& opline->opcode != ZEND_INIT_PARENT_PROPERTY_HOOK_CALL;
ZEND_FALLTHROUGH;
case ZEND_INIT_DYNAMIC_CALL:
case ZEND_INIT_USER_CALL:
@ -212,6 +216,7 @@ void zend_optimize_func_calls(zend_op_array *op_array, zend_optimizer_ctx *ctx)
}
} else if (fcall->opcode == ZEND_INIT_STATIC_METHOD_CALL
|| fcall->opcode == ZEND_INIT_METHOD_CALL
|| fcall->opcode == ZEND_INIT_PARENT_PROPERTY_HOOK_CALL
|| fcall->opcode == ZEND_NEW) {
/* We don't have specialized opcodes for this, do nothing */
} else {

View File

@ -61,6 +61,7 @@ ZEND_API void zend_analyze_calls(zend_arena **arena, zend_script *script, uint32
case ZEND_INIT_FCALL:
case ZEND_INIT_METHOD_CALL:
case ZEND_INIT_STATIC_METHOD_CALL:
case ZEND_INIT_PARENT_PROPERTY_HOOK_CALL:
call_stack[call] = call_info;
func = zend_optimizer_get_called_func(
script, op_array, opline, &is_prototype);

View File

@ -974,6 +974,28 @@ zend_function *zend_optimizer_get_called_func(
}
}
break;
case ZEND_INIT_PARENT_PROPERTY_HOOK_CALL: {
zend_class_entry *scope = op_array->scope;
ZEND_ASSERT(scope != NULL);
if ((scope->ce_flags & ZEND_ACC_LINKED) && scope->parent) {
zend_class_entry *parent_scope = scope->parent;
zend_string *prop_name = Z_STR_P(CRT_CONSTANT(opline->op1));
zend_property_hook_kind hook_kind = opline->op2.num;
zend_property_info *prop_info = zend_get_property_info(parent_scope, prop_name, /* silent */ true);
if (prop_info
&& prop_info != ZEND_WRONG_PROPERTY_INFO
&& !(prop_info->flags & ZEND_ACC_PRIVATE)
&& prop_info->hooks) {
zend_function *fbc = prop_info->hooks[hook_kind];
if (fbc) {
*is_prototype = false;
return fbc;
}
}
}
break;
}
case ZEND_NEW:
{
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
zend_foreach_op_array_helper(op_array, func, context);
}
} ZEND_HASH_FOREACH_END();
zend_property_info *property;
ZEND_HASH_MAP_FOREACH_PTR(&ce->properties_info, property) {
zend_function **hooks = property->hooks;
if (property->ce == ce && property->hooks) {
for (uint32_t i = 0; i < ZEND_PROPERTY_HOOK_COUNT; i++) {
zend_function *hook = hooks[i];
if (hook && hook->common.scope == ce) {
zend_foreach_op_array_helper(&hooks[i]->op_array, func, context);
}
}
}
} ZEND_HASH_FOREACH_END();
} ZEND_HASH_FOREACH_END();
}

View File

@ -6,4 +6,4 @@ const FOO_COMPILE_ERROR = "BAR"{0};
var_dump(FOO_COMPILE_ERROR);
?>
--EXPECTF--
Fatal error: Array and string offset access syntax with curly braces is no longer supported in %s on line 2
Parse error: syntax error, unexpected token "{", expecting "," or ";" in %s on line %d

View File

@ -6,4 +6,4 @@ $foo = 'BAR';
var_dump($foo{0});
?>
--EXPECTF--
Fatal error: Array and string offset access syntax with curly braces is no longer supported in %s on line 3
Parse error: syntax error, unexpected token "{", expecting ")" in %s on line %d

View File

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

View File

@ -10,4 +10,4 @@ class test {
echo "Done\n";
?>
--EXPECTF--
Fatal error: Cannot use the abstract modifier on a property in %s on line %d
Fatal error: Only hooked properties may be declared abstract in %s on line %d

View File

@ -1,13 +0,0 @@
--TEST--
errmsg: properties cannot be final
--FILE--
<?php
class test {
final $var = 1;
}
echo "Done\n";
?>
--EXPECTF--
Fatal error: Cannot use the final modifier on a property in %s on line %d

View File

@ -7,4 +7,4 @@ unset(new ArrayObject());
?>
--EXPECTF--
Parse error: syntax error, unexpected token ")", expecting "->" or "?->" or "{" or "[" in %s on line %d
Parse error: syntax error, unexpected token ")", expecting "->" or "?->" or "[" in %s on line %d

View File

@ -0,0 +1,22 @@
--TEST--
Abstract hooks compile successfully
--FILE--
<?php
abstract class A {
public abstract $prop {
get;
set {}
}
}
class B extends A {
public $prop {
get {}
}
}
?>
===DONE===
--EXPECT--
===DONE===

View File

@ -0,0 +1,15 @@
--TEST--
Abstract hooks in non-abstract class gives an error
--FILE--
<?php
class Test {
public abstract $prop {
get;
set {}
}
}
?>
--EXPECTF--
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

View File

@ -0,0 +1,17 @@
--TEST--
Abstract hooks that are not implemented throw an error
--FILE--
<?php
abstract class A {
public abstract $prop {
get;
set {}
}
}
class B extends A {}
?>
--EXPECTF--
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

View File

@ -0,0 +1,17 @@
--TEST--
Explicit hooked property satisfies abstract property
--FILE--
<?php
abstract class A {
abstract public $prop { get; set; }
}
class B extends A {
public $prop { get {} set {} }
}
?>
===DONE===
--EXPECT--
===DONE===

View File

@ -0,0 +1,14 @@
--TEST--
Abstract property not implemented throws an error
--FILE--
<?php
class A {
abstract public $prop { get; set; }
}
class B extends A {}
?>
--EXPECTF--
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

View File

@ -0,0 +1,17 @@
--TEST--
Plain property satisfies abstract property
--FILE--
<?php
abstract class A {
abstract public $prop { get; set; }
}
class B extends A {
public $prop;
}
?>
===DONE===
--EXPECT--
===DONE===

View File

@ -0,0 +1,12 @@
--TEST--
Abstract property without hook is illegal
--FILE--
<?php
class C {
abstract public $prop;
}
?>
--EXPECTF--
Fatal error: Only hooked properties may be declared abstract in %s on line %d

View File

@ -0,0 +1,49 @@
--TEST--
Array offset on ArrayAccess object in virtual property is allowed
--FILE--
<?php
class Collection implements ArrayAccess {
public function offsetExists(mixed $offset): bool {
echo __METHOD__ . "\n";
return true;
}
public function offsetGet(mixed $offset): mixed {
echo __METHOD__ . "\n";
return true;
}
public function offsetSet(mixed $offset, mixed $value): void {
echo __METHOD__ . "\n";
}
public function offsetUnset(mixed $offset): void {
echo __METHOD__ . "\n";
}
}
class C {
public function __construct(
public Collection $collection = new Collection(),
) {}
public $prop {
get => $this->collection;
}
}
$c = new C();
var_dump($c->prop['foo']);
var_dump($c->prop[] = 'foo');
var_dump(isset($c->prop['foo']));
unset($c->prop['foo']);
?>
--EXPECT--
Collection::offsetGet
bool(true)
Collection::offsetSet
string(3) "foo"
Collection::offsetExists
bool(true)
Collection::offsetUnset

View File

@ -0,0 +1,45 @@
--TEST--
Hook AST printing
--FILE--
<?php
try {
assert(false && new class {
public $prop1 { get; set; }
public $prop2 {
get {
return parent::$prop1::get();
}
final set {
echo 'Foo';
$this->prop1 = 42;
}
}
public $prop3 = 1 {
get => 42;
}
});
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
?>
--EXPECT--
assert(false && new class {
public $prop1 {
get;
set;
}
public $prop2 {
get {
return parent::$prop1::get();
}
final set {
echo 'Foo';
$this->prop1 = 42;
}
}
public $prop3 = 1 {
get => 42;
}
})

View File

@ -0,0 +1,40 @@
--TEST--
Hooks accept method-targeted attributes
--FILE--
<?php
#[Attribute]
class A {}
#[Attribute(Attribute::TARGET_METHOD)]
class B {}
class C {
public $prop {
#[A] get {}
#[B] set {}
}
}
$getAttr = (new ReflectionProperty(C::class, 'prop'))->getHook(PropertyHookType::Get)->getAttributes()[0];
var_dump($getAttr->getName());
var_dump($getAttr->getArguments());
var_dump($getAttr->newInstance());
$setAttr = (new ReflectionProperty(C::class, 'prop'))->getHook(PropertyHookType::Set)->getAttributes()[0];
var_dump($setAttr->getName());
var_dump($setAttr->getArguments());
var_dump($setAttr->newInstance());
?>
--EXPECT--
string(1) "A"
array(0) {
}
object(A)#3 (0) {
}
string(1) "B"
array(0) {
}
object(B)#5 (0) {
}

View File

@ -0,0 +1,66 @@
--TEST--
Attempted read/write of backing value in a delegated method throws
--SKIPIF--
<?php
if (!function_exists('zend_test_zend_call_stack_get')) die("skip zend_test_zend_call_stack_get() is not available");
?>
--EXTENSIONS--
zend_test
--INI--
; The test may use a large amount of memory on systems with a large stack limit
memory_limit=2G
--FILE--
<?php
class Test {
public $prop = 42 {
get => $this->getProp($this->prop);
set {
$this->setProp($this->prop, $value);
}
}
private function getProp($prop) {
return $this->prop;
}
private function setProp($prop, $value) {
$this->prop = $value;
}
public $prop2 = 42 {
get => $this->prop2;
set => $value;
}
}
class Child extends Test {
public $prop2 = 42 {
get => parent::$prop2::get();
set { parent::$prop2::set($value); }
}
}
$test = new Test;
try {
$test->prop = 0;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
try {
var_dump($test->prop);
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
$child = new Child();
$child->prop2 = 43;
var_dump($child->prop2);
?>
--EXPECTF--
Maximum call stack size of %d bytes (zend.max_allowed_stack_size - zend.reserved_stack_size) reached. Infinite recursion?
Maximum call stack size of %d bytes (zend.max_allowed_stack_size - zend.reserved_stack_size) reached. Infinite recursion?
int(43)

View File

@ -0,0 +1,22 @@
--TEST--
Backed property with implicit get
--FILE--
<?php
class C {
public $prop {
set {
echo __METHOD__, "\n";
$this->prop = $value;
}
}
}
$c = new C();
$c->prop = 42;
var_dump($c->prop);
?>
--EXPECT--
C::$prop::set
int(42)

View File

@ -0,0 +1,22 @@
--TEST--
Backed property with implicit set
--FILE--
<?php
class C {
public $prop {
get {
echo __METHOD__, "\n";
return $this->prop;
}
}
}
$c = new C();
$c->prop = 42;
var_dump($c->prop);
?>
--EXPECT--
C::$prop::get
int(42)

View File

@ -0,0 +1,16 @@
--TEST--
Backed property is invariant
--FILE--
<?php
class A {
public string|int $prop { get => $this->prop; }
}
class B extends A {
public string $prop { get => 'foo'; }
}
?>
--EXPECTF--
Fatal error: Type of B::$prop must be string|int (as in class A) in %s on line %d

View File

@ -0,0 +1,45 @@
--TEST--
Reads and writes from backing store are only cached for $this
--FILE--
<?php
class C {
public $switch;
public $prop = 42 {
get {
echo __METHOD__, "\n";
if ($this->switch) {
$other = new C();
$other->switch = false;
} else {
$other = $this;
}
var_dump($other->prop);
return 1;
}
set => $this->prop;
}
}
function test() {
$c = new C();
$c->switch = true;
var_dump($c->prop);
}
test();
test();
?>
--EXPECT--
C::$prop::get
C::$prop::get
int(42)
int(1)
int(1)
C::$prop::get
C::$prop::get
int(42)
int(1)
int(1)

View File

@ -0,0 +1,30 @@
--TEST--
Bug 001
--FILE--
<?php
abstract class A {
abstract public $x { get; }
}
class C extends A {
private $_x;
public $x {
get => $this->_x;
}
}
var_dump((new ReflectionProperty(C::class, 'x'))->isVirtual());
$c = new C;
try {
$c->x = 3;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
?>
--EXPECT--
bool(true)
Property C::$x is read-only

View File

@ -0,0 +1,32 @@
--TEST--
Bug 002
--FILE--
<?php
trait Foo {
public string $bar {
get => $this->getBar();
}
protected function getBar() {
return 'bar';
}
}
class A {
use Foo;
}
class B {
use Foo;
}
$a = new A();
$b = new B();
var_dump($a->bar);
var_dump($b->bar);
?>
--EXPECT--
string(3) "bar"
string(3) "bar"

View File

@ -0,0 +1,23 @@
--TEST--
Callable conversion of parent hook call
--FILE--
<?php
class B {
public mixed $x;
}
class C extends B {
public mixed $x {
set {
$f = parent::$x::set(...);
$f($value);
}
}
}
$c = new C();
$c->x = 0;
?>
--EXPECTF--
Fatal error: Cannot create Closure for parent property hook call in %s on line %d

View File

@ -0,0 +1,16 @@
--TEST--
Set hook value param must not have a default value
--FILE--
<?php
class B {
public $x {
set($value = null) {
$field = $value;
}
}
}
?>
--EXPECTF--
Fatal error: Parameter $value of set hook B::$x must not have a default value in %s on line %d

View File

@ -0,0 +1,21 @@
--TEST--
Parent hook call restriction may fail due to mangled name
--FILE--
<?php
class B {
protected mixed $x;
}
class C extends B {
protected mixed $x {
set {
parent::$x::set(1);
}
}
}
?>
===DONE===
--EXPECT--
===DONE===

View File

@ -0,0 +1,25 @@
--TEST--
Abstract properties correctly track virtualness
--FILE--
<?php
abstract class Y {
abstract public string $prop {
get;
set => "foo";
}
}
class X extends Y {
public string $prop {
get => "bar";
}
}
$x = new X;
$x->prop = 1;
var_dump($x->prop);
?>
--EXPECT--
string(3) "bar"

View File

@ -0,0 +1,25 @@
--TEST--
Assign by reference to backed property is forbidden for &get-only
--FILE--
<?php
class Test {
public $prop = 0 {
&get {
echo __METHOD__, "\n";
return $this->prop;
}
}
}
$test = new Test();
$test->prop = &$ref;
?>
--EXPECTF--
Test::$prop::get
Fatal error: Uncaught Error: Cannot assign by reference to overloaded object in %s:%d
Stack trace:
#0 {main}
thrown in %s on line %d

View File

@ -0,0 +1,24 @@
--TEST--
Assign by reference to backed property is forbidden for &get-only
--FILE--
<?php
class Foo {
private $_bar;
public $bar {
&get {
echo __METHOD__, PHP_EOL;
return $this->_bar;
}
}
}
$foo = new Foo;
$foo->bar = 'bar';
?>
--EXPECTF--
Fatal error: Uncaught Error: Property Foo::$bar is read-only in %s:%d
Stack trace:
#0 {main}
thrown in %s on line %d

View File

@ -0,0 +1,28 @@
--TEST--
Assign by reference to backed property is allowed for &get-only
--FILE--
<?php
class Foo {
public $bar = [] {
&get {
echo __METHOD__ . "\n";
return $this->bar;
}
}
}
$foo = new Foo;
$foo->bar[] = 'bar';
var_dump($foo);
?>
--EXPECTF--
Foo::$bar::get
object(Foo)#%d (1) {
["bar"]=>
array(1) {
[0]=>
string(3) "bar"
}
}

View File

@ -0,0 +1,64 @@
--TEST--
Test caching of hooked property
--FILE--
<?php
class Test {
public $prop {
get { echo __METHOD__, "\n"; return $this->prop; }
set { echo __METHOD__, "\n"; $this->prop = $value; }
}
}
function doTest(Test $test) {
$test->prop = null;
$test->prop;
$test->prop = 1;
$test->prop += 1;
$test->prop = [];
try {
$test->prop[] = 1;
} catch (\Error $e) {
echo $e->getMessage(), "\n";
}
isset($test->prop);
isset($test->prop[0]);
try {
unset($test->prop);
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
}
$test = new Test;
$test->dyn = 1;
doTest($test);
echo "\n";
doTest($test);
?>
--EXPECTF--
Deprecated: Creation of dynamic property Test::$dyn is deprecated in %s on line %d
Test::$prop::set
Test::$prop::get
Test::$prop::set
Test::$prop::get
Test::$prop::set
Test::$prop::set
Test::$prop::get
Indirect modification of Test::$prop is not allowed
Test::$prop::get
Test::$prop::get
Cannot unset hooked property Test::$prop
Test::$prop::set
Test::$prop::get
Test::$prop::set
Test::$prop::get
Test::$prop::set
Test::$prop::set
Test::$prop::get
Indirect modification of Test::$prop is not allowed
Test::$prop::get
Test::$prop::get
Cannot unset hooked property Test::$prop

View File

@ -0,0 +1,34 @@
--TEST--
Constructor property promotion
--FILE--
<?php
class Test {
public function __construct(
public $prop = 42 {
get => print("Getting\n");
set { print("Setting\n"); }
}
) {
echo "Constructor\n";
}
}
echo "Pre-test\n";
$test = new Test;
$test->prop;
$test->prop = 42;
$r = (new ReflectionProperty(Test::class, 'prop'));
var_dump($r->hasDefaultValue());
var_dump($r->getDefaultValue());
?>
--EXPECT--
Pre-test
Setting
Constructor
Getting
Setting
bool(false)
NULL

View File

@ -0,0 +1,35 @@
--TEST--
Backed property may have default value
--FILE--
<?php
class A {
public $prop = 42 {
get {
echo __METHOD__, "\n";
return $this->prop;
}
set {
echo __METHOD__, "\n";
$this->prop = $value;
}
}
}
$a = new A();
var_dump($a);
var_dump($a->prop);
$a->prop = 43;
var_dump($a->prop);
?>
--EXPECT--
object(A)#1 (1) {
["prop"]=>
int(42)
}
A::$prop::get
int(42)
A::$prop::set
A::$prop::get
int(43)

View File

@ -0,0 +1,15 @@
--TEST--
Virtual properties cannot have default value
--FILE--
<?php
class Test {
public $prop = 0 {
get {}
set {}
}
}
?>
--EXPECTF--
Fatal error: Cannot specify default value for virtual hooked property Test::$prop in %s on line %d

View File

@ -0,0 +1,19 @@
--TEST--
Virtual property cannot have default value
--FILE--
<?php
class A {
public $prop { get {} set {} }
}
class B extends A {
public $prop = 0 {
get {}
set {}
}
}
?>
--EXPECTF--
Fatal error: Cannot specify default value for virtual hooked property B::$prop in %s on line %d

View File

@ -0,0 +1,28 @@
--TEST--
Call property hooks by name
--FILE--
<?php
class Test {
public $prop {
get { echo "get called\n"; }
set { echo "set called with $value\n"; }
}
}
$test = new Test;
try {
$test->{'$prop::get'}();
} catch (\Error $e) {
echo $e->getMessage(), "\n";
}
try {
$test->{'$prop::set'}('foo');
} catch (\Error $e) {
echo $e->getMessage(), "\n";
}
?>
--EXPECT--
Call to undefined method Test::$prop::get()
Call to undefined method Test::$prop::set()

View File

@ -0,0 +1,245 @@
--TEST--
Dumping object with property hooks
--FILE--
<?php
class Test {
public $addedHooks = 'addedHooks';
public $virtual {
get { return strtoupper('virtual'); }
}
public $backed = 'backed' {
get { return strtoupper($this->backed); }
set { $this->backed = $value; }
}
public $writeOnly {
set {}
}
private $private = 'private' {
get { return strtoupper($this->private); }
set { $this->private = $value; }
}
private $changed = 'changed Test' {
get { return strtoupper($this->changed); }
}
public function dumpTest() {
var_dump($this);
var_dump(get_object_vars($this));
var_dump(get_mangled_object_vars($this));
var_export($this);
echo "\n";
echo json_encode($this), "\n";
var_dump((array) $this);
}
}
class Child extends Test {
public $addedHooks {
get { return strtoupper(parent::$addedHooks::get()); }
}
private $changed = 'changed Child' {
get { return strtoupper($this->changed); }
}
public function dumpChild() {
var_dump($this);
var_dump(get_object_vars($this));
var_export($this);
echo "\n";
echo json_encode($this), "\n";
var_dump((array) $this);
}
}
function dump($test) {
var_dump($test);
var_dump(get_object_vars($test));
var_export($test);
echo "\n";
echo json_encode($test), "\n";
var_dump((array) $test);
}
dump(new Test);
dump(new Child);
(new Child)->dumpTest();
(new Child)->dumpChild();
?>
--EXPECTF--
object(Test)#%d (4) {
["addedHooks"]=>
string(10) "addedHooks"
["backed"]=>
string(6) "backed"
["private":"Test":private]=>
string(7) "private"
["changed":"Test":private]=>
string(12) "changed Test"
}
array(3) {
["addedHooks"]=>
string(10) "addedHooks"
["virtual"]=>
string(7) "VIRTUAL"
["backed"]=>
string(6) "BACKED"
}
\Test::__set_state(array(
'addedHooks' => 'addedHooks',
'virtual' => 'VIRTUAL',
'backed' => 'BACKED',
'private' => 'PRIVATE',
'changed' => 'CHANGED TEST',
))
{"addedHooks":"addedHooks","virtual":"VIRTUAL","backed":"BACKED"}
array(4) {
["addedHooks"]=>
string(10) "addedHooks"
["backed"]=>
string(6) "backed"
["%0Test%0private"]=>
string(7) "private"
["%0Test%0changed"]=>
string(12) "changed Test"
}
object(Child)#%d (5) {
["addedHooks"]=>
string(10) "addedHooks"
["backed"]=>
string(6) "backed"
["private":"Test":private]=>
string(7) "private"
["changed":"Test":private]=>
string(12) "changed Test"
["changed":"Child":private]=>
string(13) "changed Child"
}
array(3) {
["addedHooks"]=>
string(10) "ADDEDHOOKS"
["virtual"]=>
string(7) "VIRTUAL"
["backed"]=>
string(6) "BACKED"
}
\Child::__set_state(array(
'addedHooks' => 'ADDEDHOOKS',
'changed' => 'CHANGED CHILD',
'virtual' => 'VIRTUAL',
'backed' => 'BACKED',
'private' => 'PRIVATE',
'changed' => 'changed Child',
))
{"addedHooks":"ADDEDHOOKS","virtual":"VIRTUAL","backed":"BACKED"}
array(5) {
["addedHooks"]=>
string(10) "addedHooks"
["backed"]=>
string(6) "backed"
["%0Test%0private"]=>
string(7) "private"
["%0Test%0changed"]=>
string(12) "changed Test"
["%0Child%0changed"]=>
string(13) "changed Child"
}
object(Child)#%d (5) {
["addedHooks"]=>
string(10) "addedHooks"
["backed"]=>
string(6) "backed"
["private":"Test":private]=>
string(7) "private"
["changed":"Test":private]=>
string(12) "changed Test"
["changed":"Child":private]=>
string(13) "changed Child"
}
array(4) {
["addedHooks"]=>
string(10) "ADDEDHOOKS"
["virtual"]=>
string(7) "VIRTUAL"
["backed"]=>
string(6) "BACKED"
["private"]=>
string(7) "PRIVATE"
}
array(5) {
["addedHooks"]=>
string(10) "addedHooks"
["backed"]=>
string(6) "backed"
["%0Test%0private"]=>
string(7) "private"
["%0Test%0changed"]=>
string(12) "changed Test"
["%0Child%0changed"]=>
string(13) "changed Child"
}
\Child::__set_state(array(
'addedHooks' => 'ADDEDHOOKS',
'changed' => 'CHANGED CHILD',
'virtual' => 'VIRTUAL',
'backed' => 'BACKED',
'private' => 'PRIVATE',
'changed' => 'changed Child',
))
{"addedHooks":"ADDEDHOOKS","virtual":"VIRTUAL","backed":"BACKED"}
array(5) {
["addedHooks"]=>
string(10) "addedHooks"
["backed"]=>
string(6) "backed"
["%0Test%0private"]=>
string(7) "private"
["%0Test%0changed"]=>
string(12) "changed Test"
["%0Child%0changed"]=>
string(13) "changed Child"
}
object(Child)#%d (5) {
["addedHooks"]=>
string(10) "addedHooks"
["backed"]=>
string(6) "backed"
["private":"Test":private]=>
string(7) "private"
["changed":"Test":private]=>
string(12) "changed Test"
["changed":"Child":private]=>
string(13) "changed Child"
}
array(5) {
["addedHooks"]=>
string(10) "ADDEDHOOKS"
["changed"]=>
string(13) "CHANGED CHILD"
["virtual"]=>
string(7) "VIRTUAL"
["backed"]=>
string(6) "BACKED"
["changed"]=>
string(13) "changed Child"
}
\Child::__set_state(array(
'addedHooks' => 'ADDEDHOOKS',
'changed' => 'CHANGED CHILD',
'virtual' => 'VIRTUAL',
'backed' => 'BACKED',
'private' => 'PRIVATE',
'changed' => 'changed Child',
))
{"addedHooks":"ADDEDHOOKS","virtual":"VIRTUAL","backed":"BACKED"}
array(5) {
["addedHooks"]=>
string(10) "addedHooks"
["backed"]=>
string(6) "backed"
["%0Test%0private"]=>
string(7) "private"
["%0Test%0changed"]=>
string(12) "changed Test"
["%0Child%0changed"]=>
string(13) "changed Child"
}

View File

@ -0,0 +1,15 @@
--TEST--
Cannot declare same property hook twice
--FILE--
<?php
class Test {
public $prop {
get {}
get {}
}
}
?>
--EXPECTF--
Fatal error: Cannot redeclare property hook "get" in %s on line %d

View File

@ -0,0 +1,48 @@
--TEST--
Explicit iterator implementation
--FILE--
<?php
class Foo implements Iterator {
public string $hook {
get => 'this is not the correct value';
}
private $x = ['foo', 'BAR'];
private $cursor = 0;
public function current(): string { return $this->x[$this->cursor]; }
public function key(): int { return $this->cursor; }
public function next(): void { ++$this->cursor; }
public function rewind(): void { $this->cursor = 0; }
public function valid(): bool { return isset($this->x[$this->cursor]); }
}
class Bar implements IteratorAggregate {
public string $hook {
get => 'this is not the correct value';
}
public function getIterator(): Traversable {
yield 1;
yield 2;
}
}
var_dump(iterator_to_array(new Foo()));
var_dump(iterator_to_array(new Bar()));
?>
--EXPECT--
array(2) {
[0]=>
string(3) "foo"
[1]=>
string(3) "BAR"
}
array(2) {
[0]=>
int(1)
[1]=>
int(2)
}

View File

@ -0,0 +1,19 @@
--TEST--
Explicit set property hook $value parameter
--FILE--
<?php
class Test {
public $prop {
set($customName) {
var_dump($customName);
}
}
}
$test = new Test();
$test->prop = 42;
?>
--EXPECT--
int(42)

View File

@ -0,0 +1,28 @@
--TEST--
Explicit set property hook $value parameter
--FILE--
<?php
class Test {
public string $prop {
set(string|array $prop) {
$this->prop = is_array($prop) ? join(', ', $prop) : $prop;
}
}
}
$test = new Test();
var_dump($test->prop = 'prop');
var_dump($test->prop = ['prop1', 'prop2']);
var_dump($test->prop);
?>
--EXPECT--
string(4) "prop"
array(2) {
[0]=>
string(5) "prop1"
[1]=>
string(5) "prop2"
}
string(12) "prop1, prop2"

View File

@ -0,0 +1,35 @@
--TEST--
$field in different assignments
--FILE--
<?php
class Test {
public $prop {
set {
$field ??= 42;
var_dump($field);
$field += 1;
var_dump($field);
$field -= 2;
var_dump($field);
$field *= 3;
var_dump($field);
$field++;
var_dump($field);
--$field;
var_dump($field);
}
}
}
$test = new Test;
$test->prop = null;
?>
--EXPECT--
int(42)
int(43)
int(41)
int(123)
int(124)
int(123)

View File

@ -0,0 +1,27 @@
--TEST--
$this->prop refers to backing store from either hook
--FILE--
<?php
class Test {
public $prop {
get {
echo "get\n";
$this->prop = 'prop';
}
set {
echo "set\n";
var_dump($this->prop);
}
}
}
$test = new Test;
$test->prop;
$test->prop = 42;
?>
--EXPECT--
get
set
string(4) "prop"

View File

@ -0,0 +1,20 @@
--TEST--
Final hooks
--FILE--
<?php
class A {
public $prop {
final get { return 42; }
}
}
class B extends A {
public $prop {
get { return 24; }
}
}
?>
--EXPECTF--
Fatal error: Cannot override final property hook A::$prop::get() in %s on line %d

View File

@ -0,0 +1,12 @@
--TEST--
Property cannot be both final and private
--FILE--
<?php
class Test {
final private $prop;
}
?>
--EXPECTF--
Fatal error: Property cannot be both final and private in %s on line %d

View File

@ -0,0 +1,16 @@
--TEST--
Property itself may be marked final (hook)
--FILE--
<?php
class A {
public final $prop { get {} set {} }
}
class B extends A {
public $prop { get {} set {} }
}
?>
--EXPECTF--
Fatal error: Cannot override final property A::$prop in %s on line %d

View File

@ -0,0 +1,16 @@
--TEST--
Property itself may be marked final (normal)
--FILE--
<?php
class A {
public final $prop;
}
class B extends A {
public $prop { get {} set {} }
}
?>
--EXPECTF--
Fatal error: Cannot override final property A::$prop in %s on line %d

View File

@ -0,0 +1,15 @@
--TEST--
Cannot make hook explicitly final in final property
--FILE--
<?php
class Test {
final public $prop {
final get => $field;
}
}
?>
===DONE===
--EXPECT--
===DONE===

View File

@ -0,0 +1,28 @@
--TEST--
Usage of property inside hook adds backing store
--FILE--
<?php
class Test {
public $prop1 { get => $this->prop1; }
public $prop2 { get => fn () => $this->prop2; }
public $prop3 { get => function () { return $this->prop3; }; }
public $prop4 { get => $this->prop1; }
public $prop5 { get {} }
public $prop6 { get { var_dump($this->prop6); } }
public $prop7 { get => new class { function test() { $this->prop7; } }; }
}
foreach ((new ReflectionClass(Test::class))->getProperties() as $prop) {
var_dump($prop->isVirtual());
}
?>
--EXPECT--
bool(false)
bool(true)
bool(true)
bool(true)
bool(true)
bool(false)
bool(true)

View File

@ -0,0 +1,136 @@
--TEST--
foreach over hooked properties
--FILE--
<?php
#[AllowDynamicProperties]
class ByRef {
public $plain = 'plain';
private $_virtualByRef = 'virtualByRef';
public $virtualByRef {
&get {
echo __METHOD__, "\n";
return $this->_virtualByRef;
}
set {
echo __METHOD__, "\n";
$this->_virtualByRef = $value;
}
}
public function __construct() {
$this->dynamic = 'dynamic';
}
}
#[AllowDynamicProperties]
class ByVal extends ByRef {
private $_virtualByVal = 'virtualByVal';
public $virtualByVal {
get {
echo __METHOD__, "\n";
return $this->_virtualByVal;
}
set {
echo __METHOD__, "\n";
$this->_virtualByVal = $value;
}
}
public $backed = 'backed' {
get {
echo __METHOD__, "\n";
return $this->backed;
}
set {
echo __METHOD__, "\n";
$this->backed = $value;
}
}
public string $backedUninitialized {
get {
echo __METHOD__, "\n";
$this->backedUninitialized ??= 'backedUninitialized';
return $this->backedUninitialized;
}
set {
echo __METHOD__, "\n";
$this->backedUninitialized = $value;
}
}
}
function testByRef($object) {
foreach ($object as $prop => &$value) {
echo "$prop => $value\n";
$value = strtoupper($value);
}
unset($value);
var_dump($object);
}
function testByVal($object) {
foreach ($object as $prop => $value) {
echo "$prop => $value\n";
$object->{$prop} = strtoupper($value);
}
var_dump($object);
}
testByVal(new ByVal);
testByVal(new ByRef);
testByRef(new ByRef);
?>
--EXPECTF--
ByVal::$virtualByVal::get
virtualByVal => virtualByVal
ByVal::$virtualByVal::set
ByVal::$backed::get
backed => backed
ByVal::$backed::set
ByVal::$backedUninitialized::get
backedUninitialized => backedUninitialized
ByVal::$backedUninitialized::set
plain => plain
ByRef::$virtualByRef::get
virtualByRef => virtualByRef
ByRef::$virtualByRef::set
dynamic => dynamic
object(ByVal)#%d (6) {
["plain"]=>
string(5) "PLAIN"
["_virtualByRef":"ByRef":private]=>
string(12) "VIRTUALBYREF"
["_virtualByVal":"ByVal":private]=>
string(12) "VIRTUALBYVAL"
["backed"]=>
string(6) "BACKED"
["backedUninitialized"]=>
string(19) "BACKEDUNINITIALIZED"
["dynamic"]=>
string(7) "DYNAMIC"
}
plain => plain
ByRef::$virtualByRef::get
virtualByRef => virtualByRef
ByRef::$virtualByRef::set
dynamic => dynamic
object(ByRef)#%d (3) {
["plain"]=>
string(5) "PLAIN"
["_virtualByRef":"ByRef":private]=>
string(12) "VIRTUALBYREF"
["dynamic"]=>
string(7) "DYNAMIC"
}
plain => plain
ByRef::$virtualByRef::get
virtualByRef => virtualByRef
dynamic => dynamic
object(ByRef)#%d (3) {
["plain"]=>
string(5) "PLAIN"
["_virtualByRef":"ByRef":private]=>
string(12) "VIRTUALBYREF"
["dynamic"]=>
string(7) "DYNAMIC"
}

View File

@ -0,0 +1,23 @@
--TEST--
foreach over hooked properties
--FILE--
<?php
class A extends stdClass {
public $foo {
&get => $this->foo;
}
}
$a = new A;
foreach ($a as $k => &$v) {
if ($k == "foo") {
$a->bar = "baz";
}
var_dump($k);
}
?>
--EXPECT--
string(3) "foo"
string(3) "bar"

View File

@ -0,0 +1,34 @@
--TEST--
foreach by-ref on object with by-val hooked property
--FILE--
<?php
class ByVal {
public $byRef {
&get {
$x = 42;
return $x;
}
}
public $byVal = 'byValue' {
get => $this->byVal;
set => $value;
}
}
function test($object) {
foreach ($object as $prop => &$value) {
var_dump($value);
}
}
try {
test(new ByVal);
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
?>
--EXPECT--
int(42)
Cannot create reference to property ByVal::$byVal

View File

@ -0,0 +1,45 @@
--TEST--
Generator hook
--FILE--
<?php
class A {
public $backed = 2 {
get {
yield 1;
yield $this->backed;
yield 3;
}
}
public $virtual {
get {
yield 1;
yield 2;
yield 3;
}
}
}
$a = new A();
var_dump(iterator_to_array($a->backed));
var_dump(iterator_to_array($a->virtual));
?>
--EXPECT--
array(3) {
[0]=>
int(1)
[1]=>
int(2)
[2]=>
int(3)
}
array(3) {
[0]=>
int(1)
[1]=>
int(2)
[2]=>
int(3)
}

View File

@ -0,0 +1,29 @@
--TEST--
Allow calling parent get in generator property hooks
--FILE--
<?php
class A {
public $prop = 42;
}
class B extends A {
public $prop {
get {
yield parent::$prop::get() + 1;
yield parent::$prop::get() + 2;
yield parent::$prop::get() + 3;
}
}
}
$b = new B();
foreach ($b->prop as $value) {
var_dump($value);
}
?>
--EXPECT--
int(43)
int(44)
int(45)

View File

@ -0,0 +1,24 @@
--TEST--
Basic get only property hook
--FILE--
<?php
class Test {
public $prop {
get { return 42; }
}
}
$test = new Test;
var_dump($test->prop);
try {
$test->prop = 0;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
?>
--EXPECT--
int(42)
Property Test::$prop is read-only

View File

@ -0,0 +1,34 @@
--TEST--
Get property hook by ref and indirect modification
--FILE--
<?php
class Test {
public $byVal {
get { return $this->byVal; }
set { $this->byVal = $value; }
}
}
$test = new Test;
try {
$test->byVal = [];
$test->byVal[] = 42;
} catch (\Error $e) {
echo $e->getMessage(), "\n";
}
var_dump($test->byVal);
try {
$test->byVal =& $ref;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
?>
--EXPECT--
Indirect modification of Test::$byVal is not allowed
array(0) {
}
Cannot assign by reference to overloaded object

View File

@ -0,0 +1,30 @@
--TEST--
Get by reference with hooked property
--FILE--
<?php
class Test {
public $byVal { get { return []; } }
}
$test = new Test;
try {
$test->byVal[] = 42;
} catch (\Error $e) {
echo get_class($e) . ': ' . $e->getMessage() . "\n";
}
var_dump($test->byVal);
try {
$test->byVal =& $ref;
} catch (Error $e) {
echo get_class($e) . ': ' . $e->getMessage() . "\n";
}
?>
--EXPECT--
Error: Indirect modification of Test::$byVal is not allowed
array(0) {
}
Error: Cannot assign by reference to overloaded object

View File

@ -0,0 +1,20 @@
--TEST--
Virtual get hook allows returning by reference
--FILE--
<?php
class A {
public $prop;
}
class B extends A {
private $_prop;
public $prop {
&get => $this->_prop;
set { $this->_prop = $value; }
}
}
?>
--EXPECTF--
Fatal error: Get hook of backed property B::prop with set hook may not return by reference in %s on line %d

View File

@ -0,0 +1,28 @@
--TEST--
By-reference get may be implemented as plain
--FILE--
<?php
interface I {
public $prop { &get; }
}
class A implements I {
public $prop;
}
function test(I $i) {
$ref = &$i->prop;
$ref = 42;
}
$a = new A();
test($a);
var_dump($a);
?>
--EXPECT--
object(A)#1 (1) {
["prop"]=>
int(42)
}

View File

@ -0,0 +1,18 @@
--TEST--
By-reference get may be not implemented as by-value
--FILE--
<?php
interface I {
public $prop { &get; }
}
class A implements I {
public $prop {
get => $this->prop;
}
}
?>
--EXPECTF--
Fatal error: Declaration of A::$prop::get() must be compatible with & I::$prop::get() in %s on line %d

View File

@ -0,0 +1,42 @@
--TEST--
Virtual get hook allows returning by reference
--FILE--
<?php
class Test {
private $_prop;
public $prop {
&get => $this->_prop;
set { $this->_prop = $value; }
}
}
function inc(&$ref) {
$ref++;
}
$test = new Test();
$test->prop = 42;
$prop = &$test->prop;
$prop++;
var_dump($test);
var_dump($test->prop);
unset($prop);
inc($test->prop);
var_dump($test);
var_dump($test->prop);
?>
--EXPECT--
object(Test)#1 (1) {
["_prop":"Test":private]=>
&int(43)
}
int(43)
object(Test)#1 (1) {
["_prop":"Test":private]=>
int(44)
}
int(44)

View File

@ -0,0 +1,26 @@
--TEST--
Get property hook must respect property type
--FILE--
<?php
class Test {
public int $prop1 {
get { return "foobar"; }
}
public int $prop2 {
get { return "42"; }
}
}
$test = new Test;
try {
var_dump($test->prop1);
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
var_dump($test->prop2);
?>
--EXPECT--
Test::$prop1::get(): Return value must be of type int, string returned
int(42)

View File

@ -0,0 +1,28 @@
--TEST--
Access hooked property from magic method
--FILE--
<?php
class Test {
private $prop {
get { echo __METHOD__, "\n"; return 42; }
set { echo __METHOD__, "\n"; }
}
public function __get($name) {
return $this->{$name};
}
public function __set($name, $value) {
$this->{$name} = $value;
}
}
$test = new Test;
$test->prop;
$test->prop = 42;
?>
--EXPECT--
Test::$prop::get
Test::$prop::set

View File

@ -0,0 +1,51 @@
--TEST--
Different kinds of indirect modification with by-val and by-ref getters
--FILE--
<?php
class Test {
public $byVal {
set {
echo __METHOD__, "\n";
$this->byVal = $value;
}
}
}
$test = new Test;
$test->byVal = 0;
$test->byVal++;
++$test->byVal;
$test->byVal += 1;
var_dump($test->byVal);
$test->byVal = [];
try {
$test->byVal[] = 1;
} catch (\Error $e) {
echo $e->getMessage(), "\n";
}
var_dump($test->byVal);
try {
$ref =& $test->byVal;
} catch (\Error $e) {
echo $e->getMessage(), "\n";
}
$ref = 42;
var_dump($test->byVal);
?>
--EXPECT--
Test::$byVal::set
Test::$byVal::set
Test::$byVal::set
Test::$byVal::set
int(3)
Test::$byVal::set
Indirect modification of Test::$byVal is not allowed
array(0) {
}
Indirect modification of Test::$byVal is not allowed
array(0) {
}

View File

@ -0,0 +1,32 @@
--TEST--
Basic property hook inheritance
--FILE--
<?php
class A {
public $prop {
get { return "A"; }
set { echo __METHOD__, "\n"; }
}
}
class B extends A {
public $prop {
get { return "B"; }
}
}
$a = new A;
var_dump($a->prop);
$a->prop = 1;
$b = new B;
var_dump($b->prop);
$b->prop = 1;
?>
--EXPECT--
string(1) "A"
A::$prop::set
string(1) "B"
A::$prop::set

View File

@ -0,0 +1,15 @@
--TEST--
Property hooks in interfaces
--FILE--
<?php
interface I {
public $prop { get; set; }
}
class C implements I {
}
?>
--EXPECTF--
Fatal error: Class C contains 2 abstract methods and must therefore be declared abstract or implement the remaining methods (I::$prop::get, I::$prop::set) in %s on line %d

View File

@ -0,0 +1,12 @@
--TEST--
Cannot have explicitly abstract property in interface
--FILE--
<?php
interface I {
abstract public $prop { get; set; }
}
?>
--EXPECTF--
Fatal error: Property in interface cannot be explicitly abstract. All interface members are implicitly abstract in %s on line %d

View File

@ -0,0 +1,10 @@
--TEST--
Cannot declare final hook in interface
--FILE--
<?php
interface I {
public $prop { final get; }
}
?>
--EXPECTF--
Fatal error: Property hook cannot be both abstract and final in %s on line %d

View File

@ -0,0 +1,10 @@
--TEST--
Cannot declare final property in interface
--FILE--
<?php
interface I {
final public $prop { get; set; }
}
?>
--EXPECTF--
Fatal error: Property in interface cannot be final in %s on line %d

View File

@ -0,0 +1,24 @@
--TEST--
Backed prop satisfies interface get hook by-reference
--FILE--
<?php
interface I {
public $prop { get; }
}
class A implements I {
public $prop = 42 {
get => $this->prop;
}
}
$a = new A();
var_dump($a);
?>
--EXPECT--
object(A)#1 (1) {
["prop"]=>
int(42)
}

View File

@ -0,0 +1,24 @@
--TEST--
Plain prop satisfies interface get hook by-reference
--FILE--
<?php
interface I {
public $prop { get; }
}
class A implements I {
public $prop = 42 {
get => $this->prop;
}
}
$a = new A();
var_dump($a);
?>
--EXPECT--
object(A)#1 (1) {
["prop"]=>
int(42)
}

View File

@ -0,0 +1,31 @@
--TEST--
Virtual prop satisfies interface get hook by-reference
--FILE--
<?php
interface I {
public $prop { &get; }
}
class A implements I {
private $_prop;
public $prop {
&get => $this->_prop;
}
}
function test(I $i) {
$ref = &$i->prop;
$ref = 42;
}
$a = new A();
test($a);
var_dump($a);
?>
--EXPECT--
object(A)#1 (1) {
["_prop":"A":private]=>
int(42)
}

View File

@ -0,0 +1,15 @@
--TEST--
Interface may contain only set with no implementation
--FILE--
<?php
interface I {
public $prop { set; }
}
class A implements I {
public $prop;
}
?>
--EXPECTF--

View File

@ -0,0 +1,15 @@
--TEST--
readonly property satisfies get only interface property
--FILE--
<?php
interface I {
public int $prop { get; }
}
class C implements I {
public function __construct(public readonly int $prop) {}
}
$c = new C(42);
var_dump($c->prop);
?>
--EXPECT--
int(42)

View File

@ -0,0 +1,13 @@
--TEST--
readonly property does not satisfy get/set interface property
--FILE--
<?php
interface I {
public int $prop { get; set; }
}
class C implements I {
public function __construct(public readonly int $prop) {}
}
?>
--EXPECTF--
Fatal error: Readonly property C::$prop does not satisfy abstract read-write property I::$prop in %s on line %d

View File

@ -0,0 +1,31 @@
--TEST--
By-value get may be implemented as by-reference
--FILE--
<?php
interface I {
public $prop { get; }
}
class A implements I {
private $_prop;
public $prop {
&get => $this->_prop;
}
}
function test(I $i) {
$ref = &$i->prop;
$ref = 42;
}
$a = new A();
test($a);
var_dump($a);
?>
--EXPECT--
object(A)#1 (1) {
["_prop":"A":private]=>
int(42)
}

View File

@ -0,0 +1,12 @@
--TEST--
Property hooks in interfaces cannot be explicitly abstract
--FILE--
<?php
interface I {
public abstract $prop { get; }
}
?>
--EXPECTF--
Fatal error: Property in interface cannot be explicitly abstract. All interface members are implicitly abstract in %s on line %d

View File

@ -0,0 +1,15 @@
--TEST--
Property hook in interfaces not implemented
--FILE--
<?php
interface I {
public $prop { get; set; }
}
class C implements I {
}
?>
--EXPECTF--
Fatal error: Class C contains 2 abstract methods and must therefore be declared abstract or implement the remaining methods (I::$prop::get, I::$prop::set) in %s on line %d

View File

@ -0,0 +1,12 @@
--TEST--
Cannot use non-public property hook in interface (whole property)
--FILE--
<?php
interface I {
protected $prop { get; set; }
}
?>
--EXPECTF--
Fatal error: Property in interface cannot be protected or private in %s on line %d

View File

@ -0,0 +1,15 @@
--TEST--
Interface may contain only get with no implementation
--FILE--
<?php
interface I {
public $prop { get; }
}
class A implements I {
public $prop { get {} }
}
?>
--EXPECTF--

View File

@ -0,0 +1,41 @@
--TEST--
Implementing abstract property hooks
--FILE--
<?php
abstract class A {
public abstract $prop1 {
get;
set { echo __METHOD__, "\n"; }
}
public abstract $prop2 {
get { echo __METHOD__, "\n"; }
set;
}
public abstract $prop3 { get; set; }
}
class B extends A {
public $prop1 {
get { echo __METHOD__, "\n"; }
}
public $prop2 {
set { echo __METHOD__, "\n"; }
}
public $prop3;
}
$b = new B;
$b->prop1;
$b->prop1 = 1;
$b->prop2;
$b->prop2 = 1;
$b->prop3;
$b->prop3 = 1;
?>
--EXPECT--
B::$prop1::get
A::$prop1::set
A::$prop2::get
B::$prop2::set

View File

@ -0,0 +1,14 @@
--TEST--
Abstract property hook cannot have body
--FILE--
<?php
class Test {
public abstract $prop {
get {}
}
}
?>
--EXPECTF--
Fatal error: Abstract property Test::$prop must specify at least one abstract hook in %s on line %d

View File

@ -0,0 +1,12 @@
--TEST--
Property hook cannot be both abstract and final
--FILE--
<?php
class Test {
public abstract $prop { final get; }
}
?>
--EXPECTF--
Fatal error: Property hook cannot be both abstract and final in %s on line %d

View File

@ -0,0 +1,16 @@
--TEST--
Class with abstract property hook not declared abstract (inherited 1)
--FILE--
<?php
abstract class A {
public abstract $prop { get; }
}
class B extends A {
public $prop { set {} }
}
?>
--EXPECTF--
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

View File

@ -0,0 +1,15 @@
--TEST--
Class with abstract property hook not declared abstract (inherited 2)
--FILE--
<?php
abstract class A {
public abstract $prop { get; }
}
class B extends A {
}
?>
--EXPECTF--
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

View File

@ -0,0 +1,12 @@
--TEST--
Property hook cannot be both abstract and private
--FILE--
<?php
class Test {
private abstract $prop { get; }
}
?>
--EXPECTF--
Fatal error: Property hook cannot be both abstract and private in %s on line %d

View File

@ -0,0 +1,12 @@
--TEST--
Property hook list cannot be empty
--FILE--
<?php
class Test {
public $prop {}
}
?>
--EXPECTF--
Fatal error: Property hook list cannot be empty in %s on line %d

View File

@ -0,0 +1,12 @@
--TEST--
Property hook cannot be both final and private
--FILE--
<?php
class Test {
private $prop { final get; }
}
?>
--EXPECTF--
Fatal error: Property hook cannot be both final and private in %s on line %d

View File

@ -0,0 +1,14 @@
--TEST--
Property hooks cannot have explicity visibility
--FILE--
<?php
class Test {
private $prop {
public get;
}
}
?>
--EXPECTF--
Fatal error: Cannot use the public modifier on a property hook in %s on line %d

View File

@ -0,0 +1,14 @@
--TEST--
Property hook cannot be static
--FILE--
<?php
class Test {
public $prop {
static get {}
}
}
?>
--EXPECTF--
Fatal error: Cannot use the static modifier on a property hook in %s on line %d

View File

@ -0,0 +1,12 @@
--TEST--
Cannot use hooks for static property (for now)
--FILE--
<?php
class Test {
public static $prop { get; set; }
}
?>
--EXPECTF--
Fatal error: Cannot declare hooks for static property in %s on line %d

View File

@ -0,0 +1,37 @@
--TEST--
isset() and empty() call get property hook
--FILE--
<?php
class Test {
public $prop1 {
get { return $this->prop1; }
set { $this->prop1 = $value; }
}
}
$test = new Test;
$test->prop1 = true;
var_dump(isset($test->prop1));
var_dump(!empty($test->prop1));
echo "\n",
$test->prop1 = false;
var_dump(isset($test->prop1));
var_dump(!empty($test->prop1));
echo "\n",
$test->prop1 = null;
var_dump(isset($test->prop1));
var_dump(!empty($test->prop1));
echo "\n";
?>
--EXPECT--
bool(true)
bool(true)
bool(true)
bool(false)
bool(false)
bool(false)

View File

@ -0,0 +1,24 @@
--TEST--
Magic constants in property hooks
--FILE--
<?php
class Test {
public $prop {
get {
var_dump(__FUNCTION__);
var_dump(__METHOD__);
var_dump(__CLASS__);
return null;
}
}
}
$test = new Test;
$test->prop;
?>
--EXPECT--
string(10) "$prop::get"
string(16) "Test::$prop::get"
string(4) "Test"

View File

@ -0,0 +1,62 @@
--TEST--
Interaction of inaccessible property hooks with magic methods
--FILE--
<?php
class A {
public $prop {
get {
echo __METHOD__, "\n";
return 'prop';
}
set { echo __METHOD__, "\n"; }
}
}
class B extends A {
public function __get($name) {
echo __METHOD__, "($name)\n";
try {
$this->$name;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
}
public function __set($name, $value) {
echo __METHOD__, "($name, $value)\n";
try {
$this->$name = $value;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
}
public function __isset($name) {
echo __METHOD__, "($name)\n";
try {
var_dump(isset($this->$name));
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
}
public function __unset($name) {
echo "Never reached\n";
}
}
$b = new B;
$b->prop;
var_dump(isset($b->prop));
$b->prop = 1;
try {
unset($b->prop);
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
?>
--EXPECT--
A::$prop::get
A::$prop::get
bool(true)
A::$prop::set
Cannot unset hooked property B::$prop

View File

@ -0,0 +1,30 @@
--TEST--
Accessing property from hook does not call magic method
--FILE--
<?php
class Test {
public $prop {
get => $this->prop;
set => $value;
}
public function __get($name) {
echo __METHOD__, "\n";
return 42;
}
public function __set($name, $value) {
echo __METHOD__, "\n";
}
}
$test = new Test;
var_dump($test->prop);
$test->prop = 42;
var_dump($test->prop);
?>
--EXPECT--
NULL
int(42)

Some files were not shown because too many files have changed in this diff Show More