Skip to content

Commit

Permalink
feat(mapper): allow mapping value to object via cast method
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeroen-G committed Jan 24, 2025
1 parent dec5c2f commit 0acc323
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
use Throwable;
use function Tempest\Support\arr;

final readonly class ArrayToObjectMapper implements Mapper
final readonly class MixedToObjectMapper implements Mapper
{
public function __construct(
private CasterFactory $casterFactory,
Expand All @@ -24,14 +24,10 @@ public function __construct(

public function canMap(mixed $from, mixed $to): bool
{
if (! is_array($from)) {
return false;
}

try {
$class = new ClassReflector($to);

return $class->isInstantiable();
return $class->isInstantiable() || $class->hasMethod('cast');
} catch (Throwable) {
return false;
}
Expand All @@ -47,10 +43,14 @@ public function map(mixed $from, mixed $to): object
/** @var PropertyReflector[] $unsetProperties */
$unsetProperties = [];

$from = arr($from)->unwrap()->toArray();

$isStrictClass = $class->hasAttribute(Strict::class);

if ($class->hasMethod('cast')) {
return $class->getName()::cast($from);

Check failure on line 49 in src/Tempest/Mapper/src/Mappers/MixedToObjectMapper.php

View workflow job for this annotation

GitHub Actions / Run static analysis: PHPStan

Call to an undefined static method ClassName of object::cast().
}

$from = arr($from)->unwrap()->toArray();

foreach ($class->getPublicProperties() as $property) {
if ($property->isVirtual()) {
continue;
Expand Down Expand Up @@ -136,6 +136,10 @@ private function resolveValueFromType(
return new UnknownValue();
}

if ($type->asClass()->hasMethod('cast')) {
return $type->getName()::cast($data);
}

$caster = $this->casterFactory->forProperty($property);

if (! is_array($data)) {
Expand Down
44 changes: 44 additions & 0 deletions src/Tempest/Mapper/tests/Mappers/MixedToObjectMapperTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace Tempest\Mapper\Tests\Mappers;

use PHPUnit\Framework\TestCase;
use Tempest\Mapper\Casters\CasterFactory;
use Tempest\Mapper\Mappers\MixedToObjectMapper;
use Tempest\Mapper\Tests\Support\StringCastValue;
use Tempest\Mapper\Tests\Support\StringValue;

/**
* @internal
*/
final class MixedToObjectMapperTest extends TestCase
{
private MixedToObjectMapper $subject;

protected function setUp(): void
{
$this->subject = new MixedToObjectMapper(new CasterFactory());
}

public function test_map_array_to_object(): void
{
$value = ['value' => 'Tempest'];

/** @var StringValue $object */
$object = $this->subject->map($value, StringValue::class);

$this->assertEquals('Tempest', $object->getValue());
}

public function test_map_string_to_object_with_automatic_cast(): void
{
$value = 'Tempest';

/** @var StringCastValue $object */
$object = $this->subject->map($value, StringCastValue::class);

$this->assertEquals('Tempest', $object->getValue());
}
}
22 changes: 22 additions & 0 deletions src/Tempest/Mapper/tests/Support/StringCastValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Tempest\Mapper\Tests\Support;

final readonly class StringCastValue
{
private function __construct(private string $value)
{
}

public function getValue(): string
{
return $this->value;
}

public static function cast(string $value): self
{
return new self($value);
}
}
17 changes: 17 additions & 0 deletions src/Tempest/Mapper/tests/Support/StringValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Tempest\Mapper\Tests\Support;

final readonly class StringValue
{
public function __construct(public string $value)
{
}

public function getValue(): string
{
return $this->value;
}
}
5 changes: 5 additions & 0 deletions src/Tempest/Reflection/src/ClassReflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ public function getMethod(string $name): MethodReflector
return new MethodReflector($this->reflectionClass->getMethod($name));
}

public function hasMethod(string $name): bool
{
return $this->reflectionClass->hasMethod($name);
}

public function isInstantiable(): bool
{
return $this->reflectionClass->isInstantiable();
Expand Down
8 changes: 8 additions & 0 deletions src/Tempest/Reflection/tests/ClassReflectorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,12 @@ public function test_recursive_attribute_from_parent(): void
$this->assertNull($reflector->getAttribute(RecursiveAttribute::class));
$this->assertNotNull($reflector->getAttribute(RecursiveAttribute::class, recursive: true));
}

public function test_has_method(): void
{
$reflector = new ClassReflector(TestClassB::class);
$reflection = new ReflectionClass(TestClassB::class);

$this->assertTrue($reflector->hasMethod('getName'));
}
}
5 changes: 5 additions & 0 deletions src/Tempest/Reflection/tests/Fixtures/TestClassB.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,9 @@ public function __construct(
public ?string $name,
) {
}

public function getName(): string
{
return $this->name;
}
}
4 changes: 2 additions & 2 deletions tests/Integration/Mapper/ObjectFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use Tempest\Mapper\Exceptions\CannotMapDataException;
use Tempest\Mapper\Mappers\ArrayToJsonMapper;
use Tempest\Mapper\Mappers\ArrayToObjectMapper;
use Tempest\Mapper\Mappers\MixedToObjectMapper;
use Tempest\Mapper\Mappers\ObjectToArrayMapper;
use Tempest\Mapper\ObjectFactory;
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;
Expand Down Expand Up @@ -77,7 +77,7 @@ public function test_cannot_map_exception(): void
public function test_map_with(): void
{
$result = map(['a' => 'a', 'b' => 'b'])->with(
fn (ArrayToObjectMapper $mapper, mixed $from) => $mapper->map($from, ObjectA::class),
fn (MixedToObjectMapper $mapper, mixed $from) => $mapper->map($from, ObjectA::class),
ObjectToArrayMapper::class,
ArrayToJsonMapper::class,
);
Expand Down

0 comments on commit 0acc323

Please sign in to comment.