feat(aot): 重构动态调用与instanceof解析并新增Symfony模式测试

pull/15/head
韩天峰 2 months ago
parent 035b1b09d7
commit f0e025dad4
  1. 112
      src/Php/CompilerBase.php
  2. 69
      tests/aot/symfony/attribute-loader-match-true-side-effects.phpt
  3. 68
      tests/aot/symfony/autowire-target-nullsafe-coalesce-assign.phpt
  4. 78
      tests/aot/symfony/console-attribute-nullsafe-coalesce-assign.phpt
  5. 35
      tests/aot/symfony/console-question-iterable-cache-closure.phpt
  6. 31
      tests/aot/symfony/container-arguments-array-combine-normalize.phpt
  7. 38
      tests/aot/symfony/controller-reflector-match-unpack.phpt
  8. 35
      tests/aot/symfony/httpfoundation-accept-header-specificity.phpt
  9. 49
      tests/aot/symfony/httpfoundation-json-response-static-factory.phpt
  10. 70
      tests/aot/symfony/mailer-first-class-static-method-array-map.phpt
  11. 42
      tests/aot/symfony/match-true-assignment-condition.phpt
  12. 33
      tests/aot/symfony/match-true-throw-debug-type.phpt
  13. 91
      tests/aot/symfony/messenger-envelope-clone-stamps.phpt
  14. 44
      tests/aot/symfony/messenger-nullsafe-coalesce-throw-chain.phpt
  15. 19
      tests/aot/symfony/php-dumper-indent-lines.phpt
  16. 48
      tests/aot/symfony/rememberme-details-new-static-unpack.phpt
  17. 36
      tests/aot/symfony/routing-expression-provider-dynamic-callable.phpt
  18. 59
      tests/aot/symfony/routing-extra-parameters-recursive-caster.phpt
  19. 42
      tests/aot/symfony/routing-route-coalesce-assign-condition.phpt
  20. 44
      tests/aot/symfony/security-dynamic-instanceof-union-string.phpt
  21. 35
      tests/aot/symfony/static-local-coalesce-assign-ternary-object.phpt

