技術メモ

プログラミングとか電子工作とか

Phoenix入門 (第4章 Routing その2)

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。
前回途中で終わってるので、Routingの続きです。
まずはPath Helpers。

Path Helpers

Path Helpersとはコントローラーを作成したら勝手に出てくる関数のようです。以前mix phx.routesをやった時に一番左にでてきたやつがPath Helpersだそうです。

PS > mix phx.routes
page_path  GET  /  HelloWeb.PageController :index

これのpage_pathってやつですね。
コントローラーの名前から勝手に生成される関数で、ルートからのパスを返してくれるそうです。
試しにiex -S mixで実行してみます。

PS > iex.bat -S mix
Interactive Elixir (1.8.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> HelloPhoenixWeb.Router.Helpers.page_path(HelloPhoenixWeb.Endpoint, :index)
"/"

でました。HelloPhoenixWeb.Endpointが謎ですが、たぶん名前からしてルートの位置を示す何かなのでしょう。
この箇所にはコネクション+アクションでも良いらしいく、テンプレート上で利用するには

<a href="<%= Routes.page_path(@conn, :index) %>">To the Welcome Page!</a>

となるそうです。ここでHelloPhoenixWeb.Router.Helpers.page_pathではなくRoutes.page_pathとなっているのはPhoenixのビューが使用されている箇所(ここではレンダリングしてるとき)はHelloWeb.Router.HelpersRoutesエイリアスされているようです。

Phoenixの実装的には勝手に生成されたりするようなのであまり触らなくて良さそうですが、テンプレートを使って開発する場合には強力な機能となりそうです。
裏を返せば基本API化してしまってPhoenixにビューを担当させない場合はあまり使わないかも?

More on Path Helpers

Path Helpersはアクションへ引数を渡せるようです。
前回resourcesで作成したusersだと、下記のようになります。

iex(1)> alias HelloPhoenixWeb.Router.Helpers, as: Routes
HelloPhoenixWeb.Router.Helpers
iex(2)> alias HelloPhoenixWeb.Endpoint
HelloPhoenixWeb.Endpoint
iex(3)> Routes.user_path(Endpoint, :index)
"/users"
iex(4)> Routes.user_path(Endpoint, :show, 17)
"/users/17"
iex(5)> Routes.user_path(Endpoint, :new)
"/users/new"
iex(6)> Routes.user_path(Endpoint, :create)
"/users"
iex(7)> Routes.user_path(Endpoint, :edit, 37)
"/users/37/edit"
iex(8)> Routes.user_path(Endpoint, :update, 37)
"/users/37"
iex(9)> Routes.user_path(Endpoint, :delete, 17)
"/users/17"

URLパラメーターも渡せます。キーバリューのペアで渡すとクエリ文字列に変換されるようです。

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

ルートからの相対パスではなく絶対パスも取得できるそうです。user_pathのpathをurlへ変更します。

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

この絶対パスconfig/dev.exsのデータから生成されているようです。

Nested Resources

前回やったresources、ネストもできるみたいです。
基本的にresourcesって言うぐらいなので複数の物が出てくる(複数のユーザーがでてくる、みたいな)想定なのですが、コレをネストするということはすなわち多対一の関係になります。
例えば、Twitterなどのユーザーがポストを投稿できるときなんかは下記の通りになります。

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

1個のユーザーに対して複数のポストがある事になります。
このときのmix phx.routesは以下のよう。

     page_path  GET     /                               HelloPhoenixWeb.PageController :index
     user_path  GET     /users                          HelloPhoenixWeb.UserController :index
     user_path  GET     /users/:id/edit                 HelloPhoenixWeb.UserController :edit
     user_path  GET     /users/new                      HelloPhoenixWeb.UserController :new
     user_path  GET     /users/:id                      HelloPhoenixWeb.UserController :show
     user_path  POST    /users                          HelloPhoenixWeb.UserController :create
     user_path  PATCH   /users/:id                      HelloPhoenixWeb.UserController :update
                PUT     /users/:id                      HelloPhoenixWeb.UserController :update
     user_path  DELETE  /users/:id                      HelloPhoenixWeb.UserController :delete
user_post_path  GET     /users/:user_id/posts           HelloPhoenixWeb.PostController :index
user_post_path  GET     /users/:user_id/posts/:id/edit  HelloPhoenixWeb.PostController :edit
user_post_path  GET     /users/:user_id/posts/new       HelloPhoenixWeb.PostController :new
user_post_path  GET     /users/:user_id/posts/:id       HelloPhoenixWeb.PostController :show
user_post_path  POST    /users/:user_id/posts           HelloPhoenixWeb.PostController :create
user_post_path  PATCH   /users/:user_id/posts/:id       HelloPhoenixWeb.PostController :update
                PUT     /users/:user_id/posts/:id       HelloPhoenixWeb.PostController :update
user_post_path  DELETE  /users/:user_id/posts/:id       HelloPhoenixWeb.PostController :delete

ここまでできれば下手するとモデリング無しで簡単なシステムが作れそうです。
ただし、全ポストの表示などは別の処理が要りそうです。

先ほどのPath Helpersもネスとしてもいけます。ただし各ユーザーにポストがぶら下がってるので、ポストの表示はユーザーIDを指定してやる必要があるのでその辺は要注意です。

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

URLパラメータも同様。

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

Scoped Routes

とりあえずよくもわからずscope "/", HelloPhoenixWeb doとか書いてたあたりの説明です。
どうやらこのscopeというマクロは共通パスをまとめる機能だそうです。"/"のスコープは全て共通ということですね。
とりあえずこの項目ではreviewsというresourceを想定して進めて行きます。なので、reviewsのパスは

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

ということになります。
ここで、このreviewsを管理者権限を持つ物が特別な挙動でいじくりまわす想定をすると

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

みたいになってると嬉しいです。
ここでscopeの登場です。router.exを次のように変更してみます。

...
  scope "/", HelloPhoenixWeb do
    pipe_through :browser

    get "/", PageController, :index
    resources "/users", UserController do
      resources "/posts", PostController
    end
    resources "/reviews", ReviewController
  end

  scope "/admin" do
    pipe_through :browser

    resources "/reviews", HelloPhoenixWeb.Admin.ReviewController
  end
...

このとき、mix phx.routesを実行すると

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

きちんと"/admin"が着いた形でパスが返ってきてます。
ちなみにサンプルではscope "/admin" doになってますが

  scope "/admin", HelloPhoenixWeb  do
    pipe_through :browser

    resources "/reviews", Admin.ReviewController
  end

でも同様になりました。

ただし現状では問題が。
よくよく見てみると普通の方もadminの方も同じreview_pathというpath helperになっています。これはビューのレンダリングとかでマズい気がします。
ここで、scopeの箇所をscope "/admin", as: :admin doとすることでpath helperの重複を回避できます。

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

どうやらscopeas: :hogehogeとするとhogehoge_という接頭辞をpath helperに付与することができるみたいです。
他にも画像やユーザーをadminのスコープで管理したい場合は下記のようなルーティングになるとおもいます。

scope "/admin", as: :admin do
  pipe_through :browser

  resources "/images",  HelloPhoenixWeb.Admin.ImageController
  resources "/reviews", HelloPhoenixWeb.Admin.ReviewController
  resources "/users",   HelloPhoenixWeb.Admin.UserController
end

HelloPhoenixWeb.Adminが冗長です。
ここで"/"scope同様、HelloPhoenixWeb.Adminを渡してやると、省略できるようです。

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

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

たいへんスッキリしました。

scopeのネスト

scopeのネストにも言及がありました。
一応実装としては可能なんですが、複雑化するのを避ける為に基本的には非推奨だそうです。
ただ、よくあるAPIのバージョンなんかが着いたURLを実装するのには使えそうです。

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

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

こうネストすると/api/v1/hogehogeというURLになります。WebのAPIでよく見る感じになりました。
一応こう実装しておけばバージョン毎に機能を残しておくこともできます。

同じパスのscope

同じルーティングパスを指定しないように注意すれば、scopeで同じパスを指定することもできるそうです。

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

    resources "/users", UserController
  end

  scope "/", AnotherAppWeb do
    pipe_through :browser

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

この例ではあまり旨みはでていませんが、コントローラーが増えてきてAdminの用にまとめたい時なんかは便利だと思います。

まだPipelinesとChannel Routesが残ってるんですが、Pipelinesの前にPlugをやっておいた方が良いと思うので一旦ここで切ります。