Phoenix
12 Nov 2016Phoenix notes based on “Programming Phoenix” book by Chris McCord and official guides.
endpoints
https://hexdocs.pm/phoenix/routing.html#path-helpers:
Think of endpoints as the entity that handles requests just up to the point where the router takes over. That includes starting the app/server, applying configuration, and applying the plugs common to all requests.
https://hexdocs.pm/phoenix/endpoint.html:
Phoenix applications starts the HelloWeb.Endpoint as a supervised process. By default, the Endpoint is added to the supervision tree in lib/hello/application.ex as a supervised process. Each request begins and ends its lifecycle inside your application in an endpoint. The endpoints handles starting the web server and transforming requests through several defined plugs before calling the Router.
Endpoints gather together common functionality and serve as entrance and exit for all of the HTTP requests to your application. The endpoint holds plugs that are common to all requests coming into your application.
If code reloading is enabled, a socket will be used to communicate to the browser that the page needs to be reloaded when code is changed on the server.
It is also possible for an application to have multiple endpoints, each with its own supervision tree.
routes
- Routes which begin with an HTTP verb name expand to a single clause of the match function.
- Routes which begin with ‘resources’ expand to 8 clauses of the match function.
- Resources may restrict the number of match function clauses by using the only: or except: options.
- Any of these routes may be nested.
- Any of these routes may be scoped to a given path.
- Using the as: helper option in a scope can reduce duplication.
- Using the as: helper option for scoped routes eliminates unreachable paths.
as:
option
passing as:
for scope
option allows to change the name of path helpers
for all routes inside scope
block:
scope "/v1", as: :v1 do
get "/cards", CardController
# other routes
end
or else it’s possible to do it on per route basis by passing as:
option
to individual routes:
scope "/v1" do
get "/cards", CardController, as: :v1
# other routes
end
singleton routes (singular resources in Rails)
resources "/sessions", SessionController, singleton: true
=
resource :session
scopes
Note that Phoenix will assume that the path we set ought to begin with a slash, so scope “/admin” do and scope “admin” do will both produce the same results.
according to my experience this is not true:
scope "/", BillingWeb do
pipe_through :api
scope "v1" do
# ...
end
end
$ mix phx.routes
warning: router paths should begin with a forward slash, got: "v1"
accepts
plug
accepts
plug in router lists MIME types supported in Accept
request header - it doesn’t work with Content-Type
request header.
if MIME type is not specified in Accept
request header response is
still rendered as usual - only when unsupported MIME type is explicitly
set in Accept
request header is error raised (rendering error page
in Markdown for some reason):
12:02:59.285 [debug] ** (Phoenix.NotAcceptableError) no supported media type in accept header.
Expected one of ["json"] but got the following formats:
* "application/html" with extensions: []
plugs
The basic idea of Plug is to unify the concept of a “connection” that we operate on. This differs from other HTTP middleware layers such as Rack, where the request and response are separated in the middleware stack.
module plug:
The module only needs to implement two functions:
- init/1 which initializes any arguments or options to be passed to call/2
- call/2 which carries out the connection transformation. call/2 is just a function plug that we saw earlier
controllers
Phoenix controllers also build on the Plug package, and are themselves plugs.
A step beyond this is rendering pure JSON with the json/2 function. We need to pass it something that the Poison library can parse into JSON, such as a map.
It is worth noting that the text/2, json/2, and html/2 functions require neither a Phoenix view, nor a template to render.
Phoenix allows us to change formats on the fly with the _format query string parameter.
if no format is specified explicitly with _format
query parameter it’s
considered to be the first one accepted by current pipeline in router
(say, json
format for request that is processed with api
pipeline).
NOTE: all keys and values if incoming params are strings!
scrubbing params
- https://stackoverflow.com/a/33976964/3632318
- https://hexdocs.pm/phoenix/Phoenix.Controller.html#scrub_params/2
- checks to see if the required_key is present (=
params.require(:user)
in Rails) - changes empty parameters (empty strings) of required_key (recursively) to nils
it doesn’t make sense to use it when providing API.
status codes
https://hexdocs.pm/plug/Plug.Conn.Status.html:
:continue
- 100:switching_protocols
- 101:processing
- 102:ok
- 200:created
- 201:accepted
- 202:non_authoritative_information
- 203:no_content
- 204:reset_content
- 205:partial_content
- 206:multi_status
- 207:already_reported
- 208:im_used
- 226:multiple_choices
- 300:moved_permanently
- 301:found
- 302:see_other
- 303:not_modified
- 304:use_proxy
- 305:switch_proxy
- 306:temporary_redirect
- 307:permanent_redirect
- 308:bad_request
- 400:unauthorized
- 401:payment_required
- 402:forbidden
- 403:not_found
- 404:method_not_allowed
- 405:not_acceptable
- 406:proxy_authentication_required
- 407:request_timeout
- 408:conflict
- 409:gone
- 410:length_required
- 411:precondition_failed
- 412:request_entity_too_large
- 413:request_uri_too_long
- 414:unsupported_media_type
- 415:requested_range_not_satisfiable
- 416:expectation_failed
- 417:im_a_teapot
- 418:misdirected_request
- 421:unprocessable_entity
- 422:locked
- 423:failed_dependency
- 424:unordered_collection
- 425:upgrade_required
- 426:precondition_required
- 428:too_many_requests
- 429:request_header_fields_too_large
- 431:internal_server_error
- 500:not_implemented
- 501:bad_gateway
- 502:service_unavailable
- 503:gateway_timeout
- 504:http_version_not_supported
- 505:variant_also_negotiates
- 506:insufficient_storage
- 507:loop_detected
- 508:not_extended
- 510:network_authentication_required
- 511
https://stackoverflow.com/a/2342589/3632318:
For a PUT request: HTTP 200 (OK) or HTTP 204 (No Content) should imply “resource updated successfully”.
For a DELETE request: HTTP 200 (OK) or HTTP 204 (No Content) should imply “resource deleted successfully”. HTTP 202 can also be returned which would imply that the instruction was accepted by the server and the “resource was marked for deletion”.
send response without content (head)
- https://github.com/phoenixframework/phoenix/pull/818
- https://stackoverflow.com/a/37318445/3632318
- https://hexdocs.pm/plug/1.4.5/Plug.Conn.html#send_resp/3
- https://medium.com/@kaisersly/render-different-formats-in-phoenix-2nd-attempt-8775a289ebb1
Rails version: head(:ok)
. send empty body in Phoenix:
send_resp(conn, :ok, "")
views
If you are familiar with decorators or the facade pattern, this is similar.
Note that we didn’t need to fully qualify title/0 with HelloWeb.LayoutView because our LayoutView actually does the rendering (it’s a local function call). In fact, “templates” in Phoenix area really just function definitions on their view module.
At compile-time, Phoenix precompiles all *.html.eex templates and turns them into render/2 function clauses on their respective view modules. At runtime, all templates are already loaded in memory.
say, we can replace page/index.html.eex with the following render/2
function:
defmodule HelloWeb.PageView do
use HelloWeb, :view
def render("index.html", assigns) do
# html string with interpolated variables from `assigns` map
end
end
NOTE: there’re render/2
and render/3
functions - the former implies
current view while the latter states the view explicitly.
so any template will be precompiled into corresponding render/2
function inside
view. also it’s possible to create such function manually - say, in order to render
different template or return text instead as it’s done in ErrorView
module:
defmodule HelloWeb.ErrorView do
use HelloWeb, :view
def render("404.html", _assigns) do
"Page not found"
end
end
I guess if you create such function:
def render("404.html", assigns) do
render("404.html", assigns)
end
you’ll end up with an infinite loop.
rendering in IEx
we can render any template manually in IEx:
iex> Phoenix.View.render(HelloWeb.PageView, "test.html", %{})
that’s how any template is rendered inside layout:
<%= render @view_module, @view_template, assigns %>
rendering JSON
Phoenix uses Poison to encode Maps to JSON, so all we need to do in our views is format the data we’d like to respond with as a Map, and Phoenix will do the rest. It is possible to respond with JSON back directly from the controller and skip the View. However, if we think about a controller as having the responsibilities of receiving a request and fetching data to be sent back, data manipulation and formatting don’t fall under those responsibilities.
rendering errors
- https://hexdocs.pm/phoenix/Phoenix.Endpoint.html
- https://github.com/phoenixframework/phoenix/issues/1945
for error template to be rendered correctly:
-
debug pages must be disabled (default for production)
config/prod.exs (no changes):
config :billing, BillingWeb.Endpoint, # ... debug_errors: false, # ...
-
request format must be supported in endpoint configuration
request format is determined using either
Accept
request header or extension in request url (say,.json
).config/config.exs:
config :billing, BillingWeb.Endpoint, # ... render_errors: [view: BillingWeb.ErrorView, accepts: ~w(json)],
if request format is not supported,
html
format will be used as fallback and the following warning will be printed in log:[warn] Could not render errors due to no supported media type in accept header. ... Errors will be rendered using the first accepted format "html" as fallback. Please customize the :accepts option under the :render_errors configuration in your endpoint if you want to support other formats or choose another fallback
-
ErrorView
must have correspondingrender/2
clausehttps://hexdocs.pm/phoenix/custom_errors.html:
Any errors which don’t match an existing clause of render/2 will be caught by template_not_found/2.
rendering both HTML and JSON errors
-
support both request formats in endpoint configuration
config/config.exs:
config :billing, BillingWeb.Endpoint, # ... render_errors: [view: BillingWeb.ErrorView, accepts: ~w(html json)],
-
modify
ErrorView
to process both formatslib/billing_web/views/error_view.ex:
defmodule BillingWeb.ErrorView do use BillingWeb, :view def render("404.html", _assigns) do "Page not found" end # ... def render("404.json", _assigns) do %{error: "Resource not found"} end # ... def template_not_found(template, assigns) do extname = Path.extname(template) render("500#{extname}", assigns) end end
debug pages vs. error templates (ErrorView)
-
dev
environmenthttps://stackoverflow.com/questions/30536979 (Jose Valim):
In :dev, the default is to show debug pages, because the majority of times you have an error while building your code. If you want to see the actually rendered error page, you need to set debug_errors: false in your config/dev.exs.
in
dev
environment debug pages are rendered by default (nice pages a laBetter Errors
) instead of actual error templates (that isErrorView
is ignored at all) because ofdebug_errors: true
in config.dev.exs:config :billing, BillingWeb.Endpoint, # ... debug_errors: true, # ...
set it to
false
to render errors like in production. -
prod
environmenthttps://stackoverflow.com/questions/30536979 (Jose Valim):
In :prod, the default is to render error pages, so you should see a page rendered by YourApp.ErrorView with the status code.
In :test, it works like production…
in all other environments but
dev
onedebug_errors
option is set tofalse
so actual error templates are rendered usingErrorView
.
templates
http://www.jeramysingleton.com/phoenix-templates-are-just-functions/:
At compile time,
def render("index.html", assigns), do: # your compiled eex template here
would be injected into your moduleIt is exactly the same as if you wrote the function yourself
https://hexdocs.pm/phoenix/adding_pages.html:
in templates properties from assigns
can be accessed with @
(it’s not module attribute in this case): @user
= assigns.user
.
https://hexdocs.pm/phoenix/templates.html:
The way we pass data into a template is by the assigns map, and the way we get the values out of the assigns map is by referencing the keys with a preceding @. @ is actually a macro that translates @key to Map.get(assigns, :key)
assets
- https://hexdocs.pm/phoenix/static_assets.html
- https://til.hashrocket.com/posts/1a3639476e-serve-static-filesdirectories-in-phoenix
- https://hexdocs.pm/phoenix/deployment.html#compiling-your-application-assets
I generated project with --no-brunch
option so first of all I had to
restore default configuration for Brunch and assets directory structure.
-
generate dummy project with Brunch support to copy missing parts from:
$ cd ~/tmp/ $ mix phx.new phoenix_with_brunch
- copy assets/ directory in full
-
amend .gitignore with new paths:
+ npm-debug.log + /assets/node_modules + /priv/static/
-
install npm packages:
$ cd assets/ $ npm install
-
build assets (generate digests and cache manifest):
$ cd assets/ $ node node_modules/brunch/bin/brunch build
or for production:
$ cd assets/ $ node node_modules/brunch/bin/brunch build --production
-
generate digest and cache manifest (for production only):
$ MIX_ENV=prod mix phx.digest