onBootstrap not called for CLI commands

Hi,

I recently migrated my project from ZF3 to Laminas. Since now laminas-console and laminas-mvc-console are gone, now I changed my code to use Symfony commands, following the guide in the README of laminas-cli. However I ran into a problem.

I use twig as a renderer and another module for some twig extensions. The latter relies on its onBootstrap function in its Module.php. When I tested, I discovered, that CLI commands do not call the onBootstrap functions of the initialized modules. Is there a way to overcome that?

Thanks and best regards,
Ivaylo

Hello and welcome to our forums! :smiley:

There is also a documentation: laminas-cli - Laminas Docs

The extensions are always registered, no matter if the renderer is used or not, this seems to be wrong.

The modules are loaded when the service container is created, but your application itself is not started. This means no event is triggered which is related to bootstrap or something else.

Can you show how do you get the renderer in your command class?

Hi, thanks for the welcome and your fast reply. :slight_smile:

Here is a snippet from the Module (not written by myself):

/**
     * @param EventInterface $e
     * @throws InvalidArgumentException
     */
    public function onBootstrap(EventInterface $e): void
    {
        /** @var \Laminas\Mvc\MvcEvent $e*/
        $application    = $e->getApplication();
        $serviceManager = $application->getServiceManager();
        $environment    = $serviceManager->get(Environment::class);

        /** @var ModuleOptions $options */
        $options = $serviceManager->get(ModuleOptions::class);

        // Setup extensions
        foreach ($options->getExtensions() as $extension) {
            // Allows modules to override/remove extensions.
            if (empty($extension)) {
                continue;
            } elseif (is_string($extension)) {
                if ($serviceManager->has($extension)) {
                    $extension = $serviceManager->get($extension);
                } else {
                    $extension = new $extension();
                }
            } elseif (!is_object($extension)) {
                throw new InvalidArgumentException('Extensions should be a string or object.');
            }

            $environment->addExtension($extension);
        }
    }

The CLI command is used to generate an email from a twig template. Therefore I get the renderer, render the template in a variable and use it as a mail body. Here are some snippets:
This is the Command Factory:

/**
     * @param ContainerInterface $container
     * @return SendConfirmationMailCommand
     * @throws \Psr\Container\ContainerExceptionInterface
     * @throws \Psr\Container\NotFoundExceptionInterface
     */
    public function __invoke(ContainerInterface $container)
    {
        return new SendConfirmationMailCommand(
            $container->get(Service\Order::class),
            $container->get(Service\Settings::class),
            $container->get(Service\Mailer::class),
            $container->get(Service\Auth::class),
            $container->get('ZfcTwigRenderer')
        );
    }

And this is from the command’s execute function:

protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $orderId = $input->getOption('order');
        $order = $this->orderService->getOrder($orderId);
        $view = new ViewModel(['order' => $order]);
        $view->setTemplate('outlettab/order/view.twig');
        $html = $this->renderer->render($view); // This is the ZfcTwigRenderer

        // more business logic ...

        return Command::SUCCESS;
    }

If I miss something, please let me know. Thanks again :slight_smile:

PS: The same code was working, when it was in console route and in controller / action

This method is always executed, no matter if the renderer is used or not. For example, with JSON or XML output or a redirect.
Unfortunately, I don’t know why the module uses this path.

Register the extensions yourself. You can create a delegator factory for the renderer or overwrite the existing factory for the renderer.
Then use a custom service container for the commands.

There is an example in the issue tracker:

The custom service container allows you to register factories without changing the services of your application.

(I don’t know the Twig module, so my hints may be wrong!)

I don’t entirely understand your approach, but even if I managed to do it, it doesn’t feel right to me. Even if I register the Twig extensions somehow, then the next module will introduce another problem.

The thing is, that I use a lot external modules and most of them rely on the “onBootstrap” function in theirs Module.php

Based on what you said and my quick research, the laminas application is never run (the public/index.php is not called in cli). The run function of the laminas app is not called, so that’s why each Module’s onBootstrap function is also not called. I haven’t found where and how the Modules are loaded when the laminas app is not initialized.

I am not sure how to define my question correctly, but is there any way to bootstrap the laminas application without running it? In other words, what I try to accomplish is using the app the same way in cli as in web environment (as it was the case when we got laminas-console and laminas-mvc-console)?

This often means that it is used in the wrong way. See:

Right, therefore I wrote:

I gave you the related link in my first answer:

The goal of laminas-cli is not start an application, not a laminas-mvc, Mezzio or something else. laminas-cli uses the same configuration of the service container of your laminas-mvc application but it will not launch the application or trigger any events.
Therefore, the method onBootstrap is not called in modules.

Please do not get me wrong, I understand you problem but laminas-cli can not resolve the wrong usage of the onBootstrap method.

I do not know the concrete modules you are using. Maybe this one:

