mirror of
https://github.com/danog/Valinor.git
synced 2024-11-30 04:39:05 +01:00
feat: introduce attribute DynamicConstructor
In some situations the type handled by a constructor is only known at runtime, in which case the constructor needs to know what class must be used to instantiate the object. For instance, an interface may declare a static constructor that is then implemented by several child classes. One solution would be to register the constructor for each child class, which leads to a lot of boilerplate code and would require a new registration each time a new child is created. Another way is to use the attribute `\CuyZ\Valinor\Mapper\Object\DynamicConstructor`. When a constructor uses this attribute, its first parameter must be a string and will be filled with the name of the actual class that the mapper needs to build when the constructor is called. Other arguments may be added and will be mapped normally, depending on the source given to the mapper. ```php interface InterfaceWithStaticConstructor { public static function from(string $value): self; } final class ClassWithInheritedStaticConstructor implements InterfaceWithStaticConstructor { private function __construct(private SomeValueObject $value) {} public static function from(string $value): self { return new self(new SomeValueObject($value)); } } (new \CuyZ\Valinor\MapperBuilder()) ->registerConstructor( #[\CuyZ\Valinor\Attribute\DynamicConstructor] function (string $className, string $value): InterfaceWithStaticConstructor { return $className::from($value); } ) ->mapper() ->map(ClassWithInheritedStaticConstructor::class, 'foo'); ```
This commit is contained in:
parent
4bc50e3e42
commit
e437d9405c
@ -114,6 +114,50 @@ final class Color
|
|||||||
[Color::class, 'fromHex'],
|
[Color::class, 'fromHex'],
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Dynamic constructors
|
||||||
|
|
||||||
|
In some situations the type handled by a constructor is only known at runtime,
|
||||||
|
in which case the constructor needs to know what class must be used to
|
||||||
|
instantiate the object.
|
||||||
|
|
||||||
|
For instance, an interface may declare a static constructor that is then
|
||||||
|
implemented by several child classes. One solution would be to register the
|
||||||
|
constructor for each child class, which leads to a lot of boilerplate code and
|
||||||
|
would require a new registration each time a new child is created. Another way
|
||||||
|
is to use the attribute `\CuyZ\Valinor\Mapper\Object\DynamicConstructor`.
|
||||||
|
|
||||||
|
When a constructor uses this attribute, its first parameter must be a string and
|
||||||
|
will be filled with the name of the actual class that the mapper needs to build
|
||||||
|
when the constructor is called. Other arguments may be added and will be mapped
|
||||||
|
normally, depending on the source given to the mapper.
|
||||||
|
|
||||||
|
```php
|
||||||
|
interface InterfaceWithStaticConstructor
|
||||||
|
{
|
||||||
|
public static function from(string $value): self;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ClassWithInheritedStaticConstructor implements InterfaceWithStaticConstructor
|
||||||
|
{
|
||||||
|
private function __construct(private SomeValueObject $value) {}
|
||||||
|
|
||||||
|
public static function from(string $value): self
|
||||||
|
{
|
||||||
|
return new self(new SomeValueObject($value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(new \CuyZ\Valinor\MapperBuilder())
|
||||||
|
->registerConstructor(
|
||||||
|
#[\CuyZ\Valinor\Attribute\DynamicConstructor]
|
||||||
|
function (string $className, string $value): InterfaceWithStaticConstructor {
|
||||||
|
return $className::from($value);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
->mapper()
|
||||||
|
->map(ClassWithInheritedStaticConstructor::class, 'foo');
|
||||||
|
```
|
||||||
|
|
||||||
## Properties
|
## Properties
|
||||||
|
|
||||||
If no constructor is registered, properties will determine which values are
|
If no constructor is registered, properties will determine which values are
|
||||||
|
53
src/Mapper/Object/DynamicConstructor.php
Normal file
53
src/Mapper/Object/DynamicConstructor.php
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace CuyZ\Valinor\Mapper\Object;
|
||||||
|
|
||||||
|
use Attribute;
|
||||||
|
use CuyZ\Valinor\MapperBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This attribute allows the registration of dynamic constructors used when
|
||||||
|
* mapping implementations of interfaces or abstract classes.
|
||||||
|
*
|
||||||
|
* A constructor given to {@see MapperBuilder::registerConstructor()} with this
|
||||||
|
* attribute will be called with the first parameter filled with the name of the
|
||||||
|
* class the mapper needs to build.
|
||||||
|
*
|
||||||
|
* Note that the first parameter of the constructor has to be a string otherwise
|
||||||
|
* an exception will be thrown on mapping.
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* interface SomeInterfaceWithStaticConstructor
|
||||||
|
* {
|
||||||
|
* public static function from(string $value): self;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* final class SomeClassWithInheritedStaticConstructor implements SomeInterfaceWithStaticConstructor
|
||||||
|
* {
|
||||||
|
* private function __construct(private SomeValueObject $value) {}
|
||||||
|
*
|
||||||
|
* public static function from(string $value): self
|
||||||
|
* {
|
||||||
|
* return new self(new SomeValueObject($value));
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* (new \CuyZ\Valinor\MapperBuilder())
|
||||||
|
* ->registerConstructor(
|
||||||
|
* #[\CuyZ\Valinor\Attribute\DynamicConstructor]
|
||||||
|
* function (string $className, string $value): SomeInterfaceWithStaticConstructor {
|
||||||
|
* return $className::from($value);
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ->mapper()
|
||||||
|
* ->map(SomeClassWithInheritedStaticConstructor::class, 'foo');
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
#[Attribute(Attribute::TARGET_FUNCTION | Attribute::TARGET_METHOD)]
|
||||||
|
final class DynamicConstructor
|
||||||
|
{
|
||||||
|
}
|
@ -6,16 +6,16 @@ namespace CuyZ\Valinor\Mapper\Object\Exception;
|
|||||||
|
|
||||||
use CuyZ\Valinor\Definition\FunctionDefinition;
|
use CuyZ\Valinor\Definition\FunctionDefinition;
|
||||||
use CuyZ\Valinor\Type\Type;
|
use CuyZ\Valinor\Type\Type;
|
||||||
use RuntimeException;
|
use LogicException;
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
final class InvalidClassConstructorType extends RuntimeException
|
final class InvalidConstructorClassTypeParameter extends LogicException
|
||||||
{
|
{
|
||||||
public function __construct(FunctionDefinition $function, Type $type)
|
public function __construct(FunctionDefinition $function, Type $type)
|
||||||
{
|
{
|
||||||
parent::__construct(
|
parent::__construct(
|
||||||
"Invalid type `{$type->toString()}` handled by constructor `{$function->signature()}`. It must be a valid class name.",
|
"Invalid type `{$type->toString()}` for the first parameter of the constructor `{$function->signature()}`, it should be of type `class-string`.",
|
||||||
1659446121
|
1661517000
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
20
src/Mapper/Object/Exception/InvalidConstructorReturnType.php
Normal file
20
src/Mapper/Object/Exception/InvalidConstructorReturnType.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace CuyZ\Valinor\Mapper\Object\Exception;
|
||||||
|
|
||||||
|
use CuyZ\Valinor\Definition\FunctionDefinition;
|
||||||
|
use LogicException;
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
final class InvalidConstructorReturnType extends LogicException
|
||||||
|
{
|
||||||
|
public function __construct(FunctionDefinition $function)
|
||||||
|
{
|
||||||
|
parent::__construct(
|
||||||
|
"Invalid return type `{$function->returnType()->toString()}` for constructor `{$function->signature()}`, it must be a valid class name.",
|
||||||
|
1659446121
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace CuyZ\Valinor\Mapper\Object\Exception;
|
||||||
|
|
||||||
|
use CuyZ\Valinor\Definition\FunctionDefinition;
|
||||||
|
use LogicException;
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
final class MissingConstructorClassTypeParameter extends LogicException
|
||||||
|
{
|
||||||
|
public function __construct(FunctionDefinition $function)
|
||||||
|
{
|
||||||
|
parent::__construct(
|
||||||
|
"Missing first parameter of type `class-string` for the constructor `{$function->signature()}`.",
|
||||||
|
1661516853
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -5,18 +5,23 @@ declare(strict_types=1);
|
|||||||
namespace CuyZ\Valinor\Mapper\Object\Factory;
|
namespace CuyZ\Valinor\Mapper\Object\Factory;
|
||||||
|
|
||||||
use CuyZ\Valinor\Definition\ClassDefinition;
|
use CuyZ\Valinor\Definition\ClassDefinition;
|
||||||
|
use CuyZ\Valinor\Definition\FunctionObject;
|
||||||
use CuyZ\Valinor\Definition\FunctionsContainer;
|
use CuyZ\Valinor\Definition\FunctionsContainer;
|
||||||
|
use CuyZ\Valinor\Mapper\Object\DynamicConstructor;
|
||||||
use CuyZ\Valinor\Mapper\Object\Exception\CannotInstantiateObject;
|
use CuyZ\Valinor\Mapper\Object\Exception\CannotInstantiateObject;
|
||||||
use CuyZ\Valinor\Mapper\Object\Exception\InvalidClassConstructorType;
|
use CuyZ\Valinor\Mapper\Object\Exception\InvalidConstructorClassTypeParameter;
|
||||||
|
use CuyZ\Valinor\Mapper\Object\Exception\InvalidConstructorReturnType;
|
||||||
|
use CuyZ\Valinor\Mapper\Object\Exception\MissingConstructorClassTypeParameter;
|
||||||
use CuyZ\Valinor\Mapper\Object\FunctionObjectBuilder;
|
use CuyZ\Valinor\Mapper\Object\FunctionObjectBuilder;
|
||||||
use CuyZ\Valinor\Mapper\Object\MethodObjectBuilder;
|
use CuyZ\Valinor\Mapper\Object\MethodObjectBuilder;
|
||||||
use CuyZ\Valinor\Mapper\Object\NativeConstructorObjectBuilder;
|
use CuyZ\Valinor\Mapper\Object\NativeConstructorObjectBuilder;
|
||||||
use CuyZ\Valinor\Mapper\Object\ObjectBuilder;
|
use CuyZ\Valinor\Mapper\Object\ObjectBuilder;
|
||||||
|
use CuyZ\Valinor\Type\Types\ClassStringType;
|
||||||
use CuyZ\Valinor\Type\Types\ClassType;
|
use CuyZ\Valinor\Type\Types\ClassType;
|
||||||
use CuyZ\Valinor\Type\Types\InterfaceType;
|
use CuyZ\Valinor\Type\Types\InterfaceType;
|
||||||
|
use CuyZ\Valinor\Type\Types\NativeStringType;
|
||||||
|
|
||||||
use function array_key_exists;
|
use function array_key_exists;
|
||||||
use function array_unshift;
|
|
||||||
use function count;
|
use function count;
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
@ -29,9 +34,6 @@ final class ConstructorObjectBuilderFactory implements ObjectBuilderFactory
|
|||||||
|
|
||||||
private FunctionsContainer $constructors;
|
private FunctionsContainer $constructors;
|
||||||
|
|
||||||
/** @var array<string, ObjectBuilder[]> */
|
|
||||||
private array $builders = [];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<class-string, null> $nativeConstructors
|
* @param array<class-string, null> $nativeConstructors
|
||||||
*/
|
*/
|
||||||
@ -47,7 +49,7 @@ final class ConstructorObjectBuilderFactory implements ObjectBuilderFactory
|
|||||||
|
|
||||||
public function for(ClassDefinition $class): array
|
public function for(ClassDefinition $class): array
|
||||||
{
|
{
|
||||||
$builders = $this->listBuilders($class);
|
$builders = $this->builders($class);
|
||||||
|
|
||||||
if (count($builders) === 0) {
|
if (count($builders) === 0) {
|
||||||
if ($class->methods()->hasConstructor()) {
|
if ($class->methods()->hasConstructor()) {
|
||||||
@ -63,47 +65,78 @@ final class ConstructorObjectBuilderFactory implements ObjectBuilderFactory
|
|||||||
/**
|
/**
|
||||||
* @return list<ObjectBuilder>
|
* @return list<ObjectBuilder>
|
||||||
*/
|
*/
|
||||||
private function listBuilders(ClassDefinition $class): array
|
private function builders(ClassDefinition $class): array
|
||||||
{
|
{
|
||||||
$type = $class->type();
|
|
||||||
$key = $type->toString();
|
|
||||||
|
|
||||||
if (! array_key_exists($key, $this->builders)) {
|
|
||||||
$builders = [];
|
|
||||||
|
|
||||||
$className = $class->name();
|
$className = $class->name();
|
||||||
|
$classType = $class->type();
|
||||||
$methods = $class->methods();
|
$methods = $class->methods();
|
||||||
|
|
||||||
foreach ($this->constructors as $constructor) {
|
$builders = [];
|
||||||
$definition = $constructor->definition();
|
|
||||||
$handledType = $definition->returnType();
|
|
||||||
$functionClass = $definition->class();
|
|
||||||
|
|
||||||
if (! $handledType instanceof ClassType && ! $handledType instanceof InterfaceType) {
|
foreach ($this->constructors as $function) {
|
||||||
throw new InvalidClassConstructorType($constructor->definition(), $handledType);
|
if (! $this->constructorMatches($function, $classType)) {
|
||||||
}
|
|
||||||
|
|
||||||
if (! $handledType->matches($type)) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$definition = $function->definition();
|
||||||
|
$functionClass = $definition->class();
|
||||||
|
|
||||||
if ($functionClass && $definition->isStatic()) {
|
if ($functionClass && $definition->isStatic()) {
|
||||||
$builders[] = new MethodObjectBuilder($className, $definition->name(), $definition->parameters());
|
$builders[] = new MethodObjectBuilder($className, $definition->name(), $definition->parameters());
|
||||||
} else {
|
} else {
|
||||||
$builders[] = new FunctionObjectBuilder($constructor);
|
$builders[] = new FunctionObjectBuilder($function, $classType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((array_key_exists($className, $this->nativeConstructors) || count($builders) === 0)
|
if (! array_key_exists($className, $this->nativeConstructors) && count($builders) > 0) {
|
||||||
&& $methods->hasConstructor()
|
return $builders;
|
||||||
&& $methods->constructor()->isPublic()
|
|
||||||
) {
|
|
||||||
array_unshift($builders, new NativeConstructorObjectBuilder($class));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->builders[$key] = $builders;
|
if ($methods->hasConstructor() && $methods->constructor()->isPublic()) {
|
||||||
|
$builders[] = new NativeConstructorObjectBuilder($class);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->builders[$key];
|
return $builders;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function constructorMatches(FunctionObject $function, ClassType $classType): bool
|
||||||
|
{
|
||||||
|
$definition = $function->definition();
|
||||||
|
$parameters = $definition->parameters();
|
||||||
|
$returnType = $definition->returnType();
|
||||||
|
|
||||||
|
if (! $returnType instanceof ClassType && ! $returnType instanceof InterfaceType) {
|
||||||
|
throw new InvalidConstructorReturnType($definition);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $classType->matches($returnType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $definition->attributes()->has(DynamicConstructor::class)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($parameters) === 0) {
|
||||||
|
throw new MissingConstructorClassTypeParameter($definition);
|
||||||
|
}
|
||||||
|
|
||||||
|
$parameterType = $parameters->at(0)->type();
|
||||||
|
|
||||||
|
if ($parameterType instanceof NativeStringType) {
|
||||||
|
$parameterType = ClassStringType::get();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $parameterType instanceof ClassStringType) {
|
||||||
|
throw new InvalidConstructorClassTypeParameter($definition, $parameterType);
|
||||||
|
}
|
||||||
|
|
||||||
|
$subType = $parameterType->subType();
|
||||||
|
|
||||||
|
if ($subType) {
|
||||||
|
return $classType->matches($subType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ use CuyZ\Valinor\Definition\FunctionsContainer;
|
|||||||
use CuyZ\Valinor\Mapper\Object\DateTimeObjectBuilder;
|
use CuyZ\Valinor\Mapper\Object\DateTimeObjectBuilder;
|
||||||
use CuyZ\Valinor\Mapper\Object\FunctionObjectBuilder;
|
use CuyZ\Valinor\Mapper\Object\FunctionObjectBuilder;
|
||||||
use CuyZ\Valinor\Mapper\Object\ObjectBuilder;
|
use CuyZ\Valinor\Mapper\Object\ObjectBuilder;
|
||||||
use CuyZ\Valinor\Type\Type;
|
use CuyZ\Valinor\Type\Types\ClassType;
|
||||||
use DateTimeInterface;
|
use DateTimeInterface;
|
||||||
|
|
||||||
use function count;
|
use function count;
|
||||||
@ -39,15 +39,16 @@ final class DateTimeObjectBuilderFactory implements ObjectBuilderFactory
|
|||||||
return $this->delegate->for($class);
|
return $this->delegate->for($class);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->builders($class->type(), $className);
|
return $this->builders($class->type());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param class-string<DateTimeInterface> $className
|
|
||||||
* @return list<ObjectBuilder>
|
* @return list<ObjectBuilder>
|
||||||
*/
|
*/
|
||||||
private function builders(Type $type, string $className): array
|
private function builders(ClassType $type): array
|
||||||
{
|
{
|
||||||
|
/** @var class-string<DateTimeInterface> $className */
|
||||||
|
$className = $type->className();
|
||||||
$key = $type->toString();
|
$key = $type->toString();
|
||||||
|
|
||||||
if (! isset($this->builders[$key])) {
|
if (! isset($this->builders[$key])) {
|
||||||
@ -66,7 +67,7 @@ final class DateTimeObjectBuilderFactory implements ObjectBuilderFactory
|
|||||||
$overridesDefault = true;
|
$overridesDefault = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->builders[$key][] = new FunctionObjectBuilder($function);
|
$this->builders[$key][] = new FunctionObjectBuilder($function, $type);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $overridesDefault) {
|
if (! $overridesDefault) {
|
||||||
|
@ -5,29 +5,59 @@ declare(strict_types=1);
|
|||||||
namespace CuyZ\Valinor\Mapper\Object;
|
namespace CuyZ\Valinor\Mapper\Object;
|
||||||
|
|
||||||
use CuyZ\Valinor\Definition\FunctionObject;
|
use CuyZ\Valinor\Definition\FunctionObject;
|
||||||
|
use CuyZ\Valinor\Definition\ParameterDefinition;
|
||||||
use CuyZ\Valinor\Mapper\Tree\Message\UserlandError;
|
use CuyZ\Valinor\Mapper\Tree\Message\UserlandError;
|
||||||
|
use CuyZ\Valinor\Type\Types\ClassType;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
|
use function array_map;
|
||||||
|
use function array_shift;
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
final class FunctionObjectBuilder implements ObjectBuilder
|
final class FunctionObjectBuilder implements ObjectBuilder
|
||||||
{
|
{
|
||||||
private FunctionObject $function;
|
private FunctionObject $function;
|
||||||
|
|
||||||
|
private string $className;
|
||||||
|
|
||||||
private Arguments $arguments;
|
private Arguments $arguments;
|
||||||
|
|
||||||
public function __construct(FunctionObject $function)
|
private bool $isDynamicConstructor;
|
||||||
|
|
||||||
|
public function __construct(FunctionObject $function, ClassType $type)
|
||||||
{
|
{
|
||||||
|
$definition = $function->definition();
|
||||||
|
|
||||||
|
$arguments = array_map(
|
||||||
|
fn (ParameterDefinition $parameter) => Argument::fromParameter($parameter),
|
||||||
|
array_values(iterator_to_array($definition->parameters())) // @PHP8.1 array unpacking
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->isDynamicConstructor = $definition->attributes()->has(DynamicConstructor::class);
|
||||||
|
|
||||||
|
if ($this->isDynamicConstructor) {
|
||||||
|
array_shift($arguments);
|
||||||
|
}
|
||||||
|
|
||||||
$this->function = $function;
|
$this->function = $function;
|
||||||
|
$this->className = $type->className();
|
||||||
|
$this->arguments = new Arguments(...$arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function describeArguments(): Arguments
|
public function describeArguments(): Arguments
|
||||||
{
|
{
|
||||||
return $this->arguments ??= Arguments::fromParameters($this->function->definition()->parameters());
|
return $this->arguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function build(array $arguments): object
|
public function build(array $arguments): object
|
||||||
{
|
{
|
||||||
$arguments = new MethodArguments($this->function->definition()->parameters(), $arguments);
|
$parameters = $this->function->definition()->parameters();
|
||||||
|
|
||||||
|
if ($this->isDynamicConstructor) {
|
||||||
|
$arguments[$parameters->at(0)->name()] = $this->className;
|
||||||
|
}
|
||||||
|
|
||||||
|
$arguments = new MethodArguments($parameters, $arguments);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return ($this->function->callback())(...$arguments);
|
return ($this->function->callback())(...$arguments);
|
||||||
|
@ -5,8 +5,11 @@ declare(strict_types=1);
|
|||||||
namespace CuyZ\Valinor\Tests\Integration\Mapping;
|
namespace CuyZ\Valinor\Tests\Integration\Mapping;
|
||||||
|
|
||||||
use CuyZ\Valinor\Mapper\MappingError;
|
use CuyZ\Valinor\Mapper\MappingError;
|
||||||
|
use CuyZ\Valinor\Mapper\Object\DynamicConstructor;
|
||||||
use CuyZ\Valinor\Mapper\Object\Exception\CannotInstantiateObject;
|
use CuyZ\Valinor\Mapper\Object\Exception\CannotInstantiateObject;
|
||||||
use CuyZ\Valinor\Mapper\Object\Exception\InvalidClassConstructorType;
|
use CuyZ\Valinor\Mapper\Object\Exception\InvalidConstructorClassTypeParameter;
|
||||||
|
use CuyZ\Valinor\Mapper\Object\Exception\InvalidConstructorReturnType;
|
||||||
|
use CuyZ\Valinor\Mapper\Object\Exception\MissingConstructorClassTypeParameter;
|
||||||
use CuyZ\Valinor\Mapper\Object\Exception\ObjectBuildersCollision;
|
use CuyZ\Valinor\Mapper\Object\Exception\ObjectBuildersCollision;
|
||||||
use CuyZ\Valinor\MapperBuilder;
|
use CuyZ\Valinor\MapperBuilder;
|
||||||
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\Message\FakeErrorMessage;
|
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\Message\FakeErrorMessage;
|
||||||
@ -114,6 +117,109 @@ final class ConstructorRegistrationMappingTest extends IntegrationTest
|
|||||||
self::assertSame('foo', $result->foo);
|
self::assertSame('foo', $result->foo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @requires PHP >= 8
|
||||||
|
*/
|
||||||
|
public function test_registered_constructor_with_injected_class_name_is_used_for_abstract_class(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$result = (new MapperBuilder())
|
||||||
|
->registerConstructor(
|
||||||
|
/**
|
||||||
|
* @param class-string<SomeAbstractClassWithStaticConstructor> $className
|
||||||
|
*/
|
||||||
|
#[DynamicConstructor]
|
||||||
|
fn (string $className, string $foo, int $bar): SomeAbstractClassWithStaticConstructor => $className::from($foo, $bar)
|
||||||
|
)
|
||||||
|
->mapper()
|
||||||
|
->map(SomeClassWithBothInheritedStaticConstructors::class, [
|
||||||
|
'someChild' => ['foo' => 'foo', 'bar' => 42],
|
||||||
|
'someOtherChild' => ['foo' => 'fiz', 'bar' => 1337],
|
||||||
|
]);
|
||||||
|
} catch (MappingError $error) {
|
||||||
|
$this->mappingFail($error);
|
||||||
|
}
|
||||||
|
|
||||||
|
self::assertSame('foo', $result->someChild->foo);
|
||||||
|
self::assertSame(42, $result->someChild->bar);
|
||||||
|
self::assertSame('fiz', $result->someOtherChild->foo);
|
||||||
|
self::assertSame(1337, $result->someOtherChild->bar);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @requires PHP >= 8
|
||||||
|
*/
|
||||||
|
public function test_registered_constructor_with_injected_class_name_is_used_for_interface(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$result = (new MapperBuilder())
|
||||||
|
->registerConstructor(
|
||||||
|
/**
|
||||||
|
* @param class-string<SomeInterfaceWithStaticConstructor> $className
|
||||||
|
*/
|
||||||
|
#[DynamicConstructor]
|
||||||
|
fn (string $className, string $foo, int $bar): SomeInterfaceWithStaticConstructor => $className::from($foo, $bar)
|
||||||
|
)
|
||||||
|
->mapper()
|
||||||
|
->map(SomeClassWithBothInheritedStaticConstructors::class, [
|
||||||
|
'someChild' => ['foo' => 'foo', 'bar' => 42],
|
||||||
|
'someOtherChild' => ['foo' => 'fiz', 'bar' => 1337],
|
||||||
|
]);
|
||||||
|
} catch (MappingError $error) {
|
||||||
|
$this->mappingFail($error);
|
||||||
|
}
|
||||||
|
|
||||||
|
self::assertSame('foo', $result->someChild->foo);
|
||||||
|
self::assertSame(42, $result->someChild->bar);
|
||||||
|
self::assertSame('fiz', $result->someOtherChild->foo);
|
||||||
|
self::assertSame(1337, $result->someOtherChild->bar);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @requires PHP >= 8
|
||||||
|
*/
|
||||||
|
public function test_registered_constructor_with_injected_class_name_without_class_string_type_is_used(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$object = new stdClass();
|
||||||
|
|
||||||
|
$result = (new MapperBuilder())
|
||||||
|
->registerConstructor(
|
||||||
|
#[DynamicConstructor]
|
||||||
|
fn (string $className, string $foo): stdClass => $object
|
||||||
|
)
|
||||||
|
->mapper()
|
||||||
|
->map(stdClass::class, 'foo');
|
||||||
|
} catch (MappingError $error) {
|
||||||
|
$this->mappingFail($error);
|
||||||
|
}
|
||||||
|
|
||||||
|
self::assertSame($object, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @requires PHP >= 8
|
||||||
|
*/
|
||||||
|
public function test_registered_constructor_with_injected_class_name_with_previously_other_registered_constructor_is_used(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$object = new stdClass();
|
||||||
|
|
||||||
|
$result = (new MapperBuilder())
|
||||||
|
->registerConstructor(fn (): DateTimeInterface => new DateTime())
|
||||||
|
->registerConstructor(
|
||||||
|
#[DynamicConstructor]
|
||||||
|
fn (string $className, string $foo): stdClass => $object
|
||||||
|
)
|
||||||
|
->mapper()
|
||||||
|
->map(stdClass::class, 'foo');
|
||||||
|
} catch (MappingError $error) {
|
||||||
|
$this->mappingFail($error);
|
||||||
|
}
|
||||||
|
|
||||||
|
self::assertSame($object, $result);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_native_constructor_is_not_called_if_not_registered_but_other_constructors_are_registered(): void
|
public function test_native_constructor_is_not_called_if_not_registered_but_other_constructors_are_registered(): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@ -415,11 +521,11 @@ final class ConstructorRegistrationMappingTest extends IntegrationTest
|
|||||||
->map(SomeClassWithPrivateNativeConstructor::class, []);
|
->map(SomeClassWithPrivateNativeConstructor::class, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_invalid_constructor_type_throws_exception(): void
|
public function test_invalid_constructor_return_type_throws_exception(): void
|
||||||
{
|
{
|
||||||
$this->expectException(InvalidClassConstructorType::class);
|
$this->expectException(InvalidConstructorReturnType::class);
|
||||||
$this->expectExceptionCode(1659446121);
|
$this->expectExceptionCode(1659446121);
|
||||||
$this->expectExceptionMessageMatches('/Invalid type `string` handled by constructor `.*`\. It must be a valid class name\./');
|
$this->expectExceptionMessageMatches('/Invalid return type `string` for constructor `.*`\, it must be a valid class name\./');
|
||||||
|
|
||||||
(new MapperBuilder())
|
(new MapperBuilder())
|
||||||
->registerConstructor(fn (): string => 'foo')
|
->registerConstructor(fn (): string => 'foo')
|
||||||
@ -427,6 +533,42 @@ final class ConstructorRegistrationMappingTest extends IntegrationTest
|
|||||||
->map(stdClass::class, []);
|
->map(stdClass::class, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @requires PHP >= 8
|
||||||
|
*/
|
||||||
|
public function test_missing_constructor_class_type_parameter_throws_exception(): void
|
||||||
|
{
|
||||||
|
$this->expectException(MissingConstructorClassTypeParameter::class);
|
||||||
|
$this->expectExceptionCode(1661516853);
|
||||||
|
$this->expectExceptionMessageMatches('/Missing first parameter of type `class-string` for the constructor `.*`\./');
|
||||||
|
|
||||||
|
(new MapperBuilder())
|
||||||
|
->registerConstructor(
|
||||||
|
#[DynamicConstructor]
|
||||||
|
fn (): stdClass => new stdClass()
|
||||||
|
)
|
||||||
|
->mapper()
|
||||||
|
->map(stdClass::class, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @requires PHP >= 8
|
||||||
|
*/
|
||||||
|
public function test_invalid_constructor_class_type_parameter_throws_exception(): void
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidConstructorClassTypeParameter::class);
|
||||||
|
$this->expectExceptionCode(1661517000);
|
||||||
|
$this->expectExceptionMessageMatches('/Invalid type `int` for the first parameter of the constructor `.*`, it should be of type `class-string`\./');
|
||||||
|
|
||||||
|
(new MapperBuilder())
|
||||||
|
->registerConstructor(
|
||||||
|
#[DynamicConstructor]
|
||||||
|
fn (int $invalidParameterType): stdClass => new stdClass()
|
||||||
|
)
|
||||||
|
->mapper()
|
||||||
|
->map(stdClass::class, []);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_registered_datetime_constructor_is_used(): void
|
public function test_registered_datetime_constructor_is_used(): void
|
||||||
{
|
{
|
||||||
$default = new DateTime('@1356097062');
|
$default = new DateTime('@1356097062');
|
||||||
@ -578,7 +720,13 @@ function constructorB(int $argumentA, float $argumentB): stdClass
|
|||||||
return new stdClass();
|
return new stdClass();
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class SomeAbstractClassWithStaticConstructor
|
interface SomeInterfaceWithStaticConstructor
|
||||||
|
{
|
||||||
|
// @PHP8.0 return static
|
||||||
|
public static function from(string $foo, int $bar): self;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class SomeAbstractClassWithStaticConstructor implements SomeInterfaceWithStaticConstructor
|
||||||
{
|
{
|
||||||
public string $foo;
|
public string $foo;
|
||||||
|
|
||||||
@ -604,3 +752,10 @@ final class SomeClassWithInheritedStaticConstructor extends SomeAbstractClassWit
|
|||||||
final class SomeOtherClassWithInheritedStaticConstructor extends SomeAbstractClassWithStaticConstructor
|
final class SomeOtherClassWithInheritedStaticConstructor extends SomeAbstractClassWithStaticConstructor
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class SomeClassWithBothInheritedStaticConstructors
|
||||||
|
{
|
||||||
|
public SomeClassWithInheritedStaticConstructor $someChild;
|
||||||
|
|
||||||
|
public SomeOtherClassWithInheritedStaticConstructor $someOtherChild;
|
||||||
|
}
|
||||||
|
@ -7,6 +7,7 @@ namespace CuyZ\Valinor\Tests\Unit\Mapper\Object;
|
|||||||
use CuyZ\Valinor\Definition\FunctionObject;
|
use CuyZ\Valinor\Definition\FunctionObject;
|
||||||
use CuyZ\Valinor\Mapper\Object\FunctionObjectBuilder;
|
use CuyZ\Valinor\Mapper\Object\FunctionObjectBuilder;
|
||||||
use CuyZ\Valinor\Tests\Fake\Definition\FakeFunctionDefinition;
|
use CuyZ\Valinor\Tests\Fake\Definition\FakeFunctionDefinition;
|
||||||
|
use CuyZ\Valinor\Type\Types\ClassType;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
|
|
||||||
@ -14,7 +15,10 @@ final class FunctionObjectBuilderTest extends TestCase
|
|||||||
{
|
{
|
||||||
public function test_arguments_instance_stays_the_same(): void
|
public function test_arguments_instance_stays_the_same(): void
|
||||||
{
|
{
|
||||||
$objectBuilder = new FunctionObjectBuilder(new FunctionObject(FakeFunctionDefinition::new(), fn () => new stdClass()));
|
$objectBuilder = new FunctionObjectBuilder(
|
||||||
|
new FunctionObject(FakeFunctionDefinition::new(), fn () => new stdClass()),
|
||||||
|
new ClassType(stdClass::class)
|
||||||
|
);
|
||||||
|
|
||||||
$argumentsA = $objectBuilder->describeArguments();
|
$argumentsA = $objectBuilder->describeArguments();
|
||||||
$argumentsB = $objectBuilder->describeArguments();
|
$argumentsB = $objectBuilder->describeArguments();
|
||||||
|
Loading…
Reference in New Issue
Block a user