Work In Progress

This book is currently work in progress. Some sections are not yet written. Thank you for your understanding!

Pushing dependencies to the Router

You may have noticed that in the sample code above I got the database driver object using the Factory::getContainer static method. This is not ideal. It's even worse if I'd need to get access to MVC objects such as Models and Tables — having to boot the entire component just to get its MVCFactory is something to use only if there is no other way, not something to do by default.

Fortunately, Joomla gives us the option to push any services we need — such as the database driver object and the component's MVCFactory — into the router. Unfortunately, it's a bit non-obvious.

To inject services into the Router service object they need to be injected into the object after it is created by the Router Factory object. The Router Factory object needs to have access to these services to inject them. This means injecting these services to the Router Factory object from the Router Factory Service Provider. The Router Factory Service Provider can get any of the dependencies (services) it needs since it has access to the component's service provider.

Therefore we need to create the two missing pieces of the puzzle (Router Factory and its service provider) and register the latter with our component's service provider.

[Important]Important

The component's service provider (services/provider.php) lives in the backend portion of your component. Therefore. it makes sense that the Router Factory and the Router Factory Service Provider also live in the backend of your component even though the Router is only used in the frontend of the site.

Yup. It sounds backwards. You can definitely create them in the frontend but, if you do, you and everyone else reading your code might get confused. Don't over-think it, just do what I tell you to do. There's method in this madness, I promise.

First, let's make our router MVCFactory-aware (it's already database-aware).

<?php
namespace Acme\Component\Example\Site\Service;

use Joomla\CMS\Component\Router\RouterView;
use Joomla\CMS\MVC\Factory\MVCFactoryAwareTrait;

class Router extends RouterView
{
  use MVCFactoryAwareTrait;
                
  // ... the rest of the router implementation goes here ...
}

Now, let's create a Router Factory (\Acme\Component\Example\Administrator\Service\RouterFactory). The default Joomla implementation is already database-aware. I am just extending it to also know about the MVCFactory so we can inject it to our router.

<?php
namespace Acme\Component\Example\Administrator\Service;

use Joomla\CMS\Application\CMSApplicationInterface;
use Joomla\CMS\Component\Router\RouterInterface;
use Joomla\CMS\Menu\AbstractMenu;
use Joomla\CMS\MVC\Factory\MVCFactoryAwareTrait;

class RouterFactory extends \Joomla\CMS\Component\Router\RouterFactory
{
  use MVCFactoryAwareTrait;

  public function createRouter(CMSApplicationInterface $application, AbstractMenu $menu): RouterInterface
  {
    $router = parent::createRouter($application, $menu);

    $router->setMVCFactory($this->getMVCFactory());

    return $router;
  }
}

Now, we need to create a RouterFactory service provider. Unfortunately, Joomla has a bad habit of registering factory objects in the DI container even though they are only going to return exactly one object with no initialisation, like a router. This complicates thing because we cannot extend the DI container definition. We have to do something stupid: copy Joomla's default implementation of the Router Factory Service Provider just so we can change the returned object type and register dependencies on it. Well... I guess it could be worse?

Anyway. Let's create our router factory service provider \Acme\Component\Example\Administrator\Service\Provider\RouterFactoryProvider.

<?php
namespace \Acme\Component\Example\Administrator\Service\Provider;

use Acme\Component\Example\Administrator\Service\RouterFactory;
use Joomla\CMS\Categories\CategoryFactoryInterface;
use Joomla\CMS\Component\Router\RouterFactoryInterface;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\Database\DatabaseInterface;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;

class RouterFactoryProvider implements ServiceProviderInterface
{
  /**
   * The module namespace
   *
   * @since   4.0.0
   * @var  string
   *
   */
  private $namespace;

  /**
   * DispatcherFactory constructor.
   *
   * @param   string  $namespace  The namespace
   *
   * @since   4.0.0
   */
  public function __construct(string $namespace)
  {
    $this->namespace = $namespace;
  }

  /**
   * Registers the service provider with a DI container.
   *
   * @param   Container  $container  The DI container.
   *
   * @return  void
   *
   * @since   4.0.0
   */
  public function register(Container $container)
  {
    $container->set(
      RouterFactoryInterface::class,
      function (Container $container) {
        $categoryFactory = null;

        if ($container->has(CategoryFactoryInterface::class))
        {
          $categoryFactory = $container->get(CategoryFactoryInterface::class);
        }

        $routerFactory = new RouterFactory(
          $this->namespace,
          $categoryFactory,
          $container->get(DatabaseInterface::class)
        );

        $routerFactory->setMVCFactory($container->get(MVCFactoryInterface::class));

        return $routerFactory;
      }
    );
  }
}            

Our changes to the core code in Joomla are highlighted in bold type.

Finally, we need to register this router factory service provider in our component's services/provider.php file.

$container->registerServiceProvider(
  new \Acme\Component\Example\Administrator\Service\Provider\RouterFactoryProvider(
    '\\Acme\\Component\\Example'
  )
);