Skip to content
Merged
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
23 changes: 18 additions & 5 deletions packages/mapper/src/Mappers/ArrayToObjectMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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,
Expand All @@ -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 !== []) {
Expand All @@ -91,6 +100,10 @@ public function map(mixed $from, mixed $to): object
continue;
}

if ($property->isHooked()) {
continue;
}

$property->unset($targetObject);
}

Expand Down
29 changes: 29 additions & 0 deletions packages/reflection/src/PropertyReflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
namespace Tempest\Reflection;

use Error;
use PropertyHookType;
use ReflectionMethod;
use ReflectionProperty as PHPReflectionProperty;
use Stringable;

Expand Down Expand Up @@ -120,6 +122,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 instanceof ReflectionMethod) {
return null;
}

return new MethodReflector($hook);
}

public function getSetHook(): ?MethodReflector
{
$hook = $this->reflectionProperty->getHook(PropertyHookType::Set);

if (! $hook instanceof ReflectionMethod) {
return null;
}

return new MethodReflector($hook);
}

public function unset(object $object): void
{
unset($object->{$this->getName()});
Expand Down
155 changes: 155 additions & 0 deletions tests/Integration/Database/HookedPropertyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<?php

namespace Tests\Tempest\Integration\Database;

use PHPUnit\Framework\Attributes\Test;
use Tempest\Database\IsDatabaseModel;
use Tempest\Database\MigratesUp;
use Tempest\Database\Migrations\CreateMigrationsTable;
use Tempest\Database\QueryStatement;
use Tempest\Database\QueryStatements\CreateTableStatement;
use Tempest\Database\Table;
use Tempest\Database\Virtual;
use Tempest\Mapper\SerializeAs;
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;

final class HookedPropertyTest extends FrameworkIntegrationTestCase
{
#[Test]
public function test_hooked_property_gets_persisted(): void
{
$this->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');
}
}
Loading