@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
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
Rise of The Phoenix
By chrismccord
Rise of The Phoenix
- 10,943