
どうも、靖宗です。
前回途中で終わってるので、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.HelpersはRoutesへエイリアスされているようです。
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
どうやらscopeにas: :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をやっておいた方が良いと思うので一旦ここで切ります。