@ -3185,16 +3185,6 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
}
}
protected function hasNamedCallArg(array $args): bool
{
foreach ($args as $arg) {
if ($arg instanceof Node\Arg && $arg->name !== null) {
return true;
}
}
return false;
}
protected function hasUnpackBeforeNamedArg(array $args): bool
{
$hasUnpack = false;
@ -3242,16 +3232,24 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
return false;
}
protected function genCallUserFuncArray(string $callback, array $args, string $funcName = '', string $className = ''): string
{
return 'php::call(' . $this->getFuncPtr('call_user_func_array') . ', '
. Symbol::argList() . '{' . $callback . ', ' . $this->parseCallArgs($args, $funcName, $className, false, true) . '})';
protected function genRuntimeFunctionCall(
string $callable,
array $args,
string $funcName = '',
string $className = '',
bool $separateNamedArgs = true
): string {
return 'php::call(' . $callable . ', ' . $this->parseCallArgs($args, $funcName, $className, $separateNamedArgs) . ')';
}
protected function getFunctionCallbackExpr(string $nativeFn): string
{
$functionDef = $this->getFunction($nativeFn);
return $this->getLiteralString($functionDef->getNamespacedName());
protected function genRuntimeObjectMethodCall(
string $object,
string $method,
array $args,
string $funcName = '',
string $className = ''
): string {
return $object . '.call(' . $method . ', ' . $this->parseCallArgs($args, $funcName, $className) . ')';
}
protected function validateInternalNamedCallArgs(\ReflectionFunctionAbstract $ref, array $callArgs): void
@ -3360,7 +3358,8 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
}
$this->checkNativeCallArgs($expr, $this->getFunction($nativeFn), $expr->args, $name);
if ($this->shouldUseDynamicCallForNativeArgs($nativeFn, $expr->args)) {
return $this->genCallUserFuncArray($this->getFunctionCallbackExpr($nativeFn), $expr->args, $name);
$functionDef = $this->getFunction($nativeFn);
return $this->genRuntimeFunctionCall($this->getFuncPtr($functionDef->getNamespacedName()), $expr->args, $name);
}
try {
return self::PREFIX . $nativeFn . '(' . $this->parseNativeCallArgs($expr->args, $nativeFn) . ')';
@ -3387,12 +3386,8 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
if (empty($expr->args)) {
return 'php::call(' . $fn . ')';
}
if ($this->hasNamedCallArg($expr->args)) {
$callback = $name !== '' ? $this->getLiteralString($name) : $fn;
return $this->genCallUserFuncArray($callback, $expr->args, $name);
}
try {
return 'php::call(' . $fn . ', ' . $this->parseCallArgs($expr->args, $name, '', $name !== '') . ')';
return $this->genRuntimeFunctionCall($fn, $expr->args, $name);
} catch (PlaceHolder) {
return $this->genPlaceHolder($placeHolder);
}
@ -3683,6 +3678,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
{
$list_args = [];
$arrayArgsVar = null;
$argsVar = null;
$namedArgsVar = null;
$namedArgs = [];
$hasNamedArg = false;
@ -3701,8 +3697,13 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
$this->fatalError($arg, 'Cannot use argument unpacking after named arguments');
}
$hasUnpack = true;
$arrayArgs = $this->ensureCallArrayArgs($arrayArgsVar, $list_args);
$this->context->beforeStmtLines[] = $arrayArgs . '.merge(' . $this->parseArrayArg($arg) . ');';
if (!$forceArrayArgs && $separateNamedArgs) {
$callArgs = $this->ensureCallArgs($argsVar, $list_args);
$this->context->beforeStmtLines[] = $callArgs . '.appendUnpacked(' . $this->parseArrayArg($arg) . ');';
} else {
$arrayArgs = $this->ensureCallArrayArgs($arrayArgsVar, $list_args);
$this->context->beforeStmtLines[] = $arrayArgs . '.merge(' . $this->parseArrayArg($arg) . ');';
}
continue;
}
if ($arg->name !== null) {
@ -3819,6 +3820,9 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
$this->addPositionalCallArg($value, $arrayArgsVar, $list_args);
}
if ($argsVar !== null) {
return $namedArgsVar !== null ? $argsVar . ', ' . $namedArgsVar . '.array()' : $argsVar;
}
if ($arrayArgsVar !== null) {
return $namedArgsVar !== null ? $arrayArgsVar . ', ' . $namedArgsVar . '.array()' : $arrayArgsVar;
}
@ -3866,6 +3870,16 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
return $this->genArray($items);
}
protected function ensureCallArgs(?string &$argsVar, array &$listArgs): string
{
if ($argsVar === null) {
$argsVar = $this->genTmpVarName();
$this->context->beforeStmtLines[] = self::TYPE_ARGS . ' ' . $argsVar . '{' . Symbol::argList() . '{' . implode(', ', $listArgs) . '}};';
$listArgs = [];
}
return $argsVar;
}
protected function ensureCallArrayArgs(?string &$arrayArgsVar, array &$listArgs): string
{
if ($arrayArgsVar === null) {
@ -4546,9 +4560,8 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
$this->assertExprCanBeUsedAsValue($expr->expr, 'instanceof operand');
if ($this->isNameExpr($expr->class)) {
$value = $this->parseExprAsValue($expr->expr);
$className = $this->getNamespacedClassName($this->parseIdentifier($expr->class));
$className = $this->getClassEntryPtr($className);
return 'php::instanceOf(' . $value . ', ' . $className . ')';
$classPtr = $this->resolveInstanceofClassPtr($expr->class);
return 'php::instanceOf(' . $value . ', ' . $classPtr . ')';
} else {
[$value, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr->expr);
$tmpVar = $this->addTmpVar(self::TYPE_VAR);
@ -4559,6 +4572,27 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
}
}
protected function resolveInstanceofClassPtr(NodeAbstract $class): string
{
$className = $this->parseIdentifier($class);
if ($className === 'self') {
$className = $this->getFullClassName();
} elseif ($className === 'parent') {
if (!$this->classDef || !$this->classDef->extends) {
$this->fatalError($class, 'Cannot use "parent" when current class scope has no parent');
}
$className = $this->classDef->extends;
} elseif ($className === 'static') {
if (!$this->classDef) {
$this->fatalError($class, 'Cannot use "static" outside a class');
}
return Symbol::getCalledCe();
} else {
$className = $this->getNamespacedClassName($className);
}
return $this->getClassEntryPtr($className);
}
protected function parseCastInt(Expr\Cast\Int_ $node): string
{
$this->assertExprCanBeUsedAsValue($node->expr, 'cast operand');
@ -6171,7 +6205,7 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
$expr->setAttribute('nativeCall', $nativeFunc);
try {
if ($this->shouldUseDynamicCallForNativeArgs($nativeFunc, $expr->args)) {
return $this->genCallUserFuncArray($this->genArray([$object, $method]), $expr->args, $methodName, $class);
return $this->genRuntimeObjectMethodCall($object, $this->getMethodPtr($class, $methodName), $expr->args, $methodName, $class);
}
return $this->parseNativeMethodCall($object, $nativeFunc, $expr->args);
} catch (PlaceHolder) {
@ -6226,12 +6260,9 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
if (empty($expr->args)) {
return $object . '.call(' . $methodPtr . ')';
}
if ($this->hasNamedCallArg($expr->args)) {
return $this->genCallUserFuncArray($this->genArray([$object, $method]), $expr->args, $funcName, $class);
}
try {
$class = empty($class) ? self::DYNAMIC_CALLED_CLASS : $class;
return $object . '.call(' . $methodPtr . ', ' . $this->parseCallArgs($expr->args, $funcName, $class, false) . ')';
return $this->genRuntimeObjectMethodCall($object, $methodPtr, $expr->args, $funcName, $class);
} catch (PlaceHolder) {
return $this->genPlaceHolder($this->genArray([$object, $method]));
}
@ -6320,7 +6351,12 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
if ($nativeFunc) {
try {
if ($this->shouldUseDynamicCallForNativeArgs($nativeFunc, $expr->args)) {
return $this->genCallUserFuncArray($this->genArray($callScope), $expr->args, $method, $class);
return $this->genRuntimeFunctionCall(
$this->getClassEntryPtr($class) . ', ' . $this->getFuncPtr($class . '::' . $method),
$expr->args,
$method,
$class
);
}
$args = $this->parseNativeCallArgs($expr->args, $nativeFunc);
$expr->setAttribute('nativeCall', $nativeFunc);
@ -6357,12 +6393,8 @@ class CompilerBase extends \PhpAot\Core\Translator implements PropertyAccessCont
if (empty($expr->args)) {
return $call . '(' . $fn . ')';
}
if ($this->hasNamedCallArg($expr->args)) {
return $this->genCallUserFuncArray($placeHolder, $expr->args);
}
try {
$callArgs = $this->parseCallArgs($expr->args);
return $call . '(' . $fn . ', ' . $callArgs . ')';
return $this->genRuntimeFunctionCall($fn, $expr->args);
} catch (PlaceHolder) {
return $this->genPlaceHolder($placeHolder);
}

@ -0,0 +1,69 @@
--TEST--
Symfony Serializer pattern: match true used for metadata side effects
--FILE--
<?php
class GroupsAttribute
{
public function __construct(public array $groups)
{
}
}
class IgnoreAttribute
{
}
class MaxDepthAttribute
{
public function __construct(public int $maxDepth)
{
}
}
class Metadata
{
public array $groups = [];
public bool $ignored = false;
public ?int $maxDepth = null;
public function addGroup(string $group): void
{
$this->groups[] = $group;
}
}
function applyAttribute(object $attribute, Metadata $metadata): void
{
match (true) {
$attribute instanceof MaxDepthAttribute => $metadata->maxDepth = $attribute->maxDepth,
$attribute instanceof IgnoreAttribute => $metadata->ignored = true,
$attribute instanceof GroupsAttribute => array_map($metadata->addGroup(...), $attribute->groups),
default => null,
};
}
function main(): void
{
$metadata = new Metadata();
applyAttribute(new GroupsAttribute(['read', 'write']), $metadata);
applyAttribute(new MaxDepthAttribute(3), $metadata);
applyAttribute(new IgnoreAttribute(), $metadata);
var_dump($metadata);
}
?>
--EXPECT--
object(Metadata)#1 (3) {
["groups"]=>
array(2) {
[0]=>
string(4) "read"
[1]=>
string(5) "write"
}
["ignored"]=>
bool(true)
["maxDepth"]=>
int(3)
}

@ -0,0 +1,68 @@
--TEST--
Symfony DI pattern: filtered attribute nullsafe property with coalesce assignment
--FILE--
<?php
class TargetAttribute
{
public function __construct(public ?string $name)
{
}
}
class ServiceReference
{
public function __construct(
private string $name,
private array $attributes,
) {
}
public function getAttributes(): array
{
return $this->attributes;
}
public function getName(): ?string
{
return $this->name;
}
}
function resolveTarget(ServiceReference $reference): array
{
$name = $target = (array_filter($reference->getAttributes(), static fn ($a) => $a instanceof TargetAttribute)[0] ?? null)?->name;
if (null !== $name ??= $reference->getName()) {
return [$name, $target];
}
return ['missing', $target];
}
function main(): void
{
var_dump(resolveTarget(new ServiceReference('fallback', [new TargetAttribute('explicit')])));
var_dump(resolveTarget(new ServiceReference('fallback', [])));
var_dump(resolveTarget(new ServiceReference('fallback', [new stdClass(), new TargetAttribute(null)])));
}
?>
--EXPECT--
array(2) {
[0]=>
string(8) "explicit"
[1]=>
string(8) "explicit"
}
array(2) {
[0]=>
string(8) "fallback"
[1]=>
NULL
}
array(2) {
[0]=>
string(8) "fallback"
[1]=>
NULL
}

@ -0,0 +1,78 @@
--TEST--
Symfony Console pattern: nullsafe attribute values with coalesce assignment
--FILE--
<?php
class CommandAttribute
{
public function __construct(
public ?string $description = null,
public ?string $help = null,
public ?array $usages = null,
) {
}
}
function normalizeCommandMetadata(?CommandAttribute $attribute, array $tag): array
{
$description = null;
$help = null;
$usages = null;
$description ??= $tag['description'] ?? null;
$help ??= $tag['help'] ?? null;
$usages ??= $tag['usages'] ?? null;
if ($help ??= $attribute?->help) {
$help = trim($help);
}
if ($usages ??= $attribute?->usages) {
$usages = array_values($usages);
}
if ($description ??= $attribute?->description) {
$description = strtoupper($description);
}
return [$description, $help, $usages];
}
function main(): void
{
var_dump(normalizeCommandMetadata(new CommandAttribute('from attribute', ' help ', ['a', 'b']), []));
var_dump(normalizeCommandMetadata(new CommandAttribute('ignored', 'ignored', ['x']), ['description' => 'from tag', 'help' => 'tag help']));
var_dump(normalizeCommandMetadata(null, []));
}
?>
--EXPECT--
array(3) {
[0]=>
string(14) "FROM ATTRIBUTE"
[1]=>
string(4) "help"
[2]=>
array(2) {
[0]=>
string(1) "a"
[1]=>
string(1) "b"
}
}
array(3) {
[0]=>
string(8) "FROM TAG"
[1]=>
string(8) "tag help"
[2]=>
array(1) {
[0]=>
string(1) "x"
}
}
array(3) {
[0]=>
NULL
[1]=>
NULL
[2]=>
NULL
}

@ -0,0 +1,35 @@
--TEST--
Symfony Console pattern: closure caches iterable values with coalesce assignment
--FILE--
<?php
function valueCallback(iterable $values): Closure
{
$valueCache = null;
return static function () use (&$valueCache, $values): array {
return $valueCache ??= iterator_to_array($values, false);
};
}
function main(): void
{
$callback = valueCallback(new ArrayIterator(['a' => 'A', 'b' => 'B']));
var_dump($callback());
var_dump($callback());
}
?>
--EXPECT--
array(2) {
[0]=>
string(1) "A"
[1]=>
string(1) "B"
}
array(2) {
[0]=>
string(1) "A"
[1]=>
string(1) "B"
}

@ -0,0 +1,31 @@
--TEST--
Symfony DI pattern: normalize named arguments with array_combine and preg_replace
--FILE--
<?php
function normalizeArguments(array $arguments): array
{
return array_combine(
array_map(static fn ($key) => preg_replace('/^.*\$/', '', $key), array_keys($arguments)),
$arguments,
);
}
function main(): void
{
var_dump(normalizeArguments([
'App\Service $mailer' => 'smtp',
'$logger' => 'file',
'plain' => 'value',
]));
}
?>
--EXPECT--
array(3) {
["mailer"]=>
string(4) "smtp"
["logger"]=>
string(4) "file"
["plain"]=>
string(5) "value"
}

@ -0,0 +1,38 @@
--TEST--
Symfony HttpKernel pattern: controller reflector match with unpacked callable
--FILE--
<?php
class DemoController
{
public function action(): void
{
}
public static function staticAction(): void
{
}
}
function reflectorOf(callable|string $controller): ReflectionFunctionAbstract
{
return match (true) {
is_array($controller) && method_exists(...$controller) => new ReflectionMethod(...$controller),
is_string($controller) && str_contains($controller, '::') => new ReflectionMethod(...explode('::', $controller, 2)),
default => new ReflectionFunction($controller(...)),
};
}
function main(): void
{
$object = new DemoController();
var_dump(reflectorOf([$object, 'action'])->getName());
var_dump(reflectorOf(DemoController::class.'::staticAction')->getName());
var_dump(reflectorOf(static fn () => null)->isClosure());
}
?>
--EXPECT--
string(6) "action"
string(12) "staticAction"
bool(true)

@ -0,0 +1,35 @@
--TEST--
Symfony HttpFoundation pattern: accept header specificity with explode defaults and match
--FILE--
<?php
function specificity(string $rangeValue, int $paramCount, bool $isQueryMedia, bool $isRangeMedia): int
{
if (!$isQueryMedia && !$isRangeMedia) {
return ('*' !== $rangeValue ? 2000 : 1000) + $paramCount;
}
[$rangeType, $rangeSubtype] = explode('/', $rangeValue, 2) + [1 => '*'];
$specificity = match (true) {
'*' !== $rangeSubtype => 3000,
'*' !== $rangeType => 2000,
default => 1000,
};
return $specificity + $paramCount;
}
function main(): void
{
var_dump(specificity('text/plain', 2, true, true));
var_dump(specificity('text/*', 1, true, true));
var_dump(specificity('*', 0, true, true));
var_dump(specificity('application/json', 3, false, false));
}
?>
--EXPECT--
int(3002)
int(2001)
int(1000)
int(2003)

@ -0,0 +1,49 @@
--TEST--
Symfony HttpFoundation pattern: null default new ArrayObject and new static JSON factory
--FILE--
<?php
class JsonResponseLike
{
public mixed $data;
public bool $json;
public function __construct(mixed $data = null, bool $json = false)
{
if ($json && !is_string($data) && !is_numeric($data) && !$data instanceof Stringable) {
throw new TypeError(sprintf('bad data "%s"', get_debug_type($data)));
}
$data ??= new ArrayObject();
$this->data = $data;
$this->json = $json;
}
public static function fromJsonString(string $data): static
{
return new static($data, true);
}
}
class CustomJsonResponseLike extends JsonResponseLike
{
}
function main(): void
{
$response = new JsonResponseLike();
var_dump($response->data instanceof ArrayObject);
var_dump($response->json);
$custom = CustomJsonResponseLike::fromJsonString('{"ok":true}');
var_dump($custom::class);
var_dump($custom->data);
var_dump($custom->json);
}
?>
--EXPECT--
bool(true)
bool(false)
string(22) "CustomJsonResponseLike"
string(11) "{"ok":true}"
bool(true)

@ -0,0 +1,70 @@
--TEST--
Symfony Mailer style self::method(...) callback in array_map
--FILE--
<?php
class Address
{
public function __construct(private string $address, private string $name = '') {}
public function getAddress(): string
{
return $this->address;
}
public function getName(): string
{
return $this->name;
}
}
class PayloadBuilder
{
public static function build(array $to, array $cc, array $bcc): array
{
return [
'to' => array_map(self::encodeEmail(...), $to),
'cc' => array_map(self::encodeEmail(...), $cc),
'bcc' => array_map(self::encodeEmail(...), $bcc),
];
}
private static function encodeEmail(Address $address): array
{
return array_filter(['email' => $address->getAddress(), 'name' => $address->getName()]);
}
}
function main(): void
{
$payload = PayloadBuilder::build(
[new Address('a@example.com', 'Alice')],
[new Address('b@example.com')],
[]
);
var_dump($payload);
}
?>
--EXPECT--
array(3) {
["to"]=>
array(1) {
[0]=>
array(2) {
["email"]=>
string(13) "a@example.com"
["name"]=>
string(5) "Alice"
}
}
["cc"]=>
array(1) {
[0]=>
array(1) {
["email"]=>
string(13) "b@example.com"
}
}
["bcc"]=>
array(0) {
}
}

@ -0,0 +1,42 @@
--TEST--
Symfony HttpKernel pattern: match true with assignment inside condition
--FILE--
<?php
class Metadata
{
public function __construct(private string $value)
{
}
public function evaluate(): string
{
return 'metadata:'.$this->value;
}
}
class EventWithMetadata
{
public ?Metadata $controllerMetadata = null;
}
function evaluate(EventWithMetadata $event): string
{
return match (true) {
($m = $event->controllerMetadata ?? null) instanceof Metadata => $m->evaluate(),
default => 'none',
};
}
function main(): void
{
$event = new EventWithMetadata();
var_dump(evaluate($event));
$event->controllerMetadata = new Metadata('ok');
var_dump(evaluate($event));
}
?>
--EXPECT--
string(4) "none"
string(11) "metadata:ok"

@ -0,0 +1,33 @@
--TEST--
Symfony Config pattern: match true formats scalar values or throws by debug type
--FILE--
<?php
function displayValue(mixed $value): string|int
{
return match (true) {
is_int($value) => $value,
is_string($value) => sprintf('"%s"', $value),
is_bool($value) => throw new InvalidArgumentException(sprintf('unsupported "%s"', get_debug_type($value))),
default => sprintf('of type "%s"', get_debug_type($value)),
};
}
function main(): void
{
var_dump(displayValue(7));
var_dump(displayValue('name'));
var_dump(displayValue([]));
try {
displayValue(false);
} catch (InvalidArgumentException $e) {
echo $e->getMessage(), "\n";
}
}
?>
--EXPECT--
int(7)
string(6) ""name""
string(15) "of type "array""
unsupported "bool"

@ -0,0 +1,91 @@
--TEST--
Symfony style Envelope clone with variadic stamps and dynamic stamp removal
--FILE--
<?php
interface StampInterface {}
interface RemovableStampInterface extends StampInterface {}
class FirstStamp implements RemovableStampInterface
{
public function __construct(public string $name) {}
}
class SecondStamp implements StampInterface
{
public function __construct(public string $name) {}
}
final class Envelope
{
private array $stamps = [];
public function __construct(private object $message, array $stamps = [])
{
foreach ($stamps as $stamp) {
$this->stamps[$stamp::class][] = $stamp;
}
}
public static function wrap(object $message, array $stamps = []): self
{
$envelope = $message instanceof self ? $message : new self($message);
return $envelope->with(...$stamps);
}
public function with(StampInterface ...$stamps): static
{
$cloned = clone $this;
foreach ($stamps as $stamp) {
$cloned->stamps[$stamp::class][] = $stamp;
}
return $cloned;
}
public function withoutStampsOfType(string $type): self
{
$cloned = clone $this;
foreach ($cloned->stamps as $class => $stamps) {
if ($class === $type || is_subclass_of($class, $type)) {
unset($cloned->stamps[$class]);
}
}
return $cloned;
}
public function all(?string $stampFqcn = null): array
{
if (null !== $stampFqcn) {
return $this->stamps[$stampFqcn] ?? [];
}
return $this->stamps;
}
}
function main(): void
{
$envelope = Envelope::wrap(new stdClass(), [new FirstStamp('a'), new SecondStamp('b')]);
$filtered = $envelope->withoutStampsOfType(RemovableStampInterface::class);
var_dump(array_keys($envelope->all()));
var_dump(array_keys($filtered->all()));
var_dump($filtered->all(FirstStamp::class));
}
?>
--EXPECT--
array(2) {
[0]=>
string(10) "FirstStamp"
[1]=>
string(11) "SecondStamp"
}
array(1) {
[0]=>
string(11) "SecondStamp"
}
array(0) {
}

@ -0,0 +1,44 @@
--TEST--
Symfony Messenger style nullsafe chain with coalesce throw
--FILE--
<?php
class MissingStampException extends Exception {}
class TransportStamp
{
public function __construct(private string $id) {}
public function getId(): string
{
return $this->id;
}
}
class Envelope
{
public function __construct(private ?TransportStamp $stamp) {}
public function last(string $class): ?object
{
return $this->stamp instanceof $class ? $this->stamp : null;
}
}
function stampId(Envelope $envelope): string
{
return $envelope->last(TransportStamp::class)?->getId() ?? throw new MissingStampException('No stamp found.');
}
function main(): void
{
var_dump(stampId(new Envelope(new TransportStamp('abc'))));
try {
stampId(new Envelope(null));
} catch (MissingStampException $e) {
var_dump($e->getMessage());
}
}
?>
--EXPECT--
string(3) "abc"
string(15) "No stamp found."

@ -0,0 +1,19 @@
--TEST--
Symfony DI pattern: indent generated code lines with explode array_map implode
--FILE--
<?php
function indentCode(string $code): string
{
return implode("\n", array_map(static fn ($line) => $line ? ' '.$line : $line, explode("\n", $code)));
}
function main(): void
{
var_dump(indentCode("first\n\nsecond"));
}
?>
--EXPECT--
string(21) " first
second"

@ -0,0 +1,48 @@
--TEST--
Symfony style RememberMeDetails new static(...array after unset)
--FILE--
<?php
class AuthenticationException extends Exception {}
class RememberMeDetails
{
public const COOKIE_DELIMITER = ':';
public function __construct(
public string $userIdentifier,
public int $expires,
public string $value,
) {
var_dump(get_debug_type($userIdentifier), get_debug_type($expires), get_debug_type($value));
}
public static function fromRawCookie(string $rawCookie): self
{
if (!str_contains($rawCookie, self::COOKIE_DELIMITER)) {
$rawCookie = 'prefix'.self::COOKIE_DELIMITER.$rawCookie;
}
$cookieParts = explode(self::COOKIE_DELIMITER, $rawCookie, 4);
if (4 !== count($cookieParts)) {
throw new AuthenticationException('The cookie contains invalid data.');
}
unset($cookieParts[0]);
return new static(...$cookieParts);
}
}
function main(): void
{
$raw = 'prefix:user-name:12345:series-token';
$details = RememberMeDetails::fromRawCookie($raw);
var_dump($details::class);
}
?>
--EXPECT--
string(6) "string"
string(3) "int"
string(6) "string"
string(17) "RememberMeDetails"

@ -0,0 +1,36 @@
--TEST--
Symfony Routing pattern: provider returns callable invoked with unpacked args
--FILE--
<?php
class FunctionProvider
{
/** @var array<string, callable> */
private array $functions = [];
public function set(string $name, callable $function): void
{
$this->functions[$name] = $function;
}
public function get(string $name): callable
{
return $this->functions[$name];
}
}
function evaluate(FunctionProvider $provider, string $function, array $args): string
{
return $provider->get($function)(...$args);
}
function main(): void
{
$provider = new FunctionProvider();
$provider->set('format', static fn (string $prefix, string $value): string => $prefix.':'.strtoupper($value));
var_dump(evaluate($provider, 'format', ['name', 'symfony']));
}
?>
--EXPECT--
string(12) "name:SYMFONY"

@ -0,0 +1,59 @@
--TEST--
Symfony Routing pattern: extra parameters diff and recursive object caster
--SKIPIF--
<?php exit('skip closures do not support reference parameters'); ?>
--FILE--
<?php
class Slug
{
public function __construct(private string $value)
{
}
public function __toString(): string
{
return $this->value;
}
}
function normalizeExtra(array $parameters, array $variables, array $defaults, array $queryParameters): array
{
$extra = array_udiff_assoc(array_diff_key($parameters, $variables), $defaults, static fn ($a, $b) => $a == $b ? 0 : 1);
$extra = array_replace($extra, $queryParameters);
array_walk_recursive($extra, $caster = static function (&$value) use (&$caster): void {
if (is_object($value)) {
$value = (string) $value;
} elseif (is_array($value)) {
array_walk_recursive($value, $caster);
}
});
return $extra;
}
function main(): void
{
var_dump(normalizeExtra(
['id' => 10, 'page' => 1, 'slug' => new Slug('hello'), 'tags' => [new Slug('a'), new Slug('b')]],
['id' => true],
['page' => 1, 'slug' => 'old'],
['q' => new Slug('search')]
));
}
?>
--EXPECT--
array(3) {
["slug"]=>
string(5) "hello"
["tags"]=>
array(2) {
[0]=>
string(1) "a"
[1]=>
string(1) "b"
}
["q"]=>
string(6) "search"
}

@ -0,0 +1,42 @@
--TEST--
Symfony Routing pattern: null comparison with route coalesce assignment
--FILE--
<?php
class RouteStore
{
public function __construct(private array $routes)
{
}
public function get(string $name): ?string
{
echo "lookup:$name\n";
return $this->routes[$name] ?? null;
}
}
function generate(RouteStore $routes, string $name): string
{
$route = null;
if (null === $route ??= $routes->get($name)) {
return 'missing';
}
return 'route:'.$route;
}
function main(): void
{
$routes = new RouteStore(['home' => '/']);
var_dump(generate($routes, 'home'));
var_dump(generate($routes, 'missing'));
}
?>
--EXPECT--
lookup:home
string(7) "route:/"
lookup:missing
string(7) "missing"

@ -0,0 +1,44 @@
--TEST--
Symfony style dynamic instanceof against pipe-separated type string
--FILE--
<?php
interface UserInterface {}
class AdminUser implements UserInterface {}
class GuestUser {}
function resolveUser(object $user, ?string $type): array
{
if (null === $type || $user instanceof ($type)) {
return [$user::class];
}
$types = explode('|', $type);
foreach ($types as $candidate) {
if ($user instanceof $candidate) {
return [$user::class, $candidate];
}
}
return [];
}
function main(): void
{
var_dump(resolveUser(new AdminUser(), UserInterface::class));
var_dump(resolveUser(new AdminUser(), GuestUser::class.'|'.UserInterface::class));
var_dump(resolveUser(new GuestUser(), UserInterface::class));
}
?>
--EXPECT--
array(1) {
[0]=>
string(9) "AdminUser"
}
array(2) {
[0]=>
string(9) "AdminUser"
[1]=>
string(13) "UserInterface"
}
array(0) {
}

@ -0,0 +1,35 @@
--TEST--
Symfony Serializer pattern: static local coalesce assignment with ternary object cache
--FILE--
<?php
class LocalResolver
{
public function resolve(string $value): string
{
return 'resolved:'.$value;
}
}
function resolveWhenAvailable(?string $value, bool $enabled): ?string
{
static $resolver;
if (null !== $value && $resolver ??= $enabled && class_exists(LocalResolver::class) ? new LocalResolver() : false) {
return $resolver->resolve($value);
}
return null;
}
function main(): void
{
var_dump(resolveWhenAvailable(null, true));
var_dump(resolveWhenAvailable('first', true));
var_dump(resolveWhenAvailable('second', false));
}
?>
--EXPECT--
NULL
string(14) "resolved:first"
string(15) "resolved:second"
Loading…
Cancel
Save