2021-11-28 17:43:02 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
namespace CuyZ\Valinor\Tests\Integration\Mapping\Attribute;
|
|
|
|
|
|
|
|
use Attribute;
|
|
|
|
use CuyZ\Valinor\Attribute\StaticMethodConstructor;
|
|
|
|
use CuyZ\Valinor\Definition\ClassDefinition;
|
|
|
|
use CuyZ\Valinor\Mapper\MappingError;
|
|
|
|
use CuyZ\Valinor\Mapper\Object\Exception\TooManyObjectBuilderFactoryAttributes;
|
|
|
|
use CuyZ\Valinor\Mapper\Object\Factory\ObjectBuilderFactory;
|
|
|
|
use CuyZ\Valinor\Mapper\Object\ObjectBuilder;
|
2022-05-22 20:43:01 +02:00
|
|
|
use CuyZ\Valinor\MapperBuilder;
|
2022-03-11 12:25:47 +01:00
|
|
|
use CuyZ\Valinor\Tests\Fake\Mapper\Object\FakeObjectBuilder;
|
2021-11-28 17:43:02 +01:00
|
|
|
use CuyZ\Valinor\Tests\Integration\IntegrationTest;
|
2022-03-11 12:25:47 +01:00
|
|
|
use RuntimeException;
|
2021-11-28 17:43:02 +01:00
|
|
|
|
|
|
|
final class ObjectBuilderStrategyMappingTest extends IntegrationTest
|
|
|
|
{
|
|
|
|
public function test_object_builder_attribute_is_used(): void
|
|
|
|
{
|
|
|
|
try {
|
2022-05-22 20:43:01 +02:00
|
|
|
$result = (new MapperBuilder())->mapper()->map(ObjectWithBuilderStrategyAttribute::class, [
|
2022-03-11 12:25:47 +01:00
|
|
|
'foo' => 'foo',
|
|
|
|
'bar' => 'bar',
|
|
|
|
]);
|
2021-11-28 17:43:02 +01:00
|
|
|
} catch (MappingError $error) {
|
|
|
|
$this->mappingFail($error);
|
|
|
|
}
|
|
|
|
|
2022-03-11 12:25:47 +01:00
|
|
|
self::assertSame('foo', $result->foo);
|
|
|
|
self::assertSame('bar', $result->bar);
|
2021-11-28 17:43:02 +01:00
|
|
|
self::assertTrue($result->staticConstructorCalled);
|
|
|
|
}
|
|
|
|
|
2022-03-11 12:25:47 +01:00
|
|
|
public function test_named_constructor_throwing_exception_is_caught_by_mapper(): void
|
|
|
|
{
|
|
|
|
try {
|
2022-05-22 20:43:01 +02:00
|
|
|
(new MapperBuilder())->mapper()->map(ObjectWithFailingBuilderStrategyAttribute::class, []);
|
2022-03-11 12:25:47 +01:00
|
|
|
} catch (MappingError $exception) {
|
|
|
|
$error = $exception->node()->messages()[0];
|
|
|
|
|
|
|
|
self::assertSame('some exception', (string)$error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-28 17:43:02 +01:00
|
|
|
public function test_repeated_object_builder_factory_attributes_throws_exception(): void
|
|
|
|
{
|
|
|
|
$factoryClass = ObjectBuilderFactory::class;
|
|
|
|
$objectClass = ObjectWithSeveralBuilderStrategyAttributes::class;
|
|
|
|
|
|
|
|
$this->expectException(TooManyObjectBuilderFactoryAttributes::class);
|
|
|
|
$this->expectExceptionCode(1634044714);
|
|
|
|
$this->expectExceptionMessage("Only one attribute of type `$factoryClass` is allowed, class `$objectClass` contains 2.");
|
|
|
|
|
2022-05-22 20:43:01 +02:00
|
|
|
(new MapperBuilder())->mapper()->map($objectClass, 'foo');
|
2021-11-28 17:43:02 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-26 18:59:57 +01:00
|
|
|
/**
|
|
|
|
* @Annotation
|
|
|
|
*/
|
|
|
|
#[Attribute(Attribute::TARGET_CLASS)]
|
|
|
|
final class ForeignAttribute
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2021-11-28 17:43:02 +01:00
|
|
|
/**
|
|
|
|
* @Annotation
|
|
|
|
*/
|
|
|
|
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
|
|
|
|
final class ObjectBuilderStrategyAttribute implements ObjectBuilderFactory
|
|
|
|
{
|
feat: introduce automatic named constructor resolution
An object may have several ways of being created — in such cases it is
common to use so-called named constructors, also known as static factory
methods. If one or more are found, they can be called during the mapping
to create an instance of the object.
What defines a named constructor is a method that:
1. is public
2. is static
3. returns an instance of the object
4. has one or more arguments
```php
final class Color
{
/**
* @param int<0, 255> $red
* @param int<0, 255> $green
* @param int<0, 255> $blue
*/
private function __construct(
public readonly int $red,
public readonly int $green,
public readonly int $blue
) {}
/**
* @param int<0, 255> $red
* @param int<0, 255> $green
* @param int<0, 255> $blue
*/
public static function fromRgb(
int $red,
int $green,
int $blue,
): self {
return new self($red, $green, $blue);
}
/**
* @param non-empty-string $hex
*/
public static function fromHex(string $hex): self
{
if (strlen($hex) !== 6) {
throw new DomainException('Must be 6 characters long');
}
/** @var int<0, 255> $red */
$red = hexdec(substr($hex, 0, 2));
/** @var int<0, 255> $green */
$green = hexdec(substr($hex, 2, 2));
/** @var int<0, 255> $blue */
$blue = hexdec(substr($hex, 4, 2));
return new self($red, $green, $blue);
}
}
```
2022-01-21 19:14:00 +01:00
|
|
|
public function for(ClassDefinition $class, $source): ObjectBuilder
|
2021-11-28 17:43:02 +01:00
|
|
|
{
|
2022-03-11 12:25:47 +01:00
|
|
|
return new FakeObjectBuilder();
|
2021-11-28 17:43:02 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-01-26 18:59:57 +01:00
|
|
|
* @ForeignAttribute
|
2021-11-28 17:43:02 +01:00
|
|
|
* @StaticMethodConstructor("create")
|
|
|
|
*/
|
2022-01-26 18:59:57 +01:00
|
|
|
#[ForeignAttribute]
|
2021-11-28 17:43:02 +01:00
|
|
|
#[StaticMethodConstructor('create')]
|
|
|
|
final class ObjectWithBuilderStrategyAttribute
|
|
|
|
{
|
|
|
|
public bool $staticConstructorCalled = false;
|
|
|
|
|
2022-03-11 12:25:47 +01:00
|
|
|
public string $foo;
|
|
|
|
|
|
|
|
public string $bar;
|
|
|
|
|
|
|
|
private function __construct(string $foo, string $bar)
|
2021-11-28 17:43:02 +01:00
|
|
|
{
|
2022-03-11 12:25:47 +01:00
|
|
|
$this->foo = $foo;
|
|
|
|
$this->bar = $bar;
|
2021-11-28 17:43:02 +01:00
|
|
|
}
|
|
|
|
|
2022-03-11 12:25:47 +01:00
|
|
|
public static function create(string $foo, string $bar = 'optional value'): self
|
2021-11-28 17:43:02 +01:00
|
|
|
{
|
2022-03-11 12:25:47 +01:00
|
|
|
$instance = new self($foo, $bar);
|
2021-11-28 17:43:02 +01:00
|
|
|
$instance->staticConstructorCalled = true;
|
|
|
|
|
|
|
|
return $instance;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-11 12:25:47 +01:00
|
|
|
/**
|
|
|
|
* @StaticMethodConstructor("failingConstructor")
|
|
|
|
*/
|
|
|
|
#[StaticMethodConstructor('failingConstructor')]
|
|
|
|
final class ObjectWithFailingBuilderStrategyAttribute
|
|
|
|
{
|
|
|
|
public static function failingConstructor(): self
|
|
|
|
{
|
|
|
|
throw new RuntimeException('some exception');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-28 17:43:02 +01:00
|
|
|
/**
|
|
|
|
* @ObjectBuilderStrategyAttribute
|
|
|
|
* @ObjectBuilderStrategyAttribute
|
|
|
|
*/
|
|
|
|
#[ObjectBuilderStrategyAttribute]
|
|
|
|
#[ObjectBuilderStrategyAttribute]
|
|
|
|
final class ObjectWithSeveralBuilderStrategyAttributes
|
|
|
|
{
|
|
|
|
}
|