From 59bd601f167dc508369dff5dec75602ea9fd156d Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sun, 22 Feb 2026 10:47:53 +0700 Subject: [PATCH 1/8] Fix properties with hooks --- src/ActiveRecord.php | 3 +-- src/Internal/ArArrayHelper.php | 21 ++++++++++++++++++++- src/Trait/PrivatePropertiesTrait.php | 6 ------ tests/ActiveRecordTest.php | 18 ++++++++++++++++++ tests/Stubs/ActiveRecord/Item.php | 6 ++++++ 5 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php index bf708298a..57849fd04 100644 --- a/src/ActiveRecord.php +++ b/src/ActiveRecord.php @@ -16,7 +16,6 @@ use function array_filter; use function array_keys; use function array_merge; -use function get_object_vars; use function is_array; use const ARRAY_FILTER_USE_KEY; @@ -128,7 +127,7 @@ public function primaryKey(): array protected function propertyValuesInternal(): array { - return get_object_vars($this); + return ArArrayHelper::propertyValues($this); } protected function insertInternal(?array $properties = null): void diff --git a/src/Internal/ArArrayHelper.php b/src/Internal/ArArrayHelper.php index f045a5939..de05244cc 100644 --- a/src/Internal/ArArrayHelper.php +++ b/src/Internal/ArArrayHelper.php @@ -189,7 +189,7 @@ public static function toArray(array|object $object): array } if ($object instanceof ActiveRecordInterface) { - return $object->propertyValues(); + return self::propertyValues($object); } if ($object instanceof Traversable) { @@ -201,4 +201,23 @@ public static function toArray(array|object $object): array return get_object_vars($object); } + + public static function propertyValues(ActiveRecordInterface $model): array + { + $data = (array) $model; + unset( + $data["\0Yiisoft\ActiveRecord\AbstractActiveRecord\0oldValues"], + $data["\0Yiisoft\ActiveRecord\AbstractActiveRecord\0related"], + $data["\0Yiisoft\ActiveRecord\AbstractActiveRecord\0relationsDependencies"], + ); + + $keys = array_map(self::clearPropertyName(...), array_keys($data)); + + return array_combine($keys, $data); + } + + private static function clearPropertyName(string $propertyName): string + { + return substr($propertyName, (strrpos($propertyName, "\0") ?: -1) + 1); + } } diff --git a/src/Trait/PrivatePropertiesTrait.php b/src/Trait/PrivatePropertiesTrait.php index 65756f27f..bd7a49404 100644 --- a/src/Trait/PrivatePropertiesTrait.php +++ b/src/Trait/PrivatePropertiesTrait.php @@ -15,16 +15,10 @@ * * @link https://github.com/yiisoft/active-record/blob/master/docs/create-model.md#private-properties * - * @see AbstractActiveRecord::propertyValuesInternal() * @see AbstractActiveRecord::populateProperty() */ trait PrivatePropertiesTrait { - protected function propertyValuesInternal(): array - { - return get_object_vars($this); - } - protected function populateProperty(string $name, mixed $value): void { $this->$name = $value; diff --git a/tests/ActiveRecordTest.php b/tests/ActiveRecordTest.php index 41d056a09..c88550585 100644 --- a/tests/ActiveRecordTest.php +++ b/tests/ActiveRecordTest.php @@ -18,6 +18,7 @@ use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Article; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\ArticleComment; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Cat; +use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Category; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\CategoryAfterDelete; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Customer; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\CustomerWithAlias; @@ -1892,5 +1893,22 @@ public function testSoftDeleteWithCustomDate(): void $this->assertSame($deletedAt->getTimestamp(), $softDeletedOrder->get('deleted_at')); } + public function testRelationDefinedViaPropertyHook(): void + { + $item = Item::query()->findByPk(1); + $itemCategory = $item->category; + + $this->assertInstanceOf(Category::class, $itemCategory); + $this->assertSame(1, $itemCategory->getId()); + $this->assertSame('Books', $itemCategory->getName()); + + $item->category = Category::query()->findByPk(2); + $itemCategory = $item->category; + + $this->assertInstanceOf(Category::class, $itemCategory); + $this->assertSame(2, $itemCategory->getId()); + $this->assertSame('Movies', $itemCategory->getName()); + } + abstract protected function createFactory(): Factory; } diff --git a/tests/Stubs/ActiveRecord/Item.php b/tests/Stubs/ActiveRecord/Item.php index 129b2eeed..fd0bf2dc3 100644 --- a/tests/Stubs/ActiveRecord/Item.php +++ b/tests/Stubs/ActiveRecord/Item.php @@ -10,6 +10,12 @@ class Item extends ActiveRecord { + public Category $category { + get => $this->relation('category'); + set { + $this->populateRelation('category', $value); + } + } protected int $id; protected string $name; protected int $category_id; From b9e0ade9354e5d2fe9e33fe00b76189534596f1a Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sat, 2 May 2026 11:37:34 +0700 Subject: [PATCH 2/8] Update --- src/Internal/ArArrayHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Internal/ArArrayHelper.php b/src/Internal/ArArrayHelper.php index de05244cc..b4bb2e44f 100644 --- a/src/Internal/ArArrayHelper.php +++ b/src/Internal/ArArrayHelper.php @@ -189,7 +189,7 @@ public static function toArray(array|object $object): array } if ($object instanceof ActiveRecordInterface) { - return self::propertyValues($object); + return $object->propertyValues(); } if ($object instanceof Traversable) { From 452b6c9bf46ed594e3ca4b1827292af29861b8ba Mon Sep 17 00:00:00 2001 From: Tigrov <8563175+Tigrov@users.noreply.github.com> Date: Sat, 2 May 2026 04:40:02 +0000 Subject: [PATCH 3/8] Apply PHP CS Fixer and Rector changes (CI) --- src/Trait/PrivatePropertiesTrait.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Trait/PrivatePropertiesTrait.php b/src/Trait/PrivatePropertiesTrait.php index bd7a49404..fb60d3d9a 100644 --- a/src/Trait/PrivatePropertiesTrait.php +++ b/src/Trait/PrivatePropertiesTrait.php @@ -6,8 +6,6 @@ use Yiisoft\ActiveRecord\AbstractActiveRecord; -use function get_object_vars; - /** * Trait to handle private properties in Active Record classes. * From 2a161e6bbce7526a8451758a9b9a5f637b62715b Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sat, 2 May 2026 11:47:59 +0700 Subject: [PATCH 4/8] Skip test for PHP < 8.4 --- tests/ActiveRecordTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/ActiveRecordTest.php b/tests/ActiveRecordTest.php index c88550585..810b4c230 100644 --- a/tests/ActiveRecordTest.php +++ b/tests/ActiveRecordTest.php @@ -55,6 +55,8 @@ use function in_array; use function count; +use const PHP_VERSION_ID; + abstract class ActiveRecordTest extends TestCase { public function testStoreNull(): void @@ -1895,6 +1897,10 @@ public function testSoftDeleteWithCustomDate(): void public function testRelationDefinedViaPropertyHook(): void { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Property hooks are not supported in PHP < 8.4'); + } + $item = Item::query()->findByPk(1); $itemCategory = $item->category; From d94552f45399b7fcd521521745ec035d515e9292 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sat, 2 May 2026 11:53:17 +0700 Subject: [PATCH 5/8] Fix test --- tests/ActiveRecordTest.php | 3 ++- tests/Stubs/ActiveRecord/Item.php | 6 ------ tests/Stubs/ActiveRecord/ItemWithProperyHooks.php | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 tests/Stubs/ActiveRecord/ItemWithProperyHooks.php diff --git a/tests/ActiveRecordTest.php b/tests/ActiveRecordTest.php index 810b4c230..72ce2598b 100644 --- a/tests/ActiveRecordTest.php +++ b/tests/ActiveRecordTest.php @@ -28,6 +28,7 @@ use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\DefaultValueOnInsertAr; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Dog; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Item; +use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\ItemWithProperyHooks; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\NoExist; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\NullValues; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Order; @@ -1901,7 +1902,7 @@ public function testRelationDefinedViaPropertyHook(): void $this->markTestSkipped('Property hooks are not supported in PHP < 8.4'); } - $item = Item::query()->findByPk(1); + $item = ItemWithProperyHooks::query()->findByPk(1); $itemCategory = $item->category; $this->assertInstanceOf(Category::class, $itemCategory); diff --git a/tests/Stubs/ActiveRecord/Item.php b/tests/Stubs/ActiveRecord/Item.php index fd0bf2dc3..129b2eeed 100644 --- a/tests/Stubs/ActiveRecord/Item.php +++ b/tests/Stubs/ActiveRecord/Item.php @@ -10,12 +10,6 @@ class Item extends ActiveRecord { - public Category $category { - get => $this->relation('category'); - set { - $this->populateRelation('category', $value); - } - } protected int $id; protected string $name; protected int $category_id; diff --git a/tests/Stubs/ActiveRecord/ItemWithProperyHooks.php b/tests/Stubs/ActiveRecord/ItemWithProperyHooks.php new file mode 100644 index 000000000..500566a8a --- /dev/null +++ b/tests/Stubs/ActiveRecord/ItemWithProperyHooks.php @@ -0,0 +1,15 @@ + $this->relation('category'); + set { + $this->populateRelation('category', $value); + } + } +} From 8b8fab8671337a7088518cea3663a8373cb08d70 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sat, 2 May 2026 12:03:25 +0700 Subject: [PATCH 6/8] Add doc, fix psalm --- CHANGELOG.md | 1 + src/Internal/ArArrayHelper.php | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0159ad092..41dce3053 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Bug #558: Fix `SoftDelete` with initiated custom date (@Tigrov) - Enh #564: Clarify `$relations` parameter type in `JoinWith::__construct()` from `array` to `array` (@vjik) +- Bug #567: Fix properties with hooks (@Tigrov) ## 1.0.2 March 11, 2026 diff --git a/src/Internal/ArArrayHelper.php b/src/Internal/ArArrayHelper.php index b4bb2e44f..849557e9b 100644 --- a/src/Internal/ArArrayHelper.php +++ b/src/Internal/ArArrayHelper.php @@ -202,8 +202,16 @@ public static function toArray(array|object $object): array return get_object_vars($object); } + /** + * Returns the available property values of an Active Record object. + * + * @param ActiveRecordInterface $model The ActiveRecord model instance. + * + * @psalm-return array + */ public static function propertyValues(ActiveRecordInterface $model): array { + /** @psalm-var array */ $data = (array) $model; unset( $data["\0Yiisoft\ActiveRecord\AbstractActiveRecord\0oldValues"], From b849acebd0658ffe430f17ebbc3d032978e51dd7 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sat, 2 May 2026 16:33:58 +0700 Subject: [PATCH 7/8] Fix --- tests/ActiveRecordTest.php | 4 ++-- .../{ItemWithProperyHooks.php => ItemWithPropertyHooks.php} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename tests/Stubs/ActiveRecord/{ItemWithProperyHooks.php => ItemWithPropertyHooks.php} (86%) diff --git a/tests/ActiveRecordTest.php b/tests/ActiveRecordTest.php index 72ce2598b..af82ffc54 100644 --- a/tests/ActiveRecordTest.php +++ b/tests/ActiveRecordTest.php @@ -28,7 +28,7 @@ use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\DefaultValueOnInsertAr; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Dog; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Item; -use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\ItemWithProperyHooks; +use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\ItemWithPropertyHooks; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\NoExist; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\NullValues; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Order; @@ -1902,7 +1902,7 @@ public function testRelationDefinedViaPropertyHook(): void $this->markTestSkipped('Property hooks are not supported in PHP < 8.4'); } - $item = ItemWithProperyHooks::query()->findByPk(1); + $item = ItemWithPropertyHooks::query()->findByPk(1); $itemCategory = $item->category; $this->assertInstanceOf(Category::class, $itemCategory); diff --git a/tests/Stubs/ActiveRecord/ItemWithProperyHooks.php b/tests/Stubs/ActiveRecord/ItemWithPropertyHooks.php similarity index 86% rename from tests/Stubs/ActiveRecord/ItemWithProperyHooks.php rename to tests/Stubs/ActiveRecord/ItemWithPropertyHooks.php index 500566a8a..e8bf0f09f 100644 --- a/tests/Stubs/ActiveRecord/ItemWithProperyHooks.php +++ b/tests/Stubs/ActiveRecord/ItemWithPropertyHooks.php @@ -4,7 +4,7 @@ namespace Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord; -class ItemWithProperyHooks extends Item +class ItemWithPropertyHooks extends Item { public Category $category { get => $this->relation('category'); From 12b6631a3dafe5d8a316810051d2d999cfda543d Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sun, 3 May 2026 15:37:39 +0700 Subject: [PATCH 8/8] Update --- src/Internal/ArArrayHelper.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Internal/ArArrayHelper.php b/src/Internal/ArArrayHelper.php index 849557e9b..3d31e9f91 100644 --- a/src/Internal/ArArrayHelper.php +++ b/src/Internal/ArArrayHelper.php @@ -208,10 +208,12 @@ public static function toArray(array|object $object): array * @param ActiveRecordInterface $model The ActiveRecord model instance. * * @psalm-return array + * + * @see https://www.php.net/manual/en/language.types.array.php#language.types.array.casting */ public static function propertyValues(ActiveRecordInterface $model): array { - /** @psalm-var array */ + /** @psalm-var array $data */ $data = (array) $model; unset( $data["\0Yiisoft\ActiveRecord\AbstractActiveRecord\0oldValues"], @@ -226,6 +228,12 @@ public static function propertyValues(ActiveRecordInterface $model): array private static function clearPropertyName(string $propertyName): string { - return substr($propertyName, (strrpos($propertyName, "\0") ?: -1) + 1); + $pos = strrpos($propertyName, "\0"); + + if ($pos === false) { + return $propertyName; + } + + return substr($propertyName, $pos + 1); } }