- 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