Here can you try to overwrite the factory with solution from laminas-cli’s issue tracker:

<?php // in config/custom-container.php

use Laminas\Mvc\Service\ServiceManagerConfig;
use Laminas\ServiceManager\ServiceManager;
use Laminas\Stdlib\ArrayUtils;

$config = require __DIR__ . '/application.config.php';

$extendedConfig = [
    'service_manager' => [
        'factories' => [
            ZfcTwig\View\TwigRenderer::class => MyCustomTwigRendererFactory,
        ],
    ]
];
$config = ArrayUtils::merge($config, $extendedConfig);

$container = new ServiceManager();
(new ServiceManagerConfig($config['service_manager'] ?? []))->configureServiceManager($container);
$container->setService('ApplicationConfig', $config);
$container->get('ModuleManager')->loadModules();

return $container;

See also:

Thank you for your detailed replies. I understand now, that laminas-cli cannot resolve my problem, it is after all not intended to be a replacement for laminas-console and laminas-mvc-console. I did however overcome my initial problem (thanks again for your guidance) with a tiny change, which I will show, in case it helps someone:

<?php

use Laminas\Mvc\Service\ServiceManagerConfig;
use Laminas\ServiceManager\ServiceManager;
use Laminas\Stdlib\ArrayUtils;

$config = require __DIR__ . '/../../../config/application.config.php';
$extendedConfig = [
    'service_manager' => [
        'factories' => [
            ZfcTwig\View\TwigRenderer::class => System\View\TwigRendererFactory::class,
        ],
    ]
];
$container = new ServiceManager();
(new ServiceManagerConfig())->configureServiceManager($container);
$container->setService('ApplicationConfig', $config);
/** @var \Laminas\ModuleManager\ModuleManager $moduleManager */
$moduleManager = $container->get('ModuleManager');

$events = $moduleManager->getEventManager();
$events->attach(\Laminas\ModuleManager\ModuleEvent::EVENT_MERGE_CONFIG, function (\Laminas\ModuleManager\ModuleEvent $e) use ($extendedConfig) {
    $configListener = $e->getConfigListener();
    $config = $configListener->getMergedConfig(false);
    $config = ArrayUtils::merge($config, $extendedConfig);
    // Pass the changed configuration back to the listener:
    $configListener->setMergedConfig($config);
});
$moduleManager->loadModules();

return $container;

The initial suggestion is not working because it merges the application config with the module config. Since I needed to change the module config, I didn’t alter the application config, but attached a listener to the ModuleEvent::EVENT_MERGE_CONFIG, where I changed the module config and injected my custom factory.

Then I adapted the onBootstrap code from the external Module and used it in my factory to load the extensions. It worked.

Then came the next error: I need the url view helper, which requires MvcEvent upon creation. I am sure, that this will also not be the last problem. So I guess the best solution is just to roll back to laminas-console and laminas-mvc-console.

Thanks again, you helped me to gain deeper understanding in the area so I can make a better choice.

It is a replacement, but with a different approach. laminas-console and laminas-mvc-console are abandoned and should no longer be used for new developments.

No, laminas-view and there helpers does not need Laminas\Mvc\MvcEvent.

This will not help for the future.

You marked my last answer as solution but it does not work for you.
It would be helpful if you could describe your use case, to find a correct solution. And it would help to improve laminas-cli.

The work on laminas-cli will continue and use cases like yours are needed! And if this forum post leads to a new feature request, then that’s good.

Marked it as a solution, because it actually solved the initial problem. I will be happy to resolve the other issues, so here is the situation now:

In my view, which is rendered from ZfcTwigRenderer to create the HTML for my email, I use the url view helper. Its factory is created in the ViewHelperManagerFactory of the laminas-mvc component, where it uses the MvcEvent to get the matched route:

Here is the error:

PHP Fatal error:  Uncaught Error: Call to a member function getRouteMatch() on null in /srv/prendit/dev/imitakov/outlettab/vendor/laminas/laminas-mvc/src/Service/ViewHelperManagerFactory.php:97
Stack trace:
#0 /srv/prendit/dev/imitakov/outlettab/vendor/laminas/laminas-servicemanager/src/ServiceManager.php(645): Laminas\Mvc\Service\ViewHelperManagerFactory->Laminas\Mvc\Service\{closure}(Object(Laminas\ServiceManager\ServiceManager), 'Laminas\\View\\He...', NULL)
#1 /srv/prendit/dev/imitakov/outlettab/vendor/laminas/laminas-servicemanager/src/ServiceManager.php(218): Laminas\ServiceManager\ServiceManager->doCreate('Laminas\\View\\He...')
#2 /srv/prendit/dev/imitakov/outlettab/vendor/laminas/laminas-servicemanager/src/AbstractPluginManager.php(170): Laminas\ServiceManager\ServiceManager->get('url')
#3 /srv/prendit/dev/imitakov/outlettab/vendor/kokspflanze/zfc-twig/src/View/TwigRenderer.php(136): Laminas\ServiceManager\AbstractPluginManager->get('url', NULL)
#4 /srv/prendit/dev/imitakov/outlettab/vendor/twig/twig/src/Environment.p in /srv/prendit/dev/imitakov/outlettab/vendor/laminas/laminas-mvc/src/Service/ViewHelperManagerFactory.php on line 97

