Chapter 08

ルーティング

koga1020
koga1020
2021.03.02に更新

ルーティング

前提: このガイドでは、入門ガイドの内容を理解し、Phoenixアプリケーションを実行していることを前提としています

前提: リクエストのライフサイクルガイドを理解していることを前提としています

ルーターは、Phoenixアプリケーションのメインハブです。ルーターは、HTTPリクエストをコントローラーアクションにマッチさせ、リアルタイムチャネルハンドラーをつなぎ、一連のパイプライン変換を一連のルートにスコープして定義します。

Phoenixが生成するルーターファイル lib/hello_web/router.ex は次のようになります。

defmodule HelloWeb.Router do
  use HelloWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", HelloWeb do
    pipe_through :browser

    get "/", PageController, :index
  end

  # Other scopes may use custom stacks.
  # scope "/api", HelloWeb do
  #   pipe_through :api
  # end
end

ルーター名とコントローラーモジュール名の両方とも、HelloWeb ではなく、アプリケーションに与えた名前がプレフィックスになります。

このモジュールの最初の行である use HelloWeb, :router は、Phoenixルーターの関数をルーターで利用できるようにするだけです。

スコープはこのガイドでは独自のセクションを持っているので、ここでは scope "/", HelloWeb do のブロックの説明は割愛します。pipe_through :browser 行については、このガイドのパイプラインのセクションで詳しく説明します。今のところ、パイプラインでは一連のプラグを異なるルートのセットに適用することができるということだけは知っておく必要があります。

スコープブロックの中には、最初の実際のルートがあります。

get "/", PageController, :index

get はHTTP動詞のGETに対応するPhoenixマクロです。同様のマクロは、POST、PUT、PATCH、DELETE、OPTIONS、CONNECT、TRACE、HEADなどの他のHTTP動詞にも存在します。

ルートを調べる

Phoenixはアプリケーション内のルートを調査するための素晴らしいツールを提供しています: mix phx.routes.

これがどのように動作するか見てみましょう。新しく生成されたPhoenixアプリケーションのルートに行き、mix phx.routes を実行してください。次のような、生成されたすべてのルートが確認できます。

$ mix phx.routes
page_path  GET  /  HelloWeb.PageController :index

上のルートは、アプリケーションのルートに対するHTTP GETリクエストが HelloWeb.PageControllerindex アクションによって処理されることを示しています。

page_path はPhoenixがパスヘルパーと呼ぶものの例です。これらは後ほど説明します。

リソース

ルーターは get, post, put などのHTTP動詞以外にもマクロをサポートしています。その中でもとくに重要なのは resources です。次のように lib/hello_web/router.ex ファイルにリソースを追加してみましょう。

scope "/", HelloWeb do
  pipe_through :browser

  get "/", PageController, :index
  resources "/users", UserController
end

今のところ、実際に HelloWeb.UserController を持っていないことは問題ではありません。

プロジェクトのルートでもう一度 mix phx.routes を実行してください。以下のようなものが表示されるはずです。

user_path  GET     /users           HelloWeb.UserController :index
user_path  GET     /users/:id/edit  HelloWeb.UserController :edit
user_path  GET     /users/new       HelloWeb.UserController :new
user_path  GET     /users/:id       HelloWeb.UserController :show
user_path  POST    /users           HelloWeb.UserController :create
user_path  PATCH   /users/:id       HelloWeb.UserController :update
           PUT     /users/:id       HelloWeb.UserController :update
user_path  DELETE  /users/:id       HelloWeb.UserController :delete

