Elixir Web Framework targeting full-featured, fault tolerant applications with realtime functionality
- Elixir v1.0.0+
-
Install Phoenix
git clone https://github.com/phoenixframework/phoenix.git && cd phoenix && git checkout v0.4.1 && mix do deps.get, compile
-
Create a new Phoenix application
mix phoenix.new your_app /path/to/scaffold/your_app
Important: Run this task in the Phoenix installation directory cloned in the step above. The path provided:
/path/to/scaffold/your_app/
should be outside of the framework installation directory. This will either create a new application directory or install the application into an existing directory.mix phoenix.new your_app /Users/you/projects/my_app mix phoenix.new your_app ../relative_path/my_app
-
Change directory to
/path/to/scaffold/your_app
. Install dependencies and start web servermix do deps.get, compile mix phoenix.start
When running in production, use protocol consolidation for increased performance:
MIX_ENV=prod mix compile.protocols
MIX_ENV=prod PORT=4001 elixir -pa _build/prod/consolidated -S mix phoenix.start
defmodule YourApp.Router do
use Phoenix.Router
scope alias: YourApp do
get "/pages/:page", PageController, :show, as: :pages
get "/files/*path", FileController, :show
resources "/users", UserController do
resources "/comments", CommentController
end
end
scope path: "/admin", alias: YourApp.Admin, helper: "admin" do
resources "/users", UserController
end
end
Routes specified using get
, post
, put
, and delete
respond to the corresponding HTTP method. The second and third parameters are the controller module and function, respectively. For example, the line get "/files/*path", FileController, :show
above will route GET requests matching /files/*path
to the FileController.show
function.
The resources
macro generates a set of routes for the standard CRUD operations, so:
resources "/users", UserController
is the equivalent of writing:
get "/users", UserController, :index
get "/users/:id", UserController, :show
get "/users/new", UserController, :new
post "/users", UserController, :create
get "/users/:id/edit", UserController, :edit
put "/users/:id", UserController, :update
delete "/users/:id", UserController, :destroy
Resources will also generate a set of named routes and associated helper methods:
resources "/users", UserController do
resources "/comments", CommentController
end
iex> Router.user_path(:index)
"/users"
iex> Router.user_path(:show, 123)
"/users/123"
iex> Router.user_path(:show, 123, page: 5)
"/users/123?page=5"
iex> Router.user_path(:edit, 123)
"/users/123/edit"
iex> Router.user_path(:destroy, 123)
"/users/123"
iex> Router.user_path(:new)
"/users/new"
iex> Router.user_comment_path(:show, 99, 100)
"/users/99/comments/100"
iex> Router.user_comment_path(:index, 99, foo: "bar")
"/users/99/comments?foo=bar"
iex> Router.user_comment_path(:index, 99) |> Router.url
"http://example.com/users/99/comments"
iex> Router.user_comment_path(:edit, 88, 2, [])
"/users/88/comments/2/edit"
iex> Router.user_comment_path(:new, 88)
"/users/88/comments/new"
Since browsers don't allow HTML forms to send PUT or DELETE requests, Phoenix allows the POST method to be overridden, either by adding a _method
form parameter, or specifying an x-http-method-override
HTTP header.
For example, to make a button to delete a post, you could write:
<form action="<%= post_path(:destroy, @post.id) %>" method="post">
<input type="hidden" name="_method" value="DELETE">
<input type="submit" value="Delete Post">
</form>
defmodule YourApp.PageController do
use Phoenix.Controller
alias YourApp.Router
def show(conn, %{"page" => "admin"}) do
redirect conn, Router.page_path(:show, "unauthorized")
end
def show(conn, %{"page" => page}) do
render conn, "show", title: "Showing page #{page}"
end
end
defmodule YourApp.UserController do
use Phoenix.Controller
def show(conn, %{"id" => id}) do
text conn, "Showing user #{id}"
end
def index(conn, _params) do
json conn, JSON.encode!(Repo.all(User))
end
end
Put simply, Phoenix Views render templates. Views also serve as a presentation layer for their templates where functions, alias, imports, etc are in context.
You could use Phoenix.Controller.Flash
to persist messages across redirects like below.
defmodule YourApp.PageController do
use Phoenix.Controller
alias Phoenix.Controller.Flash
def create(conn, _) do
# Code for some create action here
conn
|> Flash.put(:notice, "Created successfully")
|> redirect("/")
end
end
Phoenix.Controller.Flash
is automatically aliased in all Views. In your templates,
you would display flash messages by doing something like:
# web/templates/layout/application.html.eex
<%= if notice = Flash.get(@conn, :notice) do %>
<div class="container">
<div class="row">
<p><%= notice %></p>
</div>
</div>
<% end %>
Phoenix also supports multiple flash messages.
# web/templates/layout/application.html.eex
<%= for notice <- Flash.get_all(@conn, :notice) do %>
<div class="container">
<div class="row">
<p><%= notice %></p>
</div>
</div>
<% end %>
defmodule App.PageController do
use Phoenix.Controller
def index(conn, _params) do
render conn, "index", message: "hello"
end
end
By looking at the controller name App.PageController
, Phoenix will use App.PageView
to render web/templates/page/index.html.eex
within the template web/templates/layout/application.html.eex
. Let's break that down:
App.PageView
is the module that will render the template (more on that later)App
is your application nametemplates
is your configured templates directory. Seeweb/views.ex
page
is your controller namehtml
is the requested format (more on that later)eex
is the default rendererapplication.html
is the layout becauseapplication
is the default layout name and html is the requested format (more on that later)
Every keyword passed to render
in the controller is available as an assign within the template, so you can use <%= @message %>
in the eex template that is rendered in the controller example.
You may also create helper functions within your views or layouts. For example, the previous controller will use App.PageView
so you could have :
defmodule App.Views do
defmacro __using__(_options) do
quote do
use Phoenix.View
import unquote(__MODULE__)
# This block is expanded within all views for aliases, imports, etc
import App.I18n
import App.Router.Helpers
end
end
# Functions defined here are available to all other views/templates
def title, do: "Welcome to Phoenix!"
end
defmodule App.PageView do
use App.Views
alias Poison, as: JSON
def display(something) do
String.upcase(something)
end
def render("show.json", %{page: page}) do
JSON.encode! %{title: page.title, url: page.url}
end
end
Which would allow you to use these functions in your template : <%= display(@message) %>
, <%= title %>
Note that all views extend App.Views
, allowing you to define functions, aliases, imports, etc available in all templates. Additionally, render/2
functions can be defined to perform rendering directly as function definitions. The arguments to render/2
are controller action name with the response content-type mime extension.
To read more about eex templating, see the elixir documentation.
The template format to render is chosen based on the following priority:
format
query string parameter, ie?format=json
- The request header
accept
field, ie "text/html" - Fallback to html as default format, therefore rendering
*.html.eex
To override the render format, for example when rendering your sitemap.xml, you can explicitly set the response content-type, using put_resp_content_type/2
and the template will be chosen from the given mime-type, ie:
def sitemap(conn, _params) do
conn
|> put_resp_content_type("text/xml")
|> render "sitemap"
end
Note that the layout and view templates would be chosen by matching content types, ie application.[format].eex
would be used to render show.[format].eex
.
See this file for a list of supported mime types.
The "LayoutView" module name is hardcoded. This means that App.LayoutView
will be used and, by default, will render templates from web/templates/layout
.
The layout template can be changed easily from the controller via put_layout/2
. For example :
defmodule App.PageController do
use Phoenix.Controller
def index(conn, _params) do
conn
|> put_layout("plain")
|> render "index", message: "hello"
end
end
To render the template's content inside a layout, use the assign <%= @inner %>
that will be generated for you.
You may also omit using a layout with the following:
conn |> put_layout(:none) |> render "index", message: "hello"
By default, eex
is supported. To add haml
support, simply
include the following in your mix.exs
deps:
{:phoenix_haml, "~> 0.0.4"}
and add the PhoenixHaml.Engine
to your config/config.exs
config :phoenix, :template_engines,
haml: PhoenixHaml.Engine
To configure other third-party Phoenix template engines, add the extension and module to your Mix Config, ie:
config :phoenix, :template_engines,
slim: Slim.PhoenixEngine
Topics provide a simple publish/subscribe mechanism that can be used to facilitate messaging between components in an application. To subscribe a process to a given topic, call subscribe/2
passing in the PID and a string to identify the topic:
Phoenix.Topic.subscribe self, "foo"
Then, to broadcast messages to all subscribers to that topic:
Phoenix.Topic.broadcast "foo", { :message_type, some: 1, data: 2 }
For example, let's look at a rudimentary logger that prints messages when a controller action is invoked:
defmodule Logger do
def start_link do
sub = spawn_link &(log/0)
Phoenix.Topic.subscribe(sub, "logging")
{:ok, sub}
end
def log do
receive do
{ :action, params } ->
IO.puts "Called action #{params[:action]} in controller #{params[:controller]}"
_ ->
end
log
end
end
With this module added as a worker to the app's supervision tree, we can broadcast messages to the "logging"
topic, and they will be handled by the logger:
def index(conn, _params) do
Phoenix.Topic.broadcast "logging", { :action, controller: "pages", action: "index" }
render conn, "index"
end
Channels broker websocket connections and integrate with the Topic PubSub layer for message broadcasting. You can think of channels as controllers, with two differences: they are bidirectional and the connection stays alive after a reply.
We can implement a channel by creating a module in the channels directory and by using Phoenix.Channel
:
defmodule App.MyChannel do
use Phoenix.Channel
end
The first thing to do is to implement the join function to authorize sockets on this Channel's topic:
defmodule App.MyChannel do
use Phoenix.Channel
def join(socket, "topic", message) do
{:ok, socket}
end
def join(socket, _no, _message) do
{:error, socket, :unauthorized}
end
end
join
events are specially treated. When {:ok, socket}
is returned from the Channel, the socket is subscribed to the channel and authorized to pubsub on the channel/topic pair. When {:error, socket, reason}
is returned, the socket is denied pubsub access.
Note that we must join a topic before you can send and receive events on a channel. This will become clearer when we look at the JavaScript code, hang tight!
A channel will use a socket underneath to send responses and receive events. As said, sockets are bidirectional, which mean you can receive events (similar to requests in your controller). You handle events with pattern matching, for example:
defmodule App.MyChannel do
use Phoenix.Channel
def event(socket, "user:active", %{user_id: user_id}) do
socket
end
def event(socket, "user:idle", %{user_id: user_id}) do
socket
end
end
We can send replies directly to a single authorized socket with reply/3
defmodule App.MyChannel do
use Phoenix.Channel
def event(socket, "eventname", message) do
reply socket, "return_event", "Echo: " <> message
socket
end
end
Note that, for added clarity, events should be prefixed with their subject and a colon (i.e. "subject:event"). Instead of reply/3
, you may also use broadcast/3
. In the previous case, this would publish a message to all clients who previously joined the current socket's topic.
Remember that a client first has to join a topic before it can send events. On the JavaScript side, this is how it would be done (don't forget to include /js/phoenix.js) :
var socket = new Phoenix.Socket("/ws");
socket.join("channel", "topic", {some_auth_token: "secret"}, callback);
First you should create a socket, which uses /ws
route name. This route's name is for you to decide in your router :
defmodule App.Router do
use Phoenix.Router
use Phoenix.Router.Socket, mount: "/ws"
channel "channel", App.MyChannel
end
This mounts the socket router at /ws
and also registers the above channel as channel
. Let's recap:
- The mountpoint for the socket in the router (/ws) has to match the route used on the JavaScript side when creating the new socket.
- The channel name in the router has to match the first parameter on the JavaScript call to
socket.join
- The name of the topic used in
def join(socket, "topic", message)
function has to match the second parameter on the JavaScript call tosocket.join
Now that a channel exists and we have reached it, it's time to do something fun with it! The callback from the previous JavaScript example receives the channel as a parameter and uses that to either subscribe to topics or send events to the server. Here is a quick example of both :
var socket = new Phoenix.Socket("/ws");
socket.join("channel", "topic", {}, function(channel) {
channel.on("return_event", function(message) {
console.log("Got " + message + " while listening for event return_event");
});
onSomeEvent(function() {
channel.send("topic:event", {data: "json stuff"});
});
});
If you wish, you can send a "join" event back to the client
def join(socket, topic, message) do
reply socket, "join", %{content: "joined #{topic} successfully"}
{:ok, socket}
end
Which you can handle after you get the channel object.
channel.on("return_event", function(message) {
console.log("Got " + message + " while listening for event return_event");
});
Similarly, you can send an explicit message when denying conection.
def join(socket, topic, message) do
reply socket, "error", %{reason: "failed to join #{topic}"}
{:error, socket, :reason}
end
and handle that like any other event
channel.on("error", function(error) {
console.log("Failed to join topic. Reason: " + error.reason);
});
It should be noted that join and error messages are not returned by default, as the client implicitly knows whether it has successfuly subscribed to a channel: the socket will simply not receive any messages should the connection be denied.
There are a few other things not covered in this readme that might be worth exploring :
- By default a socket uses the ws:// protocol and the host from the current location. If you mean to use a separate router on a host other than
location.host
, be sure to specify the full path when initializing the socket, i.e.var socket = new Phoenix.Socket("//example.com/ws")
orvar socket = new Phoenix.Socket("ws://example.com/ws")
- Both the client and server side allow for leave events (as opposed to join)
- In JavaScript, you may manually
.trigger()
events which can be useful for testing - On the server side, string topics are converted into a
Topic
, which can be subscribed to from any elixir code. No need to use websockets!
Phoenix provides a configuration per environment set by the MIX_ENV
environment variable. The default environment dev
will be set if MIX_ENV
does not exist.
├── your_app/config/
│ ├── config.exs Base application configuration
│ ├── dev.exs
│ ├── prod.exs
│ └── test.exs
# your_app/config/config.exs
use Mix.Config
config :phoenix, YourApp.Router,
port: System.get_env("PORT"),
ssl: false,
cookies: true,
session_key: "_your_app_key",
secret_key_base: "super secret"
config :phoenix, :code_reloader,
enabled: false
import_config "#{Mix.env}.exs"
# your_app/config/dev.exs
use Mix.Config
config :phoenix, YourApp.Router,
port: System.get_env("PORT") || 4000,
ssl: false,
host: "localhost",
cookies: true,
session_key: "_your_app_key",
secret_key_base: "$+X2PG$PX0^88^HXB)...",
debug_errors: true
config :phoenix, :code_reloader,
enabled: true
config :logger, :console,
level: :debug
To launch your application with support for SSL, just place your keyfile and
certfile in the priv
directory and configure your router with the following
options:
# your_app/config/prod.exs
use Mix.Config
config :phoenix, YourApp.Router,
port: System.get_env("PORT"),
ssl: false,
host: "example.com",
cookies: true,
session_key: "_your_app_key",
secret_key_base: "$+X2PG$PX0^88^HXB)..."
config :logger, :console,
level: :info,
metadata: [:request_id]
When you include the otp_app
option, Plug
will search within the priv
directory of your application. If you use relative paths for keyfile
and
certfile
and do not include the otp_app
option, Plug
will throw an error.
You can leave out the otp_app
option if you provide absolute paths to the
files.
Example:
Path.expand("../../../some/path/to/ssl/key.pem", __DIR__)
If you are serving your application behind a proxy such as nginx
or
apache
, you will want to specify the proxy_port
option. This will ensure
the route helper functions will use the proxy port number.
Example:
# your_app/config/prod.exs
use Mix.Config
config :phoenix, YourApp.Router,
...
port: 4000,
proxy_port: 443,
...
Phoenix supports a session cookie store that can be easily configured. Just add the following configuration settings to your application's config module:
# your_app/config/prod.exs
use Mix.Config
config :phoenix, YourApp.Router,
...
cookies: true,
session_key: "_your_app_key",
secret_key_base: "super secret",
...
Then you can access session data from your application's controllers.
NOTE: that :key
and :secret
are required options.
Example:
defmodule YourApp.PageController do
use Phoenix.Controller
def show(conn, _params) do
conn = put_session(conn, :foo, "bar")
foo = get_session(conn, :foo)
text conn, foo
end
end
An error_controller
can be configured on the Router Mix config, where two actions must be defined for custom 404 and 500 error handling. Additionally, catch_errors
and debug_errors
settings control how errors are caught and displayed. Router configuration options include:
- error_controller - The optional Module to have
error/2
,not_found/2
actions invoked when 400/500's status occurs. DefaultPhoenix.Controller.ErrorController
- catch_errors - Bool to catch errors at the Router level. Default
true
- debug_errors - Bool to display Phoenix's route/stacktrace debug pages for 404/500 statuses.
Default
false
The error/2
action will be invoked on the page controller when a 500 status has been assigned to the connection, but a response has not been sent, as well as anytime an error is thrown or raised (provided catch_errors: true
)
The not_found/2
action will be invoked on the page controller when a 404 status is assigned to the conn and a response is not sent.
# config/config.exs
config :phoenix, YourApp.Router,
...
catch_errors: true,
debug_errors: false,
error_controller: YourApp.PageController
# config/dev.exs
config :phoenix, YourApp.Router,
...
debug_errors: true # Show Phoenix route/stacktrace debug pages for 404/500's
defmodule YourApp.PageController do
use Phoenix.Controller
def not_found(conn, _) do
text conn, 404, "The page you were looking for couldn't be found"
end
def error(conn, _) do
handle_error(conn, error(conn))
end
defp handle_error(conn, {:error, Ecto.NotSingleResult}) do
not_found(conn, [])
end
defp handle_error(conn, _any) do
text conn, 500, "Something went wrong"
end
end
Errors can be caught at the controller layer by overriding call/2
in the controller, i.e.:
defmodule YourApp.UserController do
use Phoenix.Controller
def call(conn, opts) do
try do
super(conn, opts)
rescue
Ecto.NotSingleResult -> conn |> put_status(404) |> render "user_404"
end
end
def show(conn, %{"id" => id}) do
render conn, "index", user: Repo.get!(User, id)
end
end
mix phoenix # List Phoenix tasks
mix phoenix.new app_name destination_path # Creates new Phoenix application
mix phoenix.routes [MyApp.Router] # Prints routes
mix phoenix.start [MyApp.Router] # Starts worker
mix phoenix --help # This help
Static assets are enabled by default and served from the priv/static/
directory of your application. The assets are mounted at the root path, so
priv/static/js/phoenix.js
would be served from example.com/js/phoenix.js
.
See configuration options for details on disabling assets and customizing the
mount point.
API documentation is available at http://api.phoenixframework.org/
There are no guidelines yet. Do what feels natural. Submit a bug, join a discussion, open a pull request.
$ coffee -o priv/static/js -cw priv/src/static/cs
- Clone docs repository into
../docs
. Relative to yourphoenix
directory. - Run
MIX_ENV=docs mix run release_docs.exs
inphoenix
directory. - Change directory to
../docs
. - Commit and push docs.
- Robust Routing DSL
- GET/POST/PUT/PATCH/DELETE macros
- Named route helpers
- resource routing for RESTful endpoints
- Scoped definitions
- Member/Collection resource routes
- Configuration
- Environment based configuration with ExConf
- Integration with config.exs
- Middleware
- Plug Based Connection handling
- Code Reloading
- Enviroment Based logging with log levels with Elixir's Logger
- Static File serving
- Controllers
- html/json/text helpers
- redirects
- Plug layer for action hooks
- Error page handling
- Error page handling per env
- Views
- Precompiled View handling
- I18n
- Realtime
- Websocket multiplexing/channels
- Browser js client
- iOS client (WIP)
- Android client