您在這裡

了解Drupal 8(Understanding Drupal 8)

landylan's 的頭像
landylan 在 2016-04-19 (二) 00:32 發表

想在Drupal 8上寫點東西,才發現這次的變化還真大呀!

沒辦法,只好花時間研究一下。。。永遠追不完的更新,技術人超可憐的。。。哎。

找到這篇文章,順手翻譯了一下。

原文出處:https://cipix.nl/understanding-drupal-8-part-1-general-structure-framework

我發現還有簡體中文的翻譯,雖然有點幫助,但習慣用語不同,看得有點辛苦。。。

簡中翻譯:http://drupalchina.cn/node/6082

我的翻譯過程中,有些地方也不是非常確定。

如果有翻得不好之處,還請不吝指教囉。^_^

=======================================

了解Drupal 8

第一部分:Drupal 8的結構

Drupal 8來了。而且這次是極大的變革!Drupal 6 到Drupal7 只能算是一次進化,但Drupal 8則是採用了完全不同的軟體架構,連寫程式的方法也完全不同了!我相信這些改變是必要的,因為這可以讓CMS變得更專業、更現代化。我相信偏向物件導向的做法,對開發者更具吸引力,也更能改進整體的軟體品質。Drupal核心開發者為此所付出的勇氣與努力,我深懷感激!

不過,對於現有(還有新的)Web開發者來說,要學好它將是一項巨大的挑戰。希望你閱讀本文之後,能取得一些優勢!在一頭埋進Drupal 8模組的開發之前,可以把本文做為一個出發點。我是在經過幾天廣泛研究Symfony2和Drupal 8之後才寫下這篇文章的;過程中我按部就班進行debug,而且讀了許多程式碼和線上文章。我算是一名很有經驗的Drupal 7開發者,但我完全不懂Symfony,(目前)也還沒為Drupal 8核心程式碼貢獻過任何力量。我相信分享我這幾天所學到的東西,應該可以讓各位更了解Drupal 8。

在接下來的四個禮拜,本文將分成四個部分逐一發佈。第一部分我們打算看一下Drupal 8 框架(framework)大體上的的結構,尤其是跟Symfony2組件有關的部分。我們會看到Drupal用到了哪些Symfony2組件,還有Drupal是怎麼使用這些組件的。下一部分,我們會仔細查看「服務容器」(service container)這個非常重要的部分。而在第三部分中,我們則會仔細查看Drupal 8中bootstrapping(起始引導)與routing(路徑分發)的做法。你將會學到Drupal處理請求的做法。而在最後的第四部分中,將會介紹Drupal 8中一些令人興奮、必須了解的全新功能。

Symfony2

Symfony2框架的設計,主要是希望能讓開發者建立自訂web應用程序。Symfony本身並不是CMS,無法用於網站的管理。其程式碼是以建立應用程序為出發點來撰寫的。Symfony提供了一種高效達成該目標的做法。理論上來說,整個CMS可以建立在Symfony的框架之上。但實務上來說,Symfony的預設做法對Drupal來說不夠靈活。

因此,Drupal 8選擇只採用Symfony(優秀的)核心層,並對它進行擴展,以支援Drupal的模組功能。這造就了一個相當平衡的系統,其中使用了Symfony核心優良的部分(未作修改),同時延伸出一個非常靈活而有彈性的CMS層。兩個世界截長補短,各取其最好的部分,就這樣結合了起來!

組件(Components)

Symfony框架是由好幾個組件所構成。其中有些對系統來說非常重要,例如Httpfoundation組件,能讓系統理解HTTP,並提供一個很棒的請求與回應物件,供其它組件使用。其它有些只是輔助組件,譬如Validator組件,可用來驗證資料(檢查是不是有效的email、網址、電話號碼之類的)。系統的核心部分,稱之為Kernel(內核)組件。Kernel基本上就是一個「主類別」(main class),負責管理環境(服務與bundle包)以及http請求的相關處理。開發者可以透過擴展Kernal的方式來擴展系統,譬如AppKernel就擴展了Kernel,並在其中添加了自己的bundle包。這些bundle包可用來建立一些具有特定功能的程式碼,這點跟Drupal的模組是很相像的。

Drupal只採用Symfony其中的一些組件,從下圖就可以看得出來。


Drupal 8是如何對Symfony2進行擴展的呢?

