mirror of
https://github.com/danog/Valinor.git
synced 2024-11-26 20:24:40 +01:00
feat: handle abstract constructor registration
It is now possible to register a static method constructor that can be inherited by a child class. The constructor will then be used correctly to map the child class. ```php abstract class ClassWithStaticConstructor { public string $value; final private function __construct(string $value) { $this->value = $value; } public static function from(string $value): static { return new static($value); } } final class ChildClass extends ClassWithStaticConstructor {} (new MapperBuilder()) // The constructor can be used for every child of the parent class ->registerConstructor(ClassWithStaticConstructor::from(...)) ->mapper() ->map(ChildClass::class, 'foo'); ```
This commit is contained in:
parent
73b62241b6
commit
c37ac1e259
@ -31,6 +31,6 @@ final class StaticMethodConstructor implements ObjectBuilderFactory
|
||||
|
||||
public function for(ClassDefinition $class): array
|
||||
{
|
||||
return [new MethodObjectBuilder($class, $this->methodName)];
|
||||
return [new MethodObjectBuilder($class->name(), $this->methodName, $class->methods()->get($this->methodName)->parameters())];
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,8 @@ final class FunctionDefinition
|
||||
/** @var class-string|null */
|
||||
private ?string $class;
|
||||
|
||||
private bool $isStatic;
|
||||
|
||||
private Parameters $parameters;
|
||||
|
||||
private Type $returnType;
|
||||
@ -30,6 +32,7 @@ final class FunctionDefinition
|
||||
string $signature,
|
||||
?string $fileName,
|
||||
?string $class,
|
||||
bool $isStatic,
|
||||
Parameters $parameters,
|
||||
Type $returnType
|
||||
) {
|
||||
@ -37,6 +40,7 @@ final class FunctionDefinition
|
||||
$this->signature = $signature;
|
||||
$this->fileName = $fileName;
|
||||
$this->class = $class;
|
||||
$this->isStatic = $isStatic;
|
||||
$this->parameters = $parameters;
|
||||
$this->returnType = $returnType;
|
||||
}
|
||||
@ -64,6 +68,11 @@ final class FunctionDefinition
|
||||
return $this->class;
|
||||
}
|
||||
|
||||
public function isStatic(): bool
|
||||
{
|
||||
return $this->isStatic;
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return Parameters
|
||||
* @return Parameters&ParameterDefinition[]
|
||||
|
@ -34,6 +34,7 @@ final class FunctionDefinitionCompiler implements CacheCompiler
|
||||
|
||||
$fileName = var_export($value->fileName(), true);
|
||||
$class = var_export($value->class(), true);
|
||||
$isStatic = var_export($value->isStatic(), true);
|
||||
$parameters = implode(', ', $parameters);
|
||||
$returnType = $this->typeCompiler->compile($value->returnType());
|
||||
|
||||
@ -43,6 +44,7 @@ final class FunctionDefinitionCompiler implements CacheCompiler
|
||||
'{$value->signature()}',
|
||||
$fileName,
|
||||
$class,
|
||||
$isStatic,
|
||||
new \CuyZ\Valinor\Definition\Parameters($parameters),
|
||||
$returnType
|
||||
)
|
||||
|
@ -40,6 +40,7 @@ final class ReflectionFunctionDefinitionRepository implements FunctionDefinition
|
||||
$reflection->getParameters()
|
||||
);
|
||||
|
||||
$class = $reflection->getClosureScopeClass();
|
||||
$returnType = $typeResolver->resolveType($reflection);
|
||||
|
||||
return new FunctionDefinition(
|
||||
@ -47,7 +48,8 @@ final class ReflectionFunctionDefinitionRepository implements FunctionDefinition
|
||||
Reflection::signature($reflection),
|
||||
$reflection->getFileName() ?: null,
|
||||
// @PHP 8.0 nullsafe operator
|
||||
$reflection->getClosureScopeClass() ? $reflection->getClosureScopeClass()->name : null,
|
||||
$class ? $class->name : null,
|
||||
$reflection->getClosureThis() === null,
|
||||
new Parameters(...$parameters),
|
||||
$returnType
|
||||
);
|
||||
|
@ -1,22 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Object\Exception;
|
||||
|
||||
use CuyZ\Valinor\Definition\ClassDefinition;
|
||||
use CuyZ\Valinor\Definition\MethodDefinition;
|
||||
use LogicException;
|
||||
|
||||
/** @internal */
|
||||
final class ConstructorMethodIsNotPublic extends LogicException
|
||||
{
|
||||
public function __construct(ClassDefinition $class, MethodDefinition $method)
|
||||
{
|
||||
$message = $method->name() === '__construct'
|
||||
? "The constructor of the class `{$class->name()}` is not public."
|
||||
: "The named constructor `{$method->signature()}` is not public.";
|
||||
|
||||
parent::__construct($message, 1630937169);
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Object\Exception;
|
||||
|
||||
use CuyZ\Valinor\Definition\MethodDefinition;
|
||||
use RuntimeException;
|
||||
|
||||
/** @internal */
|
||||
final class ConstructorMethodIsNotStatic extends RuntimeException
|
||||
{
|
||||
public function __construct(MethodDefinition $method)
|
||||
{
|
||||
parent::__construct(
|
||||
"Invalid constructor method `{$method->signature()}`: it is neither the constructor nor a static constructor.",
|
||||
1634044370
|
||||
);
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Object\Exception;
|
||||
|
||||
use CuyZ\Valinor\Definition\ClassDefinition;
|
||||
use RuntimeException;
|
||||
|
||||
/** @internal */
|
||||
final class MethodNotFound extends RuntimeException
|
||||
{
|
||||
public function __construct(ClassDefinition $class, string $methodName)
|
||||
{
|
||||
parent::__construct(
|
||||
"Method `$methodName` was not found in class `{$class->name()}`.",
|
||||
1634044209
|
||||
);
|
||||
}
|
||||
}
|
@ -9,9 +9,11 @@ use CuyZ\Valinor\Definition\FunctionsContainer;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\CannotInstantiateObject;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\InvalidClassConstructorType;
|
||||
use CuyZ\Valinor\Mapper\Object\FunctionObjectBuilder;
|
||||
use CuyZ\Valinor\Mapper\Object\MethodObjectBuilder;
|
||||
use CuyZ\Valinor\Mapper\Object\NativeConstructorObjectBuilder;
|
||||
use CuyZ\Valinor\Mapper\Object\ObjectBuilder;
|
||||
use CuyZ\Valinor\Type\Types\ClassType;
|
||||
use CuyZ\Valinor\Type\Types\InterfaceType;
|
||||
|
||||
use function array_key_exists;
|
||||
use function array_unshift;
|
||||
@ -69,21 +71,30 @@ final class ConstructorObjectBuilderFactory implements ObjectBuilderFactory
|
||||
if (! array_key_exists($key, $this->builders)) {
|
||||
$builders = [];
|
||||
|
||||
$className = $class->name();
|
||||
$methods = $class->methods();
|
||||
|
||||
foreach ($this->constructors as $constructor) {
|
||||
$handledType = $constructor->definition()->returnType();
|
||||
$definition = $constructor->definition();
|
||||
$handledType = $definition->returnType();
|
||||
$functionClass = $definition->class();
|
||||
|
||||
if (! $handledType instanceof ClassType) {
|
||||
if (! $handledType instanceof ClassType && ! $handledType instanceof InterfaceType) {
|
||||
throw new InvalidClassConstructorType($constructor->definition(), $handledType);
|
||||
}
|
||||
|
||||
if ($handledType->matches($type)) {
|
||||
if (! $handledType->matches($type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($functionClass && $definition->isStatic()) {
|
||||
$builders[] = new MethodObjectBuilder($className, $definition->name(), $definition->parameters());
|
||||
} else {
|
||||
$builders[] = new FunctionObjectBuilder($constructor);
|
||||
}
|
||||
}
|
||||
|
||||
if ((array_key_exists($class->name(), $this->nativeConstructors) || count($builders) === 0)
|
||||
if ((array_key_exists($className, $this->nativeConstructors) || count($builders) === 0)
|
||||
&& $methods->hasConstructor()
|
||||
&& $methods->constructor()->isPublic()
|
||||
) {
|
||||
|
@ -4,56 +4,40 @@ declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Mapper\Object;
|
||||
|
||||
use CuyZ\Valinor\Definition\ClassDefinition;
|
||||
use CuyZ\Valinor\Definition\MethodDefinition;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\ConstructorMethodIsNotPublic;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\ConstructorMethodIsNotStatic;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\MethodNotFound;
|
||||
use CuyZ\Valinor\Definition\Parameters;
|
||||
use CuyZ\Valinor\Mapper\Tree\Message\UserlandError;
|
||||
use Exception;
|
||||
|
||||
/** @internal */
|
||||
final class MethodObjectBuilder implements ObjectBuilder
|
||||
{
|
||||
private ClassDefinition $class;
|
||||
private string $className;
|
||||
|
||||
private MethodDefinition $method;
|
||||
private string $methodName;
|
||||
|
||||
private Parameters $parameters;
|
||||
|
||||
private Arguments $arguments;
|
||||
|
||||
public function __construct(ClassDefinition $class, string $methodName)
|
||||
public function __construct(string $className, string $methodName, Parameters $parameters)
|
||||
{
|
||||
$methods = $class->methods();
|
||||
|
||||
if (! $methods->has($methodName)) {
|
||||
throw new MethodNotFound($class, $methodName);
|
||||
}
|
||||
|
||||
$this->class = $class;
|
||||
$this->method = $methods->get($methodName);
|
||||
|
||||
if (! $this->method->isPublic()) {
|
||||
throw new ConstructorMethodIsNotPublic($this->class, $this->method);
|
||||
}
|
||||
|
||||
if (! $this->method->isStatic()) {
|
||||
throw new ConstructorMethodIsNotStatic($this->method);
|
||||
}
|
||||
$this->className = $className;
|
||||
$this->methodName = $methodName;
|
||||
$this->parameters = $parameters;
|
||||
}
|
||||
|
||||
public function describeArguments(): Arguments
|
||||
{
|
||||
return $this->arguments ??= Arguments::fromParameters($this->method->parameters());
|
||||
return $this->arguments ??= Arguments::fromParameters($this->parameters);
|
||||
}
|
||||
|
||||
public function build(array $arguments): object
|
||||
{
|
||||
$className = $this->class->name();
|
||||
$methodName = $this->method->name();
|
||||
$arguments = new MethodArguments($this->method->parameters(), $arguments);
|
||||
$methodName = $this->methodName;
|
||||
$arguments = new MethodArguments($this->parameters, $arguments);
|
||||
|
||||
try {
|
||||
return $className::$methodName(...$arguments); // @phpstan-ignore-line
|
||||
return ($this->className)::$methodName(...$arguments); // @phpstan-ignore-line
|
||||
} catch (Exception $exception) {
|
||||
throw UserlandError::from($exception);
|
||||
}
|
||||
@ -61,6 +45,6 @@ final class MethodObjectBuilder implements ObjectBuilder
|
||||
|
||||
public function signature(): string
|
||||
{
|
||||
return $this->method->signature();
|
||||
return "$this->className::$this->methodName()";
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ final class FakeFunctionDefinition
|
||||
'foo:42-1337',
|
||||
$fileName ?? 'foo/bar',
|
||||
stdClass::class,
|
||||
true,
|
||||
new Parameters(
|
||||
new ParameterDefinition(
|
||||
'bar',
|
||||
|
@ -32,6 +32,7 @@ final class FunctionDefinitionCompilerTest extends TestCase
|
||||
'foo:42-1337',
|
||||
'foo/bar',
|
||||
stdClass::class,
|
||||
true,
|
||||
new Parameters(
|
||||
new ParameterDefinition(
|
||||
'bar',
|
||||
@ -53,6 +54,7 @@ final class FunctionDefinitionCompilerTest extends TestCase
|
||||
self::assertSame('foo', $compiledFunction->name());
|
||||
self::assertSame('foo:42-1337', $compiledFunction->signature());
|
||||
self::assertSame('foo/bar', $compiledFunction->fileName());
|
||||
self::assertSame(true, $compiledFunction->isStatic());
|
||||
self::assertSame(stdClass::class, $compiledFunction->class());
|
||||
self::assertTrue($compiledFunction->parameters()->has('bar'));
|
||||
self::assertInstanceOf(NativeStringType::class, $compiledFunction->returnType());
|
||||
|
@ -312,6 +312,33 @@ final class ConstructorRegistrationMappingTest extends IntegrationTest
|
||||
self::assertSame('baz', $resultB->baz);
|
||||
}
|
||||
|
||||
public function test_inherited_static_constructor_is_used_to_map_child_class(): void
|
||||
{
|
||||
$class = get_class(new class () {
|
||||
public SomeClassWithInheritedStaticConstructor $someChild;
|
||||
|
||||
public SomeOtherClassWithInheritedStaticConstructor $someOtherChild;
|
||||
});
|
||||
|
||||
try {
|
||||
$result = (new MapperBuilder())
|
||||
// @PHP8.1 First-class callable syntax
|
||||
->registerConstructor([SomeAbstractClassWithStaticConstructor::class, 'from'])
|
||||
->mapper()
|
||||
->map($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);
|
||||
}
|
||||
|
||||
public function test_identical_registered_constructors_with_no_argument_throws_exception(): void
|
||||
{
|
||||
$this->expectException(ObjectBuildersCollision::class);
|
||||
@ -550,3 +577,30 @@ function constructorB(int $argumentA, float $argumentB): stdClass
|
||||
{
|
||||
return new stdClass();
|
||||
}
|
||||
|
||||
abstract class SomeAbstractClassWithStaticConstructor
|
||||
{
|
||||
public string $foo;
|
||||
|
||||
public int $bar;
|
||||
|
||||
final private function __construct(string $foo, int $bar)
|
||||
{
|
||||
$this->foo = $foo;
|
||||
$this->bar = $bar;
|
||||
}
|
||||
|
||||
// @PHP8.0 return static
|
||||
public static function from(string $foo, int $bar): self
|
||||
{
|
||||
return new static($foo, $bar);
|
||||
}
|
||||
}
|
||||
|
||||
final class SomeClassWithInheritedStaticConstructor extends SomeAbstractClassWithStaticConstructor
|
||||
{
|
||||
}
|
||||
|
||||
final class SomeOtherClassWithInheritedStaticConstructor extends SomeAbstractClassWithStaticConstructor
|
||||
{
|
||||
}
|
||||
|
@ -4,118 +4,57 @@ declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Tests\Unit\Mapper\Object;
|
||||
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\ConstructorMethodIsNotPublic;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\ConstructorMethodIsNotStatic;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\MethodNotFound;
|
||||
use CuyZ\Valinor\Definition\Parameters;
|
||||
use CuyZ\Valinor\Mapper\Object\MethodObjectBuilder;
|
||||
use CuyZ\Valinor\Mapper\Tree\Message\UserlandError;
|
||||
use CuyZ\Valinor\Tests\Fake\Definition\FakeClassDefinition;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionClass;
|
||||
use RuntimeException;
|
||||
use stdClass;
|
||||
|
||||
use function get_class;
|
||||
|
||||
final class MethodObjectBuilderTest extends TestCase
|
||||
{
|
||||
public function test_signature_is_method_signature(): void
|
||||
{
|
||||
$object = new class () {
|
||||
$class = get_class(new class () {
|
||||
public static function someMethod(): stdClass
|
||||
{
|
||||
return new stdClass();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
$class = FakeClassDefinition::fromReflection(new ReflectionClass($object));
|
||||
$objectBuilder = new MethodObjectBuilder($class, 'someMethod');
|
||||
$objectBuilder = new MethodObjectBuilder($class, 'someMethod', new Parameters());
|
||||
|
||||
self::assertSame('Signature::someMethod', $objectBuilder->signature());
|
||||
}
|
||||
|
||||
public function test_not_existing_method_throws_exception(): void
|
||||
{
|
||||
$this->expectException(MethodNotFound::class);
|
||||
$this->expectExceptionCode(1634044209);
|
||||
$this->expectExceptionMessage('Method `notExistingMethod` was not found in class `stdClass`.');
|
||||
|
||||
$class = FakeClassDefinition::fromReflection(new ReflectionClass(stdClass::class));
|
||||
new MethodObjectBuilder($class, 'notExistingMethod');
|
||||
}
|
||||
|
||||
public function test_invalid_constructor_method_throws_exception(): void
|
||||
{
|
||||
$this->expectException(ConstructorMethodIsNotStatic::class);
|
||||
$this->expectExceptionCode(1634044370);
|
||||
$this->expectExceptionMessage('Invalid constructor method `Signature::invalidConstructor`: it is neither the constructor nor a static constructor.');
|
||||
|
||||
$object = new class () {
|
||||
public function invalidConstructor(): void
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
$class = FakeClassDefinition::fromReflection(new ReflectionClass($object));
|
||||
new MethodObjectBuilder($class, 'invalidConstructor');
|
||||
self::assertSame("$class::someMethod()", $objectBuilder->signature());
|
||||
}
|
||||
|
||||
public function test_exception_thrown_by_method_is_caught_and_wrapped(): void
|
||||
{
|
||||
$class = new class () {
|
||||
$class = get_class(new class () {
|
||||
public static function someMethod(): stdClass
|
||||
{
|
||||
throw new RuntimeException('some exception', 1337);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
$class = FakeClassDefinition::fromReflection(new ReflectionClass($class));
|
||||
$objectBuilder = new MethodObjectBuilder($class, 'someMethod');
|
||||
$objectBuilder = new MethodObjectBuilder($class, 'someMethod', new Parameters());
|
||||
|
||||
$this->expectException(UserlandError::class);
|
||||
|
||||
$objectBuilder->build([]);
|
||||
}
|
||||
|
||||
public function test_constructor_builder_for_class_with_private_constructor_throws_exception(): void
|
||||
{
|
||||
$this->expectException(ConstructorMethodIsNotPublic::class);
|
||||
$this->expectExceptionCode(1630937169);
|
||||
$this->expectExceptionMessage('The constructor of the class `' . ObjectWithPrivateNativeConstructor::class . '` is not public.');
|
||||
|
||||
$class = FakeClassDefinition::fromReflection(new ReflectionClass(ObjectWithPrivateNativeConstructor::class));
|
||||
new MethodObjectBuilder($class, '__construct');
|
||||
}
|
||||
|
||||
public function test_constructor_builder_for_class_with_private_named_constructor_throws_exception(): void
|
||||
{
|
||||
$classWithPrivateNativeConstructor = new class () {
|
||||
// @phpstan-ignore-next-line
|
||||
private static function someConstructor(): void
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
$this->expectException(ConstructorMethodIsNotPublic::class);
|
||||
$this->expectExceptionCode(1630937169);
|
||||
$this->expectExceptionMessage('The named constructor `Signature::someConstructor` is not public.');
|
||||
|
||||
$class = FakeClassDefinition::fromReflection(new ReflectionClass($classWithPrivateNativeConstructor));
|
||||
new MethodObjectBuilder($class, 'someConstructor');
|
||||
}
|
||||
|
||||
public function test_arguments_instance_stays_the_same(): void
|
||||
{
|
||||
$class = new class () {
|
||||
public static function someMethod(string $string): stdClass
|
||||
$class = get_class(new class () {
|
||||
public static function someMethod(): stdClass
|
||||
{
|
||||
$class = new stdClass();
|
||||
$class->string = $string;
|
||||
|
||||
return $class;
|
||||
return new stdClass();
|
||||
}
|
||||
};
|
||||
$class = FakeClassDefinition::fromReflection(new ReflectionClass($class));
|
||||
});
|
||||
|
||||
$objectBuilder = new MethodObjectBuilder($class, 'someMethod');
|
||||
$objectBuilder = new MethodObjectBuilder($class, 'someMethod', new Parameters());
|
||||
|
||||
$argumentsA = $objectBuilder->describeArguments();
|
||||
$argumentsB = $objectBuilder->describeArguments();
|
||||
@ -123,10 +62,3 @@ final class MethodObjectBuilderTest extends TestCase
|
||||
self::assertSame($argumentsA, $argumentsB);
|
||||
}
|
||||
}
|
||||
|
||||
final class ObjectWithPrivateNativeConstructor
|
||||
{
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user