Drupal 8のルーティングシステム（メニューシステム）の解説とサンプルコード

Drupal 8 に実装されたルーティングシステムは、Symfonyコンポーネントを拡張したものであり、Drupal 7まではルーティングを処理するときに```hook_menu()```を使っていましたが完全に置き換えられました。


![drupal8_ symfony.png](https://qiita-image-store.s3.amazonaws.com/0/39468/eef1e32b-833e-148e-49ed-5af56fa7fede.png)


今回は、新しいルーティングシステムを理解するための最低限の説明と、モジュール実装をする上でたびたび使用することになる、```RouteMatch```、```CurrentRouteMatch```、```Url```の3つのAPIについて、簡単なサンプルコードを紹介していきます。


#検証環境

* Macbook Pro (Retina, 15-inch, Mid 2014)
* Mac OS X 10.11.6 El Capitan
* PHPStorm 2017.3
* Vagrant 1.9.4
* CentOS 7.4.1708
* Drupal8.4.4


#ルーティング定義

ルーティング定義とは、そのURLパスにアクセスされたときにどのクラスを読み出して、そのクラスによってデータを返すかを決めるためのものです。
定義するには、まず、モジュール直下にルーティングファイルを作成します。

以下のYAMLファイル内では、次のようなルート定義を行います。

**例）my_module.routing.yml**

```yaml
my_module.mymenu:
  path: '/mymenu'
  defaults:
    _content: '\Drupal\my_module\Controller\ExampleController::mymenu'
    _title: 'My module example'
  requirements:
    _permission: 'access content'
```

1行目では、my_moduleモジュールが読み出されたときのルート経路を定義します。 モジュール名 + ドット ＋ ルート名です。
2行目の```defaults```には、コントローラー（```_content```）とページタイトル（```_title```）と指定します。 
6行目の```requirements```には、ユーザーがページを表示するために必要なアクセス権限を指定します。

以上、基本的なパラメーターのみ紹介しましたが、それ以外の指定可能なパラメーターはDrupal.orgの[Structure of routes](https://www.drupal.org/node/2092643)が参考になります。


今回は時間がなくてコントローラーの中身までは紹介できませんが、実装サンプルを見たいときは何か適当なモジュールを見てみると参考になると思います。



#RouteMatch

RouteMatchは、Routing YAML（my_module.routing.yml）で定義されたルート名に関するルートオブジェクトを取得できます。

Drupal 7の```menu_get_object()```に相当するものというと伝わり易いかも知れません。

**例）menu_get_objectによってnodeオブジェクトを取得する in Drupal 7**

```php
$node = menu_get_object('node');
```

次は、Drupal 8でRouteMatchを使った方法を紹介していきます。

例えば、コンテンツページにアクセスしたときのパスは```/node/123```などになりますが、それに対応するルートオブジェクト```/node/{node}```のパラメーター部分のデータを取得するには次のように書きます。

**例）/node/{node} にマッチするルートオブジェクトを取得する**

```php
use Drupal\Core\Routing\RouteMatchInterface;

$node_id = \Drupal::routeMatch()->getRawParameter('node');
$node = \Drupal::routeMatch()->getParameter('node');
```

```/node/{node}``` のパスに対応するルート名は```'entity.node.canonical'```なので、以下のような判定ロジックが書けます。

**例）アクセスされたページがコンテンツかどうか判定する**

```php
$route_match = \Drupal::routeMatch();
if ($route_match->getRouteName() == 'entity.node.canonical') {
  $node = $route_match->getParameter('node');
}
```

ちなみに```/node/{node}```とルート名```'entity.node.canonical'```の関連付けは、[NodeRouteProviderクラス](https://api.drupal.org/api/drupal/core%21modules%21node%21src%21Entity%21NodeRouteProvider.php/function/NodeRouteProvider%3A%3AgetRoutes/8.5.x)の中で、YAMLファイルを定義せずにPHPコード上で動的にルーティング定義（Dynamic routes）を行っています。


###ルート名```'canonical'```とは？

'canonical'とは、コンテンツ表示する場合のルート名のことを指し、ノードエンティティの'canonical'といえば```/node/{node}```になります。<br>
そのほか、ユーザーエンティティの'canonical'といえば```/user/{user}```、タクソノミーエンティティの場合は、```/taxonomy/term/{taxonomy_term}```となります。

ユーザーエンティティのルート定義は[UserRouteProvider](https://api.drupal.org/api/drupal/core%21modules%21user%21src%21Entity%21UserRouteProvider.php/class/UserRouteProvider/8.4.x
)で行われており、タクソノミーエンティティは、[taxonomy.routing.yml](https://api.drupal.org/api/drupal/core%21modules%21taxonomy%21taxonomy.routing.yml/8.4.x)で定義されています。



#CurrentRouteMatch

```RouteMatch()``` から返されるオブジェクトと同じ内容ですが、こちらは```current_route_match```サービスからアクセス可能です。
現在読み込まれているルートに関するオブジェクトがセットされ、gettersメソッドを介してデータ取得を行います。


###1. *.service.ymlを準備する

```current_route_match``` サービスにアクセスするための準備として、まず、my_module.service.ymlを設置します。  

**例）webform.service.ymlより抜粋**

```yaml
services:
  webform.entity_reference_manager:
  class: Drupal\webform\WebformEntityReferenceManager
  arguments: ['@current_route_match', '@current_user', '@user.data']
```

4行目の```arguments```のところで```'@current_route_match'```を指定します。


###2. コンストラクタにて、routeMatchオブジェクトを受け取る

次に、*.service.ymlの```class:```に該当するクラスを作成して、以下のようにコンストラクタでrouteMatchオブジェクトを受け取ります。

**例）Drupal\webform\WebformEntityReferenceManager より抜粋**

```php
use Drupal\Core\Routing\RouteMatchInterface;

public function __construct(RouteMatchInterface $route_match, AccountInterface $current_user, UserDataInterface $user_data) {
  $this->routeMatch = $route_match;
  $this->currentUser = $current_user;
  $this->userData = $user_data;
}
```

###3. 現在のルートに関する様々な情報を取得する

あとは```$this->routeMatch```を通して、現在読み込まれているルートに関するデータ取得方法のいくつかのパターンです。

**例）様々な情報を取得する**

```php
$route_name = $this->routeMatch->getRouteName();
if ($route_name == 'entity.node.canonical') {
  $node = $this->routeMatch->getParameter('node');
}
if ($route_name == 'entity.entity_view_display.node.view_mode') {
  $node_type = $this->routeMatch->getParameter('node_type');
}
if ($route_name == 'entity.taxonomy_term.canonical') {
  $taxonomy_term = $this->routeMatch->getParameter('taxonomy_term');
}
if ($route_name == 'entity.user.canonical') {
  $user = $this->routeMatch->getParameter('user');
}
```


#Url

RouteMatchはルート名を指定するだけでしたが、こちらはルート名に加えてURLクエリーや```absolute```などのオプションが用意されています。

###Url::fromRoute

まずは、```fromRoute()```を使った基本的なデータ取得方法です。

**例）Url::fromRoute()から情報を取得する**

```php
use Drupal\Core\Url;

$url_front = Url::fromRoute('<front>')->toString();
$url_current = Url::fromRoute('<current>')->toString();
$url_user = Url::fromRoute('user.page')->toString();
```

ちなみに、```->toString()```を```->toRenderable()```にするとレンダリング可能な配列形式が返されます。

###Url::fromUserInput

有効なDrupalルートかどうかに関わらず、指定したパスを元にURLを生成できます。

**例）任意のパスを元にURLを作成する**

```php
use Drupal\Core\Link;
use Drupal\Core\Url;

//検索結果ページ用のURLを生成する
$keyword = 'search-word';
$options = ['absolute' => FALSE, 'query' => ['keyword' => $keyword]];
$url_search = Url::fromUserInput('/search', $options)->toString();

//aタグを生成する
$options = ['absolute' => TRUE, 'attributes' => ['target' => '_blank']];
$url_link = Link::fromTextAndUrl(t('It is a text link'), Url::fromUserInput('/your-path', $options))->toString();
```



###Url::fromUri

外部URLや```base:robots.txt```のようなルーティングされていないローカルURIの両方に対応するには```fromUri()```を使います。

```php
$url_ext = Url::fromUri('https://www.drupal.org/node/add/')->toString();
$url_int = Url::fromUri('base:/node/add/', ['absolute' => TRUE])->toString();
$url_image = Url::fromUri(file_create_url('public://file.png'));
$node_alias = Url::fromUri('internal:/node/88')->toString();
```

ルーティングされているパスに関しても```fromUri()```でオブジェクト生成可能ですが、基本的にDrupalルートを持つURLを取得する場合は```fromRoute()```を使いましょう。


###$nodeを元に、URLオブジェクトを操作する

エンティティオブジェクトの```toUrl()```メソッドを実行するとURLオブジェクトを取得できます。

```php
use Drupal\node\Entity\Node;

$node = Node::load(123);
$url = $node->toUrl('canonical'); //canonical or create-form or etc...
$path_alias = $url->toString();
$internal_path = $url->getInternalPath();
$path_alias_absolute = $url->setAbsolute()->toString();
```

toUrlのパラメーターには、エンティティに関連付けられているさまざまなルート名を指定できます。

**toUrl()に指定可能なルート名と、それに対応するパス**

```yaml
canonical : '/node/{node}'
edit-form : '/node/{node}/edit'
create-form : '/node/add/{entity_subtype}'
version-history : '/node/{node}/revisions'
delete-form : '/node/{node}/delete'
```

toUrl()の引数に何も指定しなければ'canonical'がデフォルトでセットされます。<br>



###Url in Twigテンプレート

twigテンプレート上で、Drupalルートに対応したURLを出力することもできます。

```twig
{{ url('<front>')}}
{{ url('<current>')}}
{{ url('paragraphs.type_add')}}
```

#メニューリンクを追加する

Drupal 7では```hook_menu()```でパスを定義してメニューに追加することができました。
そして、管理メニューにリンクを表示させたり、公開サイトのグローバルナビに表示させたりなどができました。

Drupal 8 には```hook_menu()```はありませんが、代わりにYAMLファイルを使用してメニューリンクを設定できます。

メニューリンク追加の準備として、まずはモジュール直下に my_module.links.menu.yml というファイルを設置します。

このYAMLファイルの中で、メニューリンクの定義と既存メニューの中のどこに登録するのかを定義します。

**例）管理画面のサイト構成（Structure）にリンク追加する**

```yaml
my_module.mymenu:
  title: Example Link
  description: 'This is a example link'
  parent: system.admin_structure
  route_name: my_module.mymenu
```

4行目の```parent```に指定するメニュー名によって、管理画面のどこにリンク追加されるかが決まりますが、Drupalコアに入っている```system.links.menu.yml```ファイルの中で管理メニューのすべてが定義されているので参考になると思います。

5行目の```route_name```に指定するルート名は```my_module.routing.yml```で定義したものを指定します。

あとはキャッシュクリアすれば、メニューにリンク追加されたことを確認できるはずです。


#メニューリンクを書き換える

モジュール上で追加されたメニューは、後からテーマ側で書き換えることができます。

**例）テーマ直下の.theme上でメニューを書き換える**

```php
use Drupal\Core\Url;

/**
 * Implements hook_preprocess_menu().
 */
function mytheme_preprocess_menu(&$variables) {
  if ($variables['is_admin'] && $variables['menu_name'] === 'admin') {
    foreach ($variables['items'] as $key => &$item) {
      if ($key === 'admin_toolbar_tools.help') {
        $item['url'] = Url::fromRoute('system.admin');
        $attributes = ['class' => ['toolbar-icon', 'toolbar-icon-admin-toolbar-tools-help']];
        $item['url']->setOption('attributes', $attributes);
      }
    }
  }
}


```


#参考ドキュメント

以上、ここで紹介したDrupal 8 ルーティングシステムは、Symfonyコンポーネントが基本になっており、同じ構造を持っていて仕組みがほぼ一緒なので[Symfony公式](http://symfony.com/doc/current/routing.html)を読むと理解が深まるはずです。

当然ながらdrupal.orgのドキュメントでも詳しく解説されています。

* [Routing system in Drupal 8 | Drupal.org](https://www.drupal.org/docs/8/api/routing-system)

* [RouteMatchInterface | RouteMatchInterface.php | Drupal API](https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Routing%21RouteMatchInterface.php/interface/RouteMatchInterface/8.5.x)

* [CurrentRouteMatch | CurrentRouteMatch.php | Drupal API](https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Routing%21CurrentRouteMatch.php/class/CurrentRouteMatch/8.5.x)

* [Url | Url.php | Drupal API](https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Url.php/class/Url/8.5.x)