The channel layer received significant features and an overhaul of the topic abstraction. Upgrade your 0.7.x channels should only require a few simple steps.
Notable changes:
- An updated version of
phoenix.js
is required, replace yourpriv/static/js/phoenix.js
with https://github.com/phoenixframework/phoenix/blob/v0.8.0/priv/static/js/phoenix.js - "topic" is now just an identifier. You join topics, broadcast on topics, etc. Channels are are dispatched to based on topic patterns in the router.
- Channel callbacks in 0.8.0 introduce the concept of outgoing events. Prior to 0.8.0, chanenls only processed incoming events via the
event/3
callbacks. In0.8.0
,event/3
has been renamed tohandle_in/3
, and outgoing events callbacks can be defined viahandle_out/3
- All channel callbacks, such as
join/3
,leave/2
,handle_in/3
, andhandle_out/3
now accept the socket as the last argument. This mimicks GenServer APIs - The return signature of
handle_in
,handle_out
, andleave
now requires either{:ok, socket} | {:leave, socket} | {:error, socket, reason}
. Previously onlysocket
could be returned. This new approach mirrors GenServer and allows {:leave, socket} to unsubscribe via any callback. Channel.terminate
has been removed
Example code upgrade from 0.7.x to 0.8.0:
# ============
# 0.7.x
# ============
# router
defmodule MyApp.Router do
use Phoenix.Router
use Phoenix.Router.Socket, mount: "/ws"
channel "rooms", MyApp.RoomChannel
...
end
# channel
defmodule MyApp.RoomChannel do
use Phoenix.Channel
def join(socket, "lobby", message) do
reply socket, "joined", %{status: "connected"}
{:ok, socket}
end
def join(socket, _private_topic, message) do
{:error, socket, :unauthorized}
end
def event(socket, "new:msg", message) do
broadcast socket, "new:msg", message
socket
end
end
# client js
socket.join("rooms", "lobby", {}, function(chan){ ... });
# ============
# 0.8.0
# ============
# router
defmodule MyApp.Router do
use Phoenix.Router
socket "/ws", MyApp do
channel "rooms:*", RoomChannel # match any topic starting with "rooms:"
end
...
end
# channel
defmodule MyApp.RoomChannel do
use Phoenix.Channel
def join("rooms:lobby", message, socket) do
reply socket, "joined", %{status: "connected"}
{:ok, socket}
end
# 'subtopics' can be easily matched using binary pattern matching
def join("rooms:" <> _private_topic, message, socket) do
{:error, socket, :unauthorized}
end
def handle_in("new:msg", message, socket) do
broadcast socket, "new:msg", message
{:ok, socket}
end
# optional, hook into outgoing new:msg for all sockets for customized per-socket reply
def handle_out("new:msg", message, socket) do
reply socket, "new:msg", Dict.merge(msg,
is_editable: User.can_edit_message?(socket.assigns[:user], msg)
)
{:ok, socket}
end
# client js
socket.join("rooms:lobby", {}, function(chan){ ... });
A read through the new Channel docs will help explain the usefulness of outgoing events:
After a client has successfully joined a channel, incoming events from the
client are routed through the channel's handle_in/3
callbacks. Within these
callbacks, you can perform any action. Typically you'll either foward a
message out to all listeners with Phoenix.Channel.broadcast/3
, or reply
directly to the socket with Phoenix.Channel.reply/3
.
Incoming callbacks must return the socket
to maintain ephemeral state.
Here's an example of receiving an incoming "new:msg"
event from a one client,
and broadcasting the message to all topic subscribers for this socket.
def handle_in("new:msg", %{"uid" => uid, "body" => body}, socket) do
broadcast socket, "new:msg", %{uid: uid, body: body}
{:ok, socket}
end
You can also send a reply directly to the socket:
# client asks for their current rank, reply sent directly as new event
def handle_in("current:rank", socket) do
reply socket, "current:rank", %{val: Game.get_rank(socket.assigns[:user])}
{:ok, socket}
end
When an event is broadcasted with Phoenix.Channel.broadcast/3
, each channel
subscribers' handle_out/3
callback is triggered where the event can be
relayed as is, or customized on a socket by socket basis to append extra
information, or conditionally filter the message from being delivered.
Note: broadcast/3
and reply/3
both return {:ok, socket}
.
def handle_in("new:msg", %{"uid" => uid, "body" => body}, socket) do
broadcast socket, "new:msg", %{uid: uid, body: body}
end
# for every socket subscribing on this topic, append an `is_editable`
# value for client metadata
def handle_out("new:msg", msg, socket) do
reply socket, "new:msg", Dict.merge(msg,
is_editable: User.can_edit_message?(socket.assigns[:user], msg)
)
end
# do not send broadcasted `"user:joined"` events if this socket's user
# is ignoring the user who joined
def handle_out("user:joined", msg, socket) do
if User.ignoring?(socket.assigns[:user], msg.user_id) do
{:ok, socket}
else
reply socket, "user:joined", msg
end
end
By default, unhandled outgoing events are forwarded to each client as a reply,
but you'll need to define the catch-all clause yourself once you define an
handle_out/3
clause.
Endpoints should now be explicitly started in your application supervision tree. Just add worker(YourApp.Endpoint, [])
to your supervision tree in lib/your_app.ex
mix phoenix.start
was renamed to mix phoenix.server
Additionally, YourApp.Endpoint.start/0
function was removed. You can simply remove it from your test/test_helper.ex
file.
Generated named paths now expect a conn arg. For example, MyApp.Router.Helpers.page_path(conn, :show, "hello")
instead of MyApp.Router.Helpers.page_path(:show, "hello")
CSRF protection has been added via an imported :protect_from_forgery
function importer to your Router, to enable it, simply add :protect_from_forgery
to your :browser
pipeline.
pipeline :browser do
...
plug :protect_from_forgery
end
Currently wraps Plug.CSRFProtection. From Plug's docs:
For this plug to work, it expects a session to have been previously fetched. If a CSRF token in the session does not previously exist, a CSRF token will be generated and put into the session. When a token is invalid, an
InvalidCSRFTokenError
error is raised. The session's CSRF token will be compared with a token in the params with key "csrf_token" or a token in the request headers with key "x-csrf-token". Only GET and HEAD requests are unprotected. Javascript GET requests are only allowed if they are XHR requests. Otherwise, anInvalidCrossOriginRequestError
error will be raised. You may disable this plug by doingPlug.Conn.put_private(:plug_skip_csrf_protection, true)
.
Phoenix.Controller.Flash
has been removed in favor of fetch_flash/2
, get_flash/2
, and put_flash/2
functions on Phoenix.Controller
.
Additionally, flash is now only a key/value store. The get_all
behavior of storing multiple messages per key has been removed.
Add :fetch_flash
to your browser pipeline
pipeline :browser do
plug :accepts, ~w(html)
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
end
# 0.7.x
alias Phoenix.Controller.Flash
def show(conn, params) do
conn
|> Flash.put(:notice, "It works!")
|> render("index.html")
end
# 0.8.0
def show(conn, params) do
conn
|> put_flash(:notice, "It works!")
|> render("index.html")
end
0.7.x
<%= Flash.get(@conn, :notice) %>
0.8.0
<%= get_flash(@conn, :notice) %>