Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mapper): allow mapping value to object via cast method #887

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
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,15 @@ 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')) {
// @phpstan-ignore staticMethod.notFound
return $class->getName()::cast($from);
}

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

foreach ($class->getPublicProperties() as $property) {
if ($property->isVirtual()) {
continue;
Expand Down Expand Up @@ -136,6 +137,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
Loading