0

I have a menu entry like so:

  • ID: 524
  • Type: Articles->Create Article
  • Link: index.php?option=com_content&view=form&layout=edit

In the "old days" I'do Route::_('index.php?option=com_content&view=form&layout=edit') and it would return a SEF-friendly URL path like /articles/add or whatever.

But currently, it does return this kinda ugly URL path: /component/content/form?layout=edit&Itemid=101. (Itemid 101 refers to the home menu entry).

If I do Route::_('index.php?Itemid=524') it does work OK.

I'm I missing something?

Thanks.

3
  • What you're describing is also happening on my joomla instances, which are a variety of joomla 4 releases. I think you should raise an issue on the Joomla issue tracker at issues.joomla.org and see what they say in response. As far as I can see the site router has undergone several changes in the joomla 4 releases, and there seem to be a few issues with how it's identifying the menuitem Itemid to base the route on. Sep 22, 2023 at 18:10
  • Thanks @RobbieJackson I'm doing just that. Sep 23, 2023 at 16:40
  • I raised a request for something similar a couple of weeks ago, and it got rejected outright. So yours is obviously receiving some serious consideration :-). In the meantime you can try the answer I've written up if you like. Oct 10, 2023 at 15:47

1 Answer 1

1

As there are a number of issues arising like this, this answer explains how to override the com_content router to select a better menuitem to base an SEF URL on.

WARNING This is a system plugin, and if you have a PHP syntax error in it you will get locked out of the Joomla admin back-end, and will have to go into phpmyadmin (or equivalent) to set to 0 the enabled field within the plugin record in #__extensions. If you're not comfortable doing this then I would recommend not using this plugin.

Into a folder plg_custom_menurule create the following 5 files.

plg_custom_menurule/custom_menurule.xml - this is the manifest file for the plugin.

<?xml version="1.0" encoding="utf-8"?>

<extension type="plugin" group="system" method="upgrade">
  <name>Custom Menurule</name>
  <version>1.0.0</version>
  <creationDate>today</creationDate>
  <author>me</author>
  <description>This plugin overrides the com_content site router where it selects the menuitem for the SEF URL</description>
  <namespace path="src">My\Plugin\System\CustomMenurule</namespace>
  <files>
    <folder plugin="custom_menurule">services</folder>
    <folder>src</folder>
  </files>
</extension>

plg_custom_menurule/services/provider.php - this is just standard boilerplate code for instantiating the plugin via the dependency injection container.

<?php

defined('_JEXEC') or die;

use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use My\Plugin\System\CustomMenurule\Extension\CustomMenurulePlugin;

return new class implements ServiceProviderInterface {

    public function register(Container $container) {
      $container->set(
        PluginInterface::class,
        function (Container $container) {
          $dispatcher = $container->get(DispatcherInterface::class);
          $plugin     = new CustomMenurulePlugin(
            $dispatcher,
            (array) PluginHelper::getPlugin('system', 'custom_menurule')
          );
          return $plugin;
        }
      );
    }
};

plg_custom_menurule/src/Extension/CustomMenurulePlugin.php. This is the entry point for the plugin. It registers to listen for the 'onAfterExtensionBoot' event, which is raised within loadExtension in libraries/src/Extension/ExtensionManagerTrait.php.

When the event is raised, if it's for the site com_content then it registers in the component's child Dependency Injection Container a RouterFactory with the plugin's namespace, which will overwrite the usual com_content entry (set in administrator/components/com_content/services/provider.php).

<?php

namespace My\Plugin\System\CustomMenurule\Extension;

defined('_JEXEC') or die;

use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\Event;
use Joomla\Event\SubscriberInterface;
use Joomla\CMS\Extension\ComponentInterface;
use Joomla\CMS\Extension\Service\Provider\RouterFactory;
use Joomla\CMS\Factory;

class CustomMenurulePlugin extends CMSPlugin implements SubscriberInterface {
     
    public static function getSubscribedEvents(): array {
        return [
            'onAfterExtensionBoot' => 'replaceRouterFactory',
        ];
    }

    public function replaceRouterFactory(Event $event): void {
        if (!Factory::getApplication()->isClient("site")) {
            return;
        }
        if (($event['type'] === ComponentInterface::class) && ($event['extensionName'] === "content")) {
            $container = $event["container"];
            $container->registerServiceProvider(new RouterFactory('\\My\\Plugin\\System\\CustomMenurule'));
        }
    }
}

When the RouterFactory is called up to create a Router instance, it tries to instantiate a class <namespace>\Site\Service\Router so this is what we want to define.

plg_custom_menurule/src/Site/Service/Router.php. Our class extends the com_content router class, and we use its configuration of the Router views, and the routing rules (set up in its constructor in components/com_content/src/Service/Router.php). Then we detach the menu rule that had been set and in its place attach our own. We need also to override the getName method.

<?php

namespace My\Plugin\System\CustomMenurule\Site\Service;

use Joomla\CMS\Application\SiteApplication;
use Joomla\CMS\Categories\CategoryFactoryInterface;
use Joomla\CMS\Component\Router\Rules\MenuRules;
use Joomla\CMS\Menu\AbstractMenu;
use Joomla\Database\DatabaseInterface;

\defined('_JEXEC') or die;

class Router extends \Joomla\Component\Content\Site\Service\Router
{
    public function __construct(SiteApplication $app, AbstractMenu $menu, CategoryFactoryInterface $categoryFactory, DatabaseInterface $db)
    {
        parent::__construct($app, $menu, $categoryFactory, $db);

        $rules = $this->getRules();
        foreach ($rules as $rule) {
            if ($rule instanceof \Joomla\CMS\Component\Router\Rules\MenuRules) {
                $this->detachRule($rule);
                break;
            }
        }
        
        $this->attachRule(new \My\Plugin\System\CustomMenurule\Site\Service\MenuRules($this));
    }
    
    public function getName()
    {
        return "content";
    }
}

plg_custom_menurule/src/Site/Service/MenuRules.php - This is our version of the preprocess function which is used to set the Itemid of the menuitem to build an SEF URL upon. The code extends the equivalent Joomla class in libraries/src/Component/Router/Rules/MenuRules.php, so that we reuse its buildLookup function, but override the preprocess function.

Note that this code is based on the Joomla 4.3.4 version. Other versions may have different Joomla code in this file.

This function differs from the standard Joomla router in a number of areas:

  1. If the Itemid has been set in the Route::_() call then we use it, provided that the menuitem is associated with com_content.

  2. If it's a multilingual site then we remove from the lookup array the entry associated with the home page of the "*" language. This is for the case where you assign language-specific home pages as described in https://docs.joomla.org/J3.x:Setup_a_Multilingual_Site/Creating_menus, and unpublish the main menu module. If we don't remove this entry then it's possible to get incorrect routing, eg if the "*" home page points to one article and a language-specific home page points to a different article.

  3. It looks through the lookup table to try and find an exact match between the parameters specified in the Route::_() call and a menuitem on the site. If it finds one then it uses that menuitem's Itemid.

  4. If the current page (ie the active menuitem) belongs to com_content then it uses that menuitem's Itemid.

If the above fails to find a suitable menuitem then it drops back to the standard Joomla code.

You can obviously tailor these rules to suit your own needs.

Once you have created the files above, then zip up the folder and install the extension. Then go into System / Plugins or System / Extensions and Enable the plugin.

Your feedback is particularly welcome, as it will probably benefit others as well, and I'll try and maintain this code bug-free.

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.