docs: idiomatic Elixir and Phoenix patterns with verified source citations
Extracted patterns from Elixir core and Phoenix source code with specific file:line citations, then verified all citations against the actual source in a second pass. Structure: - patterns/ — Elixir core patterns (GenServer, errors, data, types, etc.) - phoenix/ — Phoenix-specific patterns and deviations - comparison/ — Elixir vs Phoenix side-by-side - smells/ — Anti-patterns and common mistakes - changelog/ — Daily Elixir/Phoenix PR digest (auto-updated)
This commit is contained in:
+108
-9
@@ -42,7 +42,7 @@ The endpoint is four things composed together:
|
||||
|
||||
## 2. Router: Compile-Time Route Optimization
|
||||
|
||||
**Source:** `lib/phoenix/router.ex:109-123` (Why the macros? info block)
|
||||
**Source:** `lib/phoenix/router.ex:106-128` (Why the macros? info block)
|
||||
|
||||
> We use macros for two purposes:
|
||||
>
|
||||
@@ -53,7 +53,7 @@ The endpoint is four things composed together:
|
||||
>
|
||||
> * For each route you define, we also define metadata to implement `Phoenix.VerifiedRoutes`.
|
||||
|
||||
**Source:** `lib/phoenix/router.ex:280` (route accumulation)
|
||||
**Source:** `lib/phoenix/router.ex:299` (route accumulation)
|
||||
|
||||
```elixir
|
||||
Module.register_attribute(__MODULE__, :phoenix_routes, accumulate: true)
|
||||
@@ -67,7 +67,7 @@ Module.register_attribute(__MODULE__, :phoenix_routes, accumulate: true)
|
||||
|
||||
## 3. Pipeline and `pipe_through` for Request Processing
|
||||
|
||||
**Source:** `lib/phoenix/router.ex:230-260` (Pipelines and plugs section)
|
||||
**Source:** `lib/phoenix/router.ex:243-270` (Pipelines and plugs section)
|
||||
|
||||
```elixir
|
||||
pipeline :browser do
|
||||
@@ -109,7 +109,7 @@ Controllers:
|
||||
- Call domain logic (the Repo/context layer)
|
||||
- Render the result
|
||||
|
||||
**Source:** `lib/phoenix/controller.ex:1-3` (imports)
|
||||
**Source:** `lib/phoenix/controller.ex:1-5` (imports)
|
||||
|
||||
```elixir
|
||||
defmodule Phoenix.Controller do
|
||||
@@ -126,7 +126,7 @@ defmodule Phoenix.Controller do
|
||||
|
||||
## 5. Channel as GenServer with Topic-Based Routing
|
||||
|
||||
**Source:** `lib/phoenix/channel.ex:1-20` (topic pattern)
|
||||
**Source:** `lib/phoenix/channel.ex:1-25` (topic pattern)
|
||||
|
||||
```elixir
|
||||
channel "room:*", MyAppWeb.RoomChannel
|
||||
@@ -143,7 +143,7 @@ def join("room:" <> room_id, _payload, socket) do
|
||||
end
|
||||
```
|
||||
|
||||
**Source:** `lib/phoenix/channel.ex:476-479` (channels are GenServers)
|
||||
**Source:** `lib/phoenix/channel.ex:474-478` (channels are GenServers)
|
||||
|
||||
```elixir
|
||||
def start_link(triplet) do
|
||||
@@ -161,7 +161,7 @@ end
|
||||
|
||||
## 6. PubSub Integration via Endpoint
|
||||
|
||||
**Source:** `lib/phoenix/endpoint.ex:440-475` (pubsub macro)
|
||||
**Source:** `lib/phoenix/endpoint.ex:437-475` (pubsub macro)
|
||||
|
||||
```elixir
|
||||
def subscribe(topic, opts \\ []) when is_binary(topic) do
|
||||
@@ -186,7 +186,7 @@ end
|
||||
|
||||
## 7. Socket as Authentication Boundary
|
||||
|
||||
**Source:** `lib/phoenix/socket.ex` (connect callback pattern)
|
||||
**Source:** `lib/phoenix/socket.ex:1-30` (moduledoc: connect callback pattern)
|
||||
|
||||
```elixir
|
||||
defmodule MyAppWeb.UserSocket do
|
||||
@@ -210,7 +210,7 @@ end
|
||||
|
||||
## 8. Plug Pattern: `init/1` + `call/2`
|
||||
|
||||
**Source:** `lib/phoenix/router/route.ex:51-58`
|
||||
**Source:** `lib/phoenix/router/route.ex:47-57`
|
||||
|
||||
```elixir
|
||||
@doc "Used as a plug on forwarding"
|
||||
@@ -233,3 +233,102 @@ end
|
||||
This is Phoenix's fundamental composition pattern. Everything is a plug.
|
||||
|
||||
**Anti-pattern:** Doing expensive setup work in `call/2` instead of `init/1` — it runs on every request.
|
||||
|
||||
---
|
||||
|
||||
## 9. Telemetry Integration in Router Dispatch
|
||||
|
||||
**Source:** `lib/phoenix/router.ex:400-438` (telemetry instrumentation)
|
||||
|
||||
```elixir
|
||||
def __call__(conn, metadata, prepare, pipeline, {plug, opts}) do
|
||||
conn = prepare.(conn, metadata)
|
||||
start = System.monotonic_time()
|
||||
measurements = %{system_time: System.system_time()}
|
||||
metadata = %{metadata | conn: conn}
|
||||
:telemetry.execute([:phoenix, :router_dispatch, :start], measurements, metadata)
|
||||
|
||||
case pipeline.(conn) do
|
||||
%Plug.Conn{halted: true} = halted_conn ->
|
||||
measurements = %{duration: System.monotonic_time() - start}
|
||||
metadata = %{metadata | conn: halted_conn}
|
||||
:telemetry.execute([:phoenix, :router_dispatch, :stop], measurements, metadata)
|
||||
halted_conn
|
||||
|
||||
%Plug.Conn{} = piped_conn ->
|
||||
try do
|
||||
plug.call(piped_conn, plug.init(opts))
|
||||
else
|
||||
conn ->
|
||||
measurements = %{duration: System.monotonic_time() - start}
|
||||
metadata = %{metadata | conn: conn}
|
||||
:telemetry.execute([:phoenix, :router_dispatch, :stop], measurements, metadata)
|
||||
conn
|
||||
rescue
|
||||
e in Plug.Conn.WrapperError ->
|
||||
measurements = %{duration: System.monotonic_time() - start}
|
||||
:telemetry.execute([:phoenix, :router_dispatch, :exception], measurements, metadata)
|
||||
Plug.Conn.WrapperError.reraise(e)
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Source:** `lib/phoenix/logger.ex:7-50` (telemetry event catalog)
|
||||
|
||||
Phoenix emits these telemetry events:
|
||||
- `[:phoenix, :endpoint, :init]` — endpoint supervision tree started
|
||||
- `[:phoenix, :endpoint, :start]` — request begins (via `Plug.Telemetry`)
|
||||
- `[:phoenix, :endpoint, :stop]` — response sent
|
||||
- `[:phoenix, :router_dispatch, :start]` — route dispatch begins
|
||||
- `[:phoenix, :router_dispatch, :stop]` — route dispatch succeeds
|
||||
- `[:phoenix, :router_dispatch, :exception]` — route dispatch raises
|
||||
- `[:phoenix, :socket_connected]` — socket connection established
|
||||
|
||||
**Why:** Telemetry is baked into every request path. The router wraps ALL dispatches in start/stop/exception telemetry, enabling monitoring, tracing (OpenTelemetry), and logging without modifying application code.
|
||||
|
||||
**Anti-pattern:** Manual timing/logging in controllers — telemetry provides this automatically at the infrastructure level.
|
||||
|
||||
---
|
||||
|
||||
## 10. ConnTest Pattern: Endpoint-Based Integration Testing
|
||||
|
||||
**Source:** `lib/phoenix/test/conn_test.ex:1-30` (moduledoc)
|
||||
|
||||
```elixir
|
||||
@endpoint MyAppWeb.Endpoint
|
||||
|
||||
test "says welcome on the home page" do
|
||||
conn = get(build_conn(), "/")
|
||||
assert conn.resp_body =~ "Welcome!"
|
||||
end
|
||||
|
||||
test "logs in" do
|
||||
conn = post(build_conn(), "/login", [username: "john", password: "doe"])
|
||||
assert conn.resp_body =~ "Logged in!"
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** `Phoenix.ConnTest` tests against the full endpoint stack (plugs, router, controller) without starting an HTTP server. `build_conn()` creates a test connection, HTTP verb functions dispatch through the endpoint. This gives integration-level confidence with unit-test speed.
|
||||
|
||||
**Anti-pattern:** Testing controllers in isolation without the plug pipeline — you miss middleware bugs (auth, CSRF, sessions).
|
||||
|
||||
---
|
||||
|
||||
## 11. ChannelTest Pattern: Process-Based Channel Testing
|
||||
|
||||
**Source:** `lib/phoenix/test/channel_test.ex:1-30` (moduledoc)
|
||||
|
||||
```elixir
|
||||
{:ok, _, socket} =
|
||||
socket(UserSocket, "user:id", %{some_assigns: 1})
|
||||
|> subscribe_and_join(RoomChannel, "room:lobby", %{"id" => 3})
|
||||
|
||||
# Or using connect/3 to call your UserSocket.connect callback:
|
||||
{:ok, socket} = connect(UserSocket, %{"some" => "params"}, %{})
|
||||
{:ok, _, socket} = subscribe_and_join(socket, "room:lobby", %{"id" => 3})
|
||||
```
|
||||
|
||||
**Why:** Channel tests communicate via messages (not HTTP). `subscribe_and_join/4` connects a test process to the channel, and you can assert on broadcasts (`assert_broadcast`), pushes (`assert_push`), and replies (`assert_reply`). The test process subscribes to the same PubSub topic, so it sees everything the channel broadcasts.
|
||||
|
||||
**Anti-pattern:** Testing channels by connecting real WebSocket clients — too slow, too brittle, tests the transport layer unnecessarily.
|
||||
|
||||
Reference in New Issue
Block a user