Drupal並沒有對Kernel進行擴展(但一般Symfony2網路app都是這麼做的)。它還是沿用原Kernel的功能,但重新製作了Kernel的界面。因為Drupal並沒有採用Symfony中bundle包的做法。bundle包是建立網路應用程序一種很好的做法,但對於CMS來說,它不夠靈活,擴充性也不夠好。DrupalKernel載入環境(也就是可用的服務和模組)時,做法和Kernel有點不同(但很類似),而它在處理網路請求時,做法則與Symfony的Kernel一樣,都是委由HttpKernel組件來進行處理。

此外,Drupal 8也帶入了它自己(及第三方)的組件與核心程式碼。整個概況的圖解,就顯示在下面的圖片中。


結論

在此為「了解Drupal 8」這篇文章的第一部分做個結論。下個禮拜,我們將進一步說明服務容器(Service Container)——它可說是整個Drupal系統的中心脊柱。在學習其它組件之前,你一定要先對它有所了解。 

第二部分:服務容器(Service Container)

這是「了解Drupal 8」這篇文章的第二部分。在第一部分,我們已大體上看過Drupal 8的結構,還有它與Symfony之間的關係。Symfony是由一些組件所構成(Drupal 8也是)。在本文中你將學習到服務容器(service container)的概念,還有Drupal 8是怎麼運用這個概念的。在學習routing(路徑分發)之前,一定要先了解這個重要的概念。

Symfony使用了一個服務容器,高效管理應用程序中的所有服務。這個觀念也稱為Dependency Injection(相依性注入)。

服務容器是一個global廣域物件,它包含在Kernel之中,而在處理請求之前,它就會被先建立起來。稍後在程式碼中,我們還會用它來取得各種服務,或是在運行中進行所謂的lazy-loaded。所謂服務指的是一些廣域物件,可用於完成某些特定的工作,例如Mailer服務,或是資料庫連接等等。每個服務都會對應到一個Class類別。服務容器之所以非常重要,是因為它裡頭裝了所有可用的服務,很清楚這些服務之間的關係與相關的組態設定,甚至在創建服務時,也必須要用到服務容器!

相依關係與參數

有些服務可能會依賴於其他的服務。在Symfony的文件中舉了個例子:NewsletterManager服務就需要用到Mailer服務,來幫它發送email。這些相依的關係,全都是由服務容器來進行管理的。在建立服務時,Class類別的constructor(構建函式)就會透過參數提供所需的相依關係。這裡運用了Interface界面,來定義被依賴的服務需要提供哪些方法,這麼一來如果有需要的話,在實作時即使切換不同的服務,也不會有什麼問題。

組態設定

服務容器中的各種服務,都可以透過好幾種方式來進行組態設定:可透過(擴展)程式碼、XML、YAML等等。這些做法在Symfony中都有用到,但大部分核心服務的組態設定,都是採用框架Bundle包程式碼的做法。Drupal的服務組態設定方式則有所不同,主要是採用了YAML檔案的做法。所有與核心相關的東西,都包含在services.core.yml這個檔案之中。不論模組還是ServiceProvider類別,都可以對這種組態設定進行擴展。所謂的ServiceProvider類別,跟Symfony的Extension擴展很類似。所有這些組態設定的載入,都是由Kernel來負責處理的。

YAML提供了一種可讀性佳、相當具有彈性的語法。舉個Drupal 8的例子如下(取自services.core.yml):


services:
  ...
  router_listener:
   class: Symfony\Component\HttpKernel\EventListener\RouterListener
   tags:
    - { name: event_subscriber }
   arguments: ['@router']
   ...
  router:
   class: Symfony\Cmf\Component\Routing\ChainRouter
   calls:
    - [setContext, ['@router.request_context']]
    - [add, ['@router.dynamic']]
   ... 

這裡可以看到router_listener服務相關的定義。首先列出相應Class類別,以便能夠順利載入。arguments屬性定義了RouterListener構建函式(constructor)的第一個參數(url或request matcher),其值為'@router',意思就是服務ID為router的服務。router服務在這個組態設定中也有定義,而且與Symfony原本所用的Class類別不同(這點我們會再說明)。

Tagged(已標記)服務

tags的用途,是用來載入某些「已標記」的服務。在實務上,Drupal有時會以這種方式達到hook的效果。在下面的例子中(node.services.yml),node(節點)模組為「添加節點」頁面添加了一個access_check檢查,它會在routing(路徑分發)之後,判斷相應的存取權限:


