Drupal 10:使用路径处理器创建自定义路径
Drupal 中的路由在创建时可以修改,甚至在处理页面请求时能动态更改。
除了路由系统,Drupal 还有路径别名系统。像“/node/123”这样的内部路由,能被赋予类似“/about-us”这种对 SEO 友好的路径。当用户通过“/about-us”访问网站时,路径会在内部被重写,以便 Drupal 提供正确的页面。像 Pathauto 这类模块会利用内容项的信息自动生成对 SEO 友好的路径,用户无需手动输入并记住。
这一机制得益于 Drupal 内部名为“路径处理”的服务。当 Drupal 接收到请求时,会把路径传递给一个或多个路径处理器,让它们将其更改为另一个路径(可能是内部路由)。在生成页面链接时,这个过程会反向进行,这样路径处理器就可以反转处理过程。
可以使用路由订阅器在 Drupal 中更改路由,但使用路径处理器能让我们在不实际更改内部路由本身的情况下,更改或隐藏 Drupal 网站中页面的路由或路径。
在本文中,成都长风云 Drupal 开发团队将了解有哪些类型的路径处理器,如何创建自己的路径处理器,它们在 Drupal 网站中有哪些用途,以及在创建路径处理器时需要注意的其他事项。
一、路径处理器的类型
路径处理器由 Drupal 类 \Drupal\Core\PathProcessor\PathProcessorManager 管理。当你将路径处理器添加到网站时,就是这个类管理处理器的顺序并调用处理器。
Drupal 中有两种类型的路径处理器:
- 入站 - 处理入站路径,并允许在 Drupal 处理之前以某种方式对其进行更改。通常在用户向 Drupal 网站发送访问页面的请求时发生。入站路径处理器也可以由某些内部进程触发,例如,使用路径验证器时。路径验证器会将路径传递给入站路径处理器,以便对其进行更改,确保其得到正确处理。
- 出站 - 出站路径是 Drupal 生成 URL 的任何路径。将调用出站路径处理器来更改路径,以便正确生成 URL。
基本上,入站处理器用于响应路径时,出站处理器用于渲染路径时。
下面通过几个例子来展示它们是如何工作的。
二、创建入站处理器
要在 Drupal 中注册入站服务,需要创建一个带有 path_processor_inbound 标签的服务,并可以选择包含一个优先级。这让 Drupal 知道在处理入站路径时必须使用这个服务。
通常,路径处理器类会保存在自定义模块的“src”目录下的“PathProcessor”目录中。
services:
mymodule.path_processor_inbound:
class: Drupal\mymodule\PathProcessor\InboundPathProcessor
tags:
- { name: path_processor_inbound, priority: 20 }
为 path_processor_inbound 标签分配的优先级取决于你的设置。Drupal 中处理路径的内部入站处理器的优先级为 100,所以任何小于 100 的设置都会使处理在调用 Drupal 的内部处理程序之前进行。
我们创建的 InboundPathProcessor 类必须实现 \Drupal\Core\PathProcessor\InboundPathProcessorInterface 接口,这要求在类中添加一个名为 processInbound() 的方法。以下是该方法的参数。
- $path - 这是要处理的路径的字符串,以斜杠开头。
- $request - 除了路径之外,请求对象也会传递给该方法。这使我们能够对 URL 上的查询字符串或添加到请求中的其他参数进行额外的检查。
processInbound() 方法必须以字符串形式返回处理后的路径(以斜杠开头)。如果不想更改路径,那么需要返回传递给该方法的路径。
为了创建一个简单的例子,让我们确保当用户访问路径“/some-random-path”时,我们在内部将其转换为“/node/1”,这不是该页面的内部路由。在这个例子中,如果传递给方法的路径不是我们需要的路径,那么我们就直接返回它,实际上忽略除了我们正在寻找的路径之外的任何路径。
<?php
namespace Drupal\mymodule\PathProcessor;
use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Symfony\Component\HttpFoundation\Request;
class InboundPathProcessor implements InboundPathProcessorInterface {
public function processInbound($path, Request $request): string {
if ($path === '/some-random-path') {
return $path;
}
return '/node/1';
}
}
现在,当用户访问路径“/some-random-path”时,他们将看到“/node/1”页面的输出。仍然可以访问“/node/1/”页面并查看输出,所以我们只是为同一页面创建了一个重复的路径。
这是一个简单的例子,展示了 processInbound() 方法是如何工作的,我们稍后会看一个更具体的例子。
三、创建出站处理器
出站处理器的定义方式与入站处理器类似,但在这种情况下,我们使用 path_processor_outbound 标签标记服务。
services:
mymodule.path_processor_outbound:
class: Drupal\mymodule\PathProcessor\OutboundPathProcessor
tags:
- { name: path_processor_outbound, priority: 250 }
path_processor_outbound 的优先级与入站处理器大致相反,因为通常你希望出站处理在调用栈中稍后进行。Drupal 出站处理器的内部机制设置为 200,所以将我们的优先级设置为 250 意味着我们在 Drupal 创建任何别名之后处理出站链接。
我们创建的 OutboundPathProcessor 类必须实现 \Drupal\Core\PathProcessor\OutboundPathProcessorInterface 接口,这要求在类中添加一个名为 processOutbound() 的方法。以下是该方法的参数。
- $path - 这是要处理的路径的字符串,以斜杠开头。
- $options - 一个包含附加选项的关联数组,其中包括“query”、“fragment”、“absolute”和“language”等。这些是在生成 URL 时发送给 URL 类的相同选项,允许我们根据传递的选项更新出站路径。
- $request - 当前请求对象也会发送给该方法,并可以根据传递给当前路径的参数做出决策。
- $bubbleable_metadata - 一个可选对象,用于收集路径处理器的可冒泡元数据,以便我们有可能将缓存信息向上传递。
processOutbound() 方法必须返回新的路径,以斜杠开头。如果不想更改路径,那么就直接返回发送给我们的路径,否则我们可以进行任何所需的更改并返回这个字符串。
在入站处理器的简单例子基础上进一步扩展,让我们将路径“/node/1”更改为“/some-random-path”。在这个例子中,我们寻找内部路径“/node/1”,如果看到这个路径,就返回我们的新路径。
<?php
namespace Drupal\mymodule\PathProcessor;
use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Symfony\Component\HttpFoundation\Request;
class OutboundPathProcessor implements OutboundPathProcessorInterface {
/**
* {@inheritdoc}
*/
public function processOutbound($path, &$options = [], Request $request = NULL, BubbleableMetadata $bubbleable_metadata = NULL) {
if ($path !== '/node/1') {
return $path;
}
return '/some-random-path';
}
}
有了这个设置,当 Drupal 输出指向“/node/1”的链接时,它将把路径渲染为“/some-random-path”。
就这个例子本身而言,它并没有做太多事情;我们只是为单个页面重写了路径。真正的强大之处在于将入站处理和出站处理结合起来。让我们来做这件事。
四、创建用于路径处理的单个类
可以通过在单个服务中组合标签,将入站和出站处理器组合到一个类中。这可以通过在模块的服务文件中组合路径处理器来实现。
services:
mymodule.path_processor:
class: Drupal\mymodule\PathProcessor\MyModulePathProcessor
tags:
- { name: path_processor_inbound, priority: 20 }
- { name: path_processor_outbound, priority: 250 }
根据这个定义创建的类实现了 InboundPathProcessorInterface 和 OutboundPathProcessorInterface 接口,因此它包含 processInbound() 和 processOutbound() 这两个方法。
<?php
namespace Drupal\mymodule\PathProcessor;
use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Symfony\Component\HttpFoundation\Request;
class MyModulePathProcessor implements InboundPathProcessorInterface, OutboundPathProcessorInterface {
public function processInbound($path, Request $request): string {
return $path;
}
public function processOutbound($path, &$options = [], Request $request = NULL, BubbleableMetadata $bubbleable_metadata = NULL): string {
return $path;
}
}
现在你只需要添加路径处理逻辑即可。
创建这样的结构是个好主意,这样可以在 Drupal 内外转换路径。这创建了一个一致的路径模型,并防止不同页面具有相同路径的重复内容问题。
五、重定向模块
如果你打算使用入站路径处理器系统,那么应该注意重定向模块会尝试将你的入站路径处理器更改重定向到重写后的路径。重定向模块是一个很棒的模块,成都长风云 Drupal 开发团队在运行的每个 Drupal 网站上都会安装它,但为了防止这种重定向,你需要做一些额外的事情,我们将在本节中介绍。
为了防止重定向模块重定向路径,你需要在重定向模块的 RouteNormalizerRequestSubscriber 类中的 kernel.request 事件触发 之前,将 _disable_route_normalizer 属性添加到路由中。我们通过创建自己的事件订阅器并给它更高的优先级来实现这一点。
首先要做的是将我们的事件订阅器添加到自定义模块的 services.yml 文件中。
mymodule.prevent_redirect_subscriber:
class: Drupal\mymodule\EventSubscriber\PreventRedirectSubscriber
tags:
- { name: event_subscriber }
事件订阅器本身只需要监听 kernel.request 事件,该事件存储在 KernelEvents::REQUEST 常量中。我们需要在重定向模块事件之前触发我们的自定义模块,所以我们将事件的优先级设置为 40。这比重定向模块事件的优先级 30 要高。
事件订阅器只需要监听我们的路径,然后如果检测到路径,就将 _disable_route_normalizer 属性设置到路由中。
<?php
namespace Drupal\mymodule\EventSubscriber;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class PreventRedirectSubscriber implements EventSubscriberInterface {
public static function getSubscribedEvents() {
$events[KernelEvents::REQUEST][] = ['preventInboundPathRedirect', 40];
return $events;
}
public static function preventInboundPathRedirect(RequestEvent $event) {
if ($event->getRequest()->getPathInfo() === '/en/some-random-path') {
$event->getRequest()->attributes->set('_disable_route_normalizer', true);
}
}
}
当重定向模块事件触发时,它会看到这个属性并忽略重定向。
只有当你仅使用入站路径处理器更改某种实体的路径时,才会发生这种情况。仅创建入站处理器会导致外部路径和翻译后的内部路径之间出现不平衡,然后我们需要让重定向模块知道这一点,以防止重定向。如果我们也以相同(相反)的方式翻译出站路径,那么就不会发生重定向。
六、做一些有用的事情
我们已经了解了交换路径和防止重定向,现在让我们用这个系统做一些有用的事情。
最近成都长风云 Drupal 开发团队负责创建一个模块,该模块可以将任何页面渲染为 RSS。并不是我们需要一个 RSS 提要,而是每个单独的页面都应该有一个 RSS 版本。
这是必要的,因为与一个外部系统集成,该系统用于从 Drupal 网站中提取信息用于时事通讯。页面有 RSS 版本使得系统更容易解析页面内容,从而制作时事通讯。这也意味着如果主题发生变化,系统不会受到影响,因为它不会使用网站的主题。
本质上,这个需求意味着我们需要在网站上任何页面的后面添加“/rss”,它就会相应地渲染页面。
最终的模块被命名为“Node RSS”,并广泛使用路径处理器来实现这一结果。
第一步是创建一个控制器,该控制器可以响应像“/node/123/rss”这样的路径,将页面渲染为 RSS 提要。这需要设置一个简单的路由,让 Drupal 监听该路径,并将当前节点对象注入到控制器中。路由还包含一个简单的权限,这为系统准备好时激活系统提供了一种方便的方式。
node_rss.view:
path: '/node/{node}/rss'
defaults:
_title: 'RSS'
_controller: '\Drupal\node_rss\Controller\NodeRssController::rssView'
requirements:
_permission: 'node.view all rss feeds'
node: \d+
options:
parameters:
node:
type: entity:node
NodeRssController 的 rssView 操作只需要渲染节点并将其作为 RSS 文档的一部分返回。使用这个,我们现在可以访问“/node/123/rss”节点页面并看到该页面的 RSS 版本。
这里我不会详细介绍生成页面的 RSS 版本,因为它包含很多样板代码,超出了本文的范围。
到目前为止,我们只实现了所需功能的一半。通过节点 ID 查看页面的 RSS 版本没问题,但我们真正想要的是访问页面的完整路径并在末尾附加“/rss”。
下一步是设置我们的路径处理器,以便我们可以动态更改路径。除了标签之外,我们还传入另外两个服务供我们在类中使用。这些服务是用于翻译路径的 path_alias.manager 服务和确保我们获得正确语言路径的 language_manager 服务。
services:
node_rss.path_processor:
class: Drupal\node_rss\PathProcessor\NodeRssPathProcessor
arguments:
- '@path_alias.manager'
- '@language_manager'
tags:
- { name: path_processor_inbound, priority: 20 }
- { name: path_processor_outbound, priority: 220 }
processInbound() 方法会在传递的路径末尾查找“/rss”字符串。如果找到,我们就从路径中删除它,并尝试在网站中找到页面的内部路径。如果我们找到了路径,它将以“/node/123”的形式返回,而不是完整的路径别名,这意味着我们可以在路径末尾附加“/rss”,将路径指向我们的 NodeRssController::rssView 操作。
public function processInbound($path, Request $request): string {
if (preg_match('/\/rss$/', $path) === 0) {
// 字符串不是 RSS 提要字符串。
return $path;
}
$nonRssPath = str_replace('/rss', '', $path);
$internalPath = $this->pathAliasManager->getPathByAlias($nonRssPath, $this->languageManager->getCurrentLanguage()->getId());
if ($internalPath === $nonRssPath && preg_match('/^node\//', $internalPath) === 0) {
// 未找到匹配的路径,或者,这不是我们有的节点路径。
return $path;
}
return $internalPath . '/rss';
}
processOutbound() 方法需要进行相反的处理。在这种情况下,我们寻找看起来像“/node/123/rss”的路径,并将其转换回页面的完整路径别名。如果我们找到该路径的别名,就将“/rss”附加到路径并返回。
public function processOutbound($path, &$options = [], Request $request = NULL, BubbleableMetadata $bubbleable_metadata = NULL): string {
if (preg_match('/^\/node\/.*?\/rss?$/', $path) === 0) {
// 字符串不是 RSS 提要字符串。
return $path;
}
$nonRssPath = str_replace('/rss', '', $path);
$alias = $this->pathAliasManager->getAliasByPath($nonRssPath, $this->languageManager->getCurrentLanguage()->getId());
if ($nonRssPath === $alias) {
// 未找到内部别名。
return $path;
}
return $alias . '/rss';
}
现在,我们为网站上的任何内容路径(只要是某种节点页面)都提供了 RSS 提要。
如果我们尝试访问任何其他类型页面(如分类术语)的 RSS 输出,我们将收到 404 错误。这是因为我们设置的路由,因为该参数只接受节点路径。
由于我们已经完全转换了路径,这里不需要重定向模块覆盖,因为这些路径有一个连贯的输入/输出机制。只有当路径存在不平衡时,我们才需要覆盖重定向模块以防止重定向。
如果你正在寻找上述模块的完整源代码,不用担心,成都长风云 Drupal 开发团队最近在 Drupal.org 上发布了 Node RSS 模块。目前它只有一个开发版本,因为我们想添加选择哪些内容类型可用于提要的功能。我们也在不同的设置下对其进行测试,以确保提要在不同情况下都能正常工作。如果它对你有用,请告诉我们,如果你有任何问题,请创建一个工单。
如果你想查看另一个使用这种技术的模块,有动态路径重写模块。它允许在不创建路径别名的情况下动态重写任何内容路径。这是使用像 Path Auto 这样的模块的替代方法,而无需在系统中实际创建路径别名,并使用一个不错的缓存系统来加快响应速度。
七、结论
Drupal 中的路径处理系统非常强大,可以用于构建一些有趣的功能,动态重写路径。我们可以接收任何传入请求,并将其动态重定向到我们喜欢的任何路径。
如果没有这个系统,我们需要为我们想要的每个路径生成额外的别名,并在使用系统之前将它们添加到数据库中。这在小型网站上没问题,但如果管理的网站有数百万个节点,那么多数据会使数据库膨胀,而且可能不会被大量使用。
路径处理确实与其他模块(如重定向模块)有一些交互,但这些问题很容易克服。也许最复杂的部分是确保你为一些交互设置了正确的权重,因为设置错误可能会导致不必要的交互。Drupal 开发、Drupal 模块开发以及 Drupal 升级等工作中,路径处理系统都能发挥重要作用,尤其是在 Drupal 11 及后续版本中,相信它会有更出色的表现。


