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:
Aaron Weiker
2026-04-29 22:59:17 -07:00
parent 4ea9a884aa
commit 2e7a822b6b
6 changed files with 845 additions and 406 deletions
+42 -10
View File
@@ -11,8 +11,8 @@ How the same concepts are approached differently (or similarly) between Elixir c
| **Process identity** | Registry `:via` tuples | Topic-based (channels identified by topic) |
| **Supervision** | Direct supervisor reference | Endpoint supervisor manages all |
**Source (Elixir):** `lib/elixir/lib/gen_server.ex:843-848` (child_spec without restart = defaults to :permanent)
**Source (Phoenix):** `lib/phoenix/channel.ex:470-475` (explicit restart: :temporary)
**Source (Elixir):** `lib/elixir/lib/gen_server.ex:911-919` (child_spec defaults to :permanent via Supervisor.child_spec)
**Source (Phoenix):** `lib/phoenix/channel.ex:464-472` (explicit restart: :temporary)
---
@@ -25,8 +25,8 @@ How the same concepts are approached differently (or similarly) between Elixir c
| **Failure response** | `{:error, reason}` tuple | `{:error, reason}` + HTTP status |
| **Recovery** | Supervisor restart | Client reconnection |
**Source (Elixir):** Standard tagged tuples throughout (`lib/elixir/lib/agent.ex:210`)
**Source (Phoenix):** `lib/phoenix/router.ex:7-8` (NoRouteError with plug_status: 404)
**Source (Elixir):** `lib/elixir/lib/agent.ex:187` (standard on_start type: `{:ok, pid} | {:error, ...}`)
**Source (Phoenix):** `lib/phoenix/router.ex:2-6` (NoRouteError with plug_status: 404)
---
@@ -39,8 +39,8 @@ How the same concepts are approached differently (or similarly) between Elixir c
| **Configuration** | Via `use Module, opts` | Via `use Module, opts` + module attributes |
| **Before-compile** | Rarely used | Heavily used (routes, intercepts) |
**Source (Elixir):** `lib/elixir/lib/gen_server.ex:834-851`
**Source (Phoenix):** `lib/phoenix/channel.ex:450-500`
**Source (Elixir):** `lib/elixir/lib/gen_server.ex:899-919` (__using__ generates child_spec + @behaviour)
**Source (Phoenix):** `lib/phoenix/channel.ex:450-485` (__using__ generates child_spec + behaviour + DSL setup)
---
@@ -53,8 +53,8 @@ How the same concepts are approached differently (or similarly) between Elixir c
| **DSL creation** | Avoided (except Kernel/SpecialForms) | Embraced (Router DSL) |
| **Attribute accumulation** | Rare | Central pattern (routes, sockets) |
**Source (Elixir):** `lib/elixir/lib/gen_server.ex:834` — simple `__using__`
**Source (Phoenix):** `lib/phoenix/router.ex:263-290` — complex DSL setup with attribute accumulation
**Source (Elixir):** `lib/elixir/lib/gen_server.ex:899` — simple `__using__` (behaviour + child_spec + defaults)
**Source (Phoenix):** `lib/phoenix/router.ex:288-312` — complex DSL setup with attribute accumulation, imports, and @before_compile
---
@@ -80,8 +80,8 @@ Both follow the same convention: public API on the parent module, implementation
| **State shape** | Any term (developer's choice) | `%Socket{}` struct (framework-defined) |
| **State access** | Direct in callbacks | Via `socket.assigns` |
**Source (Elixir):** `lib/elixir/lib/agent.ex:62-82` (compute in server vs client)
**Source (Phoenix):** `lib/phoenix/channel.ex:463-464` (`import Phoenix.Socket, only: [assign: 3, assign: 2]`)
**Source (Elixir):** `lib/elixir/lib/agent.ex:62-82` (compute in server vs client pattern)
**Source (Phoenix):** `lib/phoenix/channel.ex:463` (`import Phoenix.Socket, only: [assign: 3, assign: 2]`)
---
@@ -115,3 +115,35 @@ var!(code_reloading?) =
```
This pattern — reading config at compile time and validating it against runtime — is Phoenix-specific. Elixir core reads config only at runtime.
---
## Telemetry
| Aspect | Elixir Core | Phoenix |
|--------|-------------|---------|
| **Built-in events** | None (telemetry is a separate library) | Extensive event catalog |
| **Instrumentation** | Manual by library authors | Baked into router, endpoint, socket |
| **Event naming** | Varies by library | `[:phoenix, :component, :phase]` convention |
| **Logging** | `Logger` calls | Telemetry → Logger adapter (`Phoenix.Logger`) |
**Source (Phoenix):** `lib/phoenix/logger.ex:7-50` (telemetry event catalog)
**Source (Phoenix):** `lib/phoenix/router.ex:400-438` (telemetry in router dispatch)
Phoenix wraps every request dispatch in telemetry start/stop/exception events. This provides distributed tracing, monitoring, and logging without any application code changes.
---
## Testing
| Aspect | Elixir Core | Phoenix |
|--------|-------------|---------|
| **Test helper** | `ExUnit.Case` | `Phoenix.ConnTest`, `Phoenix.ChannelTest` |
| **Test subject** | Module functions | Endpoint (full plug pipeline) |
| **Communication** | Direct function calls | HTTP verbs (ConnTest), messages (ChannelTest) |
| **Isolation** | Process per test | Process per test + sandbox (Ecto) |
**Source (Phoenix):** `lib/phoenix/test/conn_test.ex:1-30` (endpoint-based integration testing)
**Source (Phoenix):** `lib/phoenix/test/channel_test.ex:1-30` (process-based channel testing)
Phoenix test helpers test at the integration level by default — `ConnTest` dispatches through the full plug pipeline, `ChannelTest` exercises the full channel lifecycle via message passing. This catches middleware bugs that unit tests miss.