From f0e025dad498876b1989a1d3699aaa7d146b610d Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Wed, 8 Jul 2026 11:20:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(aot):=20=E9=87=8D=E6=9E=84=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E8=B0=83=E7=94=A8=E4=B8=8Einstanceof=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E5=B9=B6=E6=96=B0=E5=A2=9ESymfony=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Php/CompilerBase.php | 112 +++++++++++------- ...ribute-loader-match-true-side-effects.phpt | 69 +++++++++++ ...owire-target-nullsafe-coalesce-assign.phpt | 68 +++++++++++ ...le-attribute-nullsafe-coalesce-assign.phpt | 78 ++++++++++++ ...nsole-question-iterable-cache-closure.phpt | 35 ++++++ ...ner-arguments-array-combine-normalize.phpt | 31 +++++ .../controller-reflector-match-unpack.phpt | 38 ++++++ ...pfoundation-accept-header-specificity.phpt | 35 ++++++ ...undation-json-response-static-factory.phpt | 49 ++++++++ ...r-first-class-static-method-array-map.phpt | 70 +++++++++++ .../match-true-assignment-condition.phpt | 42 +++++++ .../symfony/match-true-throw-debug-type.phpt | 33 ++++++ .../messenger-envelope-clone-stamps.phpt | 91 ++++++++++++++ ...ssenger-nullsafe-coalesce-throw-chain.phpt | 44 +++++++ .../aot/symfony/php-dumper-indent-lines.phpt | 19 +++ .../rememberme-details-new-static-unpack.phpt | 48 ++++++++ ...-expression-provider-dynamic-callable.phpt | 36 ++++++ ...ing-extra-parameters-recursive-caster.phpt | 59 +++++++++ ...uting-route-coalesce-assign-condition.phpt | 42 +++++++ ...urity-dynamic-instanceof-union-string.phpt | 44 +++++++ ...-local-coalesce-assign-ternary-object.phpt | 35 ++++++ 21 files changed, 1038 insertions(+), 40 deletions(-) create mode 100644 tests/aot/symfony/attribute-loader-match-true-side-effects.phpt create mode 100644 tests/aot/symfony/autowire-target-nullsafe-coalesce-assign.phpt create mode 100644 tests/aot/symfony/console-attribute-nullsafe-coalesce-assign.phpt create mode 100644 tests/aot/symfony/console-question-iterable-cache-closure.phpt create mode 100644 tests/aot/symfony/container-arguments-array-combine-normalize.phpt create mode 100644 tests/aot/symfony/controller-reflector-match-unpack.phpt create mode 100644 tests/aot/symfony/httpfoundation-accept-header-specificity.phpt create mode 100644 tests/aot/symfony/httpfoundation-json-response-static-factory.phpt create mode 100644 tests/aot/symfony/mailer-first-class-static-method-array-map.phpt create mode 100644 tests/aot/symfony/match-true-assignment-condition.phpt create mode 100644 tests/aot/symfony/match-true-throw-debug-type.phpt create mode 100644 tests/aot/symfony/messenger-envelope-clone-stamps.phpt create mode 100644 tests/aot/symfony/messenger-nullsafe-coalesce-throw-chain.phpt create mode 100644 tests/aot/symfony/php-dumper-indent-lines.phpt create mode 100644 tests/aot/symfony/rememberme-details-new-static-unpack.phpt create mode 100644 tests/aot/symfony/routing-expression-provider-dynamic-callable.phpt create mode 100644 tests/aot/symfony/routing-extra-parameters-recursive-caster.phpt create mode 100644 tests/aot/symfony/routing-route-coalesce-assign-condition.phpt create mode 100644 tests/aot/symfony/security-dynamic-instanceof-union-string.phpt create mode 100644 tests/aot/symfony/static-local-coalesce-assign-ternary-object.phpt diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index c85c726b..7b1a8d07 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -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); } diff --git a/tests/aot/symfony/attribute-loader-match-true-side-effects.phpt b/tests/aot/symfony/attribute-loader-match-true-side-effects.phpt new file mode 100644 index 00000000..5c83b0fa --- /dev/null +++ b/tests/aot/symfony/attribute-loader-match-true-side-effects.phpt @@ -0,0 +1,69 @@ +--TEST-- +Symfony Serializer pattern: match true used for metadata side effects +--FILE-- +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) +} diff --git a/tests/aot/symfony/autowire-target-nullsafe-coalesce-assign.phpt b/tests/aot/symfony/autowire-target-nullsafe-coalesce-assign.phpt new file mode 100644 index 00000000..6d2e3e69 --- /dev/null +++ b/tests/aot/symfony/autowire-target-nullsafe-coalesce-assign.phpt @@ -0,0 +1,68 @@ +--TEST-- +Symfony DI pattern: filtered attribute nullsafe property with coalesce assignment +--FILE-- +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 +} diff --git a/tests/aot/symfony/console-attribute-nullsafe-coalesce-assign.phpt b/tests/aot/symfony/console-attribute-nullsafe-coalesce-assign.phpt new file mode 100644 index 00000000..40e29ba2 --- /dev/null +++ b/tests/aot/symfony/console-attribute-nullsafe-coalesce-assign.phpt @@ -0,0 +1,78 @@ +--TEST-- +Symfony Console pattern: nullsafe attribute values with coalesce assignment +--FILE-- +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 +} diff --git a/tests/aot/symfony/console-question-iterable-cache-closure.phpt b/tests/aot/symfony/console-question-iterable-cache-closure.phpt new file mode 100644 index 00000000..83496886 --- /dev/null +++ b/tests/aot/symfony/console-question-iterable-cache-closure.phpt @@ -0,0 +1,35 @@ +--TEST-- +Symfony Console pattern: closure caches iterable values with coalesce assignment +--FILE-- + '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" +} diff --git a/tests/aot/symfony/container-arguments-array-combine-normalize.phpt b/tests/aot/symfony/container-arguments-array-combine-normalize.phpt new file mode 100644 index 00000000..9ac10057 --- /dev/null +++ b/tests/aot/symfony/container-arguments-array-combine-normalize.phpt @@ -0,0 +1,31 @@ +--TEST-- +Symfony DI pattern: normalize named arguments with array_combine and preg_replace +--FILE-- + 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" +} diff --git a/tests/aot/symfony/controller-reflector-match-unpack.phpt b/tests/aot/symfony/controller-reflector-match-unpack.phpt new file mode 100644 index 00000000..e88e0223 --- /dev/null +++ b/tests/aot/symfony/controller-reflector-match-unpack.phpt @@ -0,0 +1,38 @@ +--TEST-- +Symfony HttpKernel pattern: controller reflector match with unpacked callable +--FILE-- + 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) diff --git a/tests/aot/symfony/httpfoundation-accept-header-specificity.phpt b/tests/aot/symfony/httpfoundation-accept-header-specificity.phpt new file mode 100644 index 00000000..d2af654a --- /dev/null +++ b/tests/aot/symfony/httpfoundation-accept-header-specificity.phpt @@ -0,0 +1,35 @@ +--TEST-- +Symfony HttpFoundation pattern: accept header specificity with explode defaults and match +--FILE-- + '*']; + + $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) diff --git a/tests/aot/symfony/httpfoundation-json-response-static-factory.phpt b/tests/aot/symfony/httpfoundation-json-response-static-factory.phpt new file mode 100644 index 00000000..381071a7 --- /dev/null +++ b/tests/aot/symfony/httpfoundation-json-response-static-factory.phpt @@ -0,0 +1,49 @@ +--TEST-- +Symfony HttpFoundation pattern: null default new ArrayObject and new static JSON factory +--FILE-- +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) diff --git a/tests/aot/symfony/mailer-first-class-static-method-array-map.phpt b/tests/aot/symfony/mailer-first-class-static-method-array-map.phpt new file mode 100644 index 00000000..5c9ab5e4 --- /dev/null +++ b/tests/aot/symfony/mailer-first-class-static-method-array-map.phpt @@ -0,0 +1,70 @@ +--TEST-- +Symfony Mailer style self::method(...) callback in array_map +--FILE-- +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) { + } +} diff --git a/tests/aot/symfony/match-true-assignment-condition.phpt b/tests/aot/symfony/match-true-assignment-condition.phpt new file mode 100644 index 00000000..3f51ce86 --- /dev/null +++ b/tests/aot/symfony/match-true-assignment-condition.phpt @@ -0,0 +1,42 @@ +--TEST-- +Symfony HttpKernel pattern: match true with assignment inside condition +--FILE-- +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" diff --git a/tests/aot/symfony/match-true-throw-debug-type.phpt b/tests/aot/symfony/match-true-throw-debug-type.phpt new file mode 100644 index 00000000..e49bce95 --- /dev/null +++ b/tests/aot/symfony/match-true-throw-debug-type.phpt @@ -0,0 +1,33 @@ +--TEST-- +Symfony Config pattern: match true formats scalar values or throws by debug type +--FILE-- + $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" diff --git a/tests/aot/symfony/messenger-envelope-clone-stamps.phpt b/tests/aot/symfony/messenger-envelope-clone-stamps.phpt new file mode 100644 index 00000000..5c10085e --- /dev/null +++ b/tests/aot/symfony/messenger-envelope-clone-stamps.phpt @@ -0,0 +1,91 @@ +--TEST-- +Symfony style Envelope clone with variadic stamps and dynamic stamp removal +--FILE-- +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) { +} diff --git a/tests/aot/symfony/messenger-nullsafe-coalesce-throw-chain.phpt b/tests/aot/symfony/messenger-nullsafe-coalesce-throw-chain.phpt new file mode 100644 index 00000000..e3778ddf --- /dev/null +++ b/tests/aot/symfony/messenger-nullsafe-coalesce-throw-chain.phpt @@ -0,0 +1,44 @@ +--TEST-- +Symfony Messenger style nullsafe chain with coalesce throw +--FILE-- +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." diff --git a/tests/aot/symfony/php-dumper-indent-lines.phpt b/tests/aot/symfony/php-dumper-indent-lines.phpt new file mode 100644 index 00000000..50c10bff --- /dev/null +++ b/tests/aot/symfony/php-dumper-indent-lines.phpt @@ -0,0 +1,19 @@ +--TEST-- +Symfony DI pattern: indent generated code lines with explode array_map implode +--FILE-- + $line ? ' '.$line : $line, explode("\n", $code))); +} + +function main(): void +{ + var_dump(indentCode("first\n\nsecond")); +} +?> +--EXPECT-- +string(21) " first + + second" diff --git a/tests/aot/symfony/rememberme-details-new-static-unpack.phpt b/tests/aot/symfony/rememberme-details-new-static-unpack.phpt new file mode 100644 index 00000000..6a75ce45 --- /dev/null +++ b/tests/aot/symfony/rememberme-details-new-static-unpack.phpt @@ -0,0 +1,48 @@ +--TEST-- +Symfony style RememberMeDetails new static(...array after unset) +--FILE-- + +--EXPECT-- +string(6) "string" +string(3) "int" +string(6) "string" +string(17) "RememberMeDetails" diff --git a/tests/aot/symfony/routing-expression-provider-dynamic-callable.phpt b/tests/aot/symfony/routing-expression-provider-dynamic-callable.phpt new file mode 100644 index 00000000..04fc302d --- /dev/null +++ b/tests/aot/symfony/routing-expression-provider-dynamic-callable.phpt @@ -0,0 +1,36 @@ +--TEST-- +Symfony Routing pattern: provider returns callable invoked with unpacked args +--FILE-- + */ + 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" diff --git a/tests/aot/symfony/routing-extra-parameters-recursive-caster.phpt b/tests/aot/symfony/routing-extra-parameters-recursive-caster.phpt new file mode 100644 index 00000000..fbd270b0 --- /dev/null +++ b/tests/aot/symfony/routing-extra-parameters-recursive-caster.phpt @@ -0,0 +1,59 @@ +--TEST-- +Symfony Routing pattern: extra parameters diff and recursive object caster +--SKIPIF-- + +--FILE-- +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" +} diff --git a/tests/aot/symfony/routing-route-coalesce-assign-condition.phpt b/tests/aot/symfony/routing-route-coalesce-assign-condition.phpt new file mode 100644 index 00000000..8a2bcd7c --- /dev/null +++ b/tests/aot/symfony/routing-route-coalesce-assign-condition.phpt @@ -0,0 +1,42 @@ +--TEST-- +Symfony Routing pattern: null comparison with route coalesce assignment +--FILE-- +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" diff --git a/tests/aot/symfony/security-dynamic-instanceof-union-string.phpt b/tests/aot/symfony/security-dynamic-instanceof-union-string.phpt new file mode 100644 index 00000000..f6bed2de --- /dev/null +++ b/tests/aot/symfony/security-dynamic-instanceof-union-string.phpt @@ -0,0 +1,44 @@ +--TEST-- +Symfony style dynamic instanceof against pipe-separated type string +--FILE-- + +--EXPECT-- +array(1) { + [0]=> + string(9) "AdminUser" +} +array(2) { + [0]=> + string(9) "AdminUser" + [1]=> + string(13) "UserInterface" +} +array(0) { +} diff --git a/tests/aot/symfony/static-local-coalesce-assign-ternary-object.phpt b/tests/aot/symfony/static-local-coalesce-assign-ternary-object.phpt new file mode 100644 index 00000000..c6245672 --- /dev/null +++ b/tests/aot/symfony/static-local-coalesce-assign-ternary-object.phpt @@ -0,0 +1,35 @@ +--TEST-- +Symfony Serializer pattern: static local coalesce assignment with ternary object cache +--FILE-- +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"