171 lines
5.2 KiB
Markdown
171 lines
5.2 KiB
Markdown
# Phoenix Deviations from Elixir Core
|
|
|
|
Where Phoenix deliberately differs from Elixir core patterns and why.
|
|
|
|
## 1. Heavy Macro Usage for Performance
|
|
|
|
**Elixir core philosophy:** Keep macro usage minimal. From the Router source:
|
|
|
|
> Phoenix does its best to keep the usage of macros low.
|
|
|
|
**Phoenix deviation:** The Router uses macros extensively.
|
|
|
|
**Source:** `lib/phoenix/router.ex:106-128`
|
|
|
|
> We use `get`, `post`, `put`, and `delete` to define your routes. We use macros
|
|
> for two purposes:
|
|
>
|
|
> * They define the routing engine... Phoenix compiles all of your routes to a
|
|
> single case-statement with pattern matching rules
|
|
>
|
|
> * For each route you define, we also define metadata to implement
|
|
> `Phoenix.VerifiedRoutes`
|
|
|
|
**Why the deviation:** Performance. Elixir core uses macros sparingly because they add cognitive complexity. Phoenix justifies them because routing is the hottest path in a web app — compile-time optimization yields measurable request/second gains.
|
|
|
|
---
|
|
|
|
## 2. `import` without Restriction in Router
|
|
|
|
**Elixir core pattern:** Always use `import Module, only: [...]` to be explicit.
|
|
|
|
**Phoenix deviation:** The Router imports entire modules:
|
|
|
|
**Source:** `lib/phoenix/router.ex:303-306`
|
|
|
|
```elixir
|
|
import Phoenix.Router
|
|
|
|
# TODO v2: No longer automatically import dependencies
|
|
import Plug.Conn
|
|
import Phoenix.Controller
|
|
```
|
|
|
|
**Why the deviation:** The Router is a DSL. Users need `get`, `post`, `pipe_through`, `scope`, `resources`, `plug`, `fetch_session`, etc. — all available without qualification. Restricting imports would make the DSL unusable.
|
|
|
|
---
|
|
|
|
## 3. Compile-Time State Accumulation
|
|
|
|
**Elixir core pattern:** Modules are generally stateless during compilation. Functions are defined and that's it.
|
|
|
|
**Phoenix deviation:** Aggressive use of module attribute accumulation.
|
|
|
|
**Source:** `lib/phoenix/router.ex:297-312`
|
|
|
|
```elixir
|
|
defp prelude(opts) do
|
|
quote do
|
|
Module.register_attribute(__MODULE__, :phoenix_routes, accumulate: true)
|
|
@phoenix_helpers Keyword.get(unquote(opts), :helpers, true)
|
|
|
|
import Phoenix.Router
|
|
import Plug.Conn
|
|
import Phoenix.Controller
|
|
|
|
# Set up initial scope
|
|
@phoenix_pipeline nil
|
|
Phoenix.Router.Scope.init(__MODULE__)
|
|
@before_compile unquote(__MODULE__)
|
|
end
|
|
end
|
|
```
|
|
|
|
**Why the deviation:** The Router needs to collect ALL routes, then compile them into a single dispatch function. This requires building up state during module compilation, then consuming it all at `@before_compile`.
|
|
|
|
---
|
|
|
|
## 4. Channel Restart Strategy: `:temporary`
|
|
|
|
**Elixir core GenServer default:** `:permanent` (always restart).
|
|
|
|
**Phoenix Channel default:** `:temporary` (never restart).
|
|
|
|
**Source:** `lib/phoenix/channel.ex:464-472`
|
|
|
|
```elixir
|
|
def child_spec(init_arg) do
|
|
%{
|
|
id: __MODULE__,
|
|
start: {__MODULE__, :start_link, [init_arg]},
|
|
shutdown: @phoenix_shutdown,
|
|
restart: :temporary
|
|
}
|
|
end
|
|
```
|
|
|
|
**Why the deviation:** A crashed channel should NOT auto-restart — the client needs to explicitly reconnect and rejoin. Auto-restarting would create a channel without a connected client, which is meaningless.
|
|
|
|
---
|
|
|
|
## 5. Auto-Hibernation
|
|
|
|
**Elixir core GenServer:** No default hibernation — processes stay in memory.
|
|
|
|
**Phoenix Channel:** Defaults to hibernate after 15 seconds of inactivity.
|
|
|
|
**Source:** `lib/phoenix/channel.ex:459`
|
|
|
|
```elixir
|
|
@phoenix_hibernate_after Keyword.get(opts, :hibernate_after, 15_000)
|
|
```
|
|
|
|
```elixir
|
|
def start_link(triplet) do
|
|
GenServer.start_link(Phoenix.Channel.Server, triplet,
|
|
hibernate_after: @phoenix_hibernate_after
|
|
)
|
|
end
|
|
```
|
|
|
|
**Why the deviation:** Web apps have many idle connections. Channels for users who are "connected but not active" are common. Hibernation reclaims memory for the heap without killing the process. A chat app with 10,000 connected users benefits enormously.
|
|
|
|
---
|
|
|
|
## 6. `Plug.Builder` vs Raw Behaviour
|
|
|
|
**Elixir core:** Behaviours define contracts. Implementations are manual.
|
|
|
|
**Phoenix Endpoint:** Uses `Plug.Builder` — a macro that generates the `call/2` pipeline by chaining plugs at compile time.
|
|
|
|
**Source:** `lib/phoenix/endpoint.ex:478-480`
|
|
|
|
```elixir
|
|
defp plug() do
|
|
quote location: :keep do
|
|
use Plug.Builder, init_mode: Phoenix.plug_init_mode()
|
|
...
|
|
end
|
|
end
|
|
```
|
|
|
|
**Why the deviation:** The Plug specification (`init/1` + `call/2`) is too low-level for composing dozens of middleware. `Plug.Builder` provides the `plug` macro that chains them automatically. It's a higher-level abstraction over the raw behaviour pattern.
|
|
|
|
---
|
|
|
|
## 7. Exception Structs with HTTP Status Codes
|
|
|
|
**Elixir core exceptions:** Pure data — message, maybe some context fields.
|
|
|
|
**Phoenix exceptions:** Include `plug_status` for HTTP response mapping.
|
|
|
|
**Source:** `lib/phoenix/router.ex:2-26`
|
|
|
|
```elixir
|
|
defmodule NoRouteError do
|
|
@moduledoc """
|
|
Exception raised when no route is found.
|
|
"""
|
|
defexception plug_status: 404, message: "no route found", conn: nil, router: nil
|
|
end
|
|
|
|
defmodule MalformedURIError do
|
|
@moduledoc """
|
|
Exception raised when the URI is malformed on matching.
|
|
"""
|
|
defexception [:message, plug_status: 400]
|
|
end
|
|
```
|
|
|
|
**Why the deviation:** In a web context, exceptions need to map to HTTP status codes. Plug's error handling middleware reads `plug_status` to determine the response code. This bridges the gap between Elixir's exception system and HTTP semantics.
|