diff --git a/.travis.yml b/.travis.yml index e26019d..7ec92c9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,9 +3,9 @@ sudo: false matrix: include: + - php: 7.2 env: SCRUTINIZER=1 env: COMPOSER_FLAGS="--prefer-lowest" - - php: 7.2 - php: 7.3 - php: nightly allow_failures: @@ -22,7 +22,6 @@ install: before_install: - composer self-update - phpenv config-rm xdebug.ini || true - - composer require phpunit/phpunit ^7.5 before_script: - composer update $COMPOSER_FLAGS --prefer-dist diff --git a/composer.json b/composer.json index a48a779..e47ca74 100644 --- a/composer.json +++ b/composer.json @@ -23,14 +23,15 @@ "require": { "php": "^7.2", "psr/log": "^1.1", + "biurad/biurad-helpers": "^0.1", "symfony/event-dispatcher-contracts": "^1.1|^2" }, "require-dev": { - "phpunit/phpunit": "~7.0" + "phpunit/phpunit": "^8.4" }, "autoload": { "psr-4": { - "BiuradPHP\\Event\\": "src/" + "BiuradPHP\\Events\\": "src/" }, "exclude-from-classmap": [ "/Tests/" @@ -38,7 +39,7 @@ }, "autoload-dev": { "psr-4": { - "BiuradPHP\\Event\\Tests\\": "Tests/" + "BiuradPHP\\Events\\Tests\\": "Tests/" } }, "minimum-stability": "dev", diff --git a/src/Concerns/StoppableTrait.php b/src/Concerns/StoppableTrait.php index 9b186a4..75838be 100644 --- a/src/Concerns/StoppableTrait.php +++ b/src/Concerns/StoppableTrait.php @@ -31,8 +31,7 @@ trait StoppableTrait * This will typically only be used by the Dispatcher to determine if the * previous listener halted propagation. * - * @return bool - * True if the Event is complete and no further listeners should be called. + * @return bool True if the Event is complete and no further listeners should be called. * False to continue calling listeners. */ public function isPropagationStopped(): bool diff --git a/src/EventAnnotation.php b/src/EventAnnotation.php index 9272e6f..332ab20 100644 --- a/src/EventAnnotation.php +++ b/src/EventAnnotation.php @@ -19,10 +19,9 @@ namespace BiuradPHP\Events; -use BiuradPHP\Events\Interfaces\EventBroadcastInterface; use BiuradPHP\Events\Interfaces\EventDispatcherInterface; use BiuradPHP\Events\Interfaces\EventSubscriberInterface; -use BiuradPHP\Loader\AnnotationLocator; +use BiuradPHP\Loader\Annotations\AnnotationLoader; use BiuradPHP\Loader\Interfaces\AnnotationInterface; use ReflectionClass; use ReflectionMethod; @@ -81,22 +80,13 @@ public function __construct(EventDispatcherInterface $events) /** * Load the annoatation for events. * - * @param AnnotationLocator $annotation + * @param AnnotationLoader $annotation */ - public function register(AnnotationLocator $annotation): void + public function register(AnnotationLoader $annotation): void { - /** - * @var ReflectionClass $reflector - * @var Annotation\Listener $classAnnotation - */ - foreach ($annotation->findClasses($this->eventAnnotationClass) as [$reflector, $classAnnotation]) { - if ($reflector->implementsInterface(EventBroadcastInterface::class)) { - $this->events->addListener( - $classAnnotation->getEvent(), - $reflector->getName(), - $classAnnotation->getPriority() - ); - } elseif ($reflector->implementsInterface(EventSubscriberInterface::class)) { + /** @var ReflectionClass $reflector */ + foreach ($annotation->findClasses($this->eventAnnotationClass) as [$reflector,]) { + if ($reflector->implementsInterface(EventSubscriberInterface::class)) { $this->events->addSubscriber($reflector->getName()); } } diff --git a/src/EventContext.php b/src/EventContext.php deleted file mode 100644 index 7f23757..0000000 --- a/src/EventContext.php +++ /dev/null @@ -1,158 +0,0 @@ - - * @copyright 2019 Biurad Group (https://biurad.com/) - * @license https://opensource.org/licenses/BSD-3-Clause License - * - * @link https://www.biurad.com/projects/eventmanager - * @since Version 0.1 - */ - -namespace BiuradPHP\Events; - -use InvalidArgumentException; - -/** - * Class to provide context information for a passed event. - * - * @author Mark Garrett - */ -class EventContext -{ - /** - * @var EventListener - */ - protected $event; - - /** - * - * @var array - */ - private $debugBacktrace = []; - - /** - * @param EventListener $event (Optional) The event to provide context to. The event must be set either here or - * with {@see setEvent()} before any other methods can be used. - */ - public function __construct(EventListener $event = null) - { - if ($event) { - $this->setEvent($event); - } - } - - /** - * @param EventListener $event The event to add context to. - * @return void - */ - public function setEvent(EventListener $event): void - { - $this->event = $event; - } - - /** - * @return EventListener - */ - public function getEvent(): EventListener - { - if (! $this->event) { - throw new InvalidArgumentException(sprintf('%s: expects an event to have been set.', __METHOD__)); - } - - return $this->event; - } - - /** - * Returns either the class name of the target, or the target string - * - * @return string - */ - public function getEventTarget() - { - $event = $this->getEvent(); - - return $this->getEventTargetAsString($event->getListener()); - } - - /** - * Determines a string label to represent an event target. - * - * @param mixed $target - * - * @return string - */ - private function getEventTargetAsString($target) - { - if (is_object($target)) { - return get_class($target); - } - - if (is_resource($target)) { - return get_resource_type($target); - } - - if (is_scalar($target)) { - return (string) $target; - } - - return gettype($target); - } - - /** - * Returns the debug_backtrace() for this object with two levels removed so that array starts where this - * class method was called. - * - * @return string - */ - private function getDebugBacktrace() - { - if (! $this->debugBacktrace) { - //Remove the levels this method introduces - $trace = debug_backtrace(); - $this->debugBacktrace = $trace; - } - - return $this->debugBacktrace; - } - - /** - * Returns the filename and parent directory of the file from which the event was triggered. - * - * @return string - */ - public function getEventTriggerFile() - { - $backtrace = $this->getDebugBacktrace(); - - if (file_exists($backtrace[4]['file'])) { - return basename(dirname($backtrace[4]['file'])) . '/' . basename($backtrace[4]['file']); - } - - return ''; - } - - /** - * Returns the line number of the file from which the event was triggered. - * - * @return integer - */ - public function getEventTriggerLine() - { - $backtrace = $this->getDebugBacktrace(); - - if (isset($backtrace[4]['line'])) { - return $backtrace[4]['line']; - } - - return ''; - } -} diff --git a/src/EventDispatcher.php b/src/EventDispatcher.php index 40ef6b2..eb3ba2a 100644 --- a/src/EventDispatcher.php +++ b/src/EventDispatcher.php @@ -20,23 +20,15 @@ namespace BiuradPHP\Events; use Psr\Container\ContainerInterface; -use BiuradPHP\Events\Exceptions\EventsException; -use BiuradPHP\DependencyInjection\Interfaces\FactoryInterface; -use BiuradPHP\Events\Interfaces\EventBroadcastInterface; use BiuradPHP\Events\Interfaces\EventSubscriberInterface; use Psr\EventDispatcher\StoppableEventInterface; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; -use Closure; use ReflectionClass; use ReflectionException; use Serializable; use SplPriorityQueue; -use function is_string; -use function serialize; -use function unserialize; - /** * The Event Dispatcher. * @@ -53,10 +45,24 @@ class EventDispatcher implements Interfaces\EventDispatcherInterface, Serializab use LoggerAwareTrait; /** @var EventListener[] */ - protected $listeners = []; + protected $listeners; /** @var ContainerInterface */ - protected static $container; + protected $container; + + /** + * Prototype to use when creating an event. + * + * @var EventListener|LazyEventListener + */ + protected $eventPrototype; + + public function __construct() + { + $this->eventPrototype = function ($eventName, $target, $priority) { + return new EventListener($eventName, $target, $priority); + }; + } /** * Set the value of container. @@ -67,7 +73,10 @@ class EventDispatcher implements Interfaces\EventDispatcherInterface, Serializab public function setContainer(ContainerInterface $container): EventDispatcher { // Just incase the container responds with a null value; - static::$container = $container; + $this->container = $container; + $this->eventPrototype = function ($eventName, $target, $priority) use ($container) { + return new LazyEventListener($eventName, $target, $priority, $container); + }; return $this; } @@ -80,7 +89,6 @@ public function setContainer(ContainerInterface $container): EventDispatcher public function getListenersForEvent(object $event): iterable { $queue = new SplPriorityQueue(); - foreach ($this->listeners as $eventName => $listeners) { if ( is_object($event) && class_exists($eventName) && @@ -88,7 +96,7 @@ public function getListenersForEvent(object $event): iterable ) { /** @var EventListener $listener */ foreach ($listeners as $listener) { - $queue->insert($listener->getListener(), $listener->getPriority()); + $queue->insert($listener, $listener->getPriority()); } } } @@ -107,44 +115,25 @@ public function dispatch($event, array $payload = []) } if (is_object($event) && $this->hasListeners(get_class($event))) { - $this->callListeners($this->getListenersForEvent($event), $event, $payload); - + $this->callRecursiveListeners($this->getListenersForEvent($event), $event, $payload); return $event; } - // When the given "event" is actually an object we will assume it is an event - // object and use the class as the event name and this event itself as the - // payload to the handler, which makes object based events quite simple. - [$event, $payload] = $this->parseEventAndPayload($event, $payload); - $eventName = is_object($event) ? get_class($event) : $event; - - if (null !== $this->logger) { - $this->logger->debug(sprintf('The "%s" event is dispatched. No listeners have been called.', $eventName)); - } - - $responses = []; - foreach ($this->getListeners($eventName) as $listener) { - $response = $listener($eventName, $payload); + try { + // When the given "event" is actually an object we will assume it is an event + // object and use the class as the event name and this event itself as the + // payload to the handler, which makes object based events quite simple. + $eventName = is_object($event) ? get_class($event) : $event; - // If a response is returned from the listener and event halting is enabled - // we will just return this response, and not call the rest of the event - // listeners. Otherwise we will add the response on the response list. - if (func_num_args() > 2 && !is_null($response)) { - return $response; - } - - // If a boolean false is returned from a listener, we will stop propagating - // the event to any further listeners down in the chain, else we keep on - // looping through the listeners and firing every one in our sequence. - if (is_bool($response) && $response !== true) { - break; + $responses = $this->callListeners($eventName, $payload, func_get_args() > 2); + } finally { + if (null !== $this->logger) { + $this->logger->debug(sprintf('The "%s" event is dispatched. No listeners have been called.', $eventName)); } - - $responses[] = $response; } - return func_num_args() > 2 ? $event : $responses; + return !is_iterable($responses) ? $responses : $event; } /** @@ -165,13 +154,9 @@ public function dispatchNow($event, array $payload = []) /** * {@inheritdoc} */ - public function hasListeners(string $eventName = null) + public function hasListeners(string $eventName) { - if (null !== $eventName) { - return !empty($this->listeners[$eventName]); - } - - return false; + return !empty($this->listeners[$eventName]); } /** @@ -179,7 +164,7 @@ public function hasListeners(string $eventName = null) */ public function addListener(string $eventName, $listener, int $priority = 1) { - $this->listeners[$eventName][] = new EventListener($eventName, $this->makeListener($listener), $priority); + $this->listeners[$eventName][] = ($this->eventPrototype)($eventName, $listener, $priority); } /** @@ -187,7 +172,7 @@ public function addListener(string $eventName, $listener, int $priority = 1) */ public function removeListener(string $eventName) { - if (empty($this->listeners[$eventName])) { + if (!$this->hasListeners($eventName)) { return; } @@ -202,7 +187,7 @@ public function removeListener(string $eventName) * Get all of the listeners for a given event name. * * @param string $eventName - * @return Closure[]|object[]|array + * @return callble[]|object[]|array */ public function getListeners(string $eventName) { @@ -213,32 +198,15 @@ public function getListeners(string $eventName) $queue = new SplPriorityQueue(); foreach ($listeners as &$listener) { - $queue->insert($listener->getListener(), $listener->getPriority()); + $queue->insert($listener, $listener->getPriority()); } return $queue; } - /** - * Get all of the contexts for a given event name. - * - * @param string $eventName - * @return EventContext[]|array - */ - public function getContexts(string $eventName): iterable - { - $listeners = $this->listeners[$eventName] ?? []; - $listeners = class_exists($eventName) - ? $this->addInterfaceListeners($eventName, $listeners) - : ($listeners); - - foreach ($listeners as $listener) { - yield new EventContext($listener); - } - } - /** * {@inheritdoc} + * * @throws ReflectionException */ public function addSubscriber($subscriber) @@ -263,6 +231,7 @@ public function addSubscriber($subscriber) /** * {@inheritdoc} + * * @throws ReflectionException */ public function removeSubscriber($subscriber) @@ -280,64 +249,6 @@ public function removeSubscriber($subscriber) } } - /** - * Register an event listener with the dispatcher. - * - * @param Closure|callable|object|string $listener - * - * @return Closure|object|mixed - */ - public function makeListener($listener) - { - if (!$listener instanceof Closure && (is_object($listener) || $listener[0] instanceof EventSubscriberInterface)) { - return $this->createClassCallable($listener); - } - - return function (/** @noinspection PhpUnusedParameterInspection */ $event, array $payload) use (&$listener) { - if ( - (!$listener instanceof Closure && is_object($listener)) || - is_string($listener) && class_exists($listener) - ) { - $listener = $this->createClassCallable($listener); - } - - return $this->resolveCallable($listener, $payload); - }; - } - - /** - * Resolve callables. - * - * @param Closure|callable|mixed $unresolved - * @param array $arguments - * - * @return Closure|string|null - */ - private function resolveCallable($unresolved, $arguments) - { - if (null === static::$container) { - return $unresolved(...array_values($arguments)); - } elseif (self::$container instanceof FactoryInterface) { - return static::$container->callMethod($unresolved, $arguments); - } elseif (method_exists(self::$container, 'call')) { - return static::$container->call($unresolved, $arguments); - } - - return $unresolved(...array_values($arguments)); - } - - /** - * Check if event should be broadcasted by condition. - * - * @param mixed $event - * - * @return bool - */ - protected function broadcastDone($event) - { - return method_exists($event, 'broadcastDone') ? $event->broadcastDone() : true; - } - /** * Triggers the listeners of an event. * @@ -345,109 +256,84 @@ protected function broadcastDone($event) * for each listener. * * @param callable[] $listeners The event listeners - * @param object|Closure|StoppableEventInterface $event The event object to pass to the event handlers/listeners\ + * @param object|callable|StoppableEventInterface $event The event object to pass to the event handlers/listeners\ * @param array $arguments * * @return void */ - protected function callListeners(iterable $listeners, $event, array $arguments): void + protected function callRecursiveListeners(iterable $listeners, $event, array $arguments): void { - $stoppable = $event instanceof StoppableEventInterface; - $parameters = array_merge((null !== self::$container ? [$event] : [$event, $this]), $arguments); + $parameters = array_merge((null !== $this->container ? [$event] : [$event, $this]), $arguments); - /** @var callable|CLosure $listener */ + /** @var EventListener $listener */ foreach ($listeners as $listener) { $context = [ 'event' => get_class($event), - 'listener' => $listener instanceof Closure ? 'Closure' : get_class($listener[0]), - 'method' => $listener instanceof Closure ? 'Type' : $listener[1] + 'listener' => $listener->getEvent() ]; - if ($stoppable && $event->isPropagationStopped()) { + if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) { if (null !== $this->logger) { - $this->logger->debug('Listener "[{listener}::{method}]" stopped propagation of the event "{event}".', $context); + $this->logger->debug('Listener "[{listener}]" stopped propagation of the event "{event}".', $context); } break; } try { - if ($listener instanceof Closure) { - $listener($context['event'], $parameters); - continue; - } - - $this->resolveCallable($listener, $parameters); + $listener($context['event'], $parameters); } finally { if (null !== $this->logger) { - $this->logger->debug('Notified event "{event}" to listener "[{listener}::{method}]".', $context); + $this->logger->debug('Notified event "{event}" to listener "[{listener}]".', $context); } } } } /** - * Create the class based event callable. - * - * @param string $listener + * Triggers the listeners of an event. * - * @return callable + * @param string $eventName + * @param array $payload + * @param boolean|null $halt + * + * @return iterable */ - protected function createClassCallable($listener) + protected function callListeners(string $eventName, array $payload, ?bool $halt): iterable { - [$class, $method] = $this->parseClassCallable($listener); - - if ((is_object($class) && mb_strpos(get_class($class), 'class@anonymous') !== false) || is_object($class)) { - $controller = $class; - } else { - $controller = null !== static::$container ? static::$container->get($class) : new $class(); - } - - if ($controller instanceof EventBroadcastInterface) { - return $this->createQueuedHandlerCallable($controller); - } + foreach ($this->getListeners($eventName) as $listener) { + $response = $listener($eventName, $payload); - return [$controller, $method]; - } + // If a response is returned from the listener and event halting is enabled + // we will just return this response, and not call the rest of the event + // listeners. Otherwise we will add the response on the response list. + if (true === $halt && !is_null($response)) { + return $response; + } - /** - * Parse the class@listener, class::method, object into class and method. - * - * @param string|callable|object $listener - * - * @return array|callable - */ - protected function parseClassCallable($listener) - { - if (is_object($listener)) { - if (!method_exists($listener, '__invoke')) { - throw new EventsException('The object has to implement %s __invoke method, else replace with callable or string with method avialable'); + // If a boolean false is returned from a listener, we will stop propagating + // the event to any further listeners down in the chain, else we keep on + // looping through the listeners and firing every one in our sequence. + if (is_bool($response) && $response !== true) { + break; } - return [$listener, '__invoke']; - } elseif ((is_array($listener) && count($listener) == 2) || is_callable($listener)) { - return $listener; - } elseif (strpos($listener, '::') !== false) { - return explode('::', $listener, 2); - } elseif (strpos($listener, '@') !== false) { - return explode('@', $listener, 2); + yield $response; } - - return $listener; } /** * Create a new instance for listener to run. * * @param string|object $class - * * @param array $arguments + * * @return string * @throws ReflectionException */ - private function createListenerInstance($class, $arguments = []) + private function createListenerInstance($class, array $arguments = null) { if (is_object($class)) { - return !empty($arguments) ? get_class($arguments) : $class; + return $class; } $instance = (new ReflectionClass($class)); @@ -455,13 +341,7 @@ private function createListenerInstance($class, $arguments = []) throw new Exceptions\EventsException("Targeted [$class] is not instantiable"); } - if (null !== static::$container) { - $instance = method_exists(self::$container, 'make') - ? static::$container->make($class, ...$arguments) - : static::$container->get($class); - } - - return $instance instanceof ReflectionClass ? $instance->newInstanceArgs($arguments) : $instance; + return $instance->newInstanceArgs($arguments); } /** @@ -515,60 +395,20 @@ private function listenerShouldSubscribe($class) $arguments = []; if (is_string($class) && strpos($class, '@') !== false) { [$class, $arguments] = explode('@', $class); - - if ((null !== self::$container && (self::$container->has($arguments) || class_exists($arguments)))) { - $arguments = self::$container->get($arguments); + if (null !== $this->container && $this->container->has($arguments)) { + $arguments = [$this->container->get($arguments)]; } - $arguments = (is_string($arguments) && class_exists($arguments)) ? new $arguments() : $arguments; + $arguments = [(is_string($arguments) && class_exists($arguments)) ? new $arguments() : $arguments]; } - $arguments = !is_array($arguments) ? [$arguments] : $arguments; - $class = $this->createListenerInstance($class, $arguments); - - if ($class instanceof EventSubscriberInterface) { + if (($class = $this->createListenerInstance($class, $arguments)) instanceof EventSubscriberInterface) { return $class; } return false; } - /** - * Create a callable for putting an event handler on the queue. - * - * @param EventBroadcastInterface $class - * - * @return bool|Closure - */ - protected function createQueuedHandlerCallable(EventBroadcastInterface $class) - { - if ($this->listenerWantsToBeQueued($class)) { - if (null !== $this->logger) { - $this->logger->debug(sprintf('The "%s" broadcaster has been dispatched on request, no need to re-dispatch.', get_class($class))); - } - - return $this->broadcastDone($class); - } - - return false; - } - - /** - * Determine if the event handler wants to be queued. - * - * @param EventBroadcastInterface $class - * - * @return bool - */ - protected function listenerWantsToBeQueued(EventBroadcastInterface $class): bool - { - if (method_exists($class, 'broadcastOn')) { - return $class->broadcastOn(); - } - - return false; - } - /** * @return array */ diff --git a/src/EventListener.php b/src/EventListener.php index ccdeaa2..873956e 100644 --- a/src/EventListener.php +++ b/src/EventListener.php @@ -19,20 +19,49 @@ namespace BiuradPHP\Events; +use BiuradPHP\Support\BoundMethod; use Closure; +use ReflectionClass; -final class EventListener +/** + * Event listener instance. + * + * Event listener definitions add the following members to what the + * EventDispatcher accepts: + * + * - event: the event name to attach to. + * - target: the targeted callback attach to. + * - priority: the priority at which to attach the listener, if not the default. + * + * @author Divine Niiquaye Ibok + */ +class EventListener { + /** + * Event name to which to attach. + * + * @var string + */ private $event; - private $listener; + + /** + * Event target to which to attach. + * + * @var string|callable|object + */ + private $target; + + /** + * @var null|int Priority at which to attach. + */ private $priority; /** * @param string $eventName - * @param callable|Closure|string $listener - * @param integer $priority + * @param callable|object|string $listener + * @param int $priority */ - public function __construct(string $eventName, $listener, int $priority = 1) + public function __construct(string $eventName, $listener, $priority = 1) { $this->setEvent($eventName); $this->setListener($listener); @@ -59,25 +88,27 @@ private function setEvent(string $eventName): void /** * Get the event's listener * - * @return object|Closure + * @return callable|string|object */ public function getListener() { - return $this->listener; + return $this->target; } /** * Set the event's listener * - * @param callable|Closure|string $listener + * @param callable|object|string $listener */ private function setListener($listener): void { - $this->listener = $listener; + $this->target = $listener; } /** * Get the event's priority + * + * @return int */ public function getPriority(): int { @@ -86,10 +117,35 @@ public function getPriority(): int /** * Set the event's priority + * * @param int $priority */ private function setPriority(int $priority): void { $this->priority = $priority; } + + /** + * Use the listener as an invokable, allowing direct attachment to an EventDispatcher. + * + * @param string $eventName + * @param array $parameters + * + * @return mixed + */ + public function __invoke(string $eventName, array $parameters) + { + if (is_object($listener = $this->target) && !$listener instanceof Closure) { + $listener = [$listener, 'invoke']; + if (!method_exists($listener, '__invoke')) { + return $listener; + } + } + + if (is_string($listener) && class_exists($listener)) { + return (new ReflectionClass($listener))->newInstanceArgs($parameters ?: null); + } + + return BoundMethod::call(null, $listener, $parameters); + } } diff --git a/src/GenericEvent.php b/src/GenericEvent.php new file mode 100644 index 0000000..f488f3e --- /dev/null +++ b/src/GenericEvent.php @@ -0,0 +1,182 @@ + + * @copyright 2019 Biurad Group (https://biurad.com/) + * @license https://opensource.org/licenses/BSD-3-Clause License + * + * @link https://www.biurad.com/projects/eventmanager + * @since Version 0.1 + */ + +namespace BiuradPHP\Events; + +use ArrayAccess; +use ArrayIterator; +use InvalidArgumentException; +use IteratorAggregate; +use Symfony\Contracts\EventDispatcher\Event; + +/** + * Event encapsulation class. + * + * Encapsulates events thus decoupling the observer from the subject they encapsulate. + * + * @author Drak + */ +class GenericEvent extends Event implements ArrayAccess, IteratorAggregate +{ + protected $subject; + protected $arguments; + + /** + * Encapsulate an event with $subject and $args. + * + * @param mixed $subject The subject of the event, usually an object or a callable + * @param array $arguments Arguments to store in the event + */ + public function __construct($subject = null, array $arguments = []) + { + $this->subject = $subject; + $this->arguments = $arguments; + } + + /** + * Getter for subject property. + * + * @return mixed The observer subject + */ + public function getSubject() + { + return $this->subject; + } + + /** + * Get argument by key. + * + * @return mixed Contents of array key + * + * @throws InvalidArgumentException if key is not found + */ + public function getArgument(string $key) + { + if ($this->hasArgument($key)) { + return $this->arguments[$key]; + } + + throw new InvalidArgumentException(sprintf('Argument "%s" not found.', $key)); + } + + /** + * Add argument to event. + * + * @param mixed $value Value + * + * @return $this + */ + public function setArgument(string $key, $value) + { + $this->arguments[$key] = $value; + + return $this; + } + + /** + * Getter for all arguments. + * + * @return array + */ + public function getArguments() + { + return $this->arguments; + } + + /** + * Set args property. + * + * @return $this + */ + public function setArguments(array $args = []) + { + $this->arguments = $args; + + return $this; + } + + /** + * Has argument. + * + * @return bool + */ + public function hasArgument(string $key) + { + return \array_key_exists($key, $this->arguments); + } + + /** + * ArrayAccess for argument getter. + * + * @param string $key Array key + * + * @return mixed + * + * @throws InvalidArgumentException if key does not exist in $this->args + */ + public function offsetGet($key) + { + return $this->getArgument($key); + } + + /** + * ArrayAccess for argument setter. + * + * @param string $key Array key to set + * @param mixed $value Value + */ + public function offsetSet($key, $value) + { + $this->setArgument($key, $value); + } + + /** + * ArrayAccess for unset argument. + * + * @param string $key Array key + */ + public function offsetUnset($key) + { + if ($this->hasArgument($key)) { + unset($this->arguments[$key]); + } + } + + /** + * ArrayAccess has argument. + * + * @param string $key Array key + * + * @return bool + */ + public function offsetExists($key) + { + return $this->hasArgument($key); + } + + /** + * IteratorAggregate for iterating over the object like an array. + * + * @return ArrayIterator + */ + public function getIterator() + { + return new ArrayIterator($this->arguments); + } +} diff --git a/src/Interfaces/EventBroadcastInterface.php b/src/Interfaces/EventBroadcastInterface.php deleted file mode 100644 index 3e389c8..0000000 --- a/src/Interfaces/EventBroadcastInterface.php +++ /dev/null @@ -1,33 +0,0 @@ - - * @copyright 2019 Biurad Group (https://biurad.com/) - * @license https://opensource.org/licenses/BSD-3-Clause License - * - * @link https://www.biurad.com/projects/eventmanager - * @since Version 0.1 - */ - -namespace BiuradPHP\Events\Interfaces; - -interface EventBroadcastInterface -{ - /** - * Get the channels the event should broadcast on. - */ - public function broadcastOn(); - - /** - * This boots or broadcast the channels in event. - */ - public function broadcastDone(): bool; -} diff --git a/src/Interfaces/EventDispatcherInterface.php b/src/Interfaces/EventDispatcherInterface.php index b8384d8..ee2439f 100644 --- a/src/Interfaces/EventDispatcherInterface.php +++ b/src/Interfaces/EventDispatcherInterface.php @@ -19,8 +19,6 @@ namespace BiuradPHP\Events\Interfaces; -use BiuradPHP\Events\EventContext; -use Closure; use Psr\EventDispatcher\ListenerProviderInterface; /** @@ -29,7 +27,7 @@ * manager. * * @author Bernhard Schussek - * @author Divine Niiquaye Ibok + * @author Divine Niiquaye Ibok */ if (PHP_VERSION_ID >= 70200) { interface EventDispatcherInterface extends ListenerProviderInterface @@ -38,7 +36,7 @@ interface EventDispatcherInterface extends ListenerProviderInterface * Adds an event listener that listens on the specified events. * * @param string $eventName - * @param callable|Closure|object|string $listener The listener + * @param callable|object|string $listener The listener * @param int $priority The higher this value, the earlier an event * listener will be triggered in the chain (defaults to 1) */ @@ -83,28 +81,11 @@ public function getListeners(string $eventName); /** * Checks whether an event has any registered listeners. * - * @param string|null $eventName + * @param string $eventName * * @return bool true if the specified event has any listeners, false otherwise */ - public function hasListeners(string $eventName = null); - - /** - * Register an event listener with the dispatcher. - * - * @param Closure|callable|object|string $listener - * - * @return Closure - */ - public function makeListener($listener); - - /** - * Get all of the contexts for a given event name. - * - * @param string $eventName - * @return EventContext[]|array - */ - public function getContexts(string $eventName): iterable; + public function hasListeners(string $eventName); /** * Fire an event until the first non-null response is returned. @@ -134,7 +115,7 @@ interface EventDispatcherInterface * Adds an event listener that listens on the specified events. * * @param string $eventName - * @param callable|Closure|object|string $listener The listener + * @param callable|object|string $listener The listener * @param int $priority The higher this value, the earlier an event * listener will be triggered in the chain (defaults to 1) */ @@ -179,28 +160,11 @@ public function getListeners(string $eventName); /** * Checks whether an event has any registered listeners. * - * @param string|null $eventName + * @param string $eventName * * @return bool true if the specified event has any listeners, false otherwise */ - public function hasListeners(string $eventName = null); - - /** - * Register an event listener with the dispatcher. - * - * @param Closure|callable|object|string $listener - * - * @return Closure - */ - public function makeListener($listener); - - /** - * Get all of the contexts for a given event name. - * - * @param string $eventName - * @return EventContext[]|array - */ - public function getContexts(string $eventName): iterable; + public function hasListeners(string $eventName); /** * Fire an event until the first non-null response is returned. diff --git a/src/LazyEventListener.php b/src/LazyEventListener.php new file mode 100644 index 0000000..4c49d61 --- /dev/null +++ b/src/LazyEventListener.php @@ -0,0 +1,102 @@ + + * @copyright 2019 Biurad Group (https://biurad.com/) + * @license https://opensource.org/licenses/BSD-3-Clause License + * + * @link https://www.biurad.com/projects/eventmanager + * @since Version 0.1 + */ + +namespace BiuradPHP\Events; + +use BiuradPHP\Events\Exceptions\EventsException; +use BiuradPHP\Support\BoundMethod; +use Closure; +use Nette\DI\Container; +use Psr\Container\ContainerInterface; + +/** + * Lazy listener instance. + * + * Used as an internal class for the EventDispatcher to allow lazy creation of + * listeners via a dependency injection container. + * + * Lazy event listener definitions add the following members to what the + * EventDispatcher accepts: + * + * - event: the event name to attach to. + * - target: the targeted callback attach to. + * - priority: the priority at which to attach the listener, if not the default. + * + * @author Divine Niiquaye Ibok + */ +class LazyEventListener extends EventListener +{ + /** + * Container from which to pull listener and resolve. + * + * @var ContainerInterface + */ + private $container; + + /** + * @param string $eventName + * @param callable|object|string $listener + * @param int $priority + * @param ContainerInterface $container + */ + public function __construct(string $eventName, $listener, int $priority, ContainerInterface $container) + { + $this->container = $container; + parent::__construct($eventName, $listener, $priority); + } + + /** + * {@inheritdoc} + */ + public function __invoke(string $eventName, array $parameters) + { + if (is_object($listener = $this->getListener()) && !$listener instanceof Closure) { + $listener = [$listener, 'invoke']; + if (!method_exists($listener, '__invoke')) { + return $listener; + } + } + + if ($this->container->has($eventName)) { + throw new EventsException( + sprintf('Lazy listener name "%s" cannot exist in in dependency injection container', $eventName) + ); + } + + // Return the listener found in any type of container. + if (is_string($listener) && $this->container->has($listener)) { + // For Laminas Container, we at ease + if (method_exists($this->container, 'build')) { + return $this->container->build($listener, $parameters ?: null); + } + + return $this->container->get($listener); + } + + // For Nette Container, we at ease... + if ( + $this->container instanceof Container && + is_string($listener) && class_exists($listener) + ) { + return $this->container->createInstance($listener, $parameters); + } + + return BoundMethod::call($this->container, $listener, $parameters); + } +}