Skip to content

Commit

Permalink
Add docs for macros on the router
Browse files Browse the repository at this point in the history
  • Loading branch information
josevalim committed Dec 19, 2023
1 parent a66e7c3 commit 05825d1
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 55 deletions.
12 changes: 12 additions & 0 deletions guides/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ get "/", PageController, :home

`get` is a Phoenix macro that corresponds to the HTTP verb GET. Similar macros exist for other HTTP verbs, including POST, PUT, PATCH, DELETE, OPTIONS, CONNECT, TRACE, and HEAD.

> #### Why the macros? {: .info}
>
> Phoenix does its best to keep the usage of macros low. You may have noticed, however, that the `Phoenix.Router` relies heavily on macros. Why is that?
>
> We use `get`, `post`, `put`, and `delete` to define your routes. We use macros for two purposes:
>
> * They define the routing engine, used on every request, to choose which controller to dispatch the request to. Thanks to macros, Phoenix compiles all of your routes to a huge case-statement with pattern matching rules, which is heavily optimized by the Erlang VM
>
> * For each route you define, we also define metadata to implement `Phoenix.VerifiedRoutes`. As we will soon learn, verified routes allows to us to reference any route as if it is a plain looking string, except it is verified by the compiler to be valid (making it much harder to ship broken links, forms, mails, etc to production)
>
> In other words, the router relies on macros to build applications that are faster and safer. Also remember that macros in Elixir are compile-time only, which gives plenty of stability after the code is compiled. As we will learn next, Phoenix also provides introspection for all defined routes via `mix phx.routes`.
## Examining routes

Phoenix provides an excellent tool for investigating routes in an application: `mix phx.routes`.
Expand Down
166 changes: 111 additions & 55 deletions lib/phoenix/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ defmodule Phoenix.Router do
defexception plug_status: 404, message: "no route found", conn: nil, router: nil

def exception(opts) do
conn = Keyword.fetch!(opts, :conn)
conn = Keyword.fetch!(opts, :conn)
router = Keyword.fetch!(opts, :router)
path = "/" <> Enum.join(conn.path_info, "/")
path = "/" <> Enum.join(conn.path_info, "/")

%NoRouteError{message: "no route found for #{conn.method} #{path} (#{inspect router})",
conn: conn, router: router}
%NoRouteError{
message: "no route found for #{conn.method} #{path} (#{inspect(router)})",
conn: conn,
router: router
}
end
end

Expand Down Expand Up @@ -100,16 +103,40 @@ defmodule Phoenix.Router do
GET /pages/hey/there/world
%{"page" => "y", "rest" => ["there" "world"]} = params
> #### Why the macros? {: .info}
>
> Phoenix does its best to keep the usage of macros low. You may have noticed,
> however, that the `Phoenix.Router` relies heavily on macros. Why is that?
>
> We use `get`, `post`, `put`, and `delete` to define your routes. We use macros
> for two purposes:
>
> * They define the routing engine, used on every request, to choose which
> controller to dispatch the request to. Thanks to macros, Phoenix compiles
> all of your routes to a single case-statement with pattern matching rules,
> which is heavily optimized by the Erlang VM
>
> * For each route you define, we also define metadata to implement `Phoenix.VerifiedRoutes`.
> As we will soon learn, verified routes allows to us to reference any route
> as if it is a plain looking string, except it is verified by the compiler
> to be valid (making it much harder to ship broken links, forms, mails, etc
> to production)
>
> In other words, the router relies on macros to build applications that are
> faster and safer. Also remember that macros in Elixir are compile-time only,
> which gives plenty of stability after the code is compiled. Phoenix also provides
> introspection for all defined routes via `mix phx.routes`.
## Generating routes
For generating routes inside your application, see the `Phoenix.VerifiedRoutes`
documentation for `~p` based route generation which is the preferred way to
generate route paths and URLs with compile-time verification.
Phoenix also supports generating function helpers, which was the default
mechanism in Phoenix v1.6 and earlier. we will explore it next.
mechanism in Phoenix v1.6 and earlier. We will explore it next.
### Helpers
### Helpers (deprecated)
Phoenix generates a module `Helpers` inside your router by default, which contains
named helpers to help developers generate and keep their routes up to date.
Expand Down Expand Up @@ -367,30 +394,52 @@ defmodule Phoenix.Router do
opts = resource.route

if resource.singleton do
Enum.each resource.actions, fn
:show -> get path, ctrl, :show, opts
:new -> get path <> "/new", ctrl, :new, opts
:edit -> get path <> "/edit", ctrl, :edit, opts
:create -> post path, ctrl, :create, opts
:delete -> delete path, ctrl, :delete, opts
:update ->
Enum.each(resource.actions, fn
:show ->
get path, ctrl, :show, opts

:new ->
get path <> "/new", ctrl, :new, opts

:edit ->
get path <> "/edit", ctrl, :edit, opts

:create ->
post path, ctrl, :create, opts

:delete ->
delete path, ctrl, :delete, opts

:update ->
patch path, ctrl, :update, opts
put path, ctrl, :update, Keyword.put(opts, :as, nil)
end
put path, ctrl, :update, Keyword.put(opts, :as, nil)
end)
else
param = resource.param

Enum.each resource.actions, fn
:index -> get path, ctrl, :index, opts
:show -> get path <> "/:" <> param, ctrl, :show, opts
:new -> get path <> "/new", ctrl, :new, opts
:edit -> get path <> "/:" <> param <> "/edit", ctrl, :edit, opts
:create -> post path, ctrl, :create, opts
:delete -> delete path <> "/:" <> param, ctrl, :delete, opts
:update ->
Enum.each(resource.actions, fn
:index ->
get path, ctrl, :index, opts

:show ->
get path <> "/:" <> param, ctrl, :show, opts

:new ->
get path <> "/new", ctrl, :new, opts

:edit ->
get path <> "/:" <> param <> "/edit", ctrl, :edit, opts

:create ->
post path, ctrl, :create, opts

:delete ->
delete path <> "/:" <> param, ctrl, :delete, opts

:update ->
patch path <> "/:" <> param, ctrl, :update, opts
put path <> "/:" <> param, ctrl, :update, Keyword.put(opts, :as, nil)
end
put path <> "/:" <> param, ctrl, :update, Keyword.put(opts, :as, nil)
end)
end
end
end
Expand All @@ -399,7 +448,10 @@ defmodule Phoenix.Router do
@doc false
def __call__(
%{private: %{phoenix_router: router, phoenix_bypass: {router, pipes}}} = conn,
metadata, prepare, pipeline, _
metadata,
prepare,
pipeline,
_
) do
conn = prepare.(conn, metadata)

Expand Down Expand Up @@ -472,13 +524,13 @@ defmodule Phoenix.Router do
def call(conn, _opts) do
%{method: method, path_info: path_info, host: host} = conn = prepare(conn)

# TODO: Remove try/catch on Elixir v1.13 as decode no longer raises
decoded =
# TODO: Remove try/catch on Elixir v1.13 as decode no longer raises
try do
Enum.map(path_info, &URI.decode/1)
rescue
ArgumentError ->
raise MalformedURIError, "malformed URI path: #{inspect conn.request_path}"
raise MalformedURIError, "malformed URI path: #{inspect(conn.request_path)}"
end

case __match_route__(decoded, method, host) do
Expand All @@ -490,7 +542,7 @@ defmodule Phoenix.Router do
end
end

defoverridable [init: 1, call: 2]
defoverridable init: 1, call: 2
end
end

Expand Down Expand Up @@ -616,9 +668,9 @@ defmodule Phoenix.Router do
quote line: route.line do
def __match_route__(unquote(path), unquote(verb_match), unquote(host)) do
{unquote(build_metadata(route, path_params)),
fn var!(conn, :conn), %{path_params: var!(path_params, :conn)} -> unquote(prepare) end,
&unquote(Macro.var(pipe_name, __MODULE__))/1,
unquote(dispatch)}
fn var!(conn, :conn), %{path_params: var!(path_params, :conn)} ->
unquote(prepare)
end, &(unquote(Macro.var(pipe_name, __MODULE__)) / 1), unquote(dispatch)}
end
end
end
Expand Down Expand Up @@ -669,7 +721,7 @@ defmodule Phoenix.Router do
end

defp build_pipes(name, pipe_through) do
plugs = pipe_through |> Enum.reverse |> Enum.map(&{&1, [], true})
plugs = pipe_through |> Enum.reverse() |> Enum.map(&{&1, [], true})
opts = [init_mode: Phoenix.plug_init_mode(), log_on_halt: :debug]
{conn, body} = Plug.Builder.compile(__ENV__, plugs, opts)

Expand Down Expand Up @@ -733,15 +785,15 @@ defmodule Phoenix.Router do
defp add_route(kind, verb, path, plug, plug_opts, options) do
quote do
@phoenix_routes Scope.route(
__ENV__.line,
__ENV__.module,
unquote(kind),
unquote(verb),
unquote(path),
unquote(plug),
unquote(plug_opts),
unquote(options)
)
__ENV__.line,
__ENV__.module,
unquote(kind),
unquote(verb),
unquote(path),
unquote(plug),
unquote(plug_opts),
unquote(options)
)
end
end

Expand Down Expand Up @@ -786,8 +838,9 @@ defmodule Phoenix.Router do
compiler =
quote unquote: false do
Scope.pipeline(__MODULE__, plug)
{conn, body} = Plug.Builder.compile(__ENV__, @phoenix_pipeline,
init_mode: Phoenix.plug_init_mode())

{conn, body} =
Plug.Builder.compile(__ENV__, @phoenix_pipeline, init_mode: Phoenix.plug_init_mode())

def unquote(plug)(unquote(conn), _) do
try do
Expand All @@ -800,6 +853,7 @@ defmodule Phoenix.Router do
Plug.Conn.WrapperError.reraise(unquote(conn), :error, reason, __STACKTRACE__)
end
end

@phoenix_pipeline nil
end

Expand All @@ -823,7 +877,7 @@ defmodule Phoenix.Router do

quote do
if pipeline = @phoenix_pipeline do
@phoenix_pipeline [{unquote(plug), unquote(opts), true}|pipeline]
@phoenix_pipeline [{unquote(plug), unquote(opts), true} | pipeline]
else
raise "cannot define plug at the router level, plug must be defined inside a pipeline"
end
Expand Down Expand Up @@ -961,32 +1015,32 @@ defmodule Phoenix.Router do
"""
defmacro resources(path, controller, opts, do: nested_context) do
add_resources path, controller, opts, do: nested_context
add_resources(path, controller, opts, do: nested_context)
end

@doc """
See `resources/4`.
"""
defmacro resources(path, controller, do: nested_context) do
add_resources path, controller, [], do: nested_context
add_resources(path, controller, [], do: nested_context)
end

defmacro resources(path, controller, opts) do
add_resources path, controller, opts, do: nil
add_resources(path, controller, opts, do: nil)
end

@doc """
See `resources/4`.
"""
defmacro resources(path, controller) do
add_resources path, controller, [], do: nil
add_resources(path, controller, [], do: nil)
end

defp add_resources(path, controller, options, do: context) do
scope =
if context do
quote do
scope resource.member, do: unquote(context)
scope(resource.member, do: unquote(context))
end
end

Expand Down Expand Up @@ -1098,18 +1152,20 @@ defmodule Phoenix.Router do
defmacro scope(path, alias, options, do: context) do
alias = expand_alias(alias, __CALLER__)

options = quote do
unquote(options)
|> Keyword.put(:path, unquote(path))
|> Keyword.put(:alias, unquote(alias))
end
options =
quote do
unquote(options)
|> Keyword.put(:path, unquote(path))
|> Keyword.put(:alias, unquote(alias))
end

do_scope(options, context)
end

defp do_scope(options, context) do
quote do
Scope.push(__MODULE__, unquote(options))

try do
unquote(context)
after
Expand Down

0 comments on commit 05825d1

Please sign in to comment.