これは、HTTP動詞、パス、コントローラーのアクションの標準的な行列です。しばらくの間、これはRESTful routesとして知られていましたが、現在ではほとんどの人がこれを誤記と考えています。少し順番を変えて、個別に見ていきましょう。

  • /users へのGETリクエストは index アクションを呼び出し、すべてのユーザーを表示します。
  • /users/new へのGETリクエストは new アクションを呼び出し、新しいユーザーを作成するためのフォームを表示します。
  • /users/:id へのGETリクエストは、idを指定して show アクションを呼び出し、そのIDで識別される個々のユーザーを表示します。
  • /user へのPOSTリクエストは create アクションを呼び出し、新しいユーザーをデータストアに保存します。
  • /users/:id/edit へのGETリクエストは、IDを指定して edit アクションを呼び出し、データストアから個々のユーザーを取得して編集用のフォームで情報を表示します。
  • /users/:id へのPATCHリクエストは、IDを指定して update アクションを呼び出し、更新されたユーザーをデータストアに保存します。
  • /users/:id へのPUTリクエストもまた update アクションを呼び出し、IDを指定して更新されたユーザーをデータストアに保存する。
  • /users/:id へのDELETEリクエストは、IDを指定して delete アクションを呼び出し、個々のユーザーをデータストアから削除します。

これらのルートをすべて必要としない場合は、:only:except オプションを使って特定のアクションをフィルタリングできます。

読み取り専用の投稿リソースがあるとしましょう。このように定義できます。

resources "/posts", PostController, only: [:index, :show]

mix phx.routes を実行すると、indexとshowアクションへのルートだけが定義されていることがわかります。

post_path  GET     /posts      HelloWeb.PostController :index
post_path  GET     /posts/:id  HelloWeb.PostController :show

同様に、コメントリソースがあり、そのリソースを削除するためのルートを提供したくない場合、次のように定義できます。

resources "/comments", CommentController, except: [:delete]

これで mix phx.routes を実行すると、削除アクションへのDELETEリクエストを除くすべてのルートがあることがわかります。

comment_path  GET    /comments           HelloWeb.CommentController :index
comment_path  GET    /comments/:id/edit  HelloWeb.CommentController :edit
comment_path  GET    /comments/new       HelloWeb.CommentController :new
comment_path  GET    /comments/:id       HelloWeb.CommentController :show
comment_path  POST   /comments           HelloWeb.CommentController :create
comment_path  PATCH  /comments/:id       HelloWeb.CommentController :update
              PUT    /comments/:id       HelloWeb.CommentController :update

Phoenix.Router.resources/4 マクロは、リソースルートをカスタマイズするための追加オプションを記述します。

パスヘルパー

パスヘルパーは、個々のアプリケーションのために Router.Helpers モジュール上で動的に定義される関数です。私たちの場合、それは HelloWeb.Router.Helpers です。各パスヘルパーの名前は、ルート定義で使用されるコントローラーの名前に由来します。コントローラーは HelloWeb.PageController で、page_path はアプリケーションのルートへのパスを返す関数です。

一言では言えないですね。実際に見てみましょう。プロジェクトのルートで iex -S mix を実行します。ルーターヘルパーの page_path 関数を Endpoint あるいは接続とアクションを引数に指定して呼び出すと、パスを返してくれます。

iex> HelloWeb.Router.Helpers.page_path(HelloWeb.Endpoint, :index)
"/"

テンプレート内で page_path 関数を使用してアプリケーションのルートにリンクすることができるので、これは重要です。テンプレートの中でこのヘルパーを使うことができます。

<%= link "Welcome Page!", to: Routes.page_path(@conn, :index) %>

完全な HelloWeb.Router.Helpers.page_path の代わりに Routes.page_path を使用できるのは、HelloWeb.Router.Helpers はデフォルトで lib/hello_web.ex 内の view/0 ブロックで Routes としてエイリアスされているからです。この定義は、use HelloWeb, :view によってテンプレートで利用できるようになっています。

もちろん、代わりに HelloWeb.Router.Helpers.page_path(@conn, :index) を使うこともできますが、簡潔さのためにエイリアス版を使うのが慣例です。エイリアスは、ビュー、コントローラー、テンプレートで使用するためだけに自動的に設定されることに注意してください - これらの外では、フルネームか、モジュール定義の中で alias HelloWeb.Router.Helpers, as.Routes でモジュール定義の中で自分でエイリアスを設定する必要があります。