services:
  ...
  access_check.node.add:
   class: Drupal\node\Access\NodeAddAccessCheck
   arguments: ['@plugin.manager.entity']
   tags:
    - { name: access_check }
   ... 

Compiler passes(編譯階段)

Drupal是怎麼解讀這些tags,如何判斷該做出哪些相應的動作呢?呃。。。Symfony還有另一種動態設定服務容器的做法,叫做compiler passes(編譯階段)。實際上服務容器根據靜態的組態設定build之後,就會直接進行編譯。在這個階段,系統允許CompilerPassInterface這個Class類別所實作出來的物件,可以在好幾個地方對組態設定進行修改。在Drupal中,CoreServiceProvider註冊了一些重要的compiler passes,例如RegisterAccessChecksPass,它會找出所有已標記access_check的服務(如前面例子),並把它加到AccessManager中(使用服務容器的addMethodCall指令)。在routing路徑分發階段,AccessManager服務就會要求檢查相關的權限(其中就包含NodeAddAccessCheck的檢查)。Drupal核心中還有好幾個其他的compiler passes,譬如把route參數轉換成物件、翻譯字串等等。在實務上有時你必須在你的模組中使用這些標記服務,以便加入自訂的權限檢查,或是轉換路徑參數!

Swapping(交換)服務

服務容器讓服務的組態設定變得更有彈性。Drupal 8使用了服務參數,來改變Symfony的工作方式,以達到它自己的目的。例如,Drupal需要一個與Symfony不同的網址routing(路徑分發)機制。Drupal提供了另一個router服務(Symfony\Cmf\Component\Routing\ChainRouter),取代了Symfony原本使用的那個(Symfony\Bundle\FrameworkBundle\Routing\Router)。這麼一來,就完全不必改動router_listener服務(Symfony\Component\HttpKernel\EventListener\RouterListener)裡的任何一行程式碼了!這之所以成為可能,是因為這兩個router都是從RequestMatcherInterface類別實作出來的,而這個Class類別正是RouterListener第一個參數所要求的router。

結論

在這個部分,我們學習了Drupal 8的服務容器組件。現在你應該比較清楚,Drupal 8是如何用它自己的服務,取代Symfony2原有的服務,而不必改動到任何一行原有的程式碼。這麼一來Symfony2更新對Drupal核心維護者來說,就會變得非常容易。在學習Drupal 8時,你經常要去查看一下core.services.yml這個檔案,以確認用到了哪些服務。

下個禮拜,我們會仔細查看Drupal 8的一般控制流程,還有routing路徑分發的做法。 

第三部分:Routing(路徑分發)

這是「了解Drupal 8」這篇文章的第三部分。在前面的部分內容中,我們學到了Drupal 8與Symfony之間在結構上的一些不同之處。此時來了解一下Drupal 8如何處理請求,應該是個不錯的主意。首先,我們會先觀察bootstrapping(起始引導)階段,以及一般的控制流程。接著,我們會學習event subscriber(事件訂閱者)的概念;在我們學習請求處理之前,這是一個很重要必須要懂的概念。

控制流程

