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.jsis required, replace yourpriv/static/js/phoenix.jswith 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/3callbacks. In0.8.0,event/3has 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/3now accept the socket as the last argument. This mimicks GenServer APIs - The return signature of
handle_in,handle_out, andleavenow requires either{:ok, socket} | {:leave, socket} | {:error, socket, reason}. Previously onlysocketcould be returned. This new approach mirrors GenServer and allows {:leave, socket} to unsubscribe via any callback. Channel.terminatehas 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
endCurrently 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
InvalidCSRFTokenErrorerror 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, anInvalidCrossOriginRequestErrorerror 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")
end0.7.x
<%= Flash.get(@conn, :notice) %>0.8.0
<%= get_flash(@conn, :notice) %>