第三回:ルーティング

「Drupal8を理解する」ブログの三回目となります。前回のブログでは、Symfony とDrupal8の構成の比較について説明しました。今回は、Drupal8でのリクエスト処理がどのように実行されかについて説明します。まずブートストラップ処理を理解して、その次に全体的な処理の流れを理解します。最後にリクエスト処理を理解する前に学ぶ必要があるイベントサブスクライバについて説明します。

コントロールの流れ

Drupal8で行われるリクエスト処理の流れを次に説明します。

  1. ブートストラップを構成します。
    • 「settings.php」ファイルを読み込んで、他に必要な設定を動的に行って、それらをグローバル変数と「Drupal\Component\Utility\Settings singleton」オブジェクトの両方に格納します。
    • クラスをロードするために「class loader」を起動します。
    • Drupalのエラー処理を設定します。
    • Drupalがインストールされているかを確認します。インストールされていなければ、インストーラスクリプトへリダイレクトされます。
  2. Drupal・カーネルを作成します。
  3. サービスコンテナを(キャッシュや再構築によって)初期化します。
  4. Drupalの静的クラスにコンテナーを追加します。
  5. (Drupal 7と同じく)静的ページキャッシュからページを作成します。
  6. variable変数をロードします(variable_get)。
  7. 他に必要なインクルードファイルをロードします。
  8. ストリームラッパ(public://、private://、temp:// とカスタムラッパ)を登録します。
  9. (SymfonyのHttpFoundation コンポーネントを使って)HTTPリクエストオブジェクトを作成します。
  10. DrupalKernelによって処理を行い、レスポンスを返します。
  11. レスポンスを送信します。
  12. リクエスト処理を終了します。(モジュールはこの終了イベントに基づいて動作できます)

リクエスト処理の段階で、何が起こっているのかを理解するのはとても興味深いことです。これを理解した後は、イベントサブスクライバについて見てみましょう。

イベントサブスクライバ

前回コンパイラパスについて見てきました。タグ付きのサービスの特に重要な使用方法の一つは、「event_subscriber」のタグ付きサービスです。これらのサービスは、基本的なイベントリスナである「EventSubscriberInterface」を実装する必要があります。イベントサブスクライバには、イベントとメソッドをマッピングする「getSubscribedEvents」メソッドが存在し、メソッドの実行順序に優先度を設定することができます。Drupalコアでは、以下の一部のイベントで使われています。

kernel.request
最初のリクエスト割り当て時に利用されます
kernel.response
クエストに対するレスポンスが生成された際に一度利用されます
routing.route_dynamic
モジュールに追加のルートを登録する際に実行されます
routing.route_alter
ルート変更を可能にするルートコレクション上で実行されます。コアの中で、チェックを追加したりパラメータ変換したりする際に利用されます。

Drupalの開発者は、イベントサブスクライバについて知っておく必要があります。特に、「kernel.request」は「hook_init」の役割も担うため、非常に重要です。他にも「routing.route_dynamic」という重要なイベントがあります。Drupal 8(YAML内)の通常のルーティング構成は静的であるため、動的ルートを作成できるようにこのイベントが追加されました。以前は「hook_menu」内ですべて行われていましたが、現在では、一般的なメニュー項目を生成するためだけに使われています。例えば、ブロックモジュールは、「\Drupal\block\Routing\RouteSubscriber 」内の(per-theme) ブロック構成ページをメニュールートに登録する為に、「routing.route_dynamic」イベントを利用します。

イベントリスナを、なぜ単純にモジュールフックを利用して実装できないかと不思議に思うかもしれません。しかし、上記の方法は、利用可能なリスナーをキャッシュされたサービスコンテナの構成でコンパイルされるように、非常に効率的です。また、Drupalコアをオブジェクト指向として開発する試みでもあります。

リクエストからレスポンスへ

リクエストがDrupalに送信されたら、システムは自動で「DrupalKernel」が起動されます。「DrupalKernel」のhandleメソッドが実行され、Symfony2の「HttpKernel」を実行し、リクエストを処理します。

「HttpKernel」は「kernel.request」イベントを割り当てます。多くのサブスクライバは、以下の順番でこのイベントから実行されます。

AuhtenticationSubscriber
セッションをロードし、グローバルユーザを設定します。
LanguageRequestSubscriber
現在の言語を検知します。
PathSubscriber
URLをシステム・パスに変換します。(urlエイリアス等)
LegacyRequestSubscriber
カスタム・テーマを設定可能にし、初期化します。
MaintenanceModeSubscriber
メンテナンスモードの場合メンテナンスページを表示します。
RouteListener
ロード済みのルートオブジェクト全体を取得します。
AccessSubscriber
クライアントがルートオブジェクトのアクセス権があるかを確認します。

「RouterListener」にてルーティング作業が行われ、「router」サービスから有効なルートプロパティを取得します。Drupalは、Symfonyとは違った、Symfony CMF 拡張によって提供されたルーターサービス(DynamicRouter)を使用します。Symfonyの通常のルーターとの主な違いは、「DynamicRouter」が次章で説明するエンハンサーに対応していることです。「DynamicRouter」は、有効なルート(Drupal 7のcurrent_pathと同様)を探すタスクを「NestedMatcher」に委ね、「NestedMatcher」 は、それを「RouteProvider」に委ねます。「RouteProvider」は、CMS内に存在している全ルートのキャッシュリストを含む「router」テーブルから適合するルートのコレクションを探します。テーブルには、後述するroute builderが含まれています。選択されたルートをパスの長さの順に並べられ、一番長いパスは最も重要なパスとします。その後、「NestedMatcher」は、「UrlMatcher」に特定のルートを有効なルートとして選択するように命じます。実際に、「UrlMatcher」は、ルートによって要求された正しいメソッド(get/post)とスキーム(http/https)を持つ最初の(そして一番長い)ルートをリストより選択します。詳しくはルートの必要条件で後述します。実際に、「UrlMatcher」はパスの重要度によって制限されるため一致するパスは一つのみとなります。最終結果として「node.add_page 」のように有効なルートのIDを取得します。

練習では実行することはがないかもしれませんが、ルート・フィルターについても知っておくことは有効です。タグ名が「route_filter」として追加可能な、これらのフィルターは、「NestedMatcher」によって呼び出され、「RouteProvider 」から返された後にルートコレクションを直接フィルタリングします。Drupalコアは、「_format」ルートの必要条件が特別に指定されている時だけ、リクエストHTTPヘッダー(MimeTypeMatcher)でリクエストされたMIMEタイプ の中で反応しないルートをフィルタリングするためだけに、この仕組みを使います。

有効なルートを探した後に「DynamicRouter 」は、ルートエンハンサー(route enhancer)を呼び出すことによって継続します。これらは、ルートプロパティをまとめ、ルートパラメータを変換します。この処理は、次のセクションで説明します。そして、このルートプロパティは、リクエストオブジェクトのプロパティを設定する「RouterListener」に返します。その後、アクセスチェック(AccessSubscriber)が行われ、最後に正しい引数でコントローラメソッドを呼び出します。コントローラメソッドが返したレスポンスをクライアントに送信します。

リクエスト処理について以上です。これからルートについて見てみましょう。

ルート

Drupal 7では、「hook_menu」は、page callbacksを登録するためにタイトル、引数やアクセス要件と一緒に使用されていました。Drupal 8では、Symfonyの柔軟だが複雑なルーティング処理を採用しています。

Drupal 8では、page callbacks は関数ではなくなりました。その代わりにcontroller クラス内のメソッドとして使われます。 使用可能なルートはモジュールフォルダ内の「{module}.routing.yml 」で呼ばれたファイルに構成されます。例えば、ユーザのログアウトページのルート構成は以下のようになります。

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

注意:各ルートにはid (user.logout)とパスがあります。パスはいつパラメータが含まれるかを規定しますが、これについては後ほど説明します。「defaults」セクションは重要で、リクエストとパスが一致した時に何が実行に必要なかを判断するために利用されます。「requirements」セクションは、リクエストが処理されるかの判断を行い、SymfonyはDrupal7の「access arguments」や「access callback」に代わる「access check」に関連する情報を含みます

使用可能な「defaults」キーを次に示します。

_controller
特定のルートパラメータを含む特定のメソッドを呼び出し、レスポンスを返します。
_content
指定された場合、「_controller」がリクエストの「mime」タイプによって初期設定し、特定メソッドの結果を(文字列や配列を使って)レスポンスコンテンツに設定します。
_form
指定された場合、「_controller」が特定フォームでレスポンスする「HtmlFormController::content」を設定します。このフォームは、通常FormBaseを拡張したFormInterfaceを実装する完全修飾クラス名(やサービスID)でなければなりません。また、フォームの構築もオブジェクト指向となっています!
_entity_form
指定された場合、「_controller」は特定のエンティティフォーム({entity_type}.{add|edit|delete}として指定される)にレスポンスを返す「HtmlEntityFormController::content」を設定します。
_entity_list
指定された場合、「_controller」はEntityListController::listing」と、エンティティタイプのリストコントローラに基づくエンティティリストを表示する「HtmlFormController::content」のために「_content」を設定します。
_entity_view
指定された場合、「_controller」は「HtmlFormController::content」と、エンティティタイプのビューコントローラに基づくエンティティを表示する「EntityViewController::view」のために「_content」を設定します。
_title
ページのタイトル(文字列)です。
_title_callback
ページのタイトル(callback メソッド)です。

ご存知のように、ルートキーは、ルーティングプロセス中に他のルートキーに基づいて変更されます。これは、「_controller」、「_content」や他のキーを設定せずにエンティティを出力することが簡単になります。(自動的に実行されます)この機能は「route_enhancer」タグ付けされたサービスである「route enhancers」を使って実装されます。「route enhancers」は、「route_enhancer」タグ付けされたサービスで、「RouteEnhancerInterface」を実装します。Drupalコアでは、ほんの一部重要なエンハンサーがあります。もし、あなたがそれらの詳細について知りたいのであれば、ContentControllerEnhancer、FormEnhancerとEntityRouteEnhancerを参照してください。おそらくカスタムエンハンサーを追加する必要は通常ないと思われますが。

使用可能な「requirements」キー:

_permission
既存ユーザは規定の権限を持たなければなりません。
_role
既存ユーザは規定のロールを持たなければなりません。
_method
HTTPメソッド(GET、POST等)を制限します。
_scheme
https またはhttpに設定します。リクエストスキーマは規定のスキームと一致しなければなりません。このプロパティは、ルーティングではなくURL(Drupal::url(..))が生成される際にも適応されます。設定がされた場合、URLはスキームを固定します。
_node_add_access
いくつかのノードタイプから新規ノードを追加するためのカスタムアクセスチェックです。
_entity_access
エンティティ用の一般的なアクセスチェッカーです。
_format
Mime タイプのフォーマットです。

上記のリストのほとんど(全てではない)の要件は、アクセス・チェッカー

アクセス確認はkernel.requestイベントにサブスクライブをするAccessManagerによって行われます。そのAccessManagerは、access_checkタグを用いてサービスとして登録される前に、AccessInterfaceで実装されたクラスのアクセスチェックを起動します。アクセスチェックは、クライアントが特定(そして有効な)ルートのアクセスを行った際に確認を行うアクセスメソッドを持ちます。もし、アクセスが拒否された場合は、「access denied」ページを表示して例外を発生させます。

パスのパラメータ

実際、かなりの頻度で、ページのcallbacksのためにパラメータ(別名プレースホルダ)が必要となります。以下のサンプルおけるルート定義では、パスに「node」パラメータが含まれています。 ルートパス内のaccoladesによってパラメータが認識されます。コントローラメソッドがこれらのパラメータを引数として受け取ります。そのパラメータは同じ名前で引数とマッピングされます。以下の場合、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'

引数として渡される値はURL(文字列)のデフォルト値ですが、たまにパラメータ・コンバーターを使って変換されます。上記の例の場合、NodeControllerにnode idではなく、ロード済みのノードエンティティが返されます。パラメータの変換は、kernel.request イベントにサブスクライブしているParamConverterManagerによって実行されます。このイベントは、 ParamConverterInterface の実装(paramconverterタグのサービス)が登録されることを含みます。リクエストを処理する際に(エンハンサーである)ParamConverterManager は有効なルートのパラメータを取得し、パラメータ変換のconvert メソッドを呼び出します。可能であれば、「string」パラメータをフルオブジェクトに変換します。

注意:コントローラメソッドの引数に型設定を明示してない場合、未変換パラメータが渡されます。

EntityConverter」は、Drupalコアの唯一のパラメータコンバータで、幅広く使用されます。 もしエンティティタイプ(「node」、「user」など)と一致するパラメータ名があれば、自動的にフルエンティティオブジェクトに変換され、結果の値をエンティティIDとしてみなします。変換に失敗した場合(エンティティが存在しない場合)、自動的に404 が設定されます。

ルート構築

ルーティングは、明白ですがリクエストの処理中に行われます。しかし、過去にビルドした「router」テーブルを使用します。RouteBuilder サービスは、必要な時(例えばインスタンスのキャッシュクリーン時)にテーブルを埋め直すことを担います。リクエストの処理中に、このテーブルは有効なリクエストを見つけるために使われ、そして、完全になります。この処理の分離はパフォーマンスの向上のために行われます。RouteBuilderは、全ての構成(静的)ルート (YMLファイル後述)を収集したり、それらを含むコレクションを作成したりすることで動作します。また、イベントサブスクライバが追加ルート(上記の例を参照)を登録可能な「route.route_dynamic」イベントも呼び出します。その後、「routing.route_alter」イベントがルートのために呼び出されます。このイベントには複数のサブスクライバがあります。いくつかの特に重要な操作は、アクセス確認とパラメータ変換に関連しています。

上記では、アクセスチェックについて説明しました。アクセスチェックは、リクエストの処理時にクライアントが特定のルートにアクセス可能かどうかを確認します。実際には、すべてのアクセスチェックは、すべてのルートで確認されません。むしろ、ルートを構築する際に、アクセスチェッカーが適応する各ルートを決定します。これは、「router.route_alter」イベントを実行可能なAccessManagerによって行われます。アクセスチェックには静的なものと動的なものの2種類があります。静的なアクセスチェッカーである「StaticAccessCheckInterface」には「methodappliesTo」メソッドがあり、適切な必要キーの配列を返します。 動的なアクセスチェッカーは、「applies」メソッドによって各ルートを確認します。「AccessManager」は、「access_checks」オプションをrouterテーブルのルートに追加します。この情報は、リクエストの処理時に有効なルートのアクセスを確認する際に使用されます。

注意:各ルートに少なくても一つのアクセスチェッカーを適用しなければなりません。アクセスチェッカーを適用しないと、該当ルートにアクセスすることができません。p>

また、パラメータ変換についても説明しました。パラメータはパラメータコンバータによって変換されます。各パラメータに対して一つ(または無し)の適切なパラメータコンバータがあります。アクセスチェックと同じく、「ParamConverterManager」サービスがルートを構築する際に適切なパラメータコンバータを検索します。このサービスは既存のパラメータコンバータのメソッドを適応するすべてのルートパラメータをチェックし、ルーターテーブルのルートパラメータ定義にオプション‘Parameter’を追加します。この情報はリクエストの処理時に有効なルートのパラメータを変換する際に使用されます。

まとめ

今回のブログで、リクエスト処理について学びました。Drupal 8でのルーティングの理解、そして、それがどのように動いているのかついても理解できたと思います。実際に、リクエストからレスポンスまで何が起こっているかを理解できたと思います。しかしながら、Drupal 8にあなたが知っておくべきである面白いコンセプトがまだまだあります。来週の最終版にてそれらを紹介します。

翻訳元URL:https://cipix.nl/understanding-drupal-8-part-3-routing