現在我們就來看看,當一個請求進入到Drupal 8時,會發生什麼事:

  1. Bootstrap(起始引導)組態設定:
    • 讀取settings.php檔案,動態生成一些其他的設定,然後將這些設定值同時存到廣域變數和Drupal\Component\Utility\Settings物件中。
    • 啟動Class類別載入程序;它負責載入Class類別的工作。
    • 設定Drupal錯誤處理程序。
    • 偵測是否已確實安裝Drupal系統。如果不是的話,就轉址到安裝script腳本。
  2. 建立DrupalKernel。
  3. 初始化服務容器(有可能來自快取,或者是重新建立)。
  4. 將容器添加到Drupal的靜態Class類別中。
  5. 嘗試從靜態頁面快取中提供相應的頁面(這個做法與Drupal 7很類似)。
  6. 載入所有變數(variable_get)。
  7. 載入其它(程序上)所需的include包含檔案。
  8. 註冊stream wrappers(public://、private://、temp://、自訂wrappers)。
  9. 創建HTTP請求物件(使用Symfony的HttpFoundation組件)。
  10. 讓DrupalKernel來處理它,並取得回應。
  11. 發送回應。
  12. 終結請求(模組可針對此事件做出一些動作)。

現在我們就來看看其中有趣的部分:在請求處理階段,究竟發生了什麼事?為了更清楚理解,你必須先了解什麼是event subscriber(事件訂閱者)。

Event subscriber(事件訂閱者)

之前我們曾看過什麼叫做compiler passes(編譯階段)。其中有個特別重要的已標記服務,就叫做event_subscriber。這些服務都是從EventSubscriberInterface類別實作出來的,基本上全都具有事件監聽的能力。它們透過getSubscribedEvents方法,即可定義事件與方法的對應關係。而透過priority優先次序的設定,則可定義每個動作要依照什麼樣的順序來執行。在Drupal核心中,只會用到以下這幾個事件:

  • kernel.request
    發生在請求一開始被發送出來的時候。
  • kernel.response
    發生在回覆請求的回應剛被創建之時。
  • routing.route_dynamic
    被觸發時可以讓模組註冊額外的routes(路徑)。
  • routing.route_alter
    在收集route(路徑)時被觸發,讓route(路徑)有機會進行改動。核心可利用這種方式來添加一些檢查或進行參數轉換。

每個Drupal開發者都應該了解event subscriber(事件訂閱者)。其中kernel.request尤其重要,因為它基本上取代了hook_init的地位。另一個重要的事件是routing.route_dynamic事件。因為在Drupal 8中,一般的routing(路徑分發)組態設定(利用YAML的方式)是靜態的,而這個事件則可用來創建動態的route(路徑)。之前這些工作都是由hook_menu來達成,但這個hook現在只被用於生成選單項目。舉例來說,block模組就使用了routing.route_dynamic事件,在\Drupal\block\Routing\RouteSubscriber裡註冊區塊設定頁面(尚未套入主題)的選單路徑。

你可能覺得奇怪,為什麼不用模組hook來實作這些事件監聽的功能?原因是這種做法比較有效率,因為可用的listener全都會被編譯到服務容器的組態設定中,而它是會被快取的!再者,這種做法也可以讓Drupal核心盡可能偏向物件導向的設計。

從請求到回應

當Drupal收到一個請求時,系統就會進入bootstrap(起始引導)程序,然後DrupalKernel就會開始啟動。系統會呼叫DrupalKernel的相應處理方法,該方法則會呼叫(Symfony2的)HttpKernel,進一步處理該請求。

此時HttpKernel會觸發「kernel.request」事件。有好幾個訂閱者,會以下列順序監聽該事件:

  • AuhtenticationSubscriber
    載入session,設置global user。
  • LanguageRequestSubscriber
    偵測目前所使用的語言。
  • PathSubscriber
    將網址轉換成系統路徑(網址別名等)。
  • LegacyRequestSubscriber
    允許設置一個自訂主題,並予以初始化。
  • MaintenanceModeSubscriber
    網站若處於維護模式,就顯示維護頁面。
  • RouteListener
    取得一個完整載入的router物件。
  • AccessSubscriber
    檢查客戶端是否有權限存取該router物件。

routing相關工作就是在RouterListener中執行的。它會詢問router服務,取得當前正在使用中的route(路徑)屬性。Drupal所使用的router服務(DynamicRouter)與Symfony不同,它是由Symfony CMF(http://cmf.symfony.com/)所延伸出來的版本。和Symfony所提供的一般router主要不同之處在於,DynamicRouter支援enhancer(強化器,下一節詳述)。接著DynamicRouter會將「找出目前使用中的路徑」這項工作(也就是Drupal 7中current_path所做的工作)交給NestedMatcher,而它又會把工作交給RouteProvider。RouteProvider會在router表(包含CMS中全部現有路徑的一個快取列表)中找出一堆相符的路徑。這個表是在「路徑建立」階段建立起來的(稍後就會說明)。所選出來的路徑會依照路徑長度排序——路徑長度越長,就被認為越重要。接著,NestedMatcher會要求UrlMatcher從那一堆路徑中選出某個特定路徑,以做為真正要使用的路徑。在實務上,UrlMatcher會根據路徑所要求的方法(get/post)和協定(http/https),從一堆路徑中選出第一個(也就是最長的那個)符合的路徑。通常相符的路徑只會有一個,所以UrlMatcher實際上並不是那麼重要。最後的結果,就會得出使用中路徑的ID(例如node.add_page)。

我覺得應該還要再提一下路徑filter(篩選器),即使你在實務上沒什麼機會碰上它。這些路徑filter可以利用route_filter這個標籤,做為服務添加到系統中,然後在RouteProvider送回許多路徑之後,NestedMatcher就會呼叫它們,直接對路徑進行篩選。Drupal核心只有在請求HTTP header(MimeTypeMatcher)時,才會利用這個機制來篩選掉不以MIME type回應的路徑,不過這也只有在_format路徑要求明確指定時,才會出現這種情況!

找到使用中的路徑之後,DynamicRouter接下來會呼叫路徑enhancer(強化器)。它會把路徑的屬性確定下來,並轉換路徑參數。下一節就會說明這個程序。然後,找到的router屬性會被送回去給RouterListener,它會被用來設定請求物件裡的屬性值。隨後會進行權限的檢查(AccessSubscriber),最後再以正確的參數,呼叫controller(控制器)方法。而controller方法所送出來的回應,就會被送回去給客戶端。

現在我們已經了解整個請求處理的程序,可以來仔細看看「route(路徑)」這個東西了。

Route(路徑)

在Drupal 7中,我們使用hook_menu來註冊頁面回呼函式(callback)和相應的標題、參數、存取權限。在Drupal 8中,則是採用Symfony 的routing程序,更顯其靈活與彈性...不過也變得更複雜了!

在Drupal 8中,頁面回呼函式已經不再是函式。現在它們變成了controller類別中的方法。可用的路徑現在是在module目錄下一個叫做{module}.routing.yml的檔案中進行設置。舉例來說,使用者登出頁面的路徑設定如下:


user.logout:
  path: '/user/logout'
  defaults:
   _controller: '\Drupal\user\Controller\UserController::logout'
  requirements:
   _user_is_logged_in: 'TRUE' 
 

注意,每個路徑都有個ID(user.logout)和一個path值。path定義中有可能會包含有參數,不過這點我們稍後再談。defaults這一段落非常重要,它可用來控制相應請求符合此路徑時要做出什麼樣的動作。requirements這個段落則定義此請求是否需要進行處理。隨後的訊息通常與權限檢查有關,它就相當於Drupal 7中的「access arguments」和「access callback」,只不過這裡採用的是Symfony的做法。

defaults這個段落中可使用的鍵值如下:

  • _controller
    這裡所指定的方法,會搭配指定路徑參數進行呼叫,而且預期應該會送回一個回應。
  • _content
    如果有指定此項目的話,_controller就會根據請求的mime type進行設定,而回應的內容就會填入所指定方法給出的結果(通常是一個字串或render陣列)(譯註:這個鍵值現已併入_controller了。參見https://www.drupal.org/node/2376791)。
  • _form
    如果有指定此項目的話,_controller就會被設定為HtmlFormController::content,它會以指定的表單做為回應。此表單必須是從FormInterface實作出來的一個完全合格的Class類別名稱(或服務ID),通常它是從FormBase擴展出來的。沒錯,表單的建立也物件導向化了!
  • _entity_form
    如果有指定此項目的話,_controller就會被設定為HtmlEntityFormController::content,它會以指定的entity(實體)表單作為回應(指定的方式是{entity_type}.{add|edit|delete})。
  • _entity_list
    如果有指定此項目的話,_controller就會被設定為HtmlFormController::content,_content則會被指定為EntityListController::listing,它會根據entity type(實體類型)的列表controller,渲染出一個entity列表。
  • _entity_view
    如果有指定此項目話,_controller就會被設定為HtmlFormController::content,_content則會被指定為EntityViewController::view,它會根據entity type的view controller來渲染該entity。
  • _title
    頁面標題(字串)。
  • _title_callback
    頁面標題(方法回呼函式)。

如你所見,路徑鍵值在routing路徑分發程序中會隨著其它路徑鍵值而改變。如果不設定_controller、_content和其它鍵值,就會簡單輸出一個entity的內容;整個過程會被自動完成。這個功能是利用許多路徑enhancer(標記為route_enhancer的服務)來實現的。而這些路徑enhancer全都是從RouteEnhancerInterface實作出來的。在Drupal核心中,有一堆重要的enhancer。如果你想看看它們,可以看一下ContentControllerEnhancer、FormEnhancer和EntityRouteEnhancer。你大概沒什麼必要再去增加你自己自訂的enhancer了。

requirements這個段落中可使用的鍵值如下:

  • _permission
    目前使用者必須擁有指定的權限。
  • _role
    目前使用者必須擁有指定的角色。
  • _method
    允許使用的HTTP方法(GET、POST等)。
  • _scheme
    可設定為https或http。請求所採用的協定必須與這裡所指定的相同。除了routing路徑分發之外,在生成網址(Drupal::url(..))時,也必須考慮這個屬性。如果有設定的話,網址就必須固定遵守這裡的設定。
  • _node_add_access
    添加某種節點類型的新節點時,可在此設定自訂權限檢查。
  • _entity_access
    entity所使用的一般權限檢查函式。
  • _format
    Mime type格式。

上面所列大部分(但非全部)的requirements,在我們馬上要說明的權限checker(檢查器)中都會進行檢查。在實務上你會發現,至少需要建立一個自訂的權限checker,因為Drupal 7的access callback已經不能用了。以requirement中_node_add_access這個鍵值為例,node模組所添加的一個自訂權限檢查NodeAddAccessCheck就會監聽這個鍵值。

AccessManager會監聽kernel.request事件,執行權限檢查。他所調用的權限檢查,必須是從AccessInterface所實作出來的Class類別。我們之前就已經看過它們被註冊為服務,並且設定了access_check標籤。權限檢查會對應一個權限方法,用來檢查客戶端是否有權限存取指定(使用中的)路徑。如果沒有權限,就會出現例外狀況,顯示「使用者沒有權限」的頁面。

path參數

在實務上你經常需要在你的頁面回呼函式中用到參數(又叫佔位符,也就是url後面那串東西)。在下面所定義的範例路徑中,path包含了一個叫做node的參數。路徑中的參數,前後被一個大括號包了起來。每個路徑都會保存著幾個特定的選項。controller方法會收到這些參數,然後代入這些參數以執行動作。這些參數會以相同的名稱一一對應。所以在這個例子中,NodeController的頁面方法就會有一個叫做$node的參數。路徑中有可能會有好幾個參數,它們的名稱都應該是獨一無二的。


node.view:
  path: '/node/{node}'
  defaults:
   _content: '\Drupal\node\Controller\NodeController::page'
   _title_callback: '\Drupal\node\Controller\NodeController::pageTitle'
  requirements:
   _entity_access: 'node.view' 
 

被送進來當做參數的值,通常就是網址中參數的值(字串),但它也經常會先被參數converter(轉換器)進行一些轉換。在上面的例子中,NodeController得到的就不是節點的ID,而是一個完整載入的節點entity。參數轉換是由ParamConverterManager來執行的,它會監聽kernel.request事件。它還包含註冊的ParamConverterInterface實作(設有paramconverter標籤的服務)。在處理網路請求時,ParamConverterManager(它也是個enhancer)會檢查每個使用中路徑的參數,並呼叫參數converter中相應的轉換方法。如果可能的話,一串參數值就會被轉換成一個完整的物件。注意,如果controller方法的參數並未明確指定type,就會直接丟出未轉換的參數。

EntityConverter是Drupal核心中唯一的一個參數converter,不過其用途非常廣泛!如果參數名稱正好是entity type(node、user等等),就會自動轉換成一個完整的entity物件!相應的參數數值,會被視為是entity ID。如果轉換失敗(entity不存在),就會自動送出404結果。

你只要在defaults段落中,為參數指定一個值,就可以讓該參數變成是可有可無的參數(因為這麼一來,在網址中就不一定需要為其指定參數值了)。不過,這種做法只對那些不會進行轉換的參數是有用的,而且所有這些可有可無的參數,全都只能放在一般參數的後方。

路徑建立

Routing路徑分發顯然是在收到請求之後才完成的。但router表則是在之前就已經先建立好了。RouteBuilder服務負責在需要的時候(例如剛清除快取後)填寫route表的內容。收到請求之後,router表就會被用來找出使用中的路徑,然後完成相應的動作。這兩個動作之所以分開,主要是考慮到效能的理由。RouteBuilder主要是收集所有預先設定(靜態)的路徑(利用yml檔案,後述),然後用一個方式把它們保存起來。它也會觸發route.route_dynamic事件,這個事件的訂閱者有可能會註冊額外的路徑(參見之前block模組中的例子)。接著還會再觸發routing.route_alter事件。這個事件有好幾個訂閱者。這裡有一些特別重要的操作,跟權限檢查和參數轉換有關。

我們已經介紹過權限檢查。在處理請求時,它會檢查客戶端是否有權限存取特定的路徑。實際上,並不是每個路徑都需要對所有權限進行檢查。取而代之的做法是,每個路徑需要對應到哪些權限檢查,在路徑建立階段就會先被決定好了。這是由AccessManager所完成的,它會監聽router.route_alter事件。權限checker(檢查器)分為靜態與動態兩種。StaticAccessCheckInterface有個叫做appliesTo的方法,它會送回一個陣列,裡頭是它所用到的requirement鍵值。動態權限checker則是採用applies方法檢查每個路徑。AccessManager會添加一個_access_checks選項到router表的路徑中。在處理請求的過程中,實際上檢查使用中路徑的權限時,就會用到這個訊息。注意,每個路徑都至少要引用一個權限檢查,否則該路徑就永遠無法被存取到了!

我們之前也已經介紹過參數converter(轉換器)。參數可藉由一個參數converter來進行轉換。每個參數只會有一個(也可能沒有)相應的參數converter。跟權限檢查很像的是,要使用的參數converter,也是在路徑建立階段由ParamConverterManager確定下來的。這個服務會針對每個現有路徑參數,檢查所有現有參數converter的applies方法,然後在router表的路徑參數定義中,添加一個converter選項。在處理請求時,實際上轉換使用中路徑的參數時,就會用到這個訊息。

結論

在這個部分我們學習的是Drupal 8處理請求的做法。而且,現在你應該也已經了解在Drupal 8中routing路徑分發的處理方式,以及它的內部工作原理了!事實上,現在你已經知道從請求到回應之間,究竟發生哪些事了。不過,Drupal 8還是有一些有趣的概念,是你應該知道的。這些都將會在本文下一部分,也就是最後一部分中進行介紹,下週就會發佈出來。 

第四部分:Drupal 8的其它概念

前幾個禮拜我們已經學過Drupal 8的結構與內部處理請求的做法。不過我們並沒有學到關於Drupal 8是如何被構建出來的知識。在你一頭栽進特定核心模組的程式碼之前,Drupal 8有一些重要的、全新的、與過去不同的觀念,你最好先有所了解。我就在本文的最後這個部分,逐一進行說明。

組態設定

Drupal 8的組態設定系統,有了相當令人興奮的改變。最明顯的就是,現在組態設定都保存在許多YAML檔案中,並且放在/sites/{name}/files/config_{HASH}/active目錄中。從檔案名稱就可以看出該檔案設定的是什麼東西。舉例來說,node.settings.yml裡頭就包含了node模組的設定,而views.view.content.yml則包含機器名稱為content的view相應的完整定義!Drupal使用了前綴的方式來命名這些組態設置檔案。

你可能會覺得,這些組態設定檔案很沒有彈性。但在實務上,你可以用YAML語法來建立資訊的關聯陣列(associative arrays)。此外,Drupal提供了一個Config物件,它可以讓你在運行過程中改變組態設定檔案,而不需要手動去編輯那些檔案!當你透過Drupal UI創建一個新的field(欄位)、node type(節點類型)、block type(區塊類型)或一個新的view時,系統就會為你建立一個新的檔案。內容(節點)和資料還是保存在資料庫中,因為這樣你才能進行快速的查詢。

在模組中可以包含一個conf子目錄。啟用模組時,該目錄中所有的組態設定檔案,都會被複製到網站的組態設定目錄中。檔案放過去之後(快取也被清除),這些檔案就會自動被找到,而且會立刻被套用。如此一來,模組就可以提供額外的新節點類型、圖片樣式(image style)、views等,而不需要再用到Drupal 7中像是hook_node_info這類的hook了。

組態設定檔案很容易就可以被複製到其他位置。這有個好處,就是你現在終於可以大批量改變組態設定了!

組態設定檔案也有相應的模板(associated schema),詳細說明一個view、區塊、圖片樣式等的組態設定應該長什麼樣子。因此,組態設定的內容也就變得更容易驗證,也更容易進行翻譯了。

Entity(實體)

Drupal 7導入了entity(實體)的概念。entity實體指的是被生成的一群具有相同功能的物件。例如,我們可以為entity設定一些欄位(field)、設定判斷權限的方法,也可以在view中使用entity作為參考資料。entity的概念非常強大。在Drupal 8中,它更成為CMS的中心概念。不只內容資料(node節點、taxonomy term分類項目、user使用者)屬於entity,像view、action動作、menu選單、圖片樣式等等這些無欄位資料,也全都是從EntityClass類別擴展而來。而且像節點類型、vocabularies詞彙這些bundle包,現在也全都被視為entity了。這背後的原因是,雖然這些物件類型是無欄位的(non-fieldable),但把它們視為entity將可使生成更簡便,例如利用組態設定檔案所保存的資料來生成entity,或是根據entity名稱自動進行路徑參數轉換等等。

entity的屬性是由attributes來定義的。舉例來說,我們可以指定一個entity是不是fieldable(可設定欄位),或者它是否可作為其它entity的bundle包。一個entity可以允許(也可以不允許)保存在組態設定檔案中,這是由controller來決定的。如果controller是ConfigStorageController(或從ConfigStorageController擴展而來),就可以被保存在組態設定檔案中。雖然組態設定檔案有一些優點,不過這麼做的話,就無法在資料庫中查詢到該資料了。如果你想對這些資料進行搜索,你就必須載入所有的項目,然後用PHP的for loop進行搜索。如果你比較喜歡資料庫的儲存方式,例如節點和使用者的做法,那麼就可以使用FieldableDatabaseStorageController。節點、使用者、分類項目等資料就屬於這種類別。當然這麼一來的話,這些內容就無法透過組態設定檔案來進行轉移了。對我來說,目前我也還不是很清楚,有沒有一種controller能處理以組態設定儲存且可設定欄位的entity!

為了更了解entity屬性,我建議你去看看ImageStyle這個Class類別(參見裡頭的註解),它是一個純粹以組態設定為基礎的entity;(taxonomy)Term則是一個儲存在資料庫的可設欄位entity;而Vocabulary則是一個bundle entity。

Plugin插件

Plugin可視為是物件導向版本的hook。它們可被用來modularly include 自訂區塊、圖片效果等等。

有個最好的Plugin例子,就是抽象的BlockBase類別。如果要添加一個新的自訂區塊,就要對這個Class類別進行擴展;forum模組中的ActiveTopicsBlock就是這麼做的。其中有些方法可以(或必須)進行複寫,像是權限檢查、建立view、改變區塊表單等等。

Plugin manager(管理器)是用來定義Plugin類型的。我們要指定一個命名空間列表和一個子目錄,定義到哪裡可以找到Plugin。這麼一來的話,任何模組都可以添加Plugin。我們可以用註釋類別(annotation class)來識別Plugin。而且,隨著Plugin類型的不同,在注釋中必須指定有好幾種不同的設定屬性。

Drupal Class類別

Drupal 8核心程式碼大部分都已物件導向化了。不過,模組hook的程式碼大部分還是循序執行的。這是個問題,因為Symfony是完全物件導向的!在Symfony中,如果你需要建立某個依賴於其他服務的功能,你就要建立一個新的服務,並在服務容器中定義所需的相依關係。但如果你需要用到Drupal hook中一個特定的服務,你就無法採用這種機制,因為你一開始就不是採用Class類別!針對這種狀況,靜態Drupal Class類別就被創造出來了。它可以用Drupal::service('{service id}'),或是用像是Drupal::request()、Drupal::currentUser()、Drupal::entityManager()這些特定的服務accessor(存取器),來取得非物件導向程式碼中的服務。後面的做法有個好處,那就是在你的程式碼編輯器中,程式碼自動完成功能應該會運作得比較好。此外,這種做法還提供了一些方便的輔助函式,像是Drupal::url(),它也就相當於Drupal 7中的url()方法。

node.module中的node_access函數,就是靜態Drupal Class類別的一個例子:


function node_access($op, $node, $account = NULL, $langcode = NULL) {
  $access_controller = \Drupal::entityManager()->getAccessController('node');
  ...
  return $access_controller->access($node, $op, $langcode, $account); } 

結論

我們在本文中已經看過Drupal 8的結構,以及它與Symfony2結構之間的比較。我們也看到服務容器可以管理所有的服務,而且也可以進行一些設定。然後我們學習了Drupal Kernel routing路徑分發和處理請求的做法。最後我們介紹了Drupal 8一些重要的新觀念:Drupal Class類別、Plugin、組態設定等等。

我希望所有的這些訊息,可以幫助你探索學習Drupal 8。我希望你可以順利學習Drupal 8模組開發,並對這個偉大的專案做出貢獻。

祝好運!