雲客Drupal源碼分析之系統出入站路徑處理

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_decode:1000
path_processor.image_styles:300
path_processor_language:300
path_processor_front:200
path_processor.files:200
path_processor_alias:100

默認提供的出站處理器及優先級如下(服務id:優先級):

path_processor_alias:300
path_processor_front:200
path_processor_language:100

入站處理:
入站的路徑處理時機是在系統派發請求事件時在路由系統中執行,路由系統訂閱了請求事件,路由是工作在內部路徑上的,在執行路由工作之前調用路徑處理器以將訪問路徑轉變爲內部路徑,具體是在得到路由集前,詳見服務: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.site的page.front鍵下,注意在設置首頁內部路徑時不要帶語言前綴

path_processor.files:
類:Drupal\system\PathProcessor\PathProcessorFiles
優先級:200
解析以'/system/files'開始的路徑

path_processor_alias:
類:Drupal\Core\PathProcessor\PathProcessorAlias
優先級:100
處理路徑別名,該服務內部使用別名管理器,該處理器同時涉及出入站處理,很重要,單獨講解,見後。

出站處理:
出站處理是指在產生頁面包含的所有超鏈接時進行路徑處理,以便用戶在站點各頁面之間連貫的導航點擊,出站處理後的站內路徑,正是入站要處理的路徑,在系統中所有用到超鏈接的地方都應該使用URL對象:
\Drupal\Core\Url
關於此詳見本系列的《Url和Link》主題,出站處理的時機正是在將url對象轉化爲字符串時,如果路徑不使用url對象,將不會進行出站處理,一些主題開發者很容易忽視該問題而將url硬編碼在模板中,這在一些情況下將導致問題,比如語言協商,對系統的擴展性也有限制,比如業務需要調整路由時還要額外處理模板中不規範的硬編碼,所以最佳實踐是在所有需要url的地方(包括模塊、主題等)使用url對象,關於如何在模板中使用url對象請見本系列《twig服務》主題。
在twig中渲染一個url對象時會調用其以下方法將其轉化爲一個字符串顯示:
\Drupal\Core\Url::toString ($collect_bubbleable_metadata = FALSE)
在該方法中將依據URL是否在系統中有路由而採用兩個服務來渲染轉化:
無路由採用服務:unrouted_url_assembler
有路由採用服務:url_generator

無路由的url處理:
服務id:unrouted_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處理:
服務id:url_generator
類:Drupal\Core\Render\MetadataBubblingUrlGenerator
獲取方法:\Drupal::urlGenerator();
在內部主要使用服務:url_generator.non_bubbling(Drupal\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),路徑處理器以引用方式接收選項參數,可以修改她以控制最終產生的url或link字符串值
$request:從請求堆棧中獲取的當前請求對象
$bubbleable_metadata:通常爲\Drupal\Core\GeneratedUrl對象,用以傳遞可冒泡渲染元數據

出站處理器按優先級依次執行,前一個處理後的路徑及選項參數等繼續傳遞給下一個
以下將介紹兩個重要的處理器,她們同時實現了入站和出站處理

語言路徑處理器:
服務id:path_processor_language
類:Drupal\language\HttpKernel\PathProcessorLanguage
參數服務:config.factory、language_manager、language_negotiator、current_user、language.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']的值應該是一個站點中存在的語言的語言對象,否則該方法不進行任何處理

別名路徑處理器:
服務id:path_processor_alias:
類:Drupal\Core\PathProcessor\PathProcessorAlias
這是一個很簡單的服務,在入站時直接依據路徑從路徑別名管理器中獲取內部路徑,出站時,如果選項參數被設置了$options['alias']且不爲空,說明url已經是別名,則什麼都不做直接返回,反之則從路徑別名管理器中依據內部路徑返回別名路徑,這就是我們可以爲節點自定義路徑的原因。

路徑別名管理器:
服務id:path.alias_manager
類:Drupal\Core\Path\AliasManager
參數服務: path.alias_storage、path.alias_whitelist、language_manager、cache.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(該表很簡單,只有四個字段:pid、source、alias、langcode)
方法介紹(以下方法涉及路徑查詢的都不區分大小寫):
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),該方法如果返回false或NULL說明別名表中沒有該源路徑,所以也就一定不存在對應的別名,如果返回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對象的選項中,也不要使用“/”後綴,也不用指定端口,端口會自動使用當前頁面的端口

 

我是雲客,【雲遊天下,做客四方】,聯繫方式見主頁,歡迎轉載,但須註明出處

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章