Skip to content

Instantly share code, notes, and snippets.

@chrismccord
Last active February 12, 2016 09:52
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chrismccord/3603fd2735019f86c74b to your computer and use it in GitHub Desktop.
Save chrismccord/3603fd2735019f86c74b to your computer and use it in GitHub Desktop.
Phoenix 0.10 to 0.11.0 upgrade instructions

mix.exs

Add these functions to the bottom of mix.exs:

  # Specifies which paths to compile per environment
  defp elixirc_paths(:test), do: ["lib", "web", "test/support"]
  defp elixirc_paths(_),     do: ["lib", "web"]

Next, add these entries to your def project config:

     elixirc_paths: elixirc_paths(Mix.env),
     compilers: [:phoenix] ++ Mix.compilers,
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     

Generators

  • mix phoenix.gen.resource renamed to mix phoenix.gen.html

Views

use Phoenix.HTML no longer imports controller functions. You must add import Phoenix.Controller, only: [get_flash: 2] manually to your views or your web.ex, ie:

# your_app/web/web.ex
defmodule MyApp.Web do
  ...
  def view do
    quote do
      ...
      import Phoenix.Controller, only: [get_flash: 2]
    end
  end
end

Endpoints

The endpoint now requires a :root entry in your config/config.exs:

config :my_app, MyApp.Endpoint,
  ...
  root: Path.expand("..", __DIR__),

Code reloader must now be configured in your endpoint instead of Phoenix. Therefore, upgrade your config/dev.exs replacing

config :phoenix, :code_reloader, true

by

config :your_app, Your.Endpoint, code_reloader: true

Live Reload

Th live reloader is now a dependency instead of being shipped with Phoenix. Please add {:phoenix_live_reload, "~> 0.3"} to your dependencies in mix.exs

Additionally, the live_reload configuration has changed to allow a :url option and to work with :patterns instead of paths:

config :your_app, Your.Endpoint,
  code_reloader: true,
  live_reload: [
    # url is optional
    url: "ws://localhost:4000", 
    # `:patterns` replace `:paths` and are required for live reload
    patterns: [~r{priv/static/.*(js|css|png|jpeg|jpg|gif)$},
               ~r{web/views/.*(ex)$},
               ~r{web/templates/.*(eex)$}]]

Next, the Code and live reloader must now be explicitly plugged in your endpoint. Wrap them inside lib/your_app/endpoint.ex in a code_reloading? block:

if code_reloading? do
  plug Phoenix.LiveReloader
  plug Phoenix.CodeReloader
end

Channels - Server

Channels received major updates in functionality and tweaking of the APIs and return signatures. Most notably, each channel now runs in its own process, supporthing handle_info/2 and more closely matching GenServer APIs. Additionally "synchronous" messaging is now supported from client to server. By synchronous, I mean being able to reply to an incoming event directly, while ensuring messaging ordering for the same incoming events. This not only lets you do proper request/response messaging where necessary, but it also fixes issues we have in our <= 0.10 apis where joins were not synchronous and messages could be dropped if you fired them before you were fully joined.

Changes:

  • The leave/2 callback has been removed. If you need to cleanup/teardown when a client disconnects, trap exits and handle in terminate/2, ie:
def join(topic, auth_msg, socket) do
  Process.flag(:trap_exit, true)
  {:ok, socket}
end

def terminate({:shutdown, :client_left}, socket) do
  # client left intentionally
end
def terminate(reason, socket) do
  # terminating for another reason (connection drop, crash, etc)
end
  • reply has been renamed to push to better signify we are only push a message down the socket, not replying to a specific request. Update your function calls accordingly.

  • The return signatures for handle_in/3 and handle_out/3 have changed, ie:

handle_in(event :: String.t, msg :: map, Socket.t) ::
  {:noreply, Socket.t} |
  {:reply, {status :: atom, response :: map}, Socket.t} |
  {:reply, status :: atom, Socket.t} |
  {:stop, reason :: term, Socket.t} |
  {:stop, reason :: term, reply :: {status :: atom, response :: map}, Socket.t} |
  {:stop, reason :: term, reply :: status :: atom, Socket.t}

handle_out(event :: String.t, msg :: map, Socket.t) ::
  {:ok, Socket.t} |
  {:noreply, Socket.t} |
  {:error, reason :: term, Socket.t} |
  {:stop, reason :: term, Socket.t}

