- 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 testpull/16/head
parent
78c01ca97b
commit
be5e553bf7
5 changed files with 296 additions and 4 deletions
@ -0,0 +1,69 @@ |
|||||||
|
--TEST-- |
||||||
|
ThinkPHP Middleware pattern: array_map nested closure and unpacked callable params |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
class ThinkMiddlewareLike |
||||||
|
{ |
||||||
|
private array $queue = []; |
||||||
|
|
||||||
|
public function add(array|Closure|string $middleware): void |
||||||
|
{ |
||||||
|
$this->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]" |
||||||
@ -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-- |
||||||
|
<?php |
||||||
|
|
||||||
|
class ThinkRequestFilterLike |
||||||
|
{ |
||||||
|
private mixed $filter = null; |
||||||
|
|
||||||
|
public function filter(mixed $filter = null): mixed |
||||||
|
{ |
||||||
|
if ($filter === null) { |
||||||
|
return $this->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 |
||||||
@ -0,0 +1,83 @@ |
|||||||
|
--TEST-- |
||||||
|
ThinkPHP Route pattern: preg_replace_callback use ref and foreach ref match cast |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
class ThinkRouteRuleLike |
||||||
|
{ |
||||||
|
private array $pattern = [ |
||||||
|
'id' => '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/<id>', '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) |
||||||
|
} |
||||||
Loading…
Reference in new issue