パスヘルパーを使用することで、コントローラー、ビュー、テンプレートがルーターが実際に処理できるページにリンクしていることを簡単に保証できます。

パスヘルパーの詳細

ユーザーリソースに対して mix phx.routes を実行すると、出力の各行のパスヘルパー関数として user_path がリストアップされます。これはそれぞれのアクションのために翻訳されています。

iex> alias HelloWeb.Router.Helpers, as: Routes
iex> alias HelloWeb.Endpoint
iex> Routes.user_path(Endpoint, :index)
"/users"

iex> Routes.user_path(Endpoint, :show, 17)
"/users/17"

iex> Routes.user_path(Endpoint, :new)
"/users/new"

iex> Routes.user_path(Endpoint, :create)
"/users"

iex> Routes.user_path(Endpoint, :edit, 37)
"/users/37/edit"

iex> Routes.user_path(Endpoint, :update, 37)
"/users/37"

iex> Routes.user_path(Endpoint, :delete, 17)
"/users/17"

クエリストリングを持つパスはどうでしょうか?オプションの第4引数にkey-valueのペアを追加することで、パスヘルパーはクエリ文字列のペアを返します。

iex> Routes.user_path(Endpoint, :show, 17, admin: true, active: false)
"/users/17?admin=true&active=false"

パスの代わりに完全なURLが必要な場合はどうすればよいでしょうか?_path_url に置き換えるだけです。

iex(3)> Routes.user_url(Endpoint, :index)
"http://localhost:4000/users"

_url 関数は、環境ごとの設定から、完全なURLを構築するために必要なホスト、ポート、プロキシポート、SSLの情報を取得します。設定については、それ自身のガイドで詳しく説明します。とりあえず、自分のプロジェクトの config/dev.exs ファイルを見て、これらの値を確認できます。

可能な限り、Endpoint の代わりに conn(ビューでは @conn)を渡すことをオススメします。

入れ子になったリソース

Phoenixルーターの中にリソースをネストすることも可能です。たとえば、users と多対1の関係にある posts リソースがあるとしましょう。つまり、1人のユーザーが多くの投稿を作成することができ、個々の投稿は1人のユーザーにしか属していないということです。これを表現するには、lib/hello_web/router.ex に次のようなネストされたルートを追加します。

resources "/users", UserController do
  resources "/posts", PostController
end

上で見た users 用のルートに加えて、mix phx.routes を実行すると、以下のようなルートが得られます。

...
user_post_path  GET     /users/:user_id/posts           HelloWeb.PostController :index
user_post_path  GET     /users/:user_id/posts/:id/edit  HelloWeb.PostController :edit
user_post_path  GET     /users/:user_id/posts/new       HelloWeb.PostController :new
user_post_path  GET     /users/:user_id/posts/:id       HelloWeb.PostController :show
user_post_path  POST    /users/:user_id/posts           HelloWeb.PostController :create
user_post_path  PATCH   /users/:user_id/posts/:id       HelloWeb.PostController :update
                PUT     /users/:user_id/posts/:id       HelloWeb.PostController :update
user_post_path  DELETE  /users/:user_id/posts/:id       HelloWeb.PostController :delete

これらのルートのそれぞれがpostをユーザーIDにスコープしていることがわかります。最初のルートでは、PostController index アクションを呼び出しますが、user_id を渡します。これは、そのユーザーのすべての投稿を表示することを意味します。これらすべてのルートに同じスコープが適用されます。

ネストしたルートに対してパスヘルパー関数を呼び出す際には、ルート定義で指定した順番でIDを渡す必要があります。次の show ルートでは、42user_id で、17post_id です。始める前に、HelloWeb.Endpoint のエイリアスを忘れないようにしましょう。

