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

Usage of SharedEventManager to use events like with laminas-mvc #92

Open
rarog opened this issue Mar 30, 2022 · 6 comments
Open

Usage of SharedEventManager to use events like with laminas-mvc #92

rarog opened this issue Mar 30, 2022 · 6 comments

Comments

@rarog
Copy link

rarog commented Mar 30, 2022

Feature Request

Q A
New Feature yes
RFC yes
BC Break no

Summary

Currently only events of ModuleManager are processed by laminas-cli, there is no built-in possibility to automatically attach listeners for console events.
Depending on the decision either 'listeners' from module.config.php could be reused or a new key like 'listeners-cli' could be introduced. This way it would be possible to define one console command and then trigger events where other modules could react to it.

@boesing
Copy link
Member

boesing commented Mar 30, 2022

In fact, we talked about this a few days ago in the laminas slack:
https://laminas.slack.com/archives/C4QBQUEG5/p1647876087036429

TL;DR: Moving listeners configuration parsing (which is actually done within the Application#init of laminas-mvc) should be moved to an EventManager delegator.
Imho, all these bootstrapping stuff, etc. should not be executed by laminas-cli since we are not running the application itself but a CLI command.

So what we can do is to attach all listeners stored in the listeners config key within a EventManager-delegator and we have the same result. Right?


So for the time being, you can create a cli-SAPI only delegator factory to your config like this:

final class LaminasMvcCliEventManagerDelegatorFactory
{
    public function __invoke(ContainerInterface $container, string $requestedName, callable $callback): EventManagerInterface
    {
         $eventManager = $callback();
         assert($eventManager instanceOf EventManagerInterface);
         
         $listeners = $this->extractListenersFromConfig($container);
         foreach ($listeners as $listener) {
             $container->get($listener)->attach($eventManager);
         }

         return $eventManager;
    }

    /** @return list<string> */
    private function extractListenersFromConfig(ContainerInterface $container): array {
         if (PHP_SAPI !== 'cli') {
             return [];
         }
         $config = $container->get('config');
         $applicationConfig = $container->get('ApplicationConfig');
         $listeners = [];
    
         if (isset($applicationConfig['listeners'])) {
             $listeners = array_merge($listeners, $applicationConfig['listeners']);
         }     

         if (isset($config['listeners'])) {
             $listeners = array_merge($listeners, $config['listeners']);
         }     

         return array_values(array_unique($listeners));
    }
}

An additional entry to your config like this would register the delegator for the EventManager and you are good to go:

return ['service_manager' => ['delegators' => ['EventManager' => [LaminasMvcCliEventManagerDelegatorFactory::class]]];

@boesing boesing added the RFC label Mar 30, 2022
@rarog
Copy link
Author

rarog commented Mar 30, 2022

So for the time being, you can create a cli-SAPI only delegator factory to your config like this:

I tried it, but this seemingly creates loop while instantiating.

PHP Fatal error:  Uncaught Error: Xdebug has detected a possible infinite loop, and aborted your script with a stack depth of '256' frames in /myPath/module/Common/src/Delegator/LaminasMvcCliEventManagerDelegatorFactory.php:15
Stack trace:
#0 /myPath/vendor/laminas/laminas-servicemanager/src/ServiceManager.php(591): Common\Delegator\LaminasMvcCliEventManagerDelegatorFactory->__invoke()
#1 /myPath/vendor/laminas/laminas-servicemanager/src/ServiceManager.php(594): Laminas\ServiceManager\ServiceManager::Laminas\ServiceManager\{closure}()
#2 /myPath/vendor/laminas/laminas-servicemanager/src/ServiceManager.php(615): Laminas\ServiceManager\ServiceManager->createDelegatorFromName()
#3 /myPath/vendor/laminas/laminas-servicemanager/src/ServiceManager.php(234): Laminas\ServiceManager\ServiceManager->doCreate()
#4 xxx in /myPath/module/Common/src/Delegator/LaminasMvcCliEventManagerDelegatorFactory.php on line 15`

@rarog
Copy link
Author

rarog commented Mar 30, 2022

If I comment out some of the listeners it seems to work. But I can't track down why the loop is happening. I added some echo commands along the creation of the first listener that seems to cause the loop. After going through the chain of factories I can see an object being created successfully (including echo in the last line of the constructor of the object), but after that I can see echo commands of the invoke inside LaminasMvcCliEventManagerDelegatorFactory again.

So basically I added for debugging this about foreach in invoke in your code:

echo 'beginforeachLaminasMvcCliEventManagerDelegatorFactory';

The factory that I

public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        echo 'ya';
        $c1 = $container->get(ClassThatIsBeingCreatedSuffessfullyInclEchoInConstructor::class); // will echo 'xxx' in constructor
        echo 'yb';
        $c2 = $container->get(ClassNotbeingCalled::class);

        return new AnotherClasse($c1, $c2);
    }

So basically I see following:
beginforeachLaminasMvcCliEventManagerDelegatorFactoryyaxxxbeginforeachLaminasMvcCliEventManagerDelegatorFactoryyaxxxbeginforeachLaminasMvcCliEventManagerDelegatorFactoryyaxxx and so on. I never see 'yb' in the output. So it looks like after constructing ClassThatIsBeingCreatedSuffessfullyInclEchoInConstructor for some reason LaminasMvcCliEventManagerDelegatorFactory is called again.

I do not have other delegators that should fiddle inbetween. What could be the reason? The same listeners works flawlessly in normal MVC calls.

@gsteel
Copy link
Member

gsteel commented Mar 30, 2022

On the surface of things, this looks like cyclic dependencies - a listener has a dependency on a service and the service is having listeners registered etc. I will normally try to figure out which listener is causing the cyclic dependency and then either refactor it to remove the dependency, or wrap the listener in a LazyListenerAggregate (Part of the Laminas Event Manager Package) - that way you defer construction of the listener until the event occurs. This is generally better anyway because it means that you don't have to construct a massive dependency graph just to listen for events that might never even get triggered. HTH

@rarog
Copy link
Author

rarog commented Mar 30, 2022

That's a good thought. The aren't real cyclic dependencies, but several of the objects have also events. If the try to reference the EventManager, while it's still instantiating in the delegator, this would cause this cycle.
In MVC and perhaps upcoming change the EventManager would be instantiated before any events are attached.
I'll go by using a different key in the delegator to only register listeners relevant to my console commands which won't trigger other events, I don't have to change to LazyListenerAggregate. Thx.

@davidwindell
Copy link

davidwindell commented Apr 25, 2022

Thanks for this, I've just implemented the same in our project and came to a similar issue/conclusion with cyclic dependencies. Ended up injecting the service container into the listener which was causing a problem instead of the actual services as runtime.

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

No branches or pull requests

4 participants