跳至主要内容
版本:5.1

系统插件路由规则

简介

这个系统插件示例说明了 Joomla 的灵活之处,它允许开发者更改 com_content 中的路由方式,而无需修改 Joomla 代码。

警告:这是一个系统插件,它在 Joomla 运行时每次加载,包括网站前端和管理员后端。如果插件中有 PHP 语法错误,那么您将无法访问 Joomla 管理员后端,并且需要进入 phpmyadmin(或等效工具)将 #__extensions 表中插件记录的 enabled 字段设置为 0。如果您不熟悉此操作,则不建议使用此插件。

有关 Joomla 路由工作原理的背景信息,您可以阅读关于 路由 的文档。

在构建 SEF URL 时,Joomla SiteRouter 类使用 MenuRules 类来确定用于创建 URL 的菜单项。这是一个重要的选择,因为菜单项不仅影响生成的 SEF URL 的格式,还影响网页的显示方式以及显示的关联模块。

com_content 组件使用这些规则,您可能会发现您希望更改网站上生成的某些 com_content SEF URL 的格式。此系统插件向您展示了如何构建您自己的规则,并使 com_content 使用这些规则来构建具有您希望格式的 SEF URL。

实现此操作的关键是编写我们自己的组件路由器类,然后让 com_content 使用我们的组件路由器类而不是它自己的。由于 com_content 使用 RouterFactory 实例化其路由器,因此我们需要让 RouterFactory 实例化我们的路由器。如 访问组件路由器类 中所述,RouterFactory 使用类名 <namespace>\Site\Service\Router 实例化一个组件路由器类,因此,如果我们将我们自己的命名空间注入到此 RouterFactory 中,它将实例化我们的路由器而不是 com_content 路由器。

您可以复制并修改下面的代码,或从 系统插件路由规则下载 下载并安装插件。

如果您要复制下面的代码,那么您需要将以下 5 个文件写入名为 plg_custom_menurule 的文件夹中。

为简单起见,该插件仅使用英语;如果您想使其支持多语言,您可以按照 基本内容插件 中的说明进行更改。

清单文件

plg_custom_menurule/custom_menurule.xml
<?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>

服务提供者文件

这是通过依赖注入容器实例化插件的标准样板代码。您只需根据自己的插件调整标准代码(基本上是 3 行,另外我们注入 Application,因为我们将在插件中使用它)。

plg_custom_menurule/services/provider.php
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')
);
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};

扩展类文件

这是插件的入口点。它注册以监听 'onAfterExtensionBoot' 事件,该事件在 libraries/src/Extension/ExtensionManagerTrait.php 中的 loadExtension 中引发。Joomla 在每次加载扩展时都会运行此代码,并且此代码

  • 运行组件的 services/provider.php 文件,将其及其依赖项加载到 组件的子 DIC
  • 触发 'onAfterExtensionBoot' 事件
  • 从子 DIC 中获取扩展及其依赖项。

因此,我们需要更改 RouterFactory 依赖项,以注入我们自己的命名空间而不是 com_content 命名空间。当我们调用 registerServiceProvider 并传递具有我们命名空间的 RouterFactory 时,它将替换子 DIC 中的 RouterFactory 条目。

plg_custom_menurule/src/Extension/CustomMenurulePlugin.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;

class CustomMenurulePlugin extends CMSPlugin implements SubscriberInterface {

public static function getSubscribedEvents(): array {
return [
'onAfterExtensionBoot' => 'replaceRouterFactory',
];
}

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

组件路由器

由于 RouterFactory 将尝试使用 <namespace>\Site\Service\Router 的完全限定类名实例化组件路由器,因此这定义了我们路由器的类名和 PHP 文件的位置。我们通过扩展 com_content 路由器类使我们的路由器类类似于 com_content 的路由器类,但我们将附加的 MenuRules 类更改为我们自己的 MenuRules 类。

我们还必须定义 getName 函数以返回字符串 "content",因为这将用于获取相关的菜单项,即与 com_content 关联的菜单项。

plg_custom_menurule/src/Site/Service/Router.php
<?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)
{
// run the com_content Router constructor
parent::__construct($app, $menu, $categoryFactory, $db);

// detach the MenuRules which was set up in the com_content constructor
$rules = $this->getRules();
foreach ($rules as $rule) {
if ($rule instanceof \Joomla\CMS\Component\Router\Rules\MenuRules) {
$this->detachRule($rule);
break;
}
}

// and attach our own MenuRules
$this->attachRule(new \My\Plugin\System\CustomMenurule\Site\Service\MenuRules($this));
}

