Phoenix is an Elixir MVC web framework providing all tools that You can expect to be productive: generators, templates and channels as WebSockets abstraction. Ecto is a domain specific language for composing queries and interacting with databases. This talk will introduce You to building a fast and reliable web applications with Elixir using Phoenix and Ecto as a solid foundation.
3. Elixir (Erlang.OTP) Applications
• In Elixir (Erlang/OTP), an application is a component implementing some
specific functionality, that can be started and stopped as a unit, and which
can be re-used in other systems.
• Applications are defined with an application file named
application_name.app in the same ebin directory as the compiled
modules of the application.
• In Elixir, the Mix build tool is responsible for compiling your source code
and generating your application .app file.
6. mix.exs
def project do
[ app: :hello_world,
version: "0.1.0",
elixir: "~> 1.6-dev",
start_permanent: Mix.env() == :prod,
deps: deps() ]
end
def application do
[ extra_applications: [:logger],
mod: {HelloWorld.Application, []} ]
end
defp deps do
[]
end
7. Starting Elixir Application
• You start one or more applications, each with their own initialization and
termination logic.
• Elixir does not have a main procedure that is responsible for starting your
system.
• Starting an application is done via the “application module callback”,
which is a module that defines the start/2 function.
8. lib/hello_world/application.ex
use Application
def start(_type, _args) do
# List all child processes to be supervised
children = [
# Starts a worker by calling: HelloWorld.Worker.start_link(arg)
# {HelloWorld.Worker, arg},
]
opts = [strategy: :one_for_one, name: HelloWorld.Supervisor]
Supervisor.start_link(children, opts)
end
9. Application Supervision Tree
The start/2 function should start a supervisor, which is often called
as the top-level supervisor, since it sits at the root of a potentially
long supervision tree.
lib/hello_phoenix/application.ex
def start(_type, _args) do
import Supervisor.Spec
children = [
# Start the Ecto repository
supervisor(HelloPhoenix.Repo, []),
# Start the endpoint when the application starts
supervisor(HelloPhoenixWeb.Endpoint, []),
]
opts = [strategy: :one_for_one,
name: HelloPhoenix.Supervisor]
Supervisor.start_link(children, opts)
end
WW
S
S S
W
13. In umbrella dependencies
hello_umbrella/apps/hello/mix.exs
def project do
[
app: :hello,
version: "0.1.0",
build_path: "../../_build",
config_path: "../../config/config.exs",
deps_path: "../../deps",
lockfile: "../../mix.lock",
elixir: "~> 1.6-dev",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
...
defp deps do
[
{:world, in_umbrella: true}
]
end
14. Phoenix Framework: Productive. Reliable. Fast.
• Phoenix is a web development framework
written in Elixir which implements the server-
side MVC pattern.
• Current version: 1.3.0
• http://phoenixframework.org/
15. Phoenix Layers
• Phoenix itself is actually the top layer of a multi-layer system
• Cowboy: the web server used by Phoenix (and Plug) is Cowboy (Erlang).
• Plug is a specification for constructing composable modules to build web
applications. Plugs are reusable modules or functions built to that
specification.
• Ecto is a language integrated query composition tool and database
wrapper for Elixir.
16. Phoenix Application
$ mix phx.new hello_phoenix
lib/hello_phoenix/application.ex
defmodule HelloPhoenix.Application do
use Application
...
lib/hello_phoenix_web/endpoint.ex
defmodule HelloPhoenixWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :hello_phoenix
...
17. Phoenix Application
mix.exs
...
def application do
[
mod: {HelloPhoenix.Application, []},
extra_applications: [:logger, :runtime_tools]
]
end
...
lib/hello_phoenix/application.ex
...
def start(_type, _args) do
import Supervisor.Spec
children = [
# Start the Ecto repository
supervisor(HelloPhoenix.Repo, []),
# Start the endpoint when the application starts
supervisor(HelloPhoenixWeb.Endpoint, []),
]
opts = [strategy: :one_for_one, name: HelloPhoenix.Supervisor]
Supervisor.start_link(children, opts)
end
...
20. Plug
• Core Phoenix components like Endpoints, Routers, and Controllers are all
just Plugs internally.
• Plug is a specification for composable modules in web application.
• Plugs are reusable modules or functions built to that specification.
• They provide discrete behaviors - like request header parsing or logging.
• Because the Plug API is small and consistent, plugs can be defined and
executed in a set order, like a pipeline.
26. Endpoint
• Provides a wrapper for starting and stopping the endpoint as part of a
supervision tree
• Handles all aspects of requests up until the point where the router takes
over
• Defines an initial plug pipeline where requests are sent through
• Hosts web specific configuration for your application
• Dispatches requests into a designated router
27. Endpoint
…
use Phoenix.Endpoint, otp_app: :hello_phoenix
socket "/socket", HelloPhoenixWeb.UserSocket
plug Plug.Static,
...
if code_reloading? do
...
end
plug Plug.RequestId
plug Plug.Logger
plug Plug.Parsers,
...
plug Plug.MethodOverride
plug Plug.Head
plug Plug.Session,
...
plug HelloPhoenixWeb.Router
…
28. Starting Endpoint
...
def start(_type, _args) do
import Supervisor.Spec
children = [
# Start the Ecto repository
supervisor(HelloPhoenix.Repo, []),
# Start the endpoint when the application starts
supervisor(HelloPhoenixWeb.Endpoint, []),
]
opts = [strategy: :one_for_one, name:
HelloPhoenix.Supervisor]
Supervisor.start_link(children, opts)
end
...
29. Router
• Parses incoming requests and dispatches them to the correct controller/
action, passing parameters as needed
• Provides helpers to generate route paths or urls to resources
• Defines named pipelines through which we may pass our requests
30. Router
...
pipeline :api_client_id do
plug :header_required, "x-consumer-metadata"
plug :client_id_exists
end
...
scope "/api", EHealth.Web do
pipe_through [:api, :api_client_id]
# Legal Entities
get "/legal_entities/:id", LegalEntityController, :show
patch "/legal_entities/:id/actions/mis_verify", LegalEntityController, :mis_verify
patch "/legal_entities/:id/actions/nhs_verify", LegalEntityController, :nhs_verify
patch "/legal_entities/:id/actions/deactivate", LegalEntityController, :deactivate
# Employees
get "/employees/:id", EmployeeController, :show
scope "/employees" do
pipe_through [:client_context_list]
patch "/:id/actions/deactivate", EmployeeController, :deactivate
end
...
31. Controllers & Actions
• Controllers provide functions, called actions, to handle requests
• Actions:
• prepare data and pass it into views
• invoke rendering via views
• perform redirects
32. Controllers & Actions
defmodule EHealth.Web.LegalEntityController do
use EHealth.Web, :controller
...
action_fallback EHealth.Web.FallbackController
def show(%Plug.Conn{req_headers: req_headers} = conn, %{"id" => id}) do
with {:ok, legal_entity, security} <- API.get_legal_entity_by_id(id, req_headers) do
conn
|> assign_security(security)
|> render("show.json", legal_entity: legal_entity)
end
end
def mis_verify(%Plug.Conn{req_headers: req_headers} = conn, %{"id" => id}) do
with {:ok, legal_entity} <- API.mis_verify(id, get_consumer_id(req_headers)) do
render(conn, "show.json", legal_entity: legal_entity)
end
end
...
33. Web Interface Entrypoint
defmodule HelloPhoenixWeb do
def controller do
quote do
use Phoenix.Controller, namespace: HelloPhoenixWeb
import Plug.Conn
import HelloPhoenixWeb.Router.Helpers
import HelloPhoenixWeb.Gettext
end
end
def view do
...
end
def router do
...
end
def channel do
...
end
...
34. Fallback Controller
action_fallback(plug)
• Registers the plug to call as a
fallback to the controller
action.
• If the controller action fails to
return a %Plug.Conn{}, the
provided plug will be called
and receive the
controller’s %Plug.Conn{} as it
was before the action was
invoked along with the value
returned from the controller
action.
web/controllers/fallback_controller.ex
defmodule EHealth.Web.FallbackController do
...
use EHealth.Web, :controller
require Logger
def call(conn, {:error, json_schema_errors}) when is_list(json_schema_errors) do
conn
|> put_status(422)
|> render(EView.Views.ValidationError, "422.json", %{schema: json_schema_errors})
end
def call(conn, {:error, errors, :query_parameter}) when is_list(errors) do
conn
|> put_status(422)
|> render(EView.Views.ValidationError, "422.query.json", %{schema: errors})
end
def call(conn, {:error, {:"422", error}}) do
conn
|> put_status(422)
|> render(EView.Views.Error, :"400", %{message: error})
end
...
35. Views
• Defines the view layer of a Phoenix application
• Render templates
• Define helper functions, available in templates, to decorate data for
presentation
36. • Phoenix assumes a strong naming convention
from controllers to views to the templates they
render.
• The PageController requires a PageView to
render templates in the lib/hello_phoenix_web/
templates/page directory.
Templates
37. Web Interface Entrypoint
defmodule HelloPhoenixWeb do
def controller do
...
end
def view do
quote do
use Phoenix.View, root: "lib/hello_phoenix_web/templates",
namespace: HelloPhoenixWeb
# Import convenience functions from controllers
import Phoenix.Controller, only: [get_flash: 2, view_module: 1]
# Use all HTML functionality (forms, tags, etc)
use Phoenix.HTML
import HelloPhoenixWeb.Router.Helpers
import HelloPhoenixWeb.ErrorHelpers
import HelloPhoenixWeb.Gettext
end
end
...
38. View Module
defmodule HelloPhoenixWeb.PageView do
use HelloPhoenixWeb, :view
end
Because we have defined the template root to be "lib/hello_phoenix_web/templates",
Phoenix.View will automatically load all templates at “lib/hello_phoenix_web/
templates/page” and include them in the HelloPhoenixWeb.PageView.
lib/hello_phoenix_web/controllers/page_controller.ex
...
def index(conn, _params) do
render conn, "index.html"
end
...
39. EEx Templates
• EEx is the default template system in Phoenix
• EEx module receives a template path and transforms its source code into
Elixir quoted expressions
• Templates are precompiled and fast
• Template name - is the name of the template as given by the user, without
the template engine extension, for example: “foo.html”
40. HTML EEx Template
Hello <%= @name %>
<h3>Keys for the conn Struct</h3>
<%= for key <- connection_keys @conn do %>
<p><%= key %></p>
<% end %>
41. JSON - Controller
defmodule EHealth.Web.DictionaryController do
…
alias EHealth.Dictionaries
alias EHealth.Dictionaries.Dictionary
action_fallback EHealth.Web.FallbackController
def index(conn, params) do
with {:ok, dictionaries} <- Dictionaries.list_dictionaries(params)
do
render(conn, "index.json", dictionaries: dictionaries)
end
end
def update(conn, %{"name" => name} = dictionary_params) do
with {:ok, %Dictionary{} = dictionary} <- Dictionaries.create_or_update_dictionary(name, dictionary_params) do
render(conn, "show.json", dictionary: dictionary)
end
end
End
42. JSON - View
defmodule EHealth.Web.DictionaryView do
use EHealth.Web, :view
alias EHealth.Web.DictionaryView
def render("index.json", %{dictionaries: dictionaries}) do
render_many(dictionaries, DictionaryView, "dictionary.json")
end
def render("show.json", %{dictionary: dictionary}) do
render_one(dictionary, DictionaryView, "dictionary.json")
end
def render("dictionary.json", %{dictionary: dictionary}) do
%{
name: dictionary.name,
values: dictionary.values,
labels: dictionary.labels,
is_active: dictionary.is_active
}
end
end
43. Channels
• Manage sockets for easy real-time communication
• Allow bi-directional communication with persistent connections
• Every time you join a channel, you need to choose which particular topic
you want to listen to.
• The topic is just an identifier, but by convention it is often made of two
parts: "topic:subtopic".
45. Channels - Socket
hello_phoenix/lib/hello_phoenix_web/channels/user_socket.ex
defmodule HelloPhoenixWeb.UserSocket do
use Phoenix.Socket
## Channels
channel "room:*", HelloPhoenixWeb.RoomChannel
## Transports
transport :websocket, Phoenix.Transports.WebSocket
# Socket params are passed from the client and can
# be used to verify and authenticate a user. After
# verification, you can put default assigns into
# the socket that will be set for all channels, ie
#
# {:ok, assign(socket, :user_id, verified_user_id)}
#
# To deny connection, return `:error`.
def connect(_params, socket) do
{:ok, socket}
end
...
46. Joining Channels
defmodule HelloPhoenixWeb.RoomChannel do
use Phoenix.Channel
def join("room:lobby", _message, socket) do
{:ok, socket}
end
def join("room:" <> _private_room_id, _params, _socket) do
{:error, %{reason: "unauthorized"}}
end
end
47. Channels: phoenix.js
hello_phoenix/assets/js/socket.js
import {Socket} from "phoenix"
let socket = new Socket("/socket", {params: {token: window.userToken}})
socket.connect()
// Now that you are connected, you can join channels with a topic:
let channel = socket.channel("topic:subtopic", {})
channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })
...
49. Channels: Incoming Events
defmodule HelloPhoenixWeb.RoomChannel do
use Phoenix.Channel
…
def handle_in("new_msg", %{"body" => body}, socket) do
broadcast! socket, "new_msg", %{body: body}
{:noreply, socket}
end
end
We can pattern match on the event names, like "new_msg", and then grab
the payload that the client passed over the channel.
50. Channels: Intercepting Outgoing Events
...
intercept ["user_joined"]
def handle_out("user_joined", msg, socket) do
if Accounts.ignoring_user?(socket.assigns[:user], msg.user_id) do
{:noreply, socket}
else
push socket, "user_joined", msg
{:noreply, socket}
end
end
...
This callback will be called for every recipient of a message, so more
expensive operations like hitting the database should be considered carefully
before being included in handle_out/3
51. Channels: Socket Assigns
Similar to connection structs, %Plug.Conn{}, it is possible to assign values
to a channel socket.
socket = assign(socket, :user, msg[“user”])
Sockets store assigned values as a map in socket.assigns.
user = socket.assigns[:user]
52. • Ecto is a domain specific language for writing queries and interacting with
databases in Elixir.
• What's new in Ecto 2.1: pages.plataformatec.com.br/ebook-whats-new-
in-ecto-2-0
53. Ecto
• Ecto.Repo - repositories are wrappers around the data store.
• Ecto.Schema - schemas are used to map any data source into an Elixir
struct.
• Ecto.Changeset - allows developers to filter, cast, and validate changes
before we apply them to the data.
• Ecto.Query - written in Elixir syntax, queries are used to retrieve
information from a given repository.
56. Repositories: config
hello_phoenix/config/dev.exs
...
# Configure your database
config :hello_phoenix, HelloPhoenix.Repo,
adapter: Ecto.Adapters.Postgres,
username: "postgres",
password: "postgres",
database: "hello_phoenix_dev",
hostname: "localhost",
pool_size: 10
...
A repository needs an adapter and credentials to communicate to the
database. Configuration for the Repo usually defined in your app config.
58. Schema
defmodule Weather do
use Ecto.Schema
# weather is the DB table
schema "weather" do
field :city, :string
field :temp_lo, :integer
field :temp_hi, :integer
field :prcp, :float, default: 0.0
end
end
An :id field with type :id (:id means :integer) is generated by default, which is
the primary key of the Schema.
59. Schema
By defining a schema, Ecto automatically defines a struct with the schema
fields:
iex> weather = %Weather{temp_lo: 30}
iex> weather.temp_lo
30
By defining a schema, Ecto automatically defines a struct with the schema
fields:
iex> weather = %Weather{temp_lo: 0, temp_hi: 23}
iex> weather = Repo.insert!(weather)
%Weather{...}
iex> weather.id
1
60. Schema
After persisting weather to the database, it will return a new copy of
%Weather{} with the primary key (the id) set. We can use this value to
interact with the repository:
# Get the struct back
iex> weather = Repo.get Weather, 1
%Weather{id: 1, ...}
# Delete it
iex> Repo.delete!(weather)
%Weather{...}
61. Changesets
Changesets allow developers to filter, cast, and validate changes before we
apply them to the data.
Changesets are also capable of transforming database constraints, like
unique indexes and foreign key checks, into errors.
62. Changesets
defmodule User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :name
field :email
field :age, :integer
end
def changeset(user, params %{}) do
user
|> cast(params, [:name, :email, :age])
|> validate_required([:name, :email])
|> validate_format(:email, ~r/@/)
|> validate_inclusion(:age, 18..100)
end
end
63. Changesets
Once a changeset is built, it can be given to functions like insert and update
in the repository that will return an :ok or :error tuple
changeset = User.changeset(%User{}, %{name: "Ivan", age: 30})
case Repo.update(changeset) do
{:ok, user} ->
# user updated
{:error, changeset} ->
# an error occurred
end
64. Changesets
We can easily provide different changesets for different use cases
def registration_changeset(user, params) do
# Changeset on create
end
def update_changeset(user, params) do
# Changeset on update
end
65. Query
Ecto allows you to write queries in Elixir and send them to the repository,
which translates them to the underlying database.
66. Query: predefined Schema
import Ecto.Query, only: [from: 2]
query = from u in User,
where: u.age > 18 or is_nil(u.email),
select: u
# Returns %User{} structs matching the query
Repo.all(query)
67. Query: directly against a table
query = from u in "users",
where: u.age > 18 or is_nil(u.email),
select: %{name: u.name, age: u.age}
# Returns maps as defined in select
Repo.all(query)
68. Query: accessing params values
# min = 33
def min_age(min) do
from u in User, where: u.age > ^min
end
# min = "35"
Repo.all(from u in "users",
where: u.age > type(^age, :integer),
select: u.name)
69. Query: fragments
def unpublished_by_title(title) do
from p in Post,
where: is_nil(p.published_at) and
fragment("lower(?)", p.title) == ^title
end
# PostgreSQL’s JSON/JSONB data type with fragments
fragment("?->>? ILIKE ?", p.map, "key_name", ^some_value)
70. Ecto.Multi
• Ecto.Multi is a data structure for grouping multiple Repo operations.
• Ecto.Multi makes it possible to pack operations that should be performed
in a single database transaction and gives a way to introspect the queued
operations without actually performing them.
• Each operation is given a name that is unique and will identify its result in
case of success or failure.
• All operations will be executed in the order they were added.
71. Ecto.Multi
defmodule PasswordManager do
alias Ecto.Multi
def reset(account, params) do
Multi.new
|> Multi.update(:account, Account.password_reset_changeset(account, params))
|> Multi.insert(:log, Log.password_reset_changeset(account, params))
|> Multi.delete_all(:sessions, Ecto.assoc(account, :sessions))
end
end
72. Ecto.Multi
result = Repo.transaction(PasswordManager.reset(account, params))
case result do
{:ok, %{account: account, log: log, sessions: sessions}} ->
# Operation was successful, we can access results under keys
# we used for naming the operations.
{:error, failed_operation, failed_value, changes_so_far} ->
# One of the operations failed. We can access the operation's failure
# value (like changeset for operations on changesets) to prepare a
# proper response. We also get access to the results of any operations
# that succeeded before the indicated operation failed. However, any
# successful operations would have been rolled back.
end
74. Accounts Context
defmodule HelloPhoenix.Accounts do
@moduledoc """
The Accounts context.
"""
import Ecto.Query, warn: false
alias HelloPhoenix.Repo
alias HelloPhoenix.Accounts.User
def list_users do
Repo.all(User)
end
def get_user!(id), do: Repo.get!(User, id)
def create_user(attrs %{}) do
%User{}
|> User.changeset(attrs)
|> Repo.insert()
end
def update_user(%User{} = user, attrs) do
user
|> User.changeset(attrs)
|> Repo.update()
end
def delete_user(%User{} = user) do
Repo.delete(user)
end
def change_user(%User{} = user) do
User.changeset(user, %{})
end
alias HelloPhoenix.Accounts.Credential
def list_credentials do
Repo.all(Credential)
end
def get_credential!(id), do: Repo.get!
(Credential, id)
def create_credential(attrs %{}) do
%Credential{}
|> Credential.changeset(attrs)
|> Repo.insert()
end
def update_credential(%Credential{} =
credential, attrs) do
credential
|> Credential.changeset(attrs)
|> Repo.update()
end
def delete_credential(%Credential{} =
credential) do
Repo.delete(credential)
end
def change_credential(%Credential{} =
credential) do
Credential.changeset(credential, %{})
end
end