For existing applications, you can simply change the return signatures of handle_in/handle_out from {:ok, socket} to {:noreply, socket}. For code moving forward, you can now reply directly to an incoming event and pick up the reply on the client using the {:reply, {status, response}, socket} or {:reply, status, socket}. More examples below.

Channels - Client

var socket = new Phoenix.Socket("/ws")
socket.connect()
  • socket.close() has been renamed to socket.disconnect()

  • socket.join(..) api has changed. See the examples below for more details, but all it means is your js goes from:

socket.join("foo:bar", {}, function(chan){
})

to

socket.join("foo:bar", {}).receive("ok", function(chan){
})
// or
var chan = socket.join("foo:bar", {})
chan.receive("ok", function(){
})

Sync Messaging

We've overhauled the channel API to allow "synchronous" messaging, and I really love the results. By synchronous, I mean being able to reply to an incoming event directly, while ensuring messaging ordering for the same incoming events. This not only lets you do proper request/response messaging where necessary, but it also fixes issues we have in our <= 0.10 apis where joins were not synchronous and messages could be dropped if you fired them before you were fully joined. With these changes, we have a few high-level concepts which make up channels:

  1. The client and server push messages down the socket to communicate
  2. The server can reply directly to a pushed message
  3. The server can broadcast events to be pushed to all subscribers

The flows looks like this:

  • client push("ev1") -> server handle_in("ev1") -> server push("ev2") -> client on("ev2")
  • client push("ev1") -> server handle_in("ev1") -> server broadcast("ev2") -> N subscribers handle_out("ev2") -> N subscribers push("ev2") -> N clients on("ev2")`
  • client push("ev1") -> server handle_in("ev") -> server {:reply, :ok, ...} -> client receive("ok", ...)

Now let's see some cli/server code:

    socket.join("rooms:lobby", {})
      .after(5000, () => console.log("We're having trouble connecting...") )
      .receive("ignore", () => console.log("auth error") )
      .receive("ok", chan => {

        // can now bind to channel crash/close events since channels are own processes
        chan.onClose( () => console.log("The channel disconnected") )
        chan.onError( () => console.log("The channel crashed!") )

        $input.onEnter( e => {
          // push without response
          chan.push("new_msg", {body: e.text, user: currentUser}) 
        })
        
        chan.on("status_change", ({status}) => $status.html(status) )
        
        chan.on("new_msg", msg => $messages.append(msg) )
        
        // push with `receive`'d response, and optional `after` hooks
        $createNotice.onClick( e => {
          chan.push("create_notice", e.data)
              .receive("ok", notice =>  console.log("notice created", notice) )
              .receive("error", reasons =>  console.log("creation failed", reasons) )
              .after(5000, () => console.log("network interruption") )
        })
    })
defmodule Chat.RoomChannel do
  use Phoenix.Channel

  def join("rooms:lobby", message, socket) do
    send(self, {:after_join, message})

    {:ok, socket}
  end
  def join("rooms:" <> _private_subtopic, _message, _socket) do
    :ignore
  end

  def handle_info({:after_join, msg}, socket) do
    broadcast! socket, "user_entered", %{user: msg["user"]}
    push socket, "status_change", %{status: "waiting for users"}
    {:noreply, socket}
  end

  def handle_in("create_notice", attrs, socket) do
    changeset = Notice.changeset(%Notice{}, attrs)

    if changeset.valid? do
      Repo.insert(changeset)
      {:reply, {:ok, changeset}, socket}
    else
      {:reply, {:error, changeset.errors}, socket}
    end
  end
  
  def handle_in("new_msg", msg, socket) do
    broadcast! socket, "new_msg", %{user: msg["user"], body: msg["body"]}
    {:noreply, socket}
  end
  
  # this is forward by the default `handle_out`, but show here for clarity
  def handle_out("new_msg", msg, socket) do
    push socket, "new_msg", msg
    {:noreply, socket}
  end
end

Note that {:reply, {:ok, resp}, socket} on the server, triggers .receive("ok", resp => {}) on the client. The "status" of the reply can be anything, ie {:reply, {:queued, resp}, socket} on the server, triggers .receive("queued", resp => { }) on the client. Also note that client joining, push, and receiving replies all have the same semantics and API now, which is quite nice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment