test(aot): add ThinkPHP pattern test cases for AOT compilation

- 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_array
pull/16/head
韩天峰 2 months ago
parent f9c6abbbf9
commit 78c01ca97b
  1. 105
      tests/aot/thinkphp/config-lazy-hook-fallback.phpt
  2. 77
      tests/aot/thinkphp/event-observer-reflection-prefix.phpt
  3. 57
      tests/aot/thinkphp/file-hashname-match-callable.phpt
  4. 89
      tests/aot/thinkphp/pipeline-array-reduce-exception-handler.phpt
  5. 72
      tests/aot/thinkphp/request-dynamic-property-cache.phpt
  6. 55
      tests/aot/thinkphp/route-magic-call-forward.phpt

@ -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…
Cancel
Save