- Add EventDispatcher wrapped listener cache test with first-class callable support - Implement Messenger nullsafe merge unpack functionality test case - Create Runtime options global coalesce assignment test scenario - Add Security badge dynamic class key with coalesce assignment test - Include comprehensive test coverage for Symfony-style PHP patterns - Verify proper handling of nullsafe operators and array unpacking operationspull/15/head
parent
7711a91dec
commit
be4835991e
4 changed files with 334 additions and 0 deletions
@ -0,0 +1,117 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony EventDispatcher style wrapped listener with first-class callable and cached info |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
interface SymfonyPriorityDispatcher |
||||||
|
{ |
||||||
|
public function getListenerPriority(string $eventName, callable|array $listener): int; |
||||||
|
} |
||||||
|
|
||||||
|
class SymfonyWrappedListenerCase |
||||||
|
{ |
||||||
|
private callable|array $listener; |
||||||
|
private ?Closure $optimizedListener; |
||||||
|
private ?int $priority = null; |
||||||
|
private mixed $stub = null; |
||||||
|
private string $pretty; |
||||||
|
private string $name; |
||||||
|
public bool $called = false; |
||||||
|
|
||||||
|
public function __construct( |
||||||
|
callable|array $listener, |
||||||
|
?string $name, |
||||||
|
private ?SymfonyPriorityDispatcher $dispatcher = null, |
||||||
|
) { |
||||||
|
$this->listener = $listener; |
||||||
|
$this->optimizedListener = $listener instanceof Closure ? $listener : (is_callable($listener) ? $listener(...) : null); |
||||||
|
|
||||||
|
if (is_array($listener)) { |
||||||
|
$this->name = get_debug_type($listener[0]); |
||||||
|
$this->pretty = $this->name . '::' . $listener[1]; |
||||||
|
} elseif ($listener instanceof Closure) { |
||||||
|
$this->pretty = $this->name = 'closure'; |
||||||
|
} elseif (is_string($listener)) { |
||||||
|
$this->pretty = $this->name = $listener; |
||||||
|
} else { |
||||||
|
$this->name = get_debug_type($listener); |
||||||
|
$this->pretty = $this->name . '::__invoke'; |
||||||
|
} |
||||||
|
|
||||||
|
if (null !== $name) { |
||||||
|
$this->name = $name; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public function getInfo(string $eventName): array |
||||||
|
{ |
||||||
|
$this->stub ??= $this->pretty . '()'; |
||||||
|
|
||||||
|
return [ |
||||||
|
'event' => $eventName, |
||||||
|
'priority' => $this->priority ??= $this->dispatcher?->getListenerPriority($eventName, $this->listener), |
||||||
|
'pretty' => $this->pretty, |
||||||
|
'stub' => $this->stub, |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
public function __invoke(object $event, string $eventName, SymfonyPriorityDispatcher $dispatcher): void |
||||||
|
{ |
||||||
|
$this->called = true; |
||||||
|
$this->priority ??= $dispatcher->getListenerPriority($eventName, $this->listener); |
||||||
|
|
||||||
|
($this->optimizedListener ?? $this->listener)($event, $eventName, $dispatcher); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class SymfonyWrappedListenerTarget |
||||||
|
{ |
||||||
|
public array $events = []; |
||||||
|
|
||||||
|
public function onEvent(object $event, string $eventName): void |
||||||
|
{ |
||||||
|
$this->events[] = $eventName . ':' . $event->name; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class SymfonyWrappedListenerDispatcher implements SymfonyPriorityDispatcher |
||||||
|
{ |
||||||
|
public int $calls = 0; |
||||||
|
|
||||||
|
public function getListenerPriority(string $eventName, callable|array $listener): int |
||||||
|
{ |
||||||
|
$this->calls++; |
||||||
|
|
||||||
|
return 32; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$target = new SymfonyWrappedListenerTarget(); |
||||||
|
$dispatcher = new SymfonyWrappedListenerDispatcher(); |
||||||
|
$wrapped = new SymfonyWrappedListenerCase([$target, 'onEvent'], null, $dispatcher); |
||||||
|
|
||||||
|
var_dump($wrapped->getInfo('kernel.request')); |
||||||
|
$wrapped((object) ['name' => 'request'], 'kernel.request', $dispatcher); |
||||||
|
var_dump($target->events); |
||||||
|
var_dump($wrapped->called); |
||||||
|
var_dump($dispatcher->calls); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECTF-- |
||||||
|
array(4) { |
||||||
|
["event"]=> |
||||||
|
string(14) "kernel.request" |
||||||
|
["priority"]=> |
||||||
|
int(32) |
||||||
|
["pretty"]=> |
||||||
|
string(%d) "%s::onEvent" |
||||||
|
["stub"]=> |
||||||
|
string(%d) "%s::onEvent()" |
||||||
|
} |
||||||
|
array(1) { |
||||||
|
[0]=> |
||||||
|
string(22) "kernel.request:request" |
||||||
|
} |
||||||
|
bool(true) |
||||||
|
int(1) |
||||||
@ -0,0 +1,94 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony Messenger style nullsafe fallback and with(...array_merge(...array_values())) |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
class SymfonyOriginalReceiverStamp |
||||||
|
{ |
||||||
|
public function __construct(private string $name) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public function getOriginalReceiverName(): string |
||||||
|
{ |
||||||
|
return $this->name; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class SymfonyReceivedStamp |
||||||
|
{ |
||||||
|
public function __construct(private string $name) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public function getTransportName(): string |
||||||
|
{ |
||||||
|
return $this->name; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class SymfonyEnvelopeMergeCase |
||||||
|
{ |
||||||
|
private array $stamps = []; |
||||||
|
|
||||||
|
public function with(object ...$stamps): self |
||||||
|
{ |
||||||
|
$clone = clone $this; |
||||||
|
foreach ($stamps as $stamp) { |
||||||
|
$clone->stamps[$stamp::class][] = $stamp; |
||||||
|
} |
||||||
|
|
||||||
|
return $clone; |
||||||
|
} |
||||||
|
|
||||||
|
public function last(string $class): ?object |
||||||
|
{ |
||||||
|
$stamps = $this->stamps[$class] ?? []; |
||||||
|
|
||||||
|
return $stamps[count($stamps) - 1] ?? null; |
||||||
|
} |
||||||
|
|
||||||
|
public function all(): array |
||||||
|
{ |
||||||
|
return $this->stamps; |
||||||
|
} |
||||||
|
|
||||||
|
public function count(): int |
||||||
|
{ |
||||||
|
$count = 0; |
||||||
|
foreach ($this->stamps as $items) { |
||||||
|
$count += count($items); |
||||||
|
} |
||||||
|
|
||||||
|
return $count; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function symfony_decode_transport(SymfonyEnvelopeMergeCase $envelope): string |
||||||
|
{ |
||||||
|
return $envelope->last(SymfonyOriginalReceiverStamp::class)?->getOriginalReceiverName() |
||||||
|
?? $envelope->last(SymfonyReceivedStamp::class)?->getTransportName() |
||||||
|
?? throw new LogicException('A ReceivedStamp is required.'); |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$envelope = (new SymfonyEnvelopeMergeCase()) |
||||||
|
->with(new SymfonyReceivedStamp('async'), new SymfonyOriginalReceiverStamp('failed')); |
||||||
|
$decoded = new SymfonyEnvelopeMergeCase(); |
||||||
|
|
||||||
|
$merged = $decoded->with(...array_merge(...array_values($envelope->all()))); |
||||||
|
|
||||||
|
var_dump(symfony_decode_transport($envelope)); |
||||||
|
var_dump($merged->count()); |
||||||
|
|
||||||
|
try { |
||||||
|
symfony_decode_transport(new SymfonyEnvelopeMergeCase()); |
||||||
|
} catch (LogicException $e) { |
||||||
|
echo $e->getMessage(), "\n"; |
||||||
|
} |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
string(6) "failed" |
||||||
|
int(2) |
||||||
|
A ReceivedStamp is required. |
||||||
@ -0,0 +1,51 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony Runtime style global options and server/env coalesce assignment |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
class SymfonyRuntimeOptionsCase |
||||||
|
{ |
||||||
|
public array $options; |
||||||
|
|
||||||
|
public function __construct(array $options = []) |
||||||
|
{ |
||||||
|
$envKey = $options['env_var_name'] ??= 'APP_ENV'; |
||||||
|
$debugKey = $options['debug_var_name'] ??= 'APP_DEBUG'; |
||||||
|
|
||||||
|
if (isset($options['project_dir']) && $projectDirVar = $options['project_dir_var'] ?? 'APP_PROJECT_DIR') { |
||||||
|
$_SERVER[$projectDirVar] = $_ENV[$projectDirVar] = $options['project_dir']; |
||||||
|
} |
||||||
|
|
||||||
|
$_SERVER[$envKey] ??= $_ENV[$envKey] ?? 'dev'; |
||||||
|
$_SERVER[$debugKey] ??= $_ENV[$debugKey] ?? !in_array($_SERVER[$envKey], (array) ($options['prod_envs'] ?? ['prod']), true); |
||||||
|
|
||||||
|
$options['debug'] ??= '1' === (string) $_SERVER[$debugKey]; |
||||||
|
$options['disable_dotenv'] = true; |
||||||
|
|
||||||
|
$this->options = $options; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
unset($_SERVER['APP_ENV'], $_SERVER['APP_DEBUG'], $_SERVER['APP_PROJECT_DIR']); |
||||||
|
unset($_ENV['APP_ENV'], $_ENV['APP_DEBUG'], $_ENV['APP_PROJECT_DIR']); |
||||||
|
|
||||||
|
$runtime = new SymfonyRuntimeOptionsCase(['project_dir' => '/app', 'prod_envs' => ['prod', 'stage']]); |
||||||
|
|
||||||
|
var_dump($_SERVER['APP_ENV']); |
||||||
|
var_dump($_SERVER['APP_DEBUG']); |
||||||
|
var_dump($_SERVER['APP_PROJECT_DIR']); |
||||||
|
var_dump($_ENV['APP_PROJECT_DIR']); |
||||||
|
var_dump($runtime->options['env_var_name']); |
||||||
|
var_dump($runtime->options['debug_var_name']); |
||||||
|
var_dump($runtime->options['debug']); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
string(3) "dev" |
||||||
|
bool(true) |
||||||
|
string(4) "/app" |
||||||
|
string(4) "/app" |
||||||
|
string(7) "APP_ENV" |
||||||
|
string(9) "APP_DEBUG" |
||||||
|
bool(true) |
||||||
@ -0,0 +1,72 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony Security style dynamic $badge::class key with coalesce assignment |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
interface SymfonyBadgeInterface |
||||||
|
{ |
||||||
|
public function name(): string; |
||||||
|
} |
||||||
|
|
||||||
|
class SymfonyCsrfBadge implements SymfonyBadgeInterface |
||||||
|
{ |
||||||
|
public function name(): string |
||||||
|
{ |
||||||
|
return 'csrf'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class SymfonyUserBadge implements SymfonyBadgeInterface |
||||||
|
{ |
||||||
|
public function name(): string |
||||||
|
{ |
||||||
|
return 'user'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class SymfonyPassportCase |
||||||
|
{ |
||||||
|
private array $badges = []; |
||||||
|
|
||||||
|
public function addBadge(SymfonyBadgeInterface $badge, ?string $badgeFqcn = null): static |
||||||
|
{ |
||||||
|
$badgeFqcn ??= $badge::class; |
||||||
|
|
||||||
|
$this->badges[$badgeFqcn] = $badge; |
||||||
|
|
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function hasBadge(string $badgeFqcn): bool |
||||||
|
{ |
||||||
|
return isset($this->badges[$badgeFqcn]); |
||||||
|
} |
||||||
|
|
||||||
|
public function names(): array |
||||||
|
{ |
||||||
|
return array_map(static fn (SymfonyBadgeInterface $badge): string => $badge->name(), $this->badges); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$passport = new SymfonyPassportCase(); |
||||||
|
$passport |
||||||
|
->addBadge(new SymfonyCsrfBadge()) |
||||||
|
->addBadge(new SymfonyUserBadge(), 'custom.user'); |
||||||
|
|
||||||
|
var_dump($passport->hasBadge(SymfonyCsrfBadge::class)); |
||||||
|
var_dump($passport->hasBadge(SymfonyUserBadge::class)); |
||||||
|
var_dump($passport->hasBadge('custom.user')); |
||||||
|
var_dump($passport->names()); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
bool(true) |
||||||
|
bool(false) |
||||||
|
bool(true) |
||||||
|
array(2) { |
||||||
|
["SymfonyCsrfBadge"]=> |
||||||
|
string(4) "csrf" |
||||||
|
["custom.user"]=> |
||||||
|
string(4) "user" |
||||||
|
} |
||||||
Loading…
Reference in new issue