Skip to content

Instantly share code, notes, and snippets.

@chrismccord
Last active August 29, 2015 14:13
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chrismccord/9434b8fa208b3aae22b6 to your computer and use it in GitHub Desktop.
Save chrismccord/9434b8fa208b3aae22b6 to your computer and use it in GitHub Desktop.
Phoenix Upgrade Instructions 0.7.x to 0.8.0

Channel Changes

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 your priv/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. In 0.8.0, event/3 has been renamed to handle_in/3, and outgoing events callbacks can be defined via handle_out/3
  • All channel callbacks, such as join/3, leave/2, handle_in/3, and handle_out/3 now accept the socket as the last argument. This mimicks GenServer APIs
  • The return signature of handle_in, handle_out, and leave now requires either {:ok, socket} | {:leave, socket} | {:error, socket, reason}. Previously only socket 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:

Incoming 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

Outgoing Events

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.

Endpoint Changes

Supervision

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

Starting your Endpoint

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.

Router Changes

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")

Cross-site request forgery Protection

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, an InvalidCrossOriginRequestError error will be raised. You may disable this plug by doing Plug.Conn.put_private(:plug_skip_csrf_protection, true).

Phoenix.Controller.Flash changes

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.

Router

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

Controller

# 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

View

0.7.x

<%= Flash.get(@conn, :notice) %>

0.8.0

<%= get_flash(@conn, :notice) %>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment