0

I have a System Plugin to filter Joomla 4 Category Blog Articles by Custom Fields. The Plugin basically overrides the core content ArticlesModel by loading it in the onAfterRoute Event like this:

require_once __DIR__ . '/models/com_content/ArticlesModel.php';
       

This somehow works but I know it is not the way to go when using the Joomla 4 service provider.

  • Is it still possible to override core Model class from within a Plugin in Joomla 4?
  • How should this be done in Joomla 4 (best practice)?

Edit:

Following @sharky's comment on this post https://joomla.stackexchange.com/a/32099/12652 which describes the overriding of the Admin Model, I tried to create a Plugin to override a Site Model. As the Plugin should also change the generated HTML, there is an onAfterRender. The onAfterExtensionBoot and onAfterRender are loaded by the getSubscribedEvents - which seems to work:

use Joomla\CMS\Extension\ComponentInterface;
use Joomla\CMS\MVC\Factory\MVCFactory;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Component\Content\Site\Model\ArticlesModel;
use Joomla\DI\Container;
use Joomla\Event\EventInterface;

use MyCompany\Plugin\System\MyPlugin\Model\MyCustomSiteArticlesModel;

final class PlgSystemOverride extends CMSPlugin implements SubscriberInterface
    {
        public static function getSubscribedEvents(): array
        {
        
        return [
            'onAfterExtensionBoot' => 'onAfterExtensionBoot',
            'onAfterRender' => ['onAfterRender', Priority::LOW],
        ];
    }
    
    public function onAfterExtensionBoot(EventInterface $event)
    {
    
        // Test that a component is being booted.
        if ($event->getArgument('type') !== ComponentInterface::class) {
            return;
        }
    
        // Test that this is com_content component.
        if ($event->getArgument('extensionName') !== 'content') {
            return;
        }
    
        // Get the service container.
        $container = $event->getArgument('container');
    
        if (!($container instanceof Container)) {
            return;
        }
    
        // Check that MVC factory is used and can be overridden.
        if (!$container->has(MVCFactoryInterface::class) || $container->isProtected(MVCFactoryInterface::class)) {
            return;
        }
          
        // Register the custom MVC factory. Here an anonymous class is used but you can use a concrete class.
        $container->set(
            MVCFactoryInterface::class,
            static fn () => new class('Joomla\\Component\\Content') extends MVCFactory
            {
                protected function getClassName(string $suffix, string $prefix)
                {
                    $class = parent::getClassName($suffix, $prefix);
    
                    // How to override the site article list model with a custom class?                        
                    if ($class === ArticlesModel::class) {
    
                        return 'MyCustomSiteArticlesModel';
                    }
    
                    return $class;
                }
            }
        );
    }

    public function onAfterRoute()
    {
        // ... do onAfterRoute stuff
    }
 }

As the code shows, I created a MyCustomSiteArticlesModel class and put it in the src/Model folder of the Plugin:

use Joomla\Component\Content\Site\Model\ArticlesModel;

class CustomfieldsFilterArticlesModel extends ArticlesModel
{
    protected function getListQuery()
    {
         // ... generate the query
    }

}

In the getClassName() the ArticlesModel::class seems to return the Joomla\Component\Content\Administrator\Model\ArticlesModel class and not the Joomla\Component\Content\Site\Model\ArticlesModel. Therefore the classname will not be returned.

Questions:

  • How does the getClassName() load the MyCustomSiteArticlesModel?
  • Where should the overridden MyCustomSiteArticlesModel class be located?
  • Is this the correct approach to make this work on Joomla Article Blog posts?

Thank you for a few explanations.

2

2 Answers 2

1

How does the getClassName() load the MyCustomSiteArticlesModel?

What this plugin does is override the MVC factory service in a given component (com_content in this case). The MVC factory class is responsible for creating MVC layer objects (models, views, and controllers) and, for some reason, table objects. Model instance is created by calling createModel() method defined in Joomla\CMS\MVC\Factory\MVCFactoryInterface:

public function createModel($name, $prefix = '', array $config = []);