There is no route in a CLI command and if you need the URL helper, the view helper must be configured.
And the view helpers of laminas-view itself does not need the MvcEvent, only a laminas-mvc application uses the MvcEvent to configure the related view helpers.

I think laminas-view needs an improvement for stand-alone use and then the use in the CLI command would also be simplified.


Unfortunately, I still don’t know your whole use case, but at the moment I have the impression you keep trying to solve everything via a controller action.
But this is not necessary, the command class is now your new controller action. And a working renderer should be available at this point.

No, I moved everything to SendConfirmationMailCommand and the command itself does not require
a view nor a view helper. However, the content of the email is generated from a twig template, where I pass the order, for which the mail should be generated. For this to work, I need the ViewRenderer (in this case from Twig), which creates the abovementioned problem.

Here is a relevant snippet from the code:

class SendConfirmationMailCommand extends Command
{
    // the name of the command (the part after "bin/console")
    protected static $defaultName = 'mailer:send-confirmation-mail';
// omitted
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $orderId = $input->getOption('order');
        $shopId = $input->getOption('shop');

        $this->shopService->init($shopId);
        $order = $this->orderService->getOrder($orderId);

        $view = new ViewModel(['order' => $order]);
        $view->setTemplate('outlettab/order/view.twig');
        $html = $this->renderer->render($view);
// omitted
        return Command::SUCCESS;
    }
}

As far as I understand, I will have to wait for this to happen, in order to be able to use a view renderer without the MVC context.

PS: A little background: I have a worker, which runs in CLI environment. The website creates a job (to send a confirmation email in this case), in order to reduce load of the confirmation page. The worker fetches the jobs and runs them in CLI. I think this is a pretty common use case.

Oh no, you can use the laminas-view as stand-alone solution already but you must configure the URL view helper yourself. See also in the documentation of laminas-view: “Application Integration – Stand-Alone

My idea is to create a factory to make this easier which includes the URL view helper with router which builds the URLs.

May be a dirty solution, but it worked! I created a factory for the url view helper, which is exactly as the original in ViewHelperManagerFactory, just without the route match part. I guess it works, because I do not use matched route part of the helper (since there is none).

<?php

namespace System\View\Helper;

use Interop\Container\ContainerInterface;
use Laminas\ServiceManager\ServiceManager;
use Laminas\View\Helper\Url;

class UrlFactory
{
    /**
     * @param ContainerInterface|ServiceManager $container
     * @return Url
     */
    public function __invoke(ContainerInterface $container)
    {
        $urlHelper = new Url();
        $urlHelper->setRouter($container->get('HttpRouter'));
        return $urlHelper;
    }
}

and the custom cli config:

<?php

use Laminas\Mvc\Service\ServiceManagerConfig;
use Laminas\ServiceManager\ServiceManager;
use Laminas\Stdlib\ArrayUtils;

$config = require __DIR__ . '/../../../config/application.config.php';
$extendedConfig = [
    'service_manager' => [
        'factories' => [
            ZfcTwig\View\TwigRenderer::class => System\View\TwigRendererFactory::class,
        ],
    ],
    'view_helpers' => [
        'factories' => [
            \Laminas\View\Helper\Url::class => \System\View\Helper\UrlFactory::class,
        ]
    ]
];
$container = new ServiceManager();
(new ServiceManagerConfig())->configureServiceManager($container);
$container->setService('ApplicationConfig', $config);
/** @var \Laminas\ModuleManager\ModuleManager $moduleManager */
$moduleManager = $container->get('ModuleManager');

$events = $moduleManager->getEventManager();
$events->attach(\Laminas\ModuleManager\ModuleEvent::EVENT_MERGE_CONFIG, function (\Laminas\ModuleManager\ModuleEvent $e) use ($extendedConfig) {
    $configListener = $e->getConfigListener();
    $config = $configListener->getMergedConfig(false);
    $config = ArrayUtils::merge($config, $extendedConfig);
    // Pass the changed configuration back to the listener:
    $configListener->setMergedConfig($config);
});
$moduleManager->loadModules();

return $container;

I hope there will be no more troubles on the way. @froschdesign thans again for your help. If I everything else works, could I mark two of your answers as solutions (to this and the initial problems)?

You can also check and see how Laminas Starte Kit commands work to compare against your generator: GitHub - divix1988/laminas-cli-commands: CLI commands for Laminas projects