Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[QUESTION / PROPOSAL] Is there a way to lazy load listeners? #36

Closed
andreigabreanu opened this issue Jan 10, 2015 · 5 comments
Closed

Comments

@andreigabreanu
Copy link

I hate the idea of having "new's" anywhere except my DiC.
So something like
$emitter->addListener('some event', 'Some Class Name which Implements the Listener Interface');
$emitter->setListenerResolver(new ClassThatImplementsAListenerResolverInterface());

class ClassThatImplementsAListenerResolverInterface {

public function __construct('my DiC goes here') ...

public function getListener($className)
{
return $this->MyDiC->get($className);
}

It's just some meta code but you get the ideea. This way you don't do any instantiations unless the event listener MUST actually be called. Otherwise you work just with strings.

What do you think ? I don't see a way to do this right now (w/o customizing stuff quite a bit)

@frankdejonge
Copy link
Member

@andreigabreanu sorry for the late reply. I'd propose a different approach for this. In your situation I'd create a DI specific listener factory which would produce listeners that implement the ListenerInterface, which would resolve the dependencies in the handle function. This would solve your problem without making the event package itself be aware of any lazy loading (which would add significant complexity). Would that be an option for you?

@praswicaksono
Copy link

@frankdejonge

create a DI specific listener factory which would produce listeners that implement the ListenerInterface

How to pass factory product to event?

this way below its not lazy load, since it will create object of listener

$emitter->addListener("some.event", $container->get("listener");

@sagikazarmark
Copy link
Member

@Atriedes I don't think it is possible. When the emitter starts to call the listeners, it creates a list of them in the right order. There is no way to add listeners WHILE they are being invoked. You would have to create a decorator/extend the emitter, to make sure those listeners are added before the emitting starts.

Something like this:

<?php

use League\Container\ContainerInterface;
use League\Event\EmitterTrait;
use League\Event\EmitterInterface;

class LazyEmitter implements EmitterInterface
{
    use EmitterTrait;

    protected $container;

    /**
     * You have to add listeners to your container
     *
     * You should register them as singletons
     *
     * @param ContainerInterface $container
     */
    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

    public function emit($event)
    {
        // ensure event is an event object
        // get its name: $name
        if (!$this->container->isSingleton($name)) {
            $listeners = $this->container[$name];
            is_array($listeners) or $listeners = [$listeners];

            foreach ($listeners as $listener) {
                $this->getEmitter()->addListener($name, $listener);
            }
        }

        return call_user_func_array([$this->getEmitter(), 'emit'], func_get_args());
    }
}

@frankdejonge
Copy link
Member

My proposed solution was a little bit different. I'd make the listener resolving the responsibility of a listener decorator instead of a Emitter replacement.

A littler decorator would look something like this:

<?php

class LazyListener implements ListenerInterface
{
    protected $container;
    protected $containerKey;
    protected $listener;

    public function __construct(Container $container, $containerKey,)
    {
        $this->container = $container;
        $this->containerKey = $containerKey;
    }

    public function handle(EventInterface $event)
    {
        $arguments = [$event] + func_get_args();
        $listener = $this->resolveListener();

        return call_user_func_array([$listener, 'handle'], $arguments);
    }

    protected function resolveListener()
    {
        if (! $this->listener) {
            $this->listener = $this->container->get($this->containerKey);
        }

        return $this->listener;
    }

    public function getContainerKey()
    {
        return $this->containerKey;
    }

    public function isListener($listener)
    {
        if ($listeners instanceof LazyListener && $this->containerKey === $listener->getContainerKey) {
            return true;
        }

        return false;
    }
}

You'd use this, with changing the container implementation details to the one you're using, like so:

$container = new Container();
// .. configure container to have the listener which needs deps
$emitter = new Emitter;
$emitter->addListener('event.name', new LazyListener($container, 'some.dependency.key'));
$emitter->emit('event.name');

@praswicaksono
Copy link

Thanks all for comments, right now I decided to extend Emitter and make them container aware. I need to override 3 methods addListener, InvokeListener, ensureListener

public function addListener($event, $listener, $priority = self::P_NORMAL)
{
    // I just remove this to make sure I can pass reference only
    // $listener = $this->ensureListener($listener);

    if (! isset($this->listeners[$event])) {
        $this->listeners[$event] = [];
    }

    $this->listeners[$event][] = [$listener, $priority];
    $this->clearSortedListeners($event);
    return $this;
}

We check and resolve listener later when there are invoked

protected function invokeListeners($name, EventInterface $event, array $arguments)
{
    $listeners = $this->getListeners($name);

    foreach ($listeners as $listener) {
        if ($event->isPropagationStopped()) {
            break;
        }

        // resolve listener
        $listener = $this->ensureListener($listener);

        call_user_func_array([$listener, 'handle'], $arguments);
    }
}

Finally if pass reference, resolve it in container otherwise throw exception

protected function ensureListener($listener)
{
    if ($listener instanceof ListenerInterface) {
        return $listener;
    }

    if (is_callable($listener)) {
        return CallbackListener::fromCallable($listener);
    }

    // If we pass reference in containter then resolve it
    if (($listener = $this->container->get($listener)) != null) {
        return $listener
    }

    throw new InvalidArgumentException('Listeners should be be ListenerInterface, Closure or callable. Received type: '.gettype($listener));
}

With this way all listener can resolve when event get invoked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants