系统插件路由规则
简介
这个系统插件示例说明了 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
的文件夹中。
为简单起见,该插件仅使用英语;如果您想使其支持多语言,您可以按照 基本内容插件 中的说明进行更改。
清单文件
<?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,因为我们将在插件中使用它)。
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 条目。
<?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
关联的菜单项。
<?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";
}
}
MenuRules 类
我们现在可以编写自己的规则来确定选择哪个菜单项作为 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 代码。
<?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 的显示方式有何不同。