public function getName()
{
return "content";
}
}

我们现在可以编写自己的规则来确定选择哪个菜单项作为 com_content SEF URL 的基础。下面是关于如何修改 libraries/src/Component/Router/Rules/MenuRules.php 中的 Joomla 代码的示例。由于我们的规则类继承自 Joomla 规则类,因此您可以在 preprocess 函数中编写自己的规则,如果找不到合适的菜单项,您可以通过调用 parent::preprocess(&$query) 回到 Joomla 版本。

此函数在以下几个方面与标准 Joomla 路由器不同

  • 如果在 Route::_() 调用中设置了 Itemid,那么我们将使用它,前提是菜单项与 com_content 关联。

  • 如果这是一个多语言站点,那么我们将从查找数组中删除与 "*" 语言的主页关联的条目。这是为了处理您按照 设置多语言站点/创建菜单 中的说明分配语言特定的主页并将主菜单模块取消发布的情况。如果我们不删除此条目,那么可能会导致不正确的路由,例如,如果 "*" 主页指向一篇文章,而语言特定的主页指向另一篇文章。

  • 它会遍历查找表,尝试在 Route::_() 调用中指定的参数和站点上的菜单项之间找到完全匹配。如果找到一个匹配,它将使用该菜单项的 Itemid

  • 如果当前页面(即 active 菜单项)属于 com_content,那么它将使用该菜单项的 Itemid

如果上述操作未能找到合适的菜单项,那么它将回到标准的 Joomla 代码。

plg_custom_menurule/src/Site/Service/MenuRules.php
<?php

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

use Joomla\CMS\Component\Router\Rules\RulesInterface;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Component\Router\RouterView;
use Joomla\CMS\Language\Multilanguage;

\defined('JPATH_PLATFORM') or die;

