-
Notifications
You must be signed in to change notification settings - Fork 62
EventManager refactor for version 3 #4
EventManager refactor for version 3 #4
Conversation
144cd39
to
bcb107a
Compare
Ping @akrabat @Ocramius @bakura10 @codeliner - this is an alternate approach that retains BC, but improves performance radically. |
I will have a look at the code but does this PR contains the lazy listener feature of my refactor? This is actually what I consider the most important feature of my refactor and is the key for intensive use of Evm without having to revert to complex solutions. |
Not yet, but I'd like to get that feature in; I think it would simplify
|
178 additions and 386 deletions. |
A lot more could be removed imho:
public function trigger($event, $target = null, $argv = array(), callable $callback = null)
{
if ($event instanceof EventInterface) {
$e = $event;
$event = $e->getName();
$callback = $target;
} else {
$e = new Event();
$e->setName($event);
$e->setTarget($target);
$e->setParams($argv);
}
// Initial value of stop propagation flag should be false
$e->stopPropagation(false);
return $this->triggerListeners($e, $callback);
}
In overall I'm not against keeping the SEM, but for me the problem is that currently, the use case for SEM is what most people want but it is complicated to use. If SEM stays here for most complex usage, why not, but the most simple use case should be the default one. That's why I think EVM SHOULD be a shared service, to simply allow attaching without having to revert to the complexity of SEM. |
@weierophinney 👍 for the performance improvments. That + BC is a strong argument for keeping the SEM. But I see problems with removing class MyClass extends AbstractEventTriggeringClass
{
public function setEventManager(EventManagerInterface $evm) {
parent::setEventManager($evm);
$evm->addIdentifiers([MyClass::class]);
}
} ... is no longer possible and adds even more complexity to the game because now AND
That sounds like a debugging nightmare! How do you want to document it so that EVERY developer is aware of it? |
@codeliner Thanks for the feedback! I've updated to re-add (However, if you attempt to add identifiers or a shared manager after the first It also addresses your second point. It's rare (in fact, I cannot think of any time it happens in the current MVC) that either identifiers or additional shared listeners will be added after you have triggered the first event. However, it's not uncommon that you'll still be setting up both after you initialize the EM instance. As such, the changes in the latest commit address those issues. |
The point of it is to allow a default event implementation per instance; as an example, a DB component could have a DbEvent that has additional methods and/or defaults. However, that could also be accomplished by sub-classing the EM.
Your recommendation looks reasonable, but before I incorporate it, I'm going to survey how
The idea behind |
I like the approach. Will try and play with the code on Sunday / early next week. |
Agreed. At least as long as you use PHP's shared nothing "feature". Running an app in a node.js like event loop (f.e. with reactphp) is a different story. In this case I can imagine that you may want to |
@@ -174,8 +164,10 @@ public function addIdentifiers($identifiers) | |||
* @return ResponseCollection All listener return values | |||
* @throws Exception\InvalidCallbackException | |||
*/ | |||
public function trigger($event, $target = null, $argv = [], $callback = null) | |||
public function trigger($event, $target = null, $argv = array(), callable $callback = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$argv = array()
can be $argv = []
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll run it through the new CS rules when complete; right now, I'm mainly
testing some concepts for refactoring.
On Jul 3, 2015 4:50 PM, "Abdul Malik Ikhsan" notifications@github.com
wrote:
In src/EventManager.php
#4 (comment)
:@@ -174,8 +164,10 @@ public function addIdentifiers($identifiers)
* @return ResponseCollection All listener return values
* @throws Exception\InvalidCallbackException
*/
- public function trigger($event, $target = null, $argv = [], $callback = null)
- public function trigger($event, $target = null, $argv = array(), callable $callback = null)
$argv = array() can be $argv = []
—
Reply to this email directly or view it on GitHub
https://github.com/zendframework/zend-eventmanager/pull/4/files#r33881585
.
This would not be hard to accomplish at this point. The logic would be far easier than we have currently, as you only need the callable.
Diactoros doesn't need them, really; its primary focus is implementation of the HTTP message interfaces. (The |
a5690b9
to
f5f4821
Compare
Odd failure on HHVM: However, tests passed (segfault was within PHPUnit after test completion). At this point, we have excellent test coverage. Creating a few TODO items, and then we can start reviewing for merge. |
I took a look at the "lazy listener" functionality as proposed by @bakura10, and I'm not convinced by the implementation. It essentially requires:
I have problems with this for a few reasons:
However, I think we already can achieve lazy listeners, due to something I've already alluded to: listeners must be callables. This allows creating lazy listeners via closures! $events->attach('foo', function ($e) use ($container) {
return $container->get('MyCustomListener')->onWhatever($e);
}); Essentially, if we want lazy listeners, we can wrap the logic in closures, pushing the performance hit to when the listener is actually triggered. With PHP 7, it would be relatively simple to achieve lazy aggregates as well via anonymous classes. As such, I'm ticking off the box "lazy services", and will move to the other items on the list. |
@codeliner — I've looked into what it would take for the I have implemented // We'll assume `$callback` is a listener we want to remove:
$listeners = $events->getListeners('event-of-interest');
$events->clearListeners('event-of-interest');
foreach ($listeners as $listener) {
if ($listener === $callback) {
continue;
}
$events->attach('event-of-interest', $listener);
} It will work:
So, the above will work. Where it gets dicey is priority. You'll be iterating them in priority order, so re-attaching them preserves the original order. The problem is if you attach other listeners later, using priority: at this time, they'll all be added with priority 1; we will have no record of the original priority. As such, any priority you specify later when attaching listeners will be relative to the default of 1. And that's why I don't want to re-add this facility; it requires we store the priority alongside the listener, inflating storage, making getting at the listeners harder (we're essentially back to the tl;dr: Essentially, if you want a solution that has both priority and the ability to detach, you lose performance. |
@weierophinney , actually the hit is when you register aggregate listener. Aggregate listener is the one that do the attach, but the dependencies are part of the aggregate listener. This is where you have the performance it. This would mean wrapping the aggregate listener, but if you do that, the aggregate listener do not have the chance to register its listeners. |
@bakura10 Attaching has very little measurable performance impact, though (particularly with the revisions in this PR); where are you arriving at the idea that it incurs a performance hit? |
The expensive part is creating all the listener aggregates, which all needs to pull many dependencies from service manager. This is what incurs a performance hit, not the attaching itself :). |
When you do that: $aggregate = $sm->get(UserListenerAggregate::class);
$aggregate->attach(); This is the "get" that is expensive. But becuase you need to create the lsitener aggregate to bind events, you have no other choice. That's what my PR tried to tackled with the static stuff :). Being able to attach events WITHOUT the expesnive part of creating the aggregate |
Understood. The static approach doesn't fix that, however; if you use that approach, of course it will be faster, because you're not fetching anything from the service manager! And that makes it less usable, because you can no longer have aggregate listeners that have dependencies. Which brings me back to my earlier suggestion: if the aggregate is only attaching to a single event, wrapping the logic of pulling it from the container will work now; just bypass the For true aggregates that attach many listeners, you would need a different approach. But that can be done in userland as well, with a little finesse: class ThisIsAnAggregate implements ListenerAggregateInterface
{
public static $attachTo = [
[ 'event' => 'event1', 'method' => 'onEvent1', 'priority' => 1000 ],
[ 'event' => 'event2', 'method' => 'onEvent2', 'priority' => -100 ],
[ 'event' => 'event3', 'method' => 'onEvent3', 'priority' => 1 ],
];
// define a constructor with dependencies ...
public function attach(EventManagerInterface $events, $priority = 1)
{
foreach (self::$attachTo as $spec) {
$events->attach($spec['event'], [ $this, $spec['method'] ], $spec['priority']);
}
}
// define the event listener methods ...
}
// In your application:
foreach (ThisIsAnAggregate::$attachTo as $spec) {
$events->attach($spec['event'], function ($e) use ($services) {
return $services->get(ThisIsAnAggregate::class)->{$spec['method']}($e);
}, $spec['priority']);
} It makes use of statics, just like what you were doing, but doesn't require any changes to any existing interfaces, either in v2 or this proposal. By using a closure, we can put anything we want into the logic, including any environmental aspects we may want to use to decide whether or not we really need to pull the service. I suppose we could do a new
and then Or we could just document the solution I indicated above somewhere. 😄 Thoughts, @bakura10 ? |
Hhhmmm yeah that sounds like a nice workaround. It would be nice if we could have some syntactic sugar around this, but it should work. |
How the FastPriorityQueue compared to my simpler, dependency-less implementation here: #2? I've been able to do that with pure array and much simpler code. My code being much much smaller, maybe you could start from this base, and start re-adding features you need (actually, only the sharedEventManager is really missing from my impl, and a few methods you may want for compatibility). |
I would like to see removed all those setters on the EventInterface. I think trigger() can typehint EventInterface without problems. $eventManager->trigger($name, $target, $params, $callback);
$eventManager->trigger(new Event($name, $target, $params), $callback);
The only thing its no possible reuse an event with different event names (Is this really a need?) $eventManager->attach('event.foo', function(EventInterface $eventFoo) {
$eventManager->trigger('event.BAZ', $eventFoo);
}); |
Unnecessary, and pointless in most cases (as these are not value objects, and methods cannot be chained naturally).
- Updated one docblock that was missed previously as well.
Identifiers are in the context of a SharedEventManager, which means that the SEM instance is required *before* identifiers have semantic meaning. Additionally, there is no reason to set identifiers passed to the constructor *unless* a shared event manager instance is present.
This patch imports and revises the original reStructured Text documentation sources to Markdown, editing them to provide new chapters and ensure examples follow the proposed API. These make reference to new chapters that are intended, specifically one on aggregates.
FastPriorityQueue is used by FilterChain. Since this is an optional feature, it can be a suggested dependency, and required in development mode for testing.
This patch is a first pass at ensuring all tests run with the refactor as proposed by @bakura10. Because the internal data structures have changed, many of the test assertions needed to change. In writing them, I also discovered some necessary interface changes: - `SharedEventManager::getListeners()` needs to *require* the `$eventName` argument, and it *cannot* be null, empty, or a wildcard. The method will only be called within the context of `EventManager::triggerListeners()`, where the event name is always known, and never a wildcard. Making this restriction simplifies the internal logic and reduces the number of required permutations. - Neither `SharedEventManagerInterface::clearListeners()` nor `detach()` need to have a return value. In order to implement the `detach()` methods of each of the `EventManager` and `SharedEventManager`, I added an optional argument, a "force" flag; this is false by default, but internally allows recursively calling the method when detaching from wildcards. This simplified logic, as it allows recursive detachment, vs attempting to hit every possible permutation inline.
Since we're no longer using the FastPriorityQueue internally, we need to ensure that priority order is being kept. This new test ensures the following order is preserved: - Listeners of different priorities are triggered in priority order. - Listeners of the same priority are triggered in attachment order. - Wildcard listeners are triggered after explicit listeners of the same priority. - Shared listeners are triggered after wildcard listeners of the same priority. - Shared wildcard listeners are triggered after shared explicit listeners of the same priority. - Shared listeners on wildcard identifiers and explicit events are triggered after shared listeners with explicit identifiers but wildcard events. - Shared listeners on wildcard identifiers and wildcard events are triggered after shared listeners on wildcard identifiers and explicit events. Tests for each explicit behavior, plus a comprehensive complex example are provided.
Increased coverage around conditionals checking for invalid input.
@bakura10 had already updated the interface in his PR; this documents the change and provides migration tips.
This patch removes the `attachAggregate()` and `detachAggregate()` methods from the `EventManagerInterface`. Since they simply proxied to the `attach()` and `detach()` methods of the provided `ListenerAggregateInterface` implementation, it was simply a redundant way to attach aggregate listeners that resulted in performance overhead. Migration documentation has been updated, as have all examples using aggregates.
Not everyone will use lazy listeners. As such, they will need to opt-in by adding container-interop to their project.
3f3d01b
to
63cdc32
Compare
This patch refactors zend-eventmanager with the following goals:
Alongside these goals, we also tried to minimize migration for end users. To this end, migration document was written, and a 2.6.0 release is planned in order to deprecate methods as well as add implementation methods to the
EventManager
that will aid in helping users prepare their code for version 3.Listeners are executed exactly as they were in version 2, using the same priorities and precedence between local explicit and wildcard listeners, and shared and shared wildcard listeners. Tests were added to describe and document these rules.
Change synopsis
Removed functionality
GlobalEventManager
andStaticEventManager
are removed (with prejudice!).ProvidesEvents
, which was previously deprecated, is removed.EventManagerInterface::setSharedManager()
is removed. Shared managers are now expected to be injected during instantiation.EventManagerInterface::getEvents()
andgetListeners()
are removed; they had now purpose within the implementation.EventManagerInterface::setEventClass()
was renamed tosetEventPrototype()
, which now expects anEventInterface
instance. That instance will be cloned whenever a new event is created.EventManagerInterface::attachAggregate()
anddetachAggregate()
are removed. Users should use theattach()
anddetach()
methods of the aggregates themselves.SharedEventAggregateAwareInterface
andSharedListenerAggregateInterface
are removed. This was an undocumented and largely unused feature.SharedEventManagerAwareInterface
is removed. A new interface,SharedEventsCapableInterface
defines thegetSharedManager()
method from the interface, andEventManagerInterface
extends that new interface.SharedEventManagerInterface::getEvents()
is removed, as it had no purpose in the implementation.Changed signatures
EventManager::__construct()
now accepts an optionalSharedEventManagerInterface
instance as the first argument, and an optional array of identifiers as the second. As identifiers have no meaning without a shared manager present, they are secondary to providing the shared manager.EventManagerInterface::trigger()
changes its signature totrigger($eventName, $target = null, $argv = [])
; each argument has exactly one possible meaning; the$eventName
can only be a string event name. The fourth$callback
argument is removed.EventManagerInterface::triggerUntil()
changes its signature totriggerUntil(callable $callback, $eventName, $target = null, $argv = null)
. Each argument has exactly one meaning.EventManagerInterface
adds two new methods for triggering providedEventInterface
arguments:triggerEvent(EventInterface $event)
andtriggerEventUntil(callable $callback, EventInterface $event)
.EventManagerInterface::attach()
anddetach()
change their signatures toattach($eventName, callable $listener, $priority = 1)
anddetach(callable $listener, $eventName = null)
, respectively. Note that$eventName
can now only be a string event name, not an array orTraversable
.EventManagerInterface::setIdentifiers()
andaddIdentifiers()
change their signatures to each only accept an array of identifiers.SharedEventManagerInterface::getListeners()
changes signature togetListeners(array $identifiers, $eventName)
and now guarantees return of an array. Note that the second argument is now required.SharedEventManagerInterface::attach()
changes signature toattach($identifier, $eventName, callable $listener, $priority = 1)
. The$identifier
and$eventName
must be strings.SharedEventManagerInterface::detach()
changes signature todetach(callable $listener, $identifier = null, $eventName = null)
;$identifier
and$eventName
must be strings if passed.ListenerAggregateInterface::attach()
adds an optional$priority = 1
argument. This was used already in v2, but not dictated by the interface.FilterInterface::attach()
anddetach()
have changed signature toattach(callable $callback)
anddetach(callable $ilter)
, respectively.FilterIterator::insert()
has been modified to raise an exception if the value provided is not a callable.ResponseCollection::setStopped()
no longer implements a fluent interface.New functionality
LazyListener
allows wrapping:LazyEventListener
extendsLazyListener
, and provides metadata for discovering the intended event name and priority at which to attach the lazy listener; these are consumed by:LazyListenerAggregate
, which, provided a list ofLazyEventListeners
and/or definitions to use to create them, acts as an aggregate for attaching a number of such listeners at once.Benchmarks
Benchmarks are listed in a gist. In particular:
The highlight is the MultipleEventMultipleLocalAndSharedListener benchmark, which demonstrates a real-world usage scenario in zend-mvc with many local and shared listeners, and many events being triggered. This benchmark jumped from 435 operations per second in version 2 to 1265 operations per second in this implementation — a change of ~300%! Considering this represents 10–50X the number of events triggered in a reasonably sized zend-mvc application, performance will be in the sub-millisecond range.