iex> alias HelloWeb.Endpoint
iex> HelloWeb.Router.Helpers.user_post_path(Endpoint, :show, 42, 17)
"/users/42/posts/17"

ここでも、関数呼び出しの最後にkey-valueのペアを追加すると、それがクエリ文字列に追加されます。

iex> HelloWeb.Router.Helpers.user_post_path(Endpoint, :index, 42, active: true)
"/users/42/posts?active=true"

Helpers モジュールをエイリアスしていれば(ビュー、テンプレート、コントローラーに対してのみ自動的にエイリアスされますが、この場合は iex の中にいるので自分でエイリアスする必要があります)、代わりに次のようにできます。

iex> alias HelloWeb.Router.Helpers, as: Routes
iex> alias HelloWeb.Endpoint
iex> Routes.user_post_path(Endpoint, :index, 42, active: true)
"/users/42/posts?active=true"

スコープされたルート

スコープは、共通のパスプレフィックスとスコープされた一連のプラグの下でルートをグループ化する方法です。これは、管理者機能やAPI、とくにバージョン管理されたAPIのために必要になるかもしれません。あるサイトにユーザーが生成したレビューがあり、それらのレビューは最初に管理者によって承認される必要があるとしましょう。これらのリソースのセマンティクスはまったく異なり、同じコントローラーを共有しているわけではないかもしれません。スコープを使用することで、これらのルートを分離できます。

ユーザーが利用するレビューへのパスは、標準的なリソースのように見えるでしょう。

/reviews
/reviews/1234
/reviews/1234/edit
...

管理者レビューのパスの前に /admin をつけることができます。

/admin/reviews
/admin/reviews/1234
/admin/reviews/1234/edit
...

これを実現するには、このように /admin にパスオプションを設定するスコープ付きルートを使用します。このスコープを別のスコープの中に入れ子にすることもできますが、その代わりにルートにスコープを設定しましょう。

scope "/admin", HelloWeb.Admin do
  pipe_through :browser

  resources "/reviews", ReviewController
end

新しいスコープを定義して、すべてのルートのプレフィックスが "/admin" で、すべてのコントローラーが HelloWeb.Admin 名前空間の下にあるようにします。

mix phx.routes を再度実行すると、以前のルートに加えて、次のような結果が得られます。

...
review_path  GET     /admin/reviews           HelloWeb.Admin.ReviewController :index
review_path  GET     /admin/reviews/:id/edit  HelloWeb.Admin.ReviewController :edit
review_path  GET     /admin/reviews/new       HelloWeb.Admin.ReviewController :new
review_path  GET     /admin/reviews/:id       HelloWeb.Admin.ReviewController :show
review_path  POST    /admin/reviews           HelloWeb.Admin.ReviewController :create
review_path  PATCH   /admin/reviews/:id       HelloWeb.Admin.ReviewController :update
             PUT     /admin/reviews/:id       HelloWeb.Admin.ReviewController :update
review_path  DELETE  /admin/reviews/:id       HelloWeb.Admin.ReviewController :delete

これは良さそうに見えますが、ここで問題があります。管理者向けの /admin/reviews と同様に、ユーザー向けのレビュールート /reviews も必要なのでした。このようにルートスコープの下にあるルーターにユーザー向けのレビューを含めると、次のようになります。

scope "/", HelloWeb do
  pipe_through :browser

  ...
  resources "/reviews", ReviewController
end

scope "/admin", HelloWeb.Admin do
  pipe_through :browser

  resources "/reviews", ReviewController
end

そして mix phx.routes を実行すると、このような出力が得られます。

...
review_path  GET     /reviews                 HelloWeb.ReviewController :index
review_path  GET     /reviews/:id/edit        HelloWeb.ReviewController :edit
review_path  GET     /reviews/new             HelloWeb.ReviewController :new
review_path  GET     /reviews/:id             HelloWeb.ReviewController :show
review_path  POST    /reviews                 HelloWeb.ReviewController :create
review_path  PATCH   /reviews/:id             HelloWeb.ReviewController :update
             PUT     /reviews/:id             HelloWeb.ReviewController :update