class MenuRules extends \Joomla\CMS\Component\Router\Rules\MenuRules
{
private static $allLangHomeRemoved = false;

public function preprocess(&$query)
{
$active = $this->router->menu->getActive();

/**
* If the active item id is not the same as the supplied item id or we have a supplied item id and no active
* menu item then we just use the supplied menu item and continue
*/
if (isset($query['Itemid']) && ($active === null || $query['Itemid'] != $active->id)) {
return;
}

// Get query language
$language = isset($query['lang']) ? $query['lang'] : '*';

// Set the language to the current one when multilang is enabled and item is tagged to ALL
if (Multilanguage::isEnabled() && $language === '*') {
$language = $this->router->app->get('language');
}

// build the reverse lookup for the language (the buildLookup() for language "*" is already done in the constructor)
// $this->lookup is a multidimensional array of the menuitems which match the component (com_content),
// the language, and filtered by access.
// It is keyed by:
// - firstly language - eg "*" or "en-GB"
// - secondly view or view:layout - the view, and possibly also layout, defined in the menuitem
// - thirdly id - whatever is the id defined in the menuitem, or 0 if no id is specified
// The value of this element in the array is the Itemid of the menuitem.
if (!isset($this->lookup[$language])) {
$this->buildLookup($language);
}

// If the &Itemid=.. has been specified in the Route::_() call then use it if it's suitable
// (ie if it's in the lookup array)
if (isset($query['Itemid'])) {
if (array_search((int)$query['Itemid'], $this->lookup, true) !== false) {
return; // just use that Itemid
}
}

/* The following is superfluous given that we'll take the supplied menu item if it's found above
// Check if the active menu item matches the requested query
if ($active !== null && isset($query['Itemid'])) {
// Check if active->query and supplied query are the same
$match = true;

foreach ($active->query as $k => $v) {
if (isset($query[$k]) && $v !== $query[$k]) {
// Compare again without alias
if (\is_string($v) && $v == current(explode(':', $query[$k], 2))) {
continue;
}

$match = false;
break;
}
}

if ($match) {
// Just use the supplied menu item
return;
}
}
*/

// If it's a multilingual site then ensure we don't use the home page of the "*" language
if (Multilanguage::isEnabled() && !self::$allLangHomeRemoved) {
$homeItems = $this->router->menu->getItems(array('language', 'home'), array('*', 1));
if ($homeItems) {
$allLangHome = $homeItems[0]->id;
foreach ($this->lookup as $lang => $viewArray) {
foreach ($viewArray as $view => $idArray) {
foreach ($idArray as $id => $itemid) {
if ($itemid == $allLangHome) {
if (count($this->lookup[$lang][$view]) == 1) {
unset($this->lookup[$lang][$view]);
} else {
unset($this->lookup[$lang][$view][$id]);
}
break;
}
}
}
}
}
self::$allLangHomeRemoved = true;
}

// Form the equivalent of view:layout based on the query parameters, and try to match in the lookup array
if (isset($query['view'])) {
$searchKey = $query['view'];
if (isset($query['layout']) && $query['layout'] !== 'default') {
$searchKey .= ":" . $query['layout'];
}
foreach ($this->lookup as $lang => $arr) {
if (array_key_exists($searchKey, $arr)) { // find if there's a matching view:layout
$matchingViews = $arr[$searchKey];
// now see if we can find an exact match with the id
if (isset($query['id'])) {
$idKey = (int) $query['id'];
if (array_key_exists($idKey, $matchingViews)) {
$query['Itemid'] = $matchingViews[$idKey];
return;
}
} else { // if we haven't got an id in the query array
if (array_key_exists(0, $matchingViews)) {
$query['Itemid'] = $matchingViews[0];
return;
}
}
}
}
}

// Use the active menuitem if it's a com_content one
if ($active && $active->component === "com_content") {
$query["Itemid"] = $active->id;
return;
}

// if we didn't find one above, then fall back to the standard Joomla code below

$needles = $this->router->getPath($query);

$layout = isset($query['layout']) && $query['layout'] !== 'default' ? ':' . $query['layout'] : '';

if ($needles) {
foreach ($needles as $view => $ids) {
$viewLayout = $view . $layout;

if ($layout && isset($this->lookup[$language][$viewLayout])) {
if (\is_bool($ids)) {
$query['Itemid'] = $this->lookup[$language][$viewLayout];

return;
}

foreach ($ids as $id => $segment) {
if (isset($this->lookup[$language][$viewLayout][(int) $id])) {
$query['Itemid'] = $this->lookup[$language][$viewLayout][(int) $id];

return;
}
}
}

if (isset($this->lookup[$language][$view])) {
if (\is_bool($ids)) {
$query['Itemid'] = $this->lookup[$language][$view];

return;
}

foreach ($ids as $id => $segment) {
if (isset($this->lookup[$language][$view][(int) $id])) {
$query['Itemid'] = $this->lookup[$language][$view][(int) $id];

return;
}
}
}
}
}

// Check if the active menuitem matches the requested language
if (
$active && $active->component === 'com_' . $this->router->getName()
&& ($language === '*' || \in_array($active->language, ['*', $language]) || !Multilanguage::isEnabled())
) {
$query['Itemid'] = $active->id;

return;
}

// If not found, return language specific home link
$default = $this->router->menu->getDefault($language);

if (!empty($default->id)) {
$query['Itemid'] = $default->id;
}
}
}

安装

创建完上述文件后,将文件夹压缩并安装扩展。然后进入系统/插件或系统/扩展,启用该插件。尝试使用 com_content 菜单项、类别和文章,查看 SEF URL 的显示方式有何不同。