- 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_arraypull/16/head
parent
f9c6abbbf9
commit
78c01ca97b
6 changed files with 455 additions and 0 deletions
@ -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-- |
||||||
|
<?php |
||||||
|
|
||||||
|
class ThinkConfigLike |
||||||
|
{ |
||||||
|
private array $config = []; |
||||||
|
private array $hook = []; |
||||||
|
|
||||||
|
public function hook(Closure $callback, ?string $key = null): void |
||||||
|
{ |
||||||
|
$this->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" |
||||||
@ -0,0 +1,77 @@ |
|||||||
|
--TEST-- |
||||||
|
ThinkPHP Event pattern: Reflection observer registration and dynamic dispatch |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
class ThinkEventLike |
||||||
|
{ |
||||||
|
private array $listener = []; |
||||||
|
|
||||||
|
public function listen(string $event, callable $listener): void |
||||||
|
{ |
||||||
|
$this->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" |
||||||
|
} |
||||||
@ -0,0 +1,57 @@ |
|||||||
|
--TEST-- |
||||||
|
ThinkPHP File pattern: match true with assignment and callable branch |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
class ThinkFileLike |
||||||
|
{ |
||||||
|
private ?string $hashName = null; |
||||||
|
|
||||||
|
public function __construct(private string $path) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public function hash(string $algo): string |
||||||
|
{ |
||||||
|
return hash($algo, $this->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" |
||||||
@ -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-- |
||||||
|
<?php |
||||||
|
|
||||||
|
class ThinkPipelineLike |
||||||
|
{ |
||||||
|
private mixed $passable = null; |
||||||
|
private array $pipes = []; |
||||||
|
private mixed $exceptionHandler = null; |
||||||
|
|
||||||
|
public function send(mixed $passable): static |
||||||
|
{ |
||||||
|
$this->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" |
||||||
@ -0,0 +1,72 @@ |
|||||||
|
--TEST-- |
||||||
|
ThinkPHP Request pattern: dynamic property cache and method fallback |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
class ThinkRequestLike |
||||||
|
{ |
||||||
|
public array $post = []; |
||||||
|
public array $get = []; |
||||||
|
private string $varMethod = '_method'; |
||||||
|
private string $method = ''; |
||||||
|
|
||||||
|
public function post(): array |
||||||
|
{ |
||||||
|
return $this->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" |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,55 @@ |
|||||||
|
--TEST-- |
||||||
|
ThinkPHP Route pattern: __call forwarding with call_user_func_array |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
class ThinkRuleGroupLike |
||||||
|
{ |
||||||
|
private array $rules = []; |
||||||
|
|
||||||
|
public function option(string $name, mixed $value): static |
||||||
|
{ |
||||||
|
$this->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" |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue