Seguimos la idea del post anterior: Phoenix adopta el diseño de enchufe o plugs para funcionalidad componible.
Por ejemplo los Endpoints estan organizados por plugs:
defmodule HelloWeb.Endpoint do
...
plug :introspect
plug HelloWeb.Router
Los conectores Endpoint predeterminados hacen bastante trabajo. Aquí están en orden:
Plug.Static: sirve activos estáticos. Dado que este complemento viene antes que el registrador, las solicitudes de activos estáticos no se registran.
Phoenix.LiveDashboard.RequestLogger: configura el registrador de solicitudes para Phoenix LiveDashboard, esto le permitirá tener la opción de pasar un parámetro de consulta para transmitir registros de solicitudes o habilitar/deshabilitar una cookie que transmite registros de solicitudes desde su tablero.
Plug.RequestId: genera un ID de solicitud único para cada solicitud.
Plug.Telemetry: agrega puntos de instrumentación para que Phoenix pueda registrar la ruta de la solicitud, el código de estado y la hora de la solicitud de forma predeterminada.
Plug.Parsers: analiza el cuerpo de la solicitud cuando hay disponible un analizador conocido. De forma predeterminada, este complemento puede manejar contenido codificado en URL, de varias partes y JSON (con Jason). El cuerpo de la solicitud se deja intacto si el tipo de contenido de la solicitud no se puede analizar.
Plug.MethodOverride: convierte el método de solicitud en PUT, PATCH o DELETE para solicitudes POST con un parámetro _method válido.
Plug.Head: convierte las solicitudes HEAD en solicitudes GET y elimina el cuerpo de la respuesta
Plug.Session: un complemento que configura la gestión de sesiones. Tenga en cuenta que fetch_session/2 aún debe llamarse explícitamente antes de usar la sesión, ya que este complemento solo configura cómo se obtiene la sesión.
En el medio del endpoint, también hay un bloque condicional:
if code_reloading? do
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :hello
end
Este bloque solo se ejecuta en desarrollo. Permite:
- recarga en vivo: si cambia un archivo CSS, se actualizan en el navegador sin actualizar la página;
- recarga de código, para que podamos ver los cambios en nuestra aplicación sin reiniciar el servidor;
- verificar el estado del repositorio, lo que garantiza que nuestra base de datos esté actualizada y, de lo contrario, genera un error legible y procesable.
En el enrutador, podemos declarar enchufes dentro de tuberías:
defmodule HelloWeb.Router do
use HelloWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, {HelloWeb.LayoutView, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
plug HelloWeb.Plugs.Locale, "en"
end
scope "/", HelloWeb do
pipe_through :browser
get "/", PageController, :index
end
Las rutas se definen dentro de los ámbitos y los ámbitos pueden canalizarse a través de múltiples canalizaciones. Una vez que una ruta coincide, Phoenix invoca todos los complementos definidos en todas las tuberías asociadas a esa ruta. Por ejemplo, acceder a "/" se canalizará a través de la canalización del navegador, invocando en consecuencia todos sus conectores.
Como veremos en la guía de enrutamiento, las tuberías en sí mismas son tapones. Allí, también discutiremos todos los complementos en la tubería :browser.
Finalmente, los controladores también son enchufes, por lo que podemos hacer:
defmodule HelloWeb.PageController do
use HelloWeb, :controller
plug HelloWeb.Plugs.Locale, "en"
En particular, los complementos del controlador brindan una función que nos permite ejecutar complementos solo dentro de ciertas acciones. Por ejemplo, puedes hacer:
defmodule HelloWeb.PageController do
use HelloWeb, :controller
plug HelloWeb.Plugs.Locale, "en" when action in [:index]
Y el complemento solo se ejecutará para la acción de índice.
Al cumplir con el contrato de conexión, convertimos una solicitud de aplicación en una serie de transformaciones explícitas. No se detiene ahí. Para ver realmente qué tan efectivo es el diseño de Plug, imaginemos un escenario en el que necesitamos verificar una serie de condiciones y luego redirigir o detenernos si una condición falla. Sin enchufe, terminaríamos con algo como esto:
defmodule HelloWeb.MessageController do
use HelloWeb, :controller
def show(conn, params) do
case Authenticator.find_user(conn) do
{:ok, user} ->
case find_message(params["id"]) do
nil ->
conn |> put_flash(:info, "That message wasn't found") |> redirect(to: ~p"/")
message ->
if Authorizer.can_access?(user, message) do
render(conn, :show, page: message)
else
conn |> put_flash(:info, "You can't access that page") |> redirect(to: ~p"/")
end
end
:error ->
conn |> put_flash(:info, "You must be logged in") |> redirect(to: ~p"/")
end
end
end
¿Observa cómo unos pocos pasos de autenticación y autorización requieren anidamiento y duplicación complicados? Mejoremos esto con un par de enchufes.
defmodule HelloWeb.MessageController do
use HelloWeb, :controller
plug :authenticate
plug :fetch_message
plug :authorize_message
def show(conn, params) do
render(conn, :show, page: conn.assigns[:message])
end
defp authenticate(conn, _) do
case Authenticator.find_user(conn) do
{:ok, user} ->
assign(conn, :user, user)
:error ->
conn |> put_flash(:info, "You must be logged in") |> redirect(to: ~p"/") |> halt()
end
end
defp fetch_message(conn, _) do
case find_message(conn.params["id"]) do
nil ->
conn |> put_flash(:info, "That message wasn't found") |> redirect(to: ~p"/") |> halt()
message ->
assign(conn, :message, message)
end
end
defp authorize_message(conn, _) do
if Authorizer.can_access?(conn.assigns[:user], conn.assigns[:message]) do
conn
else
conn |> put_flash(:info, "You can't access that page") |> redirect(to: ~p"/") |> halt()
end
end
end
Para que todo esto funcione, convertimos los bloques de código anidados y usamos halt(conn) cada vez que llegamos a una ruta de error. La funcionalidad halt(conn) es esencial aquí: le dice a Plug que no se debe invocar el siguiente plug.
Al final del día, al reemplazar los bloques de código anidados con una serie aplanada de transformaciones de complemento, podemos lograr la misma funcionalidad de una manera mucho más componible, clara y reutilizable.