<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\EventDispatcher\Debug;
use Psr\EventDispatcher\StoppableEventInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Component\VarDumper\Caster\ClassStub;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
final class WrappedListener
{
private string|array|object $listener;
private ?\Closure $optimizedListener;
private string $name;
private bool $called = false;
private bool $stoppedPropagation = false;
private Stopwatch $stopwatch;
private ?EventDispatcherInterface $dispatcher;
private string $pretty;
private string $callableRef;
private ClassStub|string $stub;
private ?int $priority = null;
private static bool $hasClassStub;
public function __construct(callable|array $listener, ?string $name, Stopwatch $stopwatch, EventDispatcherInterface $dispatcher = null, int $priority = null)
{
$this->listener = $listener;
$this->optimizedListener = $listener instanceof \Closure ? $listener : (\is_callable($listener) ? $listener(...) : null);
$this->stopwatch = $stopwatch;
$this->dispatcher = $dispatcher;
$this->priority = $priority;
if (\is_array($listener)) {
[$this->name, $this->callableRef] = $this->parseListener($listener);
$this->pretty = $this->name.'::'.$listener[1];
$this->callableRef .= '::'.$listener[1];
} elseif ($listener instanceof \Closure) {
$r = new \ReflectionFunction($listener);
if (str_contains($r->name, '{closure}')) {
$this->pretty = $this->name = 'closure';
} elseif ($class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass()) {
$this->name = $class->name;
$this->pretty = $this->name.'::'.$r->name;
} else {
$this->pretty = $this->name = $r->name;
}
} elseif (\is_string($listener)) {
$this->pretty = $this->name = $listener;
} else {
$this->name = get_debug_type($listener);
$this->pretty = $this->name.'::__invoke';
$this->callableRef = $listener::class.'::__invoke';
}
if (null !== $name) {
$this->name = $name;
}
self::$hasClassStub ??= class_exists(ClassStub::class);
}
public function getWrappedListener(): callable|array
{
return $this->listener;
}
public function wasCalled(): bool
{
return $this->called;
}
public function stoppedPropagation(): bool
{
return $this->stoppedPropagation;
}
public function getPretty(): string
{
return $this->pretty;
}
public function getInfo(string $eventName): array
{
$this->stub ??= self::$hasClassStub ? new ClassStub($this->pretty.'()', $this->callableRef ?? $this->listener) : $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, EventDispatcherInterface $dispatcher): void
{
$dispatcher = $this->dispatcher ?: $dispatcher;
$this->called = true;
$this->priority ??= $dispatcher->getListenerPriority($eventName, $this->listener);
$e = $this->stopwatch->start($this->name, 'event_listener');
try {
($this->optimizedListener ?? $this->listener)($event, $eventName, $dispatcher);
} finally {
if ($e->isStarted()) {
$e->stop();
}
}
if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
$this->stoppedPropagation = true;
}
}
private function parseListener(array $listener): array
{
if ($listener[0] instanceof \Closure) {
foreach ((new \ReflectionFunction($listener[0]))->getAttributes(\Closure::class) as $attribute) {
if ($name = $attribute->getArguments()['name'] ?? false) {
return [$name, $attribute->getArguments()['class'] ?? $name];
}
}
}
if (\is_object($listener[0])) {
return [get_debug_type($listener[0]), \get_class($listener[0])];
}
return [$listener[0], $listener[0]];
}
}