diff --git a/CHANGELOG.md b/CHANGELOG.md index 257d81e33..ea1b1846b 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) - Bug #562: Fix `ActiveRecordInterface::upsert()` to prioritize passed associative values during updates (@Tigrov) - Bug #561: Fix `ActiveRecordInterface::upsert()` with `$updateProperties = false` (@Tigrov) - Bug #550: Relation query should be created by related class, not primary model class (@batyrmastyr) diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php index 1e073b08f..410d1a5c8 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..3d31e9f91 100644 --- a/src/Internal/ArArrayHelper.php +++ b/src/Internal/ArArrayHelper.php @@ -201,4 +201,39 @@ 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 + * + * @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 $data */ + $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 + { + $pos = strrpos($propertyName, "\0"); + + if ($pos === false) { + return $propertyName; + } + + return substr($propertyName, $pos + 1); + } } diff --git a/src/Trait/PrivatePropertiesTrait.php b/src/Trait/PrivatePropertiesTrait.php index 65756f27f..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. * @@ -15,16 +13,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 7b4570dbb..b53f3844a 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\CustomerQuery; @@ -28,6 +29,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\ItemWithPropertyHooks; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\NoExist; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\NullValues; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Order; @@ -56,6 +58,8 @@ use function in_array; use function count; +use const PHP_VERSION_ID; + abstract class ActiveRecordTest extends TestCase { public function testStoreNull(): void @@ -1945,5 +1949,26 @@ public function testCreateQueryWithModelInstance(): void $this->assertSame(Customer::class, $query->getModel()::class); } + public function testRelationDefinedViaPropertyHook(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Property hooks are not supported in PHP < 8.4'); + } + + $item = ItemWithPropertyHooks::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/ItemWithPropertyHooks.php b/tests/Stubs/ActiveRecord/ItemWithPropertyHooks.php new file mode 100644 index 000000000..e8bf0f09f --- /dev/null +++ b/tests/Stubs/ActiveRecord/ItemWithPropertyHooks.php @@ -0,0 +1,15 @@ + $this->relation('category'); + set { + $this->populateRelation('category', $value); + } + } +}