- 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