- 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