The Joomla CLI Application needs to somehow know about our custom CLI command classes. The way Joomla decided to implement this is with plugins. This makes perfect sense! The “One True Joomla Way” for implementing extensible features is with plugins.
We need to create a new plugin in the console
group
which will register our commands to the Joomla CLI application. This
plugin must follow the Joomla 4 conventions[5] as it will be handling an event.
The service provider
Joomla 4 plugins do need a service provider. We are going to use the service provider to also get ahold of the MVCFactory object of our component, pass it to the plugin object which can then pass it to our command class.
defined('_JEXEC') || die; use Joomla\CMS\Extension\PluginInterface; use Joomla\CMS\Extension\Service\Provider\MVCFactory; use Joomla\CMS\MVC\Factory\MVCFactoryInterface; use Joomla\CMS\Plugin\PluginHelper; use Joomla\DI\Container; use Joomla\DI\ServiceProviderInterface; use Joomla\Event\DispatcherInterface; use Joomla\Plugin\Console\ATS\Extension\ATS; return new class implements ServiceProviderInterface { public function register(Container $container) { $container->registerServiceProvider(new MVCFactory('Acme\\Component\\Example')); $container->set( PluginInterface::class, function (Container $container) { $config = (array) PluginHelper::getPlugin('console', 'example'); $subject = $container->get(DispatcherInterface::class); $mvcFactory = $container->get(MVCFactoryInterface::class); $plugin = new Example($subject, $config) $plugin->setMVCFactory($mvcFactory); return $plugin; } ); } };
The plugin class
The plugin class only listens to one event, the
\Joomla\Application\ApplicationEvents::BEFORE_EXECUTE
one. This event is an
\Joomla\Application\Event\ApplicationEvent
which
is raised by the Joomla CLI Application before it tries to execute the
user's instructions.
To make things easier, and our example reusable in the real world
with minimal modifications, we have a private static variable which
lists the class names of the command classes to register. The
registerCLICommands
method iterates through
them, creates a command object and adds it to the CLI
application.
<?php namespace Joomla\Plugin\Console\Example\Extension; defined('_JEXEC') or die; use Acme\Component\Example\Administrator\CliCommand\ItemsList; use Joomla\Application\ApplicationEvents; use Joomla\Application\Event\ApplicationEvent; use Joomla\CMS\MVC\Factory\MVCFactoryAwareTrait; use Joomla\CMS\Plugin\CMSPlugin; use Joomla\Event\SubscriberInterface; use Throwable; class Example extends CMSPlugin implements SubscriberInterface { use MVCFactoryAwareTrait; private static $commands = [ ItemsList::class, ]; protected $autoloadLanguage = true; public static function getSubscribedEvents(): array { return [ ApplicationEvents::BEFORE_EXECUTE => 'registerCLICommands', ]; } public function registerCLICommands(ApplicationEvent $event) { foreach (self::$commands as $commandFQN) { try { if (!class_exists($commandFQN)) { continue; } $command = new $commandFQN(); if (method_exists($command, 'setMVCFactory')) { $command->setMVCFactory($this->getMVCFactory()); } $this->getApplication()->addCommand($command); } catch (Throwable $e) { continue; } } } }
As you can see, after creating the command object we push the
MVCFactory into it — if it supports that feature, i.e. it is using the
MVCFactoryAwareTrait
itself.
We could do the same for the database object. We'll let you figure it out. For the solution, you can read the footnote[6].
Remember to not just install this plugin, but also publish it. You
can publish the plugin automatically in the install
section
of your package's installation script. Remember, that code only runs on
a new installation. If you want to enable this plugin also on updates
you will need to do the same in the update
section as well.
Yes, it's a bit of a kludge but in practice it works very well.
[6] At the top of the plugin object define the $db
property to let Joomla push the database object into the plugin
object.
protected $db;
In the registerCLICommand method, right after pushing the MVCFactory, push the database object:
if (method_exists($command, 'setDbo')) { $command->setDbo($this->db); }