-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add CallbackVisitor to handle callback filters
- Loading branch information
matt
committed
Mar 2, 2024
1 parent
e7f853f
commit fd97dca
Showing
6 changed files
with
306 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Kynx\Laminas\FormShape\Filter; | ||
|
||
use Closure; | ||
use Kynx\Laminas\FormShape\FilterVisitorInterface; | ||
use Laminas\Filter\Callback; | ||
use Laminas\Filter\FilterInterface; | ||
use Psalm\Type\Atomic; | ||
use Psalm\Type\Atomic\TNamedObject; | ||
use Psalm\Type\Union; | ||
use ReflectionClass; | ||
use ReflectionException; | ||
use ReflectionFunction; | ||
use ReflectionFunctionAbstract; | ||
use ReflectionIntersectionType; | ||
use ReflectionNamedType; | ||
use ReflectionType; | ||
use ReflectionUnionType; | ||
|
||
use function array_combine; | ||
use function array_map; | ||
use function array_merge; | ||
use function array_shift; | ||
use function assert; | ||
use function count; | ||
use function is_array; | ||
use function is_object; | ||
use function is_string; | ||
|
||
final readonly class CallbackVisitor implements FilterVisitorInterface | ||
{ | ||
public function visit(FilterInterface $filter, Union $previous): Union | ||
{ | ||
if (! $filter instanceof Callback) { | ||
return $previous; | ||
} | ||
|
||
$callable = $filter->getCallback(); | ||
$union = match (true) { | ||
$callable instanceof Closure => $this->getClosureReturnType($callable), | ||
is_array($callable) => $this->getArrayReturnType($callable), | ||
is_object($callable) => $this->getInvokableReturnType($callable), | ||
is_string($callable) => $this->getStringReturnType($callable), | ||
}; | ||
|
||
return $union ?? $previous; | ||
} | ||
|
||
/** | ||
* @param array{0: class-string|object, 1: string} $callable | ||
*/ | ||
private function getArrayReturnType(array $callable): ?Union | ||
{ | ||
try { | ||
$reflection = new ReflectionClass($callable[0]); | ||
$method = $reflection->getMethod($callable[1]); | ||
} catch (ReflectionException) { | ||
return null; | ||
} | ||
|
||
return $this->getUnion($this->getReturnType($method), $reflection); | ||
} | ||
|
||
private function getInvokableReturnType(object $invokable): ?Union | ||
{ | ||
try { | ||
$reflection = new ReflectionClass($invokable); | ||
$method = $reflection->getMethod('__invoke'); | ||
} catch (ReflectionException) { | ||
return null; | ||
} | ||
|
||
return $this->getUnion($this->getReturnType($method), $reflection); | ||
} | ||
|
||
/** | ||
* @param callable-string $function | ||
*/ | ||
private function getStringReturnType(string $function): ?Union | ||
{ | ||
try { | ||
$reflection = new ReflectionFunction($function); | ||
} catch (ReflectionException) { | ||
return null; | ||
} | ||
|
||
return $this->getUnion($this->getReturnType($reflection)); | ||
} | ||
|
||
private function getClosureReturnType(Closure $closure): ?Union | ||
{ | ||
try { | ||
$reflection = new ReflectionFunction($closure); | ||
} catch (ReflectionException) { | ||
return null; | ||
} | ||
|
||
return $this->getUnion($this->getReturnType($reflection)); | ||
} | ||
|
||
private function getReturnType(ReflectionFunctionAbstract $function): ?ReflectionType | ||
{ | ||
if ($function->hasReturnType()) { | ||
return $function->getReturnType(); | ||
} | ||
|
||
return $function->getTentativeReturnType(); | ||
} | ||
|
||
private function getUnion(?ReflectionType $type, ?ReflectionClass $self = null): ?Union | ||
{ | ||
if ($type === null) { | ||
return null; | ||
} | ||
|
||
$types = []; | ||
if ($type instanceof ReflectionIntersectionType) { | ||
/** @var array<TNamedObject> $intersection */ | ||
$intersection = self::getAtomicTypes($type, $self); | ||
$first = array_shift($intersection); | ||
$keys = array_map(static fn (TNamedObject $type): string => $type->getKey(), $intersection); | ||
$types = [ | ||
new TNamedObject( | ||
$first->value, | ||
false, | ||
false, | ||
array_combine($keys, $intersection) | ||
), | ||
]; | ||
} elseif ($type instanceof ReflectionUnionType) { | ||
$types = self::getAtomicTypes($type, $self); | ||
} else { | ||
assert($type instanceof ReflectionNamedType); | ||
$types[] = self::getAtomicType($type, $self); | ||
} | ||
|
||
assert(count($types) > 0); | ||
return new Union($types); | ||
} | ||
|
||
/** | ||
* @return array<string, Atomic> | ||
*/ | ||
private static function getAtomicTypes( | ||
ReflectionIntersectionType|ReflectionUnionType $type, | ||
?ReflectionClass $self | ||
): array { | ||
$types = []; | ||
foreach ($type->getTypes() as $subType) { | ||
if ($subType instanceof ReflectionIntersectionType) { | ||
$types = array_merge($types, self::getAtomicTypes($subType, $self)); | ||
} else { | ||
$atomicType = self::getAtomicType($subType, $self); | ||
$types[$atomicType->getKey()] = $atomicType; | ||
} | ||
} | ||
|
||
return $types; | ||
} | ||
|
||
private static function getAtomicType(ReflectionNamedType $type, ?ReflectionClass $self): Atomic | ||
{ | ||
if ($self !== null) { | ||
$parent = $self->getParentClass(); | ||
$atomic = match ($type->getName()) { | ||
'parent' => new TNamedObject($parent->getName()), | ||
'self', 'static' => new TNamedObject($self->getName()), | ||
default => null, | ||
}; | ||
if ($atomic) { | ||
return $atomic; | ||
} | ||
} | ||
|
||
return Atomic::create($type->getName()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace KynxTest\Laminas\FormShape\Filter; | ||
|
||
use DateTime; | ||
use DateTimeImmutable; | ||
use Kynx\Laminas\FormShape\Filter\CallbackVisitor; | ||
use Kynx\Laminas\FormShape\FilterVisitorInterface; | ||
use Laminas\Filter\Callback; | ||
use PHPUnit\Framework\Attributes\CoversClass; | ||
use PHPUnit\Framework\MockObject\Stub; | ||
use Psalm\Type\Atomic\TInt; | ||
use Psalm\Type\Atomic\TNamedObject; | ||
use Psalm\Type\Atomic\TString; | ||
|
||
#[CoversClass(CallbackVisitor::class)] | ||
final class CallbackVisitorTest extends AbstractFilterVisitorTestCase | ||
{ | ||
public static function visitProvider(): array | ||
{ | ||
/** @psalm-suppress MissingClosureReturnType */ | ||
$noReturn = static fn () => 123; | ||
$closure = static fn (): int => 123; | ||
$union = static fn (): DateTime|DateTimeImmutable => new DateTimeImmutable('now'); | ||
$intersection = static fn (): FilterVisitorInterface&Stub => self::createStub(FilterVisitorInterface::class); | ||
$invokable = new class () { | ||
public function __invoke(): int | ||
{ | ||
return 123; | ||
} | ||
}; | ||
$callable = new class () { | ||
public function filter(): int | ||
{ | ||
return 123; | ||
} | ||
}; | ||
$self = new class () { | ||
public function __invoke(): self | ||
{ | ||
return $this; | ||
} | ||
}; | ||
$static = new class () { | ||
public function __invoke(): static | ||
{ | ||
return $this; | ||
} | ||
}; | ||
$parent = new class () extends DateTimeImmutable { | ||
public function __invoke(): parent | ||
{ | ||
return new DateTimeImmutable(); | ||
} | ||
}; | ||
|
||
return [ | ||
'no return' => [new Callback($noReturn), [new TString()], [new TString()]], | ||
'closure' => [new Callback($closure), [new TString()], [new TInt()]], | ||
'invokable' => [new Callback($invokable), [new TString()], [new TInt()]], | ||
'array' => [new Callback([$callable, 'filter']), [new TString()], [new TInt()]], | ||
'string' => [new Callback('intval'), [new TString()], [new TInt()]], | ||
'self' => [new Callback($self), [new TString()], [new TNamedObject($self::class)]], | ||
'static' => [new Callback($static), [new TString()], [new TNamedObject($static::class)]], | ||
'parent' => [ | ||
new Callback($parent), | ||
[new TString()], | ||
[new TNamedObject(DateTimeImmutable::class)], | ||
], | ||
'union' => [ | ||
new Callback($union), | ||
[new TString()], | ||
[new TNamedObject(DateTime::class), new TNamedObject(DateTimeImmutable::class)], | ||
], | ||
'intersection' => [ | ||
new Callback($intersection), | ||
[new TString()], | ||
[ | ||
new TNamedObject( | ||
FilterVisitorInterface::class, | ||
false, | ||
false, | ||
[Stub::class => new TNamedObject(Stub::class)] | ||
), | ||
], | ||
], | ||
]; | ||
} | ||
|
||
protected function getVisitor(): FilterVisitorInterface | ||
{ | ||
return new CallbackVisitor(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters