90. 系统出入站路径处理

drupal可以让你使用任意URL路径来访问某个页面,从而提供良好的SEO支持和语义性,如此强大的功能是由路径处理子系统完成的,在讲解她之前需要明白一个概念:“内部路径”,也就是路由中指定的路径,任意进来的路径都会被路径处理系统转化为内部路径(非可路由内部url除外),从而让系统内部有一个统一的环境,面向内部路径即可,而不用考虑用户到底使用的什么路径,路径处理系统实现了访问路径和系统实现的解耦,路径处理分为两部分:入站处理和出站处理,她们相互协作提供了用户层面连贯的访问体验。

 

路径处理管理器:

路径处理是由处理器链完成的,链中每一个节点是一个独立的处理器,入站和出站各有一条处理器链,链中处理器按照优先级顺次执行,前一个处理器处理后的路径及相关参数继续传给后一个处理器,最终得到完全处理的路径,路径处理管理器被用来收集处理器,并按顺序执行她们,服务定义如下:

服务: path_processor_manager

类:Drupal\Core\PathProcessor\PathProcessorManager

该服务被设置了service_collector标签,在容器编译阶段会收集被标记为以下标签的服务:

path_processor_inbound:通过addInbound方法注入,入站路径处理器服务

path_processor_outbound:通过addOutbound方法注入,出站路径处理器服务

她们按优先级执行,数值越大越优先,默认优先级为0

默认提供的入站处理器及优先级如下(服务id:优先级):

path_processor_decode1000

path_processor.image_styles300

path_processor_language300

path_processor_front200

path_processor.files200

path_processor_alias100

默认提供的出站处理器及优先级如下(服务id:优先级):

path_processor_alias300

path_processor_front200

path_processor_language100

 

 

入站处理:

入站的路径处理时机是在系统派发请求事件时在路由系统中执行,路由系统订阅了请求事件,路由是工作在内部路径上的,在执行路由工作之前调用路径处理器以将访问路径转变为内部路径,具体是在得到路由集前,详见服务:router.route_provider的以下方法:

\Drupal\Core\Routing\RouteProvider::getRouteCollectionForRequest

整个入站路径处理是从以下代码开始的:

$path =\Drupal::service("path_processor_manager")->processInbound($path, $request);

参数$path是由请求对象返回的路径信息($request->getPathInfo()),是网址中第一个“/”开始到“?”之间的部分,不含协议、主机名、端口、查询参数等,以“/ 开头,末尾不含“/”,如果没有路径则值为“/

参数$request是请求对象

在该方法内部将按优先级依次调用入站处理器的以下方法:

$path = $processor->processInbound($path, $request);

前一个处理器返回的已处理路径被传给下一个处理器,模块自定义入站处理器方法如下:

定义一个实现入站路径处理器接口的类(\Drupal\Core\PathProcessor\InboundPathProcessorInterface

将该类定义为服务,并给出服务标签:path_processor_inbound和优先级参数

自定义的入站路径处理器需要注意和核心提供的处理器(见下)的优先级顺序

核心默认入站路径处理器:

path_processor_decode

类:Drupal\Core\PathProcessor\PathProcessorDecode

优先级:1000

作用:将url编码的路径解码

 

path_processor.image_styles

类:Drupal\image\PathProcessor\PathProcessorImageStyles

优先级:300

图片模块定义,重写图像样式的URL

 

path_processor_language

类:Drupal\language\HttpKernel\PathProcessorLanguage

优先级:300

多语言站点中,有些语言协商使用特定的路径格式,在路径中包含语言信息,该处理器同时涉及出入站处理,重要,单独讲解,见后

 

path_processor_front

类:Drupal\Core\PathProcessor\PathProcessorFront

优先级:200

解析首页的内部路径,这是系统可以将任意页面设为首页的关键所在,首页的内部路径储存在配置项system.sitepage.front键下,注意在设置首页内部路径时不要带语言前缀

 

path_processor.files

类:Drupal\system\PathProcessor\PathProcessorFiles

优先级:200

解析以'/system/files'开始的路径

 

path_processor_alias

类:Drupal\Core\PathProcessor\PathProcessorAlias

优先级:100

处理路径别名,该服务内部使用别名管理器,该处理器同时涉及出入站处理,很重要,单独讲解,见后。

 

 

出站处理:

出站处理是指在产生页面包含的所有超链接时进行路径处理,以便用户在站点各页面之间连贯的导航点击,出站处理后的站内路径,正是入站要处理的路径,在系统中所有用到超链接的地方都应该使用URL对象:

\Drupal\Core\Url

关于此详见本系列的《UrlLink》主题,出站处理的时机正是在将url对象转化为字符串时,如果路径不使用url对象,将不会进行出站处理,一些主题开发者很容易忽视该问题而将url硬编码在模板中,这在一些情况下将导致问题,比如语言协商,对系统的扩展性也有限制,比如业务需要调整路由时还要额外处理模板中不规范的硬编码,所以最佳实践是在所有需要url的地方(包括模块、主题等)使用url对象,关于如何在模板中使用url对象请见本系列《twig服务》主题。

twig中渲染一个url对象时会调用其以下方法将其转化为一个字符串显示:

\Drupal\Core\Url::toString ($collect_bubbleable_metadata = FALSE)

在该方法中将依据URL是否在系统中有路由而采用两个服务来渲染转化:

无路由采用服务:unrouted_url_assembler

有路由采用服务:url_generator

 

无路由的url处理:

服务idunrouted_url_assembler

类:Drupal\Core\Utility\UnroutedUrlAssembler

获取方法:\Drupal::service('unrouted_url_assembler');

该服务只处理外部url和内部无路由的url,如图片、robots.txt等(以base:做协议名),外部url将不会运用路径处理,内部无路由的url可以通过选项参数path_processing来指定是否需要路径处理,当值不为空时将调用路径处理管理器执行路径处理,默认情况下不处理,如:

 

 $options['path_processing']=true;

     echo $url=\Drupal\Core\Url::fromUri("base:/sub/img.jpg",$options)->toString();

将输出:

/zh-hans/sub/img.jpg

这在路径前加了语言代码,是由于语言模块提供了URL语言协商器(同时也是一个路径处理)的缘故

注意在内部无路由的url在进行路径处理时,并不被传递请求对象

 

有路由的url处理:

服务idurl_generator

类:Drupal\Core\Render\MetadataBubblingUrlGenerator

获取方法:\Drupal::urlGenerator();

在内部主要使用服务:url_generator.non_bubblingDrupal\Core\Routing\UrlGenerator

入口代码位于以下方法:

\Drupal\Core\Routing\UrlGenerator::generateFromRoute

系统在处理一个有路由的URL时,默认会调用路径处理管理器进行路径处理,但可以在URL对象的选项参数中指定$options['path_processing']false来强制不处理,在内部由于选项参数会合并路由定义中options项的default_url_options值,所以也可以在路由定义中指定该参数为false达到同样目的(选项方式优先级高于路由定义),路由定义示例请见系统模块定义的system.db_update路由

 

自定义出站处理器:

定义一个实现出站路径处理器接口的类(\Drupal\Core\PathProcessor\OutboundPathProcessorInterface

将该类定义为服务,并给出服务标签:path_processor_outbound和优先级参数

自定义的出站路径处理器需要注意和核心提供的处理器的优先级顺序

出站处理器接口方法的参数含义如下:

processOutbound($path, &$options = [], Request $request = NULL, BubbleableMetadata $bubbleable_metadata = NULL);

$path上一个处理器处理后的路径,初始时是依据路由定义中的格式产生的内部路径,已经替换了参数值

$options:来源于传递给URL对象的选项参数,附加合并了路由定义中options/default_url_options的值(路由定义中的选项优先级更低),如果是有路由的url则系统还设置了$options['route']选项,值为路由对象(\Symfony\Component\Routing\Route),路径处理器以引用方式接收选项参数,可以修改她以控制最终产生的urllink字符串值

$request:从请求堆栈中获取的当前请求对象

$bubbleable_metadata:通常为\Drupal\Core\GeneratedUrl对象,用以传递可冒泡渲染元数据

 

出站处理器按优先级依次执行,前一个处理后的路径及选项参数等继续传递给下一个

以下将介绍两个重要的处理器,她们同时实现了入站和出站处理

 

 

语言路径处理器:

服务idpath_processor_language

类:Drupal\language\HttpKernel\PathProcessorLanguage

参数服务:config.factorylanguage_managerlanguage_negotiatorcurrent_userlanguage.config_subscriber

服务标签:path_processor_inbound(优先级300)、path_processor_outbound(优先级100

该服务只有在站点是多语言时才注册,见语言模块的服务提供器,实例化后将调用initConfigSubscriber方法以向配置订阅器服务传递自己,该服务比较简单,采用了装饰者模式,真正执行路径处理工作的是实现了路径处理接口的语言协商器,(是所有可配置语言类型的开启的协商器,不论这个协商器在语言协商时是否会被执行都会参与路径处理),他们按权重依次执行处理,关于语言协商请见本系列《语言模块》,这里仅讲解最常使用的“language-url”语言协商,类如下:

Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl

该协商器通过在路径前添加语言识别标志前缀或语言相关域名来判断语言信息

入站处理在该类的以下方法中:

processInbound($path, Request $request)

此方法在语言协商是通过域名判断语言的情况下将什么也不做,在使用路径前缀的情况下,前缀如果和配置的某语言识别前缀匹配时,将删除路径中的语言识别前缀后返回,否则原样返回

出站处理在该类的以下方法中:

processOutbound($path, &$options = [], Request $request = NULL, BubbleableMetadata $bubbleable_metadata = NULL)

在该方法中添加语言识别前缀到路径前或添加语言识别域名,但是并不修改传入的$path参数,而是修改$options选项参数间接控制(后续流程会在url产生器中依据选项参数生成url),前缀通过$options['prefix']设置,域名通过$options['base_url']设置,在url对象的选项参数$options['language']中用户可以指定一个语言对象,如果没有指定将使用url语言类型的协商结果,这允许一个页面中同时存在指向不同语言的链接,比如语言切换器,$options['language']的值应该是一个站点中存在的语言的语言对象,否则该方法不进行任何处理

 

 

别名路径处理器:

服务idpath_processor_alias

类:Drupal\Core\PathProcessor\PathProcessorAlias

这是一个很简单的服务,在入站时直接依据路径从路径别名管理器中获取内部路径,出站时,如果选项参数被设置了$options['alias']且不为空,说明url已经是别名,则什么都不做直接返回,反之则从路径别名管理器中依据内部路径返回别名路径,这就是我们可以为节点自定义路径的原因。

 

路径别名管理器:

服务idpath.alias_manager

类:Drupal\Core\Path\AliasManager

参数服务: path.alias_storagepath.alias_whitelistlanguage_managercache.data

别名路径的设置和语言相关是一个合理的需求,比如一篇新闻,在中文状态下别名路径可以使用拼音构成,而英文状态下拼音就莫名其妙了,需要用英文来设置别名,因此drupal在设定别名时是带了语言代码的,同一个内部路径,如节点,在不同的语言下可以有不同的别名路径,如果不需要区别语言那么语言代码可以使用特殊语言代码:“未定义”来表示,理解这一点对别名的储存很重要。

 

出站路径处理时,需要判断页面中每一个内部路径是否具备别名路径,如果是将替换,页面包含的url会很多,所以别名路径处理的性能问题就变得相当重要了,每一点优化都是对系统整体的优化,系统使用了两种优化方案:路径别名白名单(见下)和路径别名预加载

 

路径别名预加载:由于页面中ur数量众多,如果每处理一个url对象就去查一次数据库别名表,那么性能将非常糟糕,为了解决这个问题,在一个请求第一次到来时,系统会为该请求缓存页面中所有的内部路径,以后当这个请求再次到来时将一次性加载这些内部路径的别名,这样就不会分多次请求数据库了,由此提高了性能,这就是路径别名管理器实现以下接口的原因:

\Drupal\Core\CacheDecorator\CacheDecoratorInterface

该接口有两个方法:设置缓存键和写缓存,设置缓存键发生在派发核心控制器事件时,写缓存发生在核心终止事件时,由path_subscriber订阅器服务执行(\Drupal\Core\EventSubscriber\PathSubscriber

 

路径别名管理器方法介绍如下:

public function getPathByAlias($alias, $langcode = NULL)

依据别名路径得到源路径,语言代码用于在别名表中查找数据,只返回该语言或未定义语言下的别名源路径

 

public function getAliasByPath($path, $langcode = NULL)

依据源路径得到别名路径,语言代码含义同上,在该方法中可以看到前文所讲的优化运用

 

public function cacheClear($source = NULL)

清理某源路径的本地缓存以及白名单重建,如果没有传递源路径将清理全部缓存和重建整个别名白名单

 

 

路径别名储存器:

用于执行路径别名的CURD操作

服务名:path.alias_storage

类:Drupal\Core\Path\AliasStorage

数据库表名:url_alias(该表很简单,只有四个字段:pidsourcealiaslangcode

方法介绍(以下方法涉及路径查询的都不区分大小写):

save($source, $alias, $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED, $pid = NULL)

保存或更新一条别名信息,如果指定了$pid参数则为更新,失败返回false,成功时返回一个关联数组,键名和值为:

source:保存或更新后的源路径,通常就是内部路径,后称源路径

alias:保存或更新后的别名路径

langcode:保存或更新后的语言代码,指示该别名所运用的语言

pid:被保存或更新的数据的id

original:仅仅更新操作时才有,值是一个数组,包含原来源路径、别名、语言代码的值

保存或更新成功后会分别派发以下钩子:

保存时:path_insert

更新时:path_update

参数是前文讲到的返回数组,钩子可以再次操作保存的数据,如果钩子内再调用该方法需要注意无限递归问题,系统实现了这两个钩子的模块有:

系统模块:清除路径别名管理器中的缓存

菜单链接内容模块:更新菜单链接

 

public function load($conditions)

加载一条别名数据,如果没有则返回false,否则返回一个关联数组,字段名做键名,参数$conditions是一个条件数组,键名为字段名,键值为字段值,路径不区分大小写

 

public function delete($conditions)

删除一条别名数据,失败返回false,成功返回被删除数据的Pid,删除成功后会派发钩子:path_delete,参数为原来的数据(load方法返回的结果),默认只有系统模块和菜单链接内容模块实现

 

public function preloadPathAlias($preloaded, $langcode)

给定一些源路径返回其别名路径,参数$preloaded是一个源路径构成的索引数组, $langcode是语言代码,返回值是一个数组,键名为源路径,键值为别名路径,包含了“未定义”语言代码的路径,如果没有则返回空数组,如果数据库异常将返回false

 

public function lookupPathAlias($path, $langcode)

依据源路径查询其别名,如果存在则返回字符串别名,否则返回false,语言代码会包含“未定义”中的数据

 

public function lookupPathSource($path, $langcode)

和前一个方法lookupPathSource一样,不过是依据别名返回源路径

 

public function aliasExists($alias, $langcode, $source = NULL)

判断某语言下别名是否存在,不包含“未定义”语言,如果传递了源路径参数$source,那么将判断除该源路径之外在该语言代码下是否还存在该别名,返回布尔值

 

public function languageAliasExists()

查找“未定义”语言之外是否存在别名记录

 

public function getAliasesForAdminListing($header, $keys = NULL)

根据别名是否包含关键词来查询表中的数据,应用于所有语言,关键词以参数$keys指定,可以包含通配符“*”或“%”,通配符将统一转变为数据库LIKE查询的“%”通配符,只要别名中包含了关键词(或其通过通配符指定的模式)就会被选中返回,返回数据以参数$header排序,返回值是一个索引数组,元素是标准stdClass对象,字段名做属性,字段值做属性值,如果没有或异常将返回空数组

 

public function pathHasMatchingAlias($initial_substring)

查询表中是否有特定前缀的源路径,前缀由参数$initial_substring给出,返回布尔值,该方法相当于查看某前缀的源路径是否有设置别名

 

路径别名白名单:

服务名:path.alias_whitelist

类:Drupal\Core\Path\AliasWhitelist

该服务是在路径处理器中对依据源路径得到别名操作的一种优化措施,用到了缓存收集器,缓存收集器有个特点是一开始并不缓存所有数据,而是伴随时间的流逝,把真正用到过的数据缓存起来,这样在大量数据时能大大减低查询次数、内存消耗等等,以此提高整体操作的性能,在主题钩子注册中就用到了缓存收集器。

在路径别名白名单中可以通过以下代码判断某前缀的源路径是否存在于别名表中:

\Drupal::service("path.alias_whitelist")->get($key);

参数$key是源路径的第一段字符串(假设源路径是:/node/31,那么$key值就是node),该方法如果返回falseNULL说明别名表中没有该源路径,所以也就一定不存在对应的别名,如果返回true则说明有此前缀的源路径,但是否有别名还需要进一步查询确定。

别名白名单中的数据可以通过以下代码得到:

\Drupal::service("cache.bootstrap")->get('path_alias_whitelist')->data;

这是一个数组,键名为路径前缀,键值为true表示该前缀有源路径存在,为false表示没有,为NULL表示需要执行缓存收集以进一步查询

初始时白名单包含了系统中存在的所有路由的路径前缀,值全为null,这意味着只有存在路由的路径才会有别名,路径前缀来自:

\Drupal::state()->get('router.path_roots');

在别名管理器中清理缓存时,将视情况重建白名单

 

当前路径服务:

服务:path.current

类:Drupal\Core\Path\CurrentPathStack

这是一个很简单的服务,储存当前请求的路径,在入站路径处理完成后,会将解析得到的内部路径注入该服务,所以使用该服务得到的路径通常是内部路径,如果有代码向其注入了NULL值作为路径或在入站路径处理前获取路径,那么将得到从请求对象返回的路径:$request->getPathInfo(),通常我们的代码不应该依赖于这里设置的路径,更好的选择是其他不影响系统发展的指示标志,如路由及其参数

 

 

补充:

1、在url对象的选项参数中$options['fragment']的值不用加“#”前缀,可通过$options['prefix']设置路径前缀,无路由的内部url可通过$options['script']设置脚本名,如:

 

        $options['absolute']=true;

        $options['script']="home.php/";

        $options['prefix']="file_prefix/";

        $options['query']=['a'=>1,'b'=>2];

        $options['fragment']='yunke';

        echo $url=\Drupal\Core\Url::fromUri("base:/sub/img.jpg",$options)->toString();

将输出:

http://www.dp.com/home.php/file_prefix/sub/img.jpg?a=1&b=2#yunke

 

2、有些路径处理并不在路径处理管理器中进行,比如跨域保护(路由中定义了_csrf_token),也就是在网址中加token验证,这是在路由处理管理器中进行的

 

3、如果一个处理器同时实现了入站和出站处理,那么在入站和出站上的优先级设置应该是反的,换句话说如果入站优先级设置的很高,那么出站优先级就应该设置的很低,这样才能保证整体处理上的顺序,就像我们从一楼到顶楼再回到一楼一样,上楼时首先穿过的是一楼(优先级最高),下楼时最后穿过的是一楼(优先级最低)

 

4、首页“/”不会有别名,其内部路径是通过配置系统设置,但在配置系统中可以设置为别名路径,指向首页的url不应该使用配置系统中指定的首页路径,而应该使用首页路由“<front>

 

5、如果希望建立一个主要面向中国用户的多语言站点,云客推荐以英文作为默认安装语言进行系统安装,安装后添加中文语言,并将中文设置为站点默认语言,语言协商采用路径前缀的方式,中文的前缀留空,其他语言采用语言代码

 

6、在“language-url”语言协商方法设置中,如果是域名方式,不要在域名前加协议,协议的指定应该在url对象的选项中,也不要使用“/”后缀,也不用指定端口,端口会自动使用当前页面的端口

 

 

本书共94小节:

评论 (写第一个评论)