From 785fc476bf9fa9c55af6dbff1a8e70af84c98358 Mon Sep 17 00:00:00 2001 From: brendt Date: Thu, 16 Jul 2026 09:34:39 +0200 Subject: [PATCH 1/3] wip --- .../src/Mappers/ArrayToObjectMapper.php | 23 ++- packages/reflection/src/PropertyReflector.php | 28 ++++ .../Database/HookedPropertyTest.php | 153 ++++++++++++++++++ 3 files changed, 199 insertions(+), 5 deletions(-) create mode 100644 tests/Integration/Database/HookedPropertyTest.php diff --git a/packages/mapper/src/Mappers/ArrayToObjectMapper.php b/packages/mapper/src/Mappers/ArrayToObjectMapper.php index 4c4dc54b7..278a8f04f 100644 --- a/packages/mapper/src/Mappers/ArrayToObjectMapper.php +++ b/packages/mapper/src/Mappers/ArrayToObjectMapper.php @@ -49,6 +49,7 @@ public function map(mixed $from, mixed $to): object $isStrictClass = $targetClass->hasAttribute(Strict::class); $missingValues = []; + /** @var PropertyReflector[] $unsetProperties */ $unsetProperties = []; foreach ($targetClass->getPublicProperties() as $property) { @@ -58,7 +59,11 @@ public function map(mixed $from, mixed $to): object $propertyName = $this->resolvePropertyName($property, $from); - if (! array_key_exists($propertyName, $from)) { + $isMissing = ! array_key_exists($propertyName, $from); + + if ($isMissing && ($getHook = $property->getGetHook())) { + $from[$propertyName] = $getHook->invokeArgs($targetObject); + } elseif ($isMissing) { $this->handleMissingProperty( property: $property, propertyName: $propertyName, @@ -70,10 +75,14 @@ public function map(mixed $from, mixed $to): object continue; } - $property->setValue( - object: $targetObject, - value: $this->resolveValue($property, $from[$propertyName]), - ); + if (($setHook = $property->getSetHook()) && $setHook->getParameter(0)?->getType()->accepts($from[$propertyName])) { + $setHook->invokeArgs($targetObject, [$from[$propertyName]]); + } else { + $property->setValue( + object: $targetObject, + value: $this->resolveValue($property, $from[$propertyName]), + ); + } } if ($missingValues !== []) { @@ -91,6 +100,10 @@ public function map(mixed $from, mixed $to): object continue; } + if ($property->isHooked()) { + continue; + } + $property->unset($targetObject); } diff --git a/packages/reflection/src/PropertyReflector.php b/packages/reflection/src/PropertyReflector.php index eafb3dae4..3bc462969 100644 --- a/packages/reflection/src/PropertyReflector.php +++ b/packages/reflection/src/PropertyReflector.php @@ -5,6 +5,7 @@ namespace Tempest\Reflection; use Error; +use PropertyHookType; use ReflectionProperty as PHPReflectionProperty; use Stringable; @@ -120,6 +121,33 @@ public function isVirtual(): bool return $this->reflectionProperty->isVirtual(); } + public function isHooked(): bool + { + return $this->getGetHook() || $this->getSetHook(); + } + + public function getGetHook(): ?MethodReflector + { + $hook = $this->reflectionProperty->getHook(PropertyHookType::Get); + + if (! $hook) { + return null; + } + + return new MethodReflector($hook); + } + + public function getSetHook(): ?MethodReflector + { + $hook = $this->reflectionProperty->getHook(PropertyHookType::Set); + + if (! $hook) { + return null; + } + + return new MethodReflector($hook); + } + public function unset(object $object): void { unset($object->{$this->getName()}); diff --git a/tests/Integration/Database/HookedPropertyTest.php b/tests/Integration/Database/HookedPropertyTest.php new file mode 100644 index 000000000..555ce5727 --- /dev/null +++ b/tests/Integration/Database/HookedPropertyTest.php @@ -0,0 +1,153 @@ +database->migrate(CreateMigrationsTable::class, HookedModel::class); + + $model = HookedModel::create(); + + $this->assertSame( + 'default', + $model->hooked, + ); + + $this->database->assertTableHasRow( + 'hooked_model', + hooked: 'default', + ); + } + + #[Test] + public function test_hooked_property_can_be_overwritten(): void + { + $this->database->migrate(CreateMigrationsTable::class, HookedModel::class); + + $model = HookedModel::create( + hooked: 'other', + ); + + $this->assertSame( + 'other', + $model->hooked, + ); + + $this->database->assertTableHasRow( + 'hooked_model', + hooked: 'other', + ); + } + + #[Test] + public function test_hooked_property_with_dto(): void + { + $this->database->migrate(CreateMigrationsTable::class, HookedModelWithKey::class); + + $model = HookedModelWithKey::create(); + + $this->assertSame('default', $model->key->value); + } + + #[Test] + public function test_hooked_property_with_dto_and_provided_value(): void + { + $this->database->migrate(CreateMigrationsTable::class, HookedModelWithKey::class); + + $model = HookedModelWithKey::create( + key: new Key('other'), + ); + + $this->assertSame('other', $model->key->value); + } + + #[Test] + public function test_hooked_property_with_set_hook(): void + { + $this->database->migrate(CreateMigrationsTable::class, HookedModelWithKey::class); + + $model = HookedModelWithKey::create( + key: 'other', + ); + + $this->assertSame('other', $model->key->value); + } +} + +#[Table('hooked_model')] +class HookedModel implements MigratesUp +{ + use IsDatabaseModel; + + public string $hooked { + get { + $this->hooked ??= 'default'; + + return $this->hooked; + } + set (string $hooked) { + $this->hooked = $hooked; + } + } + + #[Virtual] + public string $name = 'hooked_model'; + + public function up(): QueryStatement + { + return new CreateTableStatement('hooked_model') + ->primary() + ->string('hooked'); + } +} + +#[SerializeAs('key')] +class Key +{ + public function __construct(public string $value) {} +} + +#[Table('hooked_model_with_key')] +class HookedModelWithKey implements MigratesUp +{ + use IsDatabaseModel; + + public Key $key { + get { + $this->key ??= new Key('default'); + + return $this->key; + } + set (string|Key $value) { + if (! $value instanceof Key) { + $value = new Key($value); + } + + $this->key = $value; + } + } + + #[Virtual] + public string $name = 'hooked_model_with_key'; + + public function up(): QueryStatement + { + return new CreateTableStatement('hooked_model_with_key') + ->primary() + ->dto('key'); + } +} \ No newline at end of file From e550bfe19c863bdcf984fc96062ebe70561d68ca Mon Sep 17 00:00:00 2001 From: brendt Date: Thu, 16 Jul 2026 09:36:10 +0200 Subject: [PATCH 2/3] wip --- packages/reflection/src/PropertyReflector.php | 5 +++-- tests/Integration/Database/HookedPropertyTest.php | 10 ++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/reflection/src/PropertyReflector.php b/packages/reflection/src/PropertyReflector.php index 3bc462969..e331ce7f0 100644 --- a/packages/reflection/src/PropertyReflector.php +++ b/packages/reflection/src/PropertyReflector.php @@ -4,6 +4,7 @@ namespace Tempest\Reflection; +use ReflectionMethod; use Error; use PropertyHookType; use ReflectionProperty as PHPReflectionProperty; @@ -130,7 +131,7 @@ public function getGetHook(): ?MethodReflector { $hook = $this->reflectionProperty->getHook(PropertyHookType::Get); - if (! $hook) { + if (!$hook instanceof ReflectionMethod) { return null; } @@ -141,7 +142,7 @@ public function getSetHook(): ?MethodReflector { $hook = $this->reflectionProperty->getHook(PropertyHookType::Set); - if (! $hook) { + if (!$hook instanceof ReflectionMethod) { return null; } diff --git a/tests/Integration/Database/HookedPropertyTest.php b/tests/Integration/Database/HookedPropertyTest.php index 555ce5727..e80b7028a 100644 --- a/tests/Integration/Database/HookedPropertyTest.php +++ b/tests/Integration/Database/HookedPropertyTest.php @@ -99,7 +99,7 @@ class HookedModel implements MigratesUp return $this->hooked; } - set (string $hooked) { + set(string $hooked) { $this->hooked = $hooked; } } @@ -118,7 +118,9 @@ public function up(): QueryStatement #[SerializeAs('key')] class Key { - public function __construct(public string $value) {} + public function __construct( + public string $value, + ) {} } #[Table('hooked_model_with_key')] @@ -132,7 +134,7 @@ class HookedModelWithKey implements MigratesUp return $this->key; } - set (string|Key $value) { + set(string|Key $value) { if (! $value instanceof Key) { $value = new Key($value); } @@ -150,4 +152,4 @@ public function up(): QueryStatement ->primary() ->dto('key'); } -} \ No newline at end of file +} From d52f4cfa984a6e39381afaea4496de78839047be Mon Sep 17 00:00:00 2001 From: brendt Date: Thu, 16 Jul 2026 09:38:26 +0200 Subject: [PATCH 3/3] wip --- packages/reflection/src/PropertyReflector.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/reflection/src/PropertyReflector.php b/packages/reflection/src/PropertyReflector.php index e331ce7f0..f98a6330d 100644 --- a/packages/reflection/src/PropertyReflector.php +++ b/packages/reflection/src/PropertyReflector.php @@ -4,9 +4,9 @@ namespace Tempest\Reflection; -use ReflectionMethod; use Error; use PropertyHookType; +use ReflectionMethod; use ReflectionProperty as PHPReflectionProperty; use Stringable; @@ -131,7 +131,7 @@ public function getGetHook(): ?MethodReflector { $hook = $this->reflectionProperty->getHook(PropertyHookType::Get); - if (!$hook instanceof ReflectionMethod) { + if (! $hook instanceof ReflectionMethod) { return null; } @@ -142,7 +142,7 @@ public function getSetHook(): ?MethodReflector { $hook = $this->reflectionProperty->getHook(PropertyHookType::Set); - if (!$hook instanceof ReflectionMethod) { + if (! $hook instanceof ReflectionMethod) { return null; }