Inside this method getClassName() is called to resolve the given arguments (name and prefix) to a class name. What your custom factory does is simply tell that whenever the arguments resolve to Joomla\Component\Content\Site\Model\ArticlesModel class (i.e. frontend articles model), your custom class should be used instead. The model instance is created later inside createModel() method. If you wanted to make other customizations, such as change how the model is instantiated or what dependencies are injected, you'd have to override the whole createModel() method.

Where should the overridden MyCustomSiteArticlesModel class be located?

In theory it could be anywhere. The class name and the way it's loaded is an implementation detail of your factory. In fact, allowing custom class name and directory structure conventions is one of the key points of having a factory in the first place. For simplicity you should stick to using the built-in PSR4 autoloader so you wouldn't have to manually look up and load files using statements like require_once. As mentioned in the linked answer, there are some pieces of code that may derive certain variables from the model name. Using a name like MyCompany\Plugin\System\MyPlugin\Model\ArticlesModel instead of MyCompany\Plugin\System\MyPlugin\Model\MyCustomSiteArticlesModel should be safer in that regard.

Is this the correct approach to make this work on Joomla Article Blog posts?

This is certainly better than using autoloader hacks or some other magic code. This uses a somewhat documented API and is as close as it gets to being the correct approach. But it's questionable whether extending components can be considered correct at all. First, there is a policy issue. Maintainers can't seem to decide whether core components belong to the public API and thus fall under B/C promise or not. Because of that, breaking changes may be introduced at any point, some services could be removed and your plugin would stop working. So you're really relying on implementation details. Then there's the question of interaction between multiple extensions. Other extensions could be overriding the same code, also introducing breaking changes for your plugin.

Finally, if you want to be actually correct, there is a major issue with the plugin code itself. Since multiple extensions could be overriding services, all classes should really be decorated and that increases the complexity a lot. For starters, use Joomla\DI\Container::extend() method to decorate the service. This provides the previously registered service which you can then inject into your service. Then, proxy all calls to the original service, except where you need implement your changes. So the new factory could look something like this:

$container->extend(
    MVCFactoryInterface::class,
    fn ($mvcFactory) => new class($mvcFactory, $this->app) implements MVCFactoryInterface
    {
        public function __construct(private MVCFactoryInterface $mvcFactory, private CMSApplicationInterface $app)
        {
        }

        public function createModel($name, $prefix = '', array $config = [])
        {
            $model = $this->mvcFactory->createModel($name, $prefix, $config);

            if (!$model instanceof ArticlesModel)
            {
                return $model;
            }

            return new MyCustomSiteArticlesModel($model, $config);
        }

        public function createView($name, $prefix = '', $type = '', array $config = [])
        {
            return $this->mvcFactory->createView($name, $prefix, $config);
        }

        public function createController($name, $prefix, array $config, CMSApplicationInterface $app, Input $input)
        {
            return $this->mvcFactory->createController($name, $prefix, $config, $this->app, $this->app->getInput());
        }

        public function createTable($name, $prefix = '', array $config = [])
        {
            return $this->mvcFactory->createTable($name, $prefix, $config);
        }
    }
);

As you can see, there's a lot more code now and that's just the MVC factory. You need to do the exact same thing in your custom model to ensure that overrides are properly respected. Overall, it might be simpler to write your own component with a single view/model.

0

Thank you @Sharky for that insightful response. To be honest, it took me a while to get through it.

The basic idea of the Plugin is just to override just one function of the ArticlesModel to change the behavior of the blog layout. I appreciate all your valuable comments about the side effect this might have. Anyway, I am still trying to follow the original path and avoid the overload that comes with writing a component, too.

Returning the 'MyCustomSieArticlesModel' inside the onAfterExtensionBoot() Event function did not work. First of all I had to add the namespace like this 'MyCompany\Plugin\System\MyPlugin\Model\MyCustomSieArticlesModel'. Then I get this error message:

Joomla\CMS\Dispatcher\ComponentDispatcher::getController(): Return value must be of type Joomla\CMS\MVC\Controller\BaseController, MyCompany\Plugin\System\MyPlugin\Model\MyCustomSieArticlesModel returned.

Any ideas?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.