Skip to content

[RFC] Allow hooks for backed readonly properties #18757

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ PHP NEWS
evaluation) and GH-18464 (Recursion protection for deprecation constants not
released on bailout). (DanielEScherzer and ilutov)
. Fixed AST printing for immediately invoked Closure. (Dmitrii Derepko)
. Property hooks are now allowed on backed readonly properties. (Crell, NickSdot and iluuu1994)

- Curl:
. Added curl_multi_get_handles(). (timwolla)
Expand Down
2 changes: 2 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ PHP 8.5 UPGRADE NOTES
RFC: https://wiki.php.net/rfc/attributes-on-constants
. The #[\Deprecated] attribute can now be used on constants.
RFC: https://wiki.php.net/rfc/attributes-on-constants
. Property hooks are now allowed on backed readonly properties.
RFC: https://wiki.php.net/rfc/readonly_hooks

- Curl:
. Added support for share handles that are persisted across multiple PHP
Expand Down
12 changes: 0 additions & 12 deletions Zend/tests/property_hooks/gh15419_1.phpt

This file was deleted.

14 changes: 0 additions & 14 deletions Zend/tests/property_hooks/gh15419_2.phpt

This file was deleted.

12 changes: 0 additions & 12 deletions Zend/tests/property_hooks/readonly.phpt

This file was deleted.

41 changes: 41 additions & 0 deletions Zend/tests/property_hooks/readonly_class_property_backed.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
--TEST--
Backed property in readonly class may have hooks
--FILE--
<?php

// readonly class
readonly class Test {
public int $prop {
get => $this->prop;
set => $value;
}

public function __construct(int $v) {
$this->prop = $v;
}

public function set($v)
{
$this->prop = $v;
}
}

$t = new Test(42);
var_dump($t->prop);
try {
$t->set(43);
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
try {
$t->prop = 43;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
var_dump($t->prop);
?>
--EXPECT--
int(42)
Cannot modify readonly property Test::$prop
Cannot modify protected(set) readonly property Test::$prop from global scope
int(42)
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
--TEST--
Non-readonly class cannot extend readonly class
--FILE--
<?php

readonly class ParentClass {
public int $prop;
}

class Test extends ParentClass {
public function __construct(
public int $prop {
get => $this->prop;
set => $value;
}
) {}
}

?>
--EXPECTF--
Fatal error: Non-readonly class Test cannot extend readonly class ParentClass in %s on line %d
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
--TEST--
Readonly class cannot extend non-readonly class
--FILE--
<?php

class ParentClass {
public int $prop;
}

readonly class Test extends ParentClass {
public function __construct(
public int $prop {
get => $this->prop;
set => $value;
}
) {}
}

?>
--EXPECTF--
Fatal error: Readonly class Test cannot extend non-readonly class ParentClass in %s on line %d
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
--TEST--
Backed promoted property in readonly class may have hooks
--FILE--
<?php

// readonly class, promoted
readonly class Test {
public function __construct(
public int $prop {
get => $this->prop;
set => $value;
}
) {}

public function set($v)
{
$this->prop = $v;
}
}

$t = new Test(42);
var_dump($t->prop);
try {
$t->set(43);
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
try {
$t->prop = 43;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
var_dump($t->prop);
?>
--EXPECT--
int(42)
Cannot modify readonly property Test::$prop
Cannot modify protected(set) readonly property Test::$prop from global scope
int(42)

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
--TEST--
Virtual promoted property in readonly class cannot have hooks
--FILE--
<?php

readonly class Test {
public function __construct(
public int $prop {
get => 42;
}
) {}
}

?>
--EXPECTF--
Fatal error: Hooked virtual properties cannot be readonly in %s on line %d
105 changes: 105 additions & 0 deletions Zend/tests/property_hooks/readonly_lazy.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
--TEST--
Readonly classes can be constructed via reflection by ORM
--FILE--
<?php

interface DbConnection {
public function loadCategory(string $id): Category;
}

class Category {
public function __construct(public string $name) {}
}

class MockDbConnection implements DbConnection {
public function loadCategory(string $id): Category {
echo "hit database\n";
return new Category("Category {$id}");
}
}

readonly class Product
{
public function __construct(
public string $name,
public float $price,
public Category $category,
) {}
}

readonly class LazyProduct extends Product
{
private DbConnection $dbApi;

private string $categoryId;

public Category $category {
get {
return $this->category ??= $this->dbApi->loadCategory($this->categoryId);
}
}
}

$reflect = new ReflectionClass(LazyProduct::class);
$product = $reflect->newInstanceWithoutConstructor();

$nameProperty = $reflect->getProperty('name');
$nameProperty->setAccessible(true);
$nameProperty->setValue($product, 'Iced Chocolate');

$priceProperty = $reflect->getProperty('price');
$priceProperty->setAccessible(true);
$priceProperty->setValue($product, 1.99);

$db = $reflect->getProperty('dbApi');
$db->setAccessible(true);
$db->setValue($product, new MockDbConnection());

$categoryId = $reflect->getProperty('categoryId');
$categoryId->setAccessible(true);
$categoryId->setValue($product, '42');

// lazy loading, hit db
$category1 = $product->category;
echo $category1->name . "\n";

// cached category returned
$category2 = $product->category;
echo $category2->name . "\n";

// same category instance returned
var_dump($category1 === $category2);

// can't be wrong, huh?
var_dump($product);

// cannot set twice
try {
$categoryId->setValue($product, '420');
} catch (Error $e) {
echo $e->getMessage(), "\n";
}

?>
--EXPECT--
hit database
Category 42
Category 42
bool(true)
object(LazyProduct)#2 (5) {
["name"]=>
string(14) "Iced Chocolate"
["price"]=>
float(1.99)
["category"]=>
object(Category)#8 (1) {
["name"]=>
string(11) "Category 42"
}
["dbApi":"LazyProduct":private]=>
object(MockDbConnection)#6 (0) {
}
["categoryId":"LazyProduct":private]=>
string(2) "42"
}
Cannot modify readonly property LazyProduct::$categoryId
Loading