diff --git a/src/Attribute/StaticMethodConstructor.php b/src/Attribute/StaticMethodConstructor.php index d448f66..75e1f9e 100644 --- a/src/Attribute/StaticMethodConstructor.php +++ b/src/Attribute/StaticMethodConstructor.php @@ -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())]; } } diff --git a/src/Definition/FunctionDefinition.php b/src/Definition/FunctionDefinition.php index 4ac6bd1..0eac318 100644 --- a/src/Definition/FunctionDefinition.php +++ b/src/Definition/FunctionDefinition.php @@ -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[] diff --git a/src/Definition/Repository/Cache/Compiler/FunctionDefinitionCompiler.php b/src/Definition/Repository/Cache/Compiler/FunctionDefinitionCompiler.php index 02ad69d..5a48ecc 100644 --- a/src/Definition/Repository/Cache/Compiler/FunctionDefinitionCompiler.php +++ b/src/Definition/Repository/Cache/Compiler/FunctionDefinitionCompiler.php @@ -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 ) diff --git a/src/Definition/Repository/Reflection/ReflectionFunctionDefinitionRepository.php b/src/Definition/Repository/Reflection/ReflectionFunctionDefinitionRepository.php index 3468f88..20eaba4 100644 --- a/src/Definition/Repository/Reflection/ReflectionFunctionDefinitionRepository.php +++ b/src/Definition/Repository/Reflection/ReflectionFunctionDefinitionRepository.php @@ -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 ); diff --git a/src/Mapper/Object/Exception/ConstructorMethodIsNotPublic.php b/src/Mapper/Object/Exception/ConstructorMethodIsNotPublic.php deleted file mode 100644 index 9b2efed..0000000 --- a/src/Mapper/Object/Exception/ConstructorMethodIsNotPublic.php +++ /dev/null @@ -1,22 +0,0 @@ -name() === '__construct' - ? "The constructor of the class `{$class->name()}` is not public." - : "The named constructor `{$method->signature()}` is not public."; - - parent::__construct($message, 1630937169); - } -} diff --git a/src/Mapper/Object/Exception/ConstructorMethodIsNotStatic.php b/src/Mapper/Object/Exception/ConstructorMethodIsNotStatic.php deleted file mode 100644 index d13958c..0000000 --- a/src/Mapper/Object/Exception/ConstructorMethodIsNotStatic.php +++ /dev/null @@ -1,20 +0,0 @@ -signature()}`: it is neither the constructor nor a static constructor.", - 1634044370 - ); - } -} diff --git a/src/Mapper/Object/Exception/MethodNotFound.php b/src/Mapper/Object/Exception/MethodNotFound.php deleted file mode 100644 index afb5ddd..0000000 --- a/src/Mapper/Object/Exception/MethodNotFound.php +++ /dev/null @@ -1,20 +0,0 @@ -name()}`.", - 1634044209 - ); - } -} diff --git a/src/Mapper/Object/Factory/ConstructorObjectBuilderFactory.php b/src/Mapper/Object/Factory/ConstructorObjectBuilderFactory.php index ab51650..dd7453e 100644 --- a/src/Mapper/Object/Factory/ConstructorObjectBuilderFactory.php +++ b/src/Mapper/Object/Factory/ConstructorObjectBuilderFactory.php @@ -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() ) { diff --git a/src/Mapper/Object/MethodObjectBuilder.php b/src/Mapper/Object/MethodObjectBuilder.php index 1b851a1..66df00e 100644 --- a/src/Mapper/Object/MethodObjectBuilder.php +++ b/src/Mapper/Object/MethodObjectBuilder.php @@ -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()"; } } diff --git a/tests/Fake/Definition/FakeFunctionDefinition.php b/tests/Fake/Definition/FakeFunctionDefinition.php index 1011e37..32ee1c6 100644 --- a/tests/Fake/Definition/FakeFunctionDefinition.php +++ b/tests/Fake/Definition/FakeFunctionDefinition.php @@ -20,6 +20,7 @@ final class FakeFunctionDefinition 'foo:42-1337', $fileName ?? 'foo/bar', stdClass::class, + true, new Parameters( new ParameterDefinition( 'bar', diff --git a/tests/Functional/Definition/Repository/Cache/Compiler/FunctionDefinitionCompilerTest.php b/tests/Functional/Definition/Repository/Cache/Compiler/FunctionDefinitionCompilerTest.php index 4e35b15..dad59cb 100644 --- a/tests/Functional/Definition/Repository/Cache/Compiler/FunctionDefinitionCompilerTest.php +++ b/tests/Functional/Definition/Repository/Cache/Compiler/FunctionDefinitionCompilerTest.php @@ -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()); diff --git a/tests/Integration/Mapping/ConstructorRegistrationMappingTest.php b/tests/Integration/Mapping/ConstructorRegistrationMappingTest.php index 6c05b6d..f504710 100644 --- a/tests/Integration/Mapping/ConstructorRegistrationMappingTest.php +++ b/tests/Integration/Mapping/ConstructorRegistrationMappingTest.php @@ -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 +{ +} diff --git a/tests/Unit/Mapper/Object/MethodObjectBuilderTest.php b/tests/Unit/Mapper/Object/MethodObjectBuilderTest.php index 559dff7..df6e5f5 100644 --- a/tests/Unit/Mapper/Object/MethodObjectBuilderTest.php +++ b/tests/Unit/Mapper/Object/MethodObjectBuilderTest.php @@ -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() - { - } -}