review_path  DELETE  /reviews/:id             HelloWeb.ReviewController :delete
...
review_path  GET     /admin/reviews           HelloWeb.Admin.ReviewController :index
review_path  GET     /admin/reviews/:id/edit  HelloWeb.Admin.ReviewController :edit
review_path  GET     /admin/reviews/new       HelloWeb.Admin.ReviewController :new
review_path  GET     /admin/reviews/:id       HelloWeb.Admin.ReviewController :show
review_path  POST    /admin/reviews           HelloWeb.Admin.ReviewController :create
review_path  PATCH   /admin/reviews/:id       HelloWeb.Admin.ReviewController :update
             PUT     /admin/reviews/:id       HelloWeb.Admin.ReviewController :update
review_path  DELETE  /admin/reviews/:id       HelloWeb.Admin.ReviewController :delete

実際のルートは、各行の先頭にあるパスヘルパー review_path を除いて、すべて正しく見えます。ユーザーが利用するレビュールートと管理者が利用するレビュールートの両方で同じヘルパーを取得していますが、これは正しくありません。

この問題は、管理者スコープに as: :admin オプションを追加することで解決できます。


scope "/admin", HelloWeb.Admin, as: :admin do
  pipe_through :browser

  resources "/reviews", ReviewController
end

これで mix phx.routes で欲しい結果が得られました。

...
      review_path  GET     /reviews                        HelloWeb.ReviewController :index
      review_path  GET     /reviews/:id/edit               HelloWeb.ReviewController :edit
      review_path  GET     /reviews/new                    HelloWeb.ReviewController :new
      review_path  GET     /reviews/:id                    HelloWeb.ReviewController :show
      review_path  POST    /reviews                        HelloWeb.ReviewController :create
      review_path  PATCH   /reviews/:id                    HelloWeb.ReviewController :update
                   PUT     /reviews/:id                    HelloWeb.ReviewController :update
      review_path  DELETE  /reviews/:id                    HelloWeb.ReviewController :delete
...
admin_review_path  GET     /admin/reviews                  HelloWeb.Admin.ReviewController :index
admin_review_path  GET     /admin/reviews/:id/edit         HelloWeb.Admin.ReviewController :edit
admin_review_path  GET     /admin/reviews/new              HelloWeb.Admin.ReviewController :new
admin_review_path  GET     /admin/reviews/:id              HelloWeb.Admin.ReviewController :show
admin_review_path  POST    /admin/reviews                  HelloWeb.Admin.ReviewController :create
admin_review_path  PATCH   /admin/reviews/:id              HelloWeb.Admin.ReviewController :update
                   PUT     /admin/reviews/:id              HelloWeb.Admin.ReviewController :update
admin_review_path  DELETE  /admin/reviews/:id              HelloWeb.Admin.ReviewController :delete

パスヘルパーは、私たちが望むものを返すようになりました。iex -S mix を実行して、自分で試してみてください。

iex(1)> HelloWeb.Router.Helpers.review_path(HelloWeb.Endpoint, :index)
"/reviews"

iex(2)> HelloWeb.Router.Helpers.admin_review_path(HelloWeb.Endpoint, :show, 1234)
"/admin/reviews/1234"

複数のリソースを管理者がすべて処理するとしたらどうでしょうか?このように、同じスコープ内にすべてのリソースを配置できます。

scope "/admin", HelloWeb.Admin, as: :admin do
  pipe_through :browser

  resources "/images",  ImageController
  resources "/reviews", ReviewController
  resources "/users",   UserController
end

以下が mix phx.routes の結果です。

...
 admin_image_path  GET     /admin/images            HelloWeb.Admin.ImageController :index
 admin_image_path  GET     /admin/images/:id/edit   HelloWeb.Admin.ImageController :edit
 admin_image_path  GET     /admin/images/new        HelloWeb.Admin.ImageController :new
 admin_image_path  GET     /admin/images/:id        HelloWeb.Admin.ImageController :show
 admin_image_path  POST    /admin/images            HelloWeb.Admin.ImageController :create
 admin_image_path  PATCH   /admin/images/:id        HelloWeb.Admin.ImageController :update
                   PUT     /admin/images/:id        HelloWeb.Admin.ImageController :update
 admin_image_path  DELETE  /admin/images/:id        HelloWeb.Admin.ImageController :delete
admin_review_path  GET     /admin/reviews           HelloWeb.Admin.ReviewController :index
admin_review_path  GET     /admin/reviews/:id/edit  HelloWeb.Admin.ReviewController :edit
admin_review_path  GET     /admin/reviews/new       HelloWeb.Admin.ReviewController :new
admin_review_path  GET     /admin/reviews/:id       HelloWeb.Admin.ReviewController :show
admin_review_path  POST    /admin/reviews           HelloWeb.Admin.ReviewController :create
admin_review_path  PATCH   /admin/reviews/:id       HelloWeb.Admin.ReviewController :update
                   PUT     /admin/reviews/:id       HelloWeb.Admin.ReviewController :update
admin_review_path  DELETE  /admin/reviews/:id       HelloWeb.Admin.ReviewController :delete
  admin_user_path  GET     /admin/users             HelloWeb.Admin.UserController :index
  admin_user_path  GET     /admin/users/:id/edit    HelloWeb.Admin.UserController :edit
  admin_user_path  GET     /admin/users/new         HelloWeb.Admin.UserController :new
  admin_user_path  GET     /admin/users/:id         HelloWeb.Admin.UserController :show
  admin_user_path  POST    /admin/users             HelloWeb.Admin.UserController :create
  admin_user_path  PATCH   /admin/users/:id         HelloWeb.Admin.UserController :update
                   PUT     /admin/users/:id         HelloWeb.Admin.UserController :update
  admin_user_path  DELETE  /admin/users/:id         HelloWeb.Admin.UserController :delete

これは素晴らしいですね。まさに我々が欲しいものです。すべてのルート、パスヘルパー、コントローラーが適切な名前空間になっていることに注目してください。

スコープは入れ子にすることもできますが、入れ子にするとコードが混乱してわかりにくくなることがあるので、慎重に行う必要があります。とはいえ、画像、レビュー、ユーザー用のリソースを定義したバージョニングされたAPIがあったとしましょう。その場合、技術的には次のようにバージョニングされたAPIのルートを設定できます。

scope "/api", HelloWeb.Api, as: :api do
  pipe_through :api

  scope "/v1", V1, as: :v1 do
    resources "/images",  ImageController
    resources "/reviews", ReviewController
    resources "/users",   UserController
  end
end

これらの定義がどのようになるかは mix phx.routes を実行することで確認できます。

興味深いことに、ルートが重複しないように注意していれば、同じパスで複数のスコープを使用できます。このルーターは、同じパスに2つのスコープが定義されていてもまったく問題ありません。

defmodule HelloWeb.Router do
  use Phoenix.Router
  ...
  scope "/", HelloWeb do
    pipe_through :browser

    resources "/users", UserController
  end

  scope "/", AnotherAppWeb do
    pipe_through :browser

    resources "/posts", PostController
  end
  ...
end

ルートを複製した場合、このおなじみの警告が表示されます。

warning: this clause cannot match because a previous clause at line 16 always matches

パイプライン

このガイドでは、ルーターで最初に見た行の1つである pipe_through :browser について語らずに、かなり長い道のりを歩んできました。それを語るときがきました。

パイプラインとは、特定のスコープに取り付けることができる一連のplugのことです。Plugに詳しくない方には、詳細なガイドがあります。

ルートはスコープ内で定義され、スコープは複数のパイプラインを通過できます。ルートが一致すると、Phoenixはそのルートに関連付けられたすべてのパイプラインで定義されたすべてのプラグを呼び出します。たとえば、"/" にアクセスすると :browser パイプラインを通過し、その結果、すべてのプラグが呼び出されます。

Phoenixはデフォルトで :browser:api という2つのパイプラインを定義しており、多くの一般的なタスクに使用できます。これらのパイプラインをカスタマイズしたり、必要に応じて新しいパイプラインを作成したりできます。

:browser:api のパイプライン

名前が示すように、:browser パイプラインはブラウザへのリクエストをレンダリングするためのルートを準備します。api パイプラインはapiのデータを生成するルートの準備します。

plug :accepts, ["html"] はリクエストのフォーマットを定義し、:fetch_session はセッションデータを取得してコネクションで利用可能にするもので、:fetch_flash はセットされているフラッシュメッセージを取得するもので、:protect_from_forgery:put_secure_browser_headers はクロスサイトフォージェリからフォーム投稿を保護するものです。

現在のところ、:api パイプラインは plug :accepts, ["json"] のみを定義しています。

ルーターは、スコープ内で定義されたルート上でパイプラインを呼び出します。スコープ外のルートにはパイプラインはありません。ネストされたスコープの使用はオススメしませんが (上記参照)、ネストされたスコープ内で pipe_through を呼び出すと、ルーターは親スコープからすべての pipe_through を呼び出し、その後にネストされたスコープを呼び出します。

たくさんの用語が束になっています。いくつかの例を見て、意味を紐解いてみましょう。

新しく生成されたPhoenixアプリケーションからルーターを見てみましょう。今回はapiスコープがコメントされておらず、ルートが追加されています。

defmodule HelloWeb.Router do
  use HelloWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", HelloWeb do
    pipe_through :browser

    get "/", PageController, :index
  end

  # Other scopes may use custom stacks.
  scope "/api", HelloWeb do
    pipe_through :api

    resources "/reviews", ReviewController
  end
end

サーバーがリクエストを受け入れるとき、リクエストは常に最初にEndpointのプラグを通過し、その後、パスとHTTP動詞にマッチしようとします。

リクエストが最初のルートである / へのGETにマッチしたとしましょう。ルーターはまずそのリクエストを :browser パイプラインにパイプし、セッションデータを取得してフラッシュを取得してフォージェリ保護を実行します。

逆に、リクエストが resources/2 マクロで定義されたルートのいずれかにマッチした場合、ルーターは :api パイプライン(現在は何もしていない)を経由して HelloWeb.ReviewController の正しいアクションにディスパッチする前に、リクエストをパイプします。

ルートが一致しない場合、パイプラインは呼び出されず、404エラーが発生します。

これらの考えをもう少し広げてみましょう。リクエストを :browser と1つ以上のカスタムパイプラインの両方にパイプする必要があるとしたらどうでしょうか?パイプラインのリストを pipe_through するだけで、Phoenixはそれらのパイプラインを順番に呼び出します。

defmodule HelloWeb.Router do
  use HelloWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end
  ...

  scope "/reviews" do
    pipe_through [:browser, :review_checks, :other_great_stuff]

    resources "/", HelloWeb.ReviewController
  end
end

ここでは、異なるパイプラインを持つ2つのスコープの別の例を示します。

defmodule HelloWeb.Router do
  use HelloWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end
  ...

  scope "/", HelloWeb do
    pipe_through :browser

    resources "/posts", PostController
  end

  scope "/reviews", HelloWeb do
    pipe_through [:browser, :review_checks]

    resources "/", ReviewController
  end
end

一般的に、パイプラインのスコーピングルールは期待通りの振る舞いをします。この例では、すべてのルートが :browser パイプラインを通過します。しかし、reviews リソースのルートだけが :review_checks パイプラインを通過します。pipe_through [:browser, :review_checks] と両方のパイプをパイプラインのリストで宣言しているので、Phoenixはそれぞれのパイプを順番に pipe_through します。

新しいパイプラインの作成

Phoenixでは、ルーター内の任意の場所に独自のカスタムパイプラインを作成できます。そのためには、pipeline/2 マクロをこれらの引数で呼び出します。

defmodule HelloWeb.Router do
  use HelloWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :review_checks do
    plug :ensure_authenticated_user
    plug :ensure_user_owns_review
  end

  scope "/reviews", HelloWeb do
    pipe_through [:browser, :review_checks]

    resources "/", ReviewController
  end
end

パイプライン自体はplugなので、パイプラインを別のパイプラインの中に組み込むことができます。たとえば、上の review_checks パイプラインを browser を自動的に呼び出すように書き換えることで、下流のパイプライン呼び出しを簡素化できます。

  pipeline :review_checks do
    plug :browser
    plug :ensure_authenticated_user
    plug :ensure_user_owns_review
  end

  scope "/reviews", HelloWeb do
    pipe_through [:review_checks]

    resources "/", ReviewController
  end

フォワード

Phoenix.Router.forward/4 マクロを使用して、特定のパスで始まるすべてのリクエストを特定のplugへ送信できます。システムの一部がバックグラウンドでジョブを実行しているとします(別のアプリケーションやライブラリであっても構いません)。この管理者インターフェイスに転送するには、次のようにします。

defmodule HelloWeb.Router do
  use HelloWeb, :router

  ...

  scope "/", HelloWeb do
    ...
  end

  forward "/jobs", BackgroundJob.Plug
end

これは、/jobs で始まるすべてのルートが HelloWeb.BackgroundJob.Plug モジュールに送られることを意味します。プラグの中では、/pending/active のように、特定のジョブのステータスを示すサブルートにマッチさせることができます。

パイプラインと forward/4 マクロを混在させることもできます。ユーザーが認証済みで管理者であることを確認してジョブページを表示させたい場合は、ルーターで次のようにすればよいでしょう。

defmodule HelloWeb.Router do
  use HelloWeb, :router

  ...

  scope "/" do
    pipe_through [:authenticate_user, :ensure_admin]
    forward "/jobs", BackgroundJob.Plug
  end
end

これは、authenticate_userensure_admin パイプラインのプラグが BackgroundJob.Plug の前に呼び出され、適切なレスポンスを送信し、それに応じてリクエストを停止できることを意味します。

モジュールplugの init/1 コールバックで受け取る opts は、第3引数として渡すことができます。たとえば、バックグラウンドジョブでページに表示するアプリケーションの名前を設定できます。と一緒に渡すことができます。

forward "/jobs", BackgroundJob.Plug, name: "Hello Phoenix"

4番目の引数 router_opts を渡すことができます。これらのオプションの概要は Phoenix.Router.scope/2 のドキュメントで説明されています。

BackgroundJob.Plug は、Plugガイドで説明されているモジュールPlugとして実装できます。しかし、別のPhoenixエンドポイントに転送することはオススメしません。これは、あなたのアプリで定義されたplugと転送されたエンドポイントが2回呼び出され、エラーにつながる可能性があるからです。

まとめ

ルーティングは大きなトピックであり、ここでは多くの分野をカバーしています。このガイドから取り上げる重要なポイントは以下の通りです。

  • HTTP動詞名で始まるルートは、1つのmatch関数定義に展開されます。
  • 'resources' で始まるルートは、8つのmatch関数定義に展開されます。
  • リソースは、only: または except: オプションを使用してmatch関数の数を制限できます。
  • これらのルートはいずれもネストさせることができます。
  • これらのルートはいずれも指定したパスにスコープできます
  • スコープで as: オプションを使うと重複を減らすことができます。
  • スコープされたルートにヘルパーオプションを使用すると、到達不可能なパスを排除できます。