From 78c01ca97b621b66bc4cba668901b46e8e5b855d Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 10 Jul 2026 13:25:25 +0800 Subject: [PATCH] test(aot): add ThinkPHP pattern test cases for AOT compilation - Add config lazy hook fallback test with dotted key lookup - Add event observer reflection prefix test for dynamic dispatch - Add file hashname match callable test with closure branch handling - Add pipeline array reduce exception handler test with nested closures - Add request dynamic property cache test with method fallback - Add route magic call forward test using call_user_func_array --- .../thinkphp/config-lazy-hook-fallback.phpt | 105 ++++++++++++++++++ .../event-observer-reflection-prefix.phpt | 77 +++++++++++++ .../file-hashname-match-callable.phpt | 57 ++++++++++ ...peline-array-reduce-exception-handler.phpt | 89 +++++++++++++++ .../request-dynamic-property-cache.phpt | 72 ++++++++++++ .../thinkphp/route-magic-call-forward.phpt | 55 +++++++++ 6 files changed, 455 insertions(+) create mode 100644 tests/aot/thinkphp/config-lazy-hook-fallback.phpt create mode 100644 tests/aot/thinkphp/event-observer-reflection-prefix.phpt create mode 100644 tests/aot/thinkphp/file-hashname-match-callable.phpt create mode 100644 tests/aot/thinkphp/pipeline-array-reduce-exception-handler.phpt create mode 100644 tests/aot/thinkphp/request-dynamic-property-cache.phpt create mode 100644 tests/aot/thinkphp/route-magic-call-forward.phpt diff --git a/tests/aot/thinkphp/config-lazy-hook-fallback.phpt b/tests/aot/thinkphp/config-lazy-hook-fallback.phpt new file mode 100644 index 00000000..36c96206 --- /dev/null +++ b/tests/aot/thinkphp/config-lazy-hook-fallback.phpt @@ -0,0 +1,105 @@ +--TEST-- +ThinkPHP Config pattern: lazy hook fallback and dotted key lookup +--XFAIL-- +Known AOT bug: nullable mixed default/value flow can be narrowed incorrectly after dotted array lookup. +--FILE-- +hook[$key ?? 'global'] = $callback; + } + + public function set(array $config, ?string $name = null): array + { + if (empty($name)) { + $this->config = array_merge($this->config, array_change_key_case($config)); + return $this->config; + } + + $result = isset($this->config[$name]) ? array_merge($this->config[$name], $config) : $config; + $this->config[$name] = $result; + return $result; + } + + public function get(?string $name = null, mixed $default = null): mixed + { + if (empty($name)) { + return $this->config; + } + + if (!str_contains($name, '.')) { + $name = strtolower($name); + $result = $this->config[$name] ?? []; + return $this->hook ? $this->lazy($name, $result, []) : $result; + } + + $item = explode('.', $name); + $item[0] = strtolower($item[0]); + $config = $this->config; + + foreach ($item as $val) { + if (isset($config[$val])) { + $config = $config[$val]; + } else { + return $this->hook ? $this->lazy($name, null, $default) : $default; + } + } + + return $this->hook ? $this->lazy($name, $config, $default) : $config; + } + + private function lazy(string $name, mixed $value = null, mixed $default = null): mixed + { + $key = strpos($name, '.') ? strstr($name, '.', true) : $name; + if (isset($this->hook[$key])) { + $call = $this->hook[$key]; + } elseif (isset($this->hook['global'])) { + $call = $this->hook['global']; + } + + if (isset($call)) { + $result = call_user_func_array($call, [$name, $value]); + if (is_null($result)) { + return $default; + } + } + + return $result ?? ($value ?: $default); + } +} + +function main(): void +{ + $config = new ThinkConfigLike(); + $config->set(['Debug' => true, 'cache' => ['ttl' => 60]]); + $config->hook(fn ($name, $value) => $name === 'cache.missing' ? null : ['hooked', $name, $value]); + + var_dump($config->get('debug')); + var_dump($config->get('cache.ttl')); + var_dump($config->get('cache.missing', 'fallback')); +} +?> +--EXPECT-- +array(3) { + [0]=> + string(6) "hooked" + [1]=> + string(5) "debug" + [2]=> + bool(true) +} +array(3) { + [0]=> + string(6) "hooked" + [1]=> + string(9) "cache.ttl" + [2]=> + int(60) +} +string(8) "fallback" diff --git a/tests/aot/thinkphp/event-observer-reflection-prefix.phpt b/tests/aot/thinkphp/event-observer-reflection-prefix.phpt new file mode 100644 index 00000000..c007a8db --- /dev/null +++ b/tests/aot/thinkphp/event-observer-reflection-prefix.phpt @@ -0,0 +1,77 @@ +--TEST-- +ThinkPHP Event pattern: Reflection observer registration and dynamic dispatch +--FILE-- +listener[$event][] = $listener; + } + + public function observe(object $observer, string $prefix = ''): static + { + $reflect = new ReflectionClass($observer); + $methods = $reflect->getMethods(ReflectionMethod::IS_PUBLIC); + + if (empty($prefix) && $reflect->hasProperty('eventPrefix')) { + $reflectProperty = $reflect->getProperty('eventPrefix'); + $prefix = $reflectProperty->getValue($observer); + } + + foreach ($methods as $method) { + $name = $method->getName(); + if (str_starts_with($name, 'on')) { + $this->listen($prefix . substr($name, 2), [$observer, $name]); + } + } + + return $this; + } + + public function trigger(string $event, mixed $params = null): array + { + $result = []; + foreach ($this->listener[$event] ?? [] as $key => $listener) { + $result[$key] = call_user_func_array($listener, [$params]); + } + return $result; + } +} + +class ThinkOrderObserver +{ + public string $eventPrefix = 'Order.'; + + public function onCreated(array $payload): string + { + return 'created:' . $payload['id']; + } + + public function onPaid(array $payload): string + { + return 'paid:' . $payload['id']; + } +} + +function main(): void +{ + $event = new ThinkEventLike(); + $event->observe(new ThinkOrderObserver()); + + var_dump($event->trigger('Order.Created', ['id' => 42])); + var_dump($event->trigger('Order.Paid', ['id' => 42])); +} +?> +--EXPECT-- +array(1) { + [0]=> + string(10) "created:42" +} +array(1) { + [0]=> + string(7) "paid:42" +} diff --git a/tests/aot/thinkphp/file-hashname-match-callable.phpt b/tests/aot/thinkphp/file-hashname-match-callable.phpt new file mode 100644 index 00000000..3da98205 --- /dev/null +++ b/tests/aot/thinkphp/file-hashname-match-callable.phpt @@ -0,0 +1,57 @@ +--TEST-- +ThinkPHP File pattern: match true with assignment and callable branch +--FILE-- +path); + } + + public function getPathname(): string + { + return $this->path; + } + + public function hashName(string|Closure|null $rule = null): string + { + if (!$this->hashName) { + if ($rule instanceof Closure) { + $this->hashName = call_user_func_array($rule, [$this]); + } else { + $this->hashName = match (true) { + in_array($rule, hash_algos(), true) && $hash = $this->hash($rule) => substr($hash, 0, 2) . '/' . substr($hash, 2), + is_callable($rule) => call_user_func($rule), + default => 'date/' . md5($this->getPathname()), + }; + } + } + + return $this->hashName; + } +} + +function main(): void +{ + $file = new ThinkFileLike('thinkphp'); + var_dump($file->hashName('md5')); + + $file = new ThinkFileLike('thinkphp'); + var_dump($file->hashName(fn (ThinkFileLike $f) => 'closure:' . basename($f->getPathname()))); + + $file = new ThinkFileLike('thinkphp'); + var_dump($file->hashName('phpversion')); +} +?> +--EXPECTF-- +string(33) "%s/%s" +string(16) "closure:thinkphp" +string(%d) "%s" diff --git a/tests/aot/thinkphp/pipeline-array-reduce-exception-handler.phpt b/tests/aot/thinkphp/pipeline-array-reduce-exception-handler.phpt new file mode 100644 index 00000000..9deea218 --- /dev/null +++ b/tests/aot/thinkphp/pipeline-array-reduce-exception-handler.phpt @@ -0,0 +1,89 @@ +--TEST-- +ThinkPHP Pipeline pattern: array_reduce nested closures with exception handler +--XFAIL-- +Known AOT bug: method using func_get_args() with explicit arguments can hit arginfo/zpp mismatch. +--FILE-- +passable = $passable; + return $this; + } + + public function through(mixed $pipes): static + { + $this->pipes = is_array($pipes) ? $pipes : func_get_args(); + return $this; + } + + public function whenException(callable $handler): static + { + $this->exceptionHandler = $handler; + return $this; + } + + public function then(Closure $destination): mixed + { + $pipeline = array_reduce( + array_reverse($this->pipes), + $this->carry(), + function ($passable) use ($destination) { + try { + return $destination($passable); + } catch (Throwable | Exception $e) { + return $this->handleException($passable, $e); + } + } + ); + + return $pipeline($this->passable); + } + + private function carry(): Closure + { + return function ($stack, $pipe) { + return function ($passable) use ($stack, $pipe) { + try { + return $pipe($passable, $stack); + } catch (Throwable | Exception $e) { + return $this->handleException($passable, $e); + } + }; + }; + } + + private function handleException(mixed $passable, Throwable $e): mixed + { + if ($this->exceptionHandler) { + return call_user_func($this->exceptionHandler, $passable, $e); + } + throw $e; + } +} + +function main(): void +{ + $pipeline = new ThinkPipelineLike(); + $result = $pipeline + ->send('start') + ->through( + fn ($value, $next) => $next($value . ':a') . ':after-a', + function ($value, $next) { + throw new RuntimeException($value . ':boom'); + }, + ) + ->whenException(fn ($value, Throwable $e) => $value . ':' . $e->getMessage()) + ->then(fn ($value) => $value . ':done'); + + var_dump($result); +} +?> +--EXPECT-- +string(27) "start:a:start:a:boom:after-a" diff --git a/tests/aot/thinkphp/request-dynamic-property-cache.phpt b/tests/aot/thinkphp/request-dynamic-property-cache.phpt new file mode 100644 index 00000000..9c556aa1 --- /dev/null +++ b/tests/aot/thinkphp/request-dynamic-property-cache.phpt @@ -0,0 +1,72 @@ +--TEST-- +ThinkPHP Request pattern: dynamic property cache and method fallback +--FILE-- +post; + } + + public function get(): array + { + return $this->get; + } + + public function method(): string + { + if (!$this->method && isset($this->post[$this->varMethod])) { + $method = strtolower($this->post[$this->varMethod]); + if (in_array($method, ['get', 'post'], true)) { + $this->method = strtoupper($method); + $this->{$method} = $this->post; + } + unset($this->post[$this->varMethod]); + } + + return $this->method ?: 'GET'; + } + + public function has(string $name, string $type = 'post'): bool + { + $param = empty($this->$type) ? $this->$type() : $this->$type; + foreach (explode('.', $name) as $key) { + if (!isset($param[$key])) { + return false; + } + $param = $param[$key]; + } + return true; + } +} + +function main(): void +{ + $request = new ThinkRequestLike(); + $request->post = [ + '_method' => 'get', + 'user' => ['name' => 'thinkphp'], + ]; + + var_dump($request->method()); + var_dump($request->has('user.name', 'get')); + var_dump($request->post); +} +?> +--EXPECT-- +string(3) "GET" +bool(true) +array(1) { + ["user"]=> + array(1) { + ["name"]=> + string(8) "thinkphp" + } +} diff --git a/tests/aot/thinkphp/route-magic-call-forward.phpt b/tests/aot/thinkphp/route-magic-call-forward.phpt new file mode 100644 index 00000000..1698f09a --- /dev/null +++ b/tests/aot/thinkphp/route-magic-call-forward.phpt @@ -0,0 +1,55 @@ +--TEST-- +ThinkPHP Route pattern: __call forwarding with call_user_func_array +--FILE-- +rules[$name] = $value; + return $this; + } + + public function getRules(): array + { + return $this->rules; + } +} + +class ThinkRouteLike +{ + public function __construct(private ThinkRuleGroupLike $group) + { + } + + public function __call(string $method, array $args): mixed + { + return call_user_func_array([$this->group, $method], $args); + } +} + +function main(): void +{ + $group = new ThinkRuleGroupLike(); + $route = new ThinkRouteLike($group); + + $result = $route->option('middleware', ['auth', 'log']); + + var_dump($result === $group); + var_dump($group->getRules()); +} +?> +--EXPECT-- +bool(true) +array(1) { + ["middleware"]=> + array(2) { + [0]=> + string(4) "auth" + [1]=> + string(3) "log" + } +}