@chris_mccord
ElixirConf July 26, 2014






littlelines.com

Phoenix


  • Why?
  • Goals
  • Main Features
    • Robust routing
    • WebSocket / PubSub layer (client & server)
    • Plug-based Routers & Controllers
    • Precompiled View layer
  • The Future



Why?


"Elixir Web Framework targeting full-featured, fault tolerant applications with realtime functionality"

Why Elixir?


"What if I could just..." Driven Development

Metaprogramming
Fault Tolerance
Concurrency
Distribution

"Web" Framework Goals


  • "Distributed Web Services" framework
  • Batteries Included
  • Shared Conventions
  • Common tasks should be easy
    • WebSockets
    • Realtime events
    • SOA
  • No productivity sacrifices for performance
  • No performance sacrifices for productivity

Routing


defmodule App.Router do
  use Phoenix.Router

  get "/pages/:slug", App.PageController, :show, as: :page
  get "/files/*path", App.FileController, :show

  resources "users", App.UserController do
    resources "comments", App.CommentController
  end

  scope path: "admin", alias: App.Admin, helper: "admin" do
    resources "users", UserController
  end
end

defmodule App.Router do
  def match(conn, "GET",    ["pages", slug])
  def match(conn, "GET",    ["files" | path])
  def match(conn, "GET",    ["users"])
  def match(conn, "GET",    ["users", id, "edit"])
  def match(conn, "GET",    ["users", id])
  def match(conn, "GET",    ["users", "new"])
  def match(conn, "POST",   ["users"])
  def match(conn, "PUT",    ["users", id])
  def match(conn, "PATCH",  ["users", id])
  def match(conn, "DELETE", ["users", id])
  def match(conn, "GET",    ["users", user_id, "comments"])
  def match(conn, "GET",    ["users", user_id, "comments", id, "edit"])
  def match(conn, "GET",    ["users", user_id, "comments", id])
  def match(conn, "GET",    ["users", user_id, "comments", "new"])
  def match(conn, "POST",   ["users", user_id, "comments"])
  def match(conn, "PUT",    ["users", user_id, "comments", id])
  def match(conn, "PATCH",  ["users", user_id, "comments", id])
  def match(conn, "DELETE", ["users", user_id, "comments", id])
  def match(conn, "GET",    ["admin", "users"])
  def match(conn, "GET",    ["admin", "users", id, "edit"])
  def match(conn, "GET",    ["admin", "users", id])
  def match(conn, "GET",    ["admin", "users", "new"])
  def match(conn, "POST",   ["admin", "users"])
  def match(conn, "PUT",    ["admin", "users", id])
  def match(conn, "PATCH",  ["admin", "users", id])
  def match(conn, "DELETE", ["admin", "users", id])
end

Generated match definitions
get "/pages/:slug", PageCtrl, :show
# GET /pages/about
#                      ["pages", "about"]
def match(conn, "GET", ["pages", slug]) do
  Controller.perform_action(conn, PageCtrl, :show, slug: slug)
end



get "/files/*path", FileCtrl, :show
# GET /files/Users/chris/Downloads
#                      ["files", "Users", "chris", "Downlaods"]
def match(conn, "GET", ["files" | path]) do
  Controller.perform_action(conn, FileCtrl, :show, path: path)
end


Controllers

defmodule App.PageController do
  use Phoenix.Controller
 
  plug :authenticate
  plug :action
  # plug :render
  
  def authenticate(conn, _) do
    assign conn, :current_user, find_authorized_user!(conn)
  end
  
  def show(conn, %{"slug" => slug}) do
    render conn, "show", page: Repo.get(Page, slug)
  end
end

Views & Templates

  • Views render templates
  • Views serve as a presentation layer
  • Module hierarchy for shared context
  • Templates are precompiled into views
  • EEx & Haml engine support


Views

defmodule App.Views do
  defmacro __using__(_options) do
    quote do
      use Phoenix.View, templates_root: unquote(Path.join([__DIR__, 
                                                          "templates"]))
      import unquote(__MODULE__)
      # Expanded within all views for aliases, imports, etc
      alias App.Views
    end
  end

  # Funcs defined here are available to all other views/templates
  def new_line_to_br(txt) do
    String.split(txt, "\n") |> Enum.map_join("<br>", &escape/1) |> safe
  end
end
defmodule App.PagesView do
  use App.Views
  
  def title(%Page{title: title, date: date}), do: "#{title} - #{date}"
end

Templates

<html>
  <body>
    <h1><%= @page_title %></h1>
    <%= @inner %>
  </body>
</html>
<h2>Entries</h2>
<ul>
  <%= for entry <- @entries do %>
    <li>
      <%= new_line_to_br entry.body %>
    </li>
  <% end %>
</ul>

├── templates
│   ├── layout
│   │   └── application.html.eex
│   └── page
│       └── index.html.eex
│       └── show.html.eex
├── views
│   ├── layout_view.ex
│   └── page_view.ex

iex> App.PageView.render("index.html", [])
"<h1>Index</h1>..."
defmodule App.PageView do
  use App.Views

  def render("index.json", attributes \\ []) do
    JSON.encode! %{
      results: attributes[:results]
    }
  end
end

iex> App.PageView.render("index.json", [])
"{\"results\":[\"id\":123...."

Channels

  • WebSocket / PubSub Abstraction
  • Handle socket events and broadcast
  • Handle socket authorization for channel subscription
  • phoenix.js browser client
  • Mobile clients
    • iOS client (alpha)
    • Android client, asap

Multiplatform


Channels -Router


defmodule Chat.Router do
  use Phoenix.Router
  use Phoenix.Router.Socket, mount: "/ws"

  ...
  get "/", Chat.PageController, :index, as: :page  ...

  channel "rooms", Chat.RoomChannel
end

Channels - Handler

defmodule Chat.RoomChannel do
  use Phoenix.Channel

  def join(socket, "lobby", %{"user" => user}) do
    reply socket, "join", %{status: "connected"}    
    broadcast socket, "user:entered", %{user: user}
    
    {:ok, socket}
  end

  def join(socket, private_topic, message) do
    {:error, socket, :unauthorized}
  end
  def event(socket, "new:msg", %{user: user, body: body}) do
    broadcast socket, "new:msg", %{user: user, body: body}
    socket
  end
end

Channels - Client


var socket = new Phoenix.Socket("ws://" + location.host +  "/ws");

socket.join("rooms", "lobby", {}, function(chan){
  $input.on("enterPressed", function(e) {
    chan.send("new:msg", {
      user: $user.val(),
      body: $input.val()
    });
  });
  chan.on("join", function(message){
    $status.text("joined");
  });
  chan.on("new:msg", function(message){
    $messages.append(messageTemplate(message));
  });
});

PUBSUB - FROM anywhere



iex> Channel.broadcast "rooms", "lobby", "new:msg", %{
...>   user: "iex",
...>   body: "Hello from iex!"
...> }
        




Channel DEMO

LooKing AHEAD


  • Phoenix 1.0
    • Standard Web stack
      • Routing, Controllers, Views, I18n
    • Realtime Layer
      • Channels
      • Topics
      • iOS & Android clients
    • Elixir Logger integration
    • Resource Protocol
    • Comprehensive Guides
      • phoenixframework.org


Looking Ahead


  • Phoenix 1.0+
    • Distributed Services
    • Service/Node discovery

Distributed Services


  • Inspired by riak_core
  • Toolkit for easily building fault tolerance services that scale
  • Decentralized, masterless service registration (eventually)

Phoenix Services

  • Phoenix Nodes advertise services
  • Automatically added, removed from local service register on node ups/downs
config :phoenix, :nodes, [:"n1@host", :"n2@host"]
$ mix phoenix.start
Running App.Router with Cowboy on port 4000

$ mix phoenix.start PageCrawler PageIndexer
Registering PageCrawler, PageIndexer on cluster

Phoenix Services - pseudo code

defmodule PageCrawler do
  use Phoenix.Service
  
  def command({:crawl, %{url: url}}, _sender, state) do
    state |> status |> crawl
  end
  defp crawl(:busy, state) do
    reply({:next, :busy}, state)
  end
  defp crawl(_status, state) do
    case Page.fetch(url) do
      {:ok, content}    -> reply({:ok, content}, state)
      {:error, :reason} -> reply({:error, reason}, state)
    end
  end
end

{:ok, page} = Service.call(PageCrawler, {:crawl, %{url: url}})


LET's Build the future



@chris_mccord

chris@chrismccord.com

github.com/phoenixframework/phoenix

irc.freenode.net/elixir-lang