From be5e553bf7c460905e75531611f716a082e2427a Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 10 Jul 2026 14:02:49 +0800 Subject: [PATCH] test(aot): add ThinkPHP middleware and request filter test cases - Add test case for array_map nested closure and unpacked callable params in ThinkPHP middleware - Add test case for array_walk_recursive by-ref filter and match throw type cast in ThinkPHP request filter - Add test case for preg_replace_callback use ref and foreach ref match cast in ThinkPHP route parsing - Remove XFAIL annotations from Symfony nullable first-class callable test - Remove XFAIL annotations from Symfony uksort priority fallback test --- .../nullable-first-class-callable.phpt | 2 - .../aot/symfony/uksort-priority-fallback.phpt | 2 - .../middleware-array-map-unpack-callable.phpt | 69 +++++++++ ...est-filter-walk-recursive-match-throw.phpt | 144 ++++++++++++++++++ ...route-parse-rule-callback-foreach-ref.phpt | 83 ++++++++++ 5 files changed, 296 insertions(+), 4 deletions(-) create mode 100644 tests/aot/thinkphp/middleware-array-map-unpack-callable.phpt create mode 100644 tests/aot/thinkphp/request-filter-walk-recursive-match-throw.phpt create mode 100644 tests/aot/thinkphp/route-parse-rule-callback-foreach-ref.phpt diff --git a/tests/aot/symfony/nullable-first-class-callable.phpt b/tests/aot/symfony/nullable-first-class-callable.phpt index 96dbe5bd..3e1201d5 100644 --- a/tests/aot/symfony/nullable-first-class-callable.phpt +++ b/tests/aot/symfony/nullable-first-class-callable.phpt @@ -1,7 +1,5 @@ --TEST-- Symfony pattern: nullable callable converted to first-class callable ---XFAIL-- -Known AOT bug: first-class callable stored in a typed nullable Closure property can crash during shutdown. --FILE-- queue[] = $this->buildMiddleware($middleware); + } + + private function buildMiddleware(array|Closure|string $middleware): array + { + if (is_array($middleware)) { + [$middleware, $params] = $middleware; + } + + if ($middleware instanceof Closure) { + return [$middleware, $params ?? []]; + } + + return [[$middleware, 'handle'], $params ?? []]; + } + + public function pipeline(): array + { + return array_map(function ($middleware) { + return function ($request, $next) use ($middleware) { + [$call, $params] = $middleware; + $response = call_user_func($call, $request, $next, ...$params); + if (!$response instanceof ThinkResponseLike) { + throw new LogicException('The middleware must return Response instance'); + } + return $response; + }; + }, $this->queue); + } +} + +class ThinkResponseLike +{ + public function __construct(public string $body) + { + } +} + +function main(): void +{ + $middleware = new ThinkMiddlewareLike(); + $middleware->add([ + function ($request, $next, string $prefix, string $suffix): ThinkResponseLike { + $response = $next($prefix . $request); + $response->body .= $suffix; + return $response; + }, + ['[', ']'], + ]); + + $pipes = $middleware->pipeline(); + $response = $pipes[0]('thinkphp', fn ($request) => new ThinkResponseLike($request . ':next')); + + var_dump($response->body); +} +?> +--EXPECT-- +string(15) "[thinkphp:next]" diff --git a/tests/aot/thinkphp/request-filter-walk-recursive-match-throw.phpt b/tests/aot/thinkphp/request-filter-walk-recursive-match-throw.phpt new file mode 100644 index 00000000..dd0d086c --- /dev/null +++ b/tests/aot/thinkphp/request-filter-walk-recursive-match-throw.phpt @@ -0,0 +1,144 @@ +--TEST-- +ThinkPHP Request pattern: array_walk_recursive by-ref filter and match throw type cast +--XFAIL-- +Known AOT bug: mixed parameter narrowed to array after array_walk_recursive path and reused for scalar input. +--FILE-- +filter; + } + + $this->filter = $filter; + return $this; + } + + public function input(array $data, string|bool $name = '', mixed $default = null, string|array|null $filter = ''): mixed + { + if (false === $name) { + return $data; + } + + $name = (string) $name; + if ('' !== $name) { + if (str_contains($name, '/')) { + [$name, $type] = explode('/', $name); + } + + $data = $this->getData($data, $name); + } + + return $this->filterData($data, $filter, $name, $default, $type ?? ''); + } + + private function filterData(mixed $data, mixed $filter, string $name, mixed $default, string $type): mixed + { + if ($data === null) { + return $default; + } + + $filter = $this->getFilter($filter, $default); + if (is_array($data)) { + array_walk_recursive($data, [$this, 'filterValue'], $filter); + } else { + $this->filterValue($data, $name, $filter); + } + + if ($type) { + $this->typeCast($data, $type); + } + + return $data; + } + + private function getData(array $data, string $name, mixed $default = null): mixed + { + foreach (explode('.', $name) as $val) { + if (isset($data[$val])) { + $data = $data[$val]; + } else { + return $default; + } + } + + return $data; + } + + private function getFilter(mixed $filter, mixed $default): array + { + if ($filter === null) { + $filter = []; + } else { + $filter = $filter ?: $this->filter; + if (is_string($filter) && !str_contains($filter, '/')) { + $filter = explode(',', $filter); + } else { + $filter = (array) $filter; + } + } + + $filter[] = $default; + return $filter; + } + + public function filterValue(mixed &$value, mixed $key, array $filters): void + { + $default = array_pop($filters); + foreach ($filters as $filter) { + if (is_callable($filter)) { + if ($value === null) { + continue; + } + $value = call_user_func($filter, $value); + } elseif (is_scalar($value) && is_string($filter) && str_contains($filter, '/')) { + if (!preg_match($filter, (string) $value)) { + $value = $default; + break; + } + } + } + } + + private function typeCast(mixed &$data, string $type): void + { + $data = match (strtolower($type)) { + 'a' => (array) $data, + 'b' => (bool) $data, + 'd' => (int) $data, + 'f' => (float) $data, + 's' => is_scalar($data) ? (string) $data : throw new InvalidArgumentException('variable type error:' . gettype($data)), + default => $data, + }; + } +} + +function main(): void +{ + $request = new ThinkRequestFilterLike(); + $request->filter(fn ($value) => is_string($value) ? trim($value) : $value); + + var_dump($request->input(['user' => ['name' => ' thinkphp ', 'role' => ' admin ']], 'user')); + var_dump($request->input(['id' => '42'], 'id/d')); + + try { + $request->input(['user' => ['name' => 'thinkphp']], 'user/s'); + } catch (InvalidArgumentException $e) { + echo "type error\n"; + } +} +?> +--EXPECT-- +array(2) { + ["name"]=> + string(8) "thinkphp" + ["role"]=> + string(5) "admin" +} +int(42) +type error diff --git a/tests/aot/thinkphp/route-parse-rule-callback-foreach-ref.phpt b/tests/aot/thinkphp/route-parse-rule-callback-foreach-ref.phpt new file mode 100644 index 00000000..aca0eddb --- /dev/null +++ b/tests/aot/thinkphp/route-parse-rule-callback-foreach-ref.phpt @@ -0,0 +1,83 @@ +--TEST-- +ThinkPHP Route pattern: preg_replace_callback use ref and foreach ref match cast +--FILE-- + 'int', + 'price' => 'float', + ]; + + public array $vars = []; + + public function parseRule(string $rule, string $route, string $url, array $matches = []): string + { + $extraParams = true; + $search = $replace = []; + $depr = '/'; + + foreach ($matches as $key => $value) { + $search[] = '<' . $key . '>'; + $replace[] = $value; + $search[] = '{' . $key . '}'; + $replace[] = $value; + $search[] = ':' . $key; + $replace[] = $value; + + if (str_contains($value, $depr)) { + $extraParams = false; + } + } + + $route = str_replace($search, $replace, $route); + + if ($extraParams) { + $count = substr_count($rule, '/'); + $extra = array_slice(explode('|', $url), $count + 1); + $this->parseUrlParams(implode('/', $extra), $matches); + } + + foreach ($matches as $key => &$val) { + if (isset($this->pattern[$key]) && in_array($this->pattern[$key], ['\d+', 'int', 'float'], true)) { + $val = match ($this->pattern[$key]) { + 'int', '\d+' => (int) $val, + 'float' => (float) $val, + default => $val, + }; + } elseif (in_array($key, ['__module__', '__controller__', '__action__'], true)) { + unset($matches[$key]); + } + } + unset($val); + + $this->vars = $matches; + return $route; + } + + private function parseUrlParams(string $url, array &$var = []): void + { + if ($url) { + preg_replace_callback('/(\w+)\/([^\/]+)/', function ($match) use (&$var) { + $var[$match[1]] = strip_tags($match[2]); + }, $url); + } + } +} + +function main(): void +{ + $rule = new ThinkRouteRuleLike(); + var_dump($rule->parseRule('shop/', 'product/:id', 'shop|42|price/12.5/__action__/show', ['id' => '42'])); + var_dump($rule->vars); +} +?> +--EXPECT-- +string(10) "product/42" +array(2) { + ["id"]=> + int(42) + ["price"]=> + float(12.5) +}