mirror of
https://github.com/danog/Valinor.git
synced 2024-11-26 20:24:40 +01:00
fix: properly handle static anonymous functions
The `MethodObjectBuilder` was incorrectly used when a registered constructor is a static anonymous functions — it was handled like a static method closure `Class::method(...)` and would yield errors like this: ``` Error: Call to undefined method stdClass::CuyZ\Valinor\Tests\Integration\Mapping\{closure}() ``` PHP Reflection does not provide any way of telling static functions and closures of static methods apart, other than checking for the name `{closure}`. We check that `{closure}` is actually the last part of the fully-qualified name, instead of just checking that the string ends with `{closure}`.
This commit is contained in:
parent
0e8f12e5f7
commit
c009ab98cc
@ -22,6 +22,8 @@ final class FunctionDefinition
|
||||
|
||||
private bool $isStatic;
|
||||
|
||||
private bool $isClosure;
|
||||
|
||||
private Parameters $parameters;
|
||||
|
||||
private Type $returnType;
|
||||
@ -36,6 +38,7 @@ final class FunctionDefinition
|
||||
?string $fileName,
|
||||
?string $class,
|
||||
bool $isStatic,
|
||||
bool $isClosure,
|
||||
Parameters $parameters,
|
||||
Type $returnType
|
||||
) {
|
||||
@ -45,6 +48,7 @@ final class FunctionDefinition
|
||||
$this->fileName = $fileName;
|
||||
$this->class = $class;
|
||||
$this->isStatic = $isStatic;
|
||||
$this->isClosure = $isClosure;
|
||||
$this->parameters = $parameters;
|
||||
$this->returnType = $returnType;
|
||||
}
|
||||
@ -82,6 +86,11 @@ final class FunctionDefinition
|
||||
return $this->isStatic;
|
||||
}
|
||||
|
||||
public function isClosure(): bool
|
||||
{
|
||||
return $this->isClosure;
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return Parameters
|
||||
* @return Parameters&ParameterDefinition[]
|
||||
|
@ -40,6 +40,7 @@ final class FunctionDefinitionCompiler implements CacheCompiler
|
||||
$fileName = var_export($value->fileName(), true);
|
||||
$class = var_export($value->class(), true);
|
||||
$isStatic = var_export($value->isStatic(), true);
|
||||
$isClosure = var_export($value->isClosure(), true);
|
||||
$parameters = implode(', ', $parameters);
|
||||
$returnType = $this->typeCompiler->compile($value->returnType());
|
||||
|
||||
@ -51,6 +52,7 @@ final class FunctionDefinitionCompiler implements CacheCompiler
|
||||
$fileName,
|
||||
$class,
|
||||
$isStatic,
|
||||
$isClosure,
|
||||
new \CuyZ\Valinor\Definition\Parameters($parameters),
|
||||
$returnType
|
||||
)
|
||||
|
@ -12,6 +12,7 @@ use CuyZ\Valinor\Type\Parser\Factory\Specifications\AliasSpecification;
|
||||
use CuyZ\Valinor\Type\Parser\Factory\Specifications\ClassContextSpecification;
|
||||
use CuyZ\Valinor\Type\Parser\Factory\Specifications\HandleClassGenericSpecification;
|
||||
use CuyZ\Valinor\Type\Parser\Factory\TypeParserFactory;
|
||||
use CuyZ\Valinor\Utility\Polyfill;
|
||||
use CuyZ\Valinor\Utility\Reflection\Reflection;
|
||||
use ReflectionFunction;
|
||||
use ReflectionParameter;
|
||||
@ -44,17 +45,20 @@ final class ReflectionFunctionDefinitionRepository implements FunctionDefinition
|
||||
$reflection->getParameters()
|
||||
);
|
||||
|
||||
$name = $reflection->getName();
|
||||
$class = $reflection->getClosureScopeClass();
|
||||
$returnType = $typeResolver->resolveType($reflection);
|
||||
$isClosure = $name === '{closure}' || Polyfill::str_ends_with($name, '\\{closure}');
|
||||
|
||||
return new FunctionDefinition(
|
||||
$reflection->getName(),
|
||||
$name,
|
||||
Reflection::signature($reflection),
|
||||
$this->attributesRepository->for($reflection),
|
||||
$reflection->getFileName() ?: null,
|
||||
// @PHP 8.0 nullsafe operator
|
||||
$class ? $class->name : null,
|
||||
$reflection->getClosureThis() === null,
|
||||
$isClosure,
|
||||
new Parameters(...$parameters),
|
||||
$returnType
|
||||
);
|
||||
|
@ -83,7 +83,7 @@ final class ConstructorObjectBuilderFactory implements ObjectBuilderFactory
|
||||
$definition = $function->definition();
|
||||
$functionClass = $definition->class();
|
||||
|
||||
if ($functionClass && $definition->isStatic()) {
|
||||
if ($functionClass && $definition->isStatic() && ! $definition->isClosure()) {
|
||||
$builders[] = new MethodObjectBuilder($className, $definition->name(), $definition->parameters());
|
||||
} else {
|
||||
$builders[] = new FunctionObjectBuilder($function, $classType);
|
||||
|
@ -22,6 +22,7 @@ final class FakeFunctionDefinition
|
||||
$fileName ?? 'foo/bar',
|
||||
stdClass::class,
|
||||
true,
|
||||
true,
|
||||
new Parameters(
|
||||
new ParameterDefinition(
|
||||
'bar',
|
||||
|
@ -35,6 +35,7 @@ final class FunctionDefinitionCompilerTest extends TestCase
|
||||
'foo/bar',
|
||||
stdClass::class,
|
||||
true,
|
||||
true,
|
||||
new Parameters(
|
||||
new ParameterDefinition(
|
||||
'bar',
|
||||
@ -57,6 +58,7 @@ final class FunctionDefinitionCompilerTest extends TestCase
|
||||
self::assertSame('foo:42-1337', $compiledFunction->signature());
|
||||
self::assertSame('foo/bar', $compiledFunction->fileName());
|
||||
self::assertSame(true, $compiledFunction->isStatic());
|
||||
self::assertSame(true, $compiledFunction->isClosure());
|
||||
self::assertSame(stdClass::class, $compiledFunction->class());
|
||||
self::assertTrue($compiledFunction->parameters()->has('bar'));
|
||||
self::assertInstanceOf(NativeStringType::class, $compiledFunction->returnType());
|
||||
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace CuyZ\Valinor\Tests\Integration\Mapping;
|
||||
|
||||
use Closure;
|
||||
use CuyZ\Valinor\Mapper\MappingError;
|
||||
use CuyZ\Valinor\Mapper\Object\DynamicConstructor;
|
||||
use CuyZ\Valinor\Mapper\Object\Exception\CannotInstantiateObject;
|
||||
@ -37,6 +38,38 @@ final class ConstructorRegistrationMappingTest extends IntegrationTest
|
||||
self::assertSame($object, $result);
|
||||
}
|
||||
|
||||
public function test_registered_static_anonymous_function_constructor_is_used(): void
|
||||
{
|
||||
$object = new stdClass();
|
||||
|
||||
try {
|
||||
$result = (new MapperBuilder())
|
||||
->registerConstructor(static fn (): stdClass => $object)
|
||||
->mapper()
|
||||
->map(stdClass::class, []);
|
||||
} catch (MappingError $error) {
|
||||
$this->mappingFail($error);
|
||||
}
|
||||
|
||||
self::assertSame($object, $result);
|
||||
}
|
||||
|
||||
public function test_registered_anonymous_function_from_static_scope_constructor_is_used(): void
|
||||
{
|
||||
$object = new stdClass();
|
||||
|
||||
try {
|
||||
$result = (new MapperBuilder())
|
||||
->registerConstructor(SomeClassProvidingStaticClosure::getConstructor($object))
|
||||
->mapper()
|
||||
->map(stdClass::class, []);
|
||||
} catch (MappingError $error) {
|
||||
$this->mappingFail($error);
|
||||
}
|
||||
|
||||
self::assertSame($object, $result);
|
||||
}
|
||||
|
||||
public function test_registered_anonymous_function_constructor_with_docblock_is_used(): void
|
||||
{
|
||||
$object = new stdClass();
|
||||
@ -780,3 +813,11 @@ final class SomeClassWithBothInheritedStaticConstructors
|
||||
|
||||
public SomeOtherClassWithInheritedStaticConstructor $someOtherChild;
|
||||
}
|
||||
|
||||
final class SomeClassProvidingStaticClosure
|
||||
{
|
||||
public static function getConstructor(stdClass $object): Closure
|
||||
{
|
||||
return fn (): stdClass => $object;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user