Phoenix per principianti
di Paolo Montrasio
paolo.montrasio@connettiva.eu
Slide a
connettiva.eu/phoenix-per-principianti.pdf
(C) Connettiva www.connettiva.eu 2
Ovvero...
2005: Conoscenza(Ruby) == 0 → Rails
2014: Conoscenza(Elixir) == 0 → Phoenix
Imparare Elixir mentre si impara Phoenix
● Le guide di Phoenix
http://www.phoenixframework.org/docs/overview
● I generatori per i controller
(C) Connettiva www.connettiva.eu 3
Per ambientarsi
● Phoenix è MVC
● È giovane ma si innalza sulle spalle dei giganti
● Per chi conosce Rails:
– Models e Controllers → Models e Controllers
– Views → Template
– Helpers → Views (circa)
– Cables (Rails 5) → Channels (da sempre)
– ActiveRecord → Ecto
– Migrations → Migrations
(C) Connettiva www.connettiva.eu 4
Differenze
● Ecto
– Lo schema va dichiarato
– Changeset per modifiche e validazioni
● Controller singolari (UserController ma /users)
● app/ → web/
● config/database.yml → config/<env>.exs
● config/routes.rb → web/router.ex
(C) Connettiva www.connettiva.eu 5
Creare una web app
http://www.phoenixframework.org/docs/up-and-ru
nning
mix phoenix.new bologna_2015
git add .gitignore config/ lib/ mix.* package.json
priv/ README.md test/ web/
git commit -a -m "Demo for today!"
(C) Connettiva www.connettiva.eu 6
config/dev.exs
config :bologna_2015, Bologna_2015.Repo,
adapter: Ecto.Adapters.Postgres,
username: "bologna_2015",
password: "RJP4Q1_2vPYX4UOR",
database: "bologna_2015_dev",
hostname: "localhost",
pool_size: 10
(C) Connettiva www.connettiva.eu 7
Lancio della web app
$ mix phoenix.run # rails s
$ iex -S mix # rails c + rails s
http://localhost:4000
$ mix -h # rake -T
(C) Connettiva www.connettiva.eu 8
Debug
iex -S mix phoenix.server
include IEx.pry
IEx.pry nel punto dove ispezionare lo stato
(C) Connettiva www.connettiva.eu 9
web/router.ex
defmodule Bologna_2015.Router do
use Bologna_2015.Web, :router
scope "/", Bologna_2015 do
pipe_through :browser
get "/", PageController, :index
resources "/users", UserController
end
end
(C) Connettiva www.connettiva.eu 10
Restful routes
$ mix phoenix.routes
page_path GET / Bologna_2015.PageController :index
user_path GET /users Bologna_2015.UserController :index
user_path GET /users/:id/edit Bologna_2015.UserController :edit
user_path GET /users/new Bologna_2015.UserController :new
user_path GET /users/:id Bologna_2015.UserController :show
user_path POST /users Bologna_2015.UserController :create
user_path PATCH /users/:id Bologna_2015.UserController :update
PUT /users/:id Bologna_2015.UserController :update
user_path DELETE /users/:id Bologna_2015.UserController :delete
(C) Connettiva www.connettiva.eu 11
Path helpers
iex(1)> Bologna_2015.Router.Helpers.user_path(
Bologna_2015.Endpoint, :index)
"/users"
iex(2)> Bologna_2015.Router.Helpers.user_path(
Bologna_2015.Endpoint, :show, 1)
"/users/1"
(C) Connettiva www.connettiva.eu 12
Più compatto
iex(3)> import Bologna_2015.Router.Helpers
nil
iex(4)> user_path(Bologna_2015.Endpoint, :index)
"/users"
iex(5)> user_path(Bologna_2015.Endpoint, :show, 1)
"/users/1"
(C) Connettiva www.connettiva.eu 13
Ancora più compatto
iex(8)> alias Bologna_2015.Endpoint
nil
iex(9)> user_path(Endpoint, :index)
"/users"
iex(10)> user_path(Endpoint, :show, 1)
"/users/1"
(C) Connettiva www.connettiva.eu 14
URL con parametri
iex(11)> user_path(Endpoint, :index,
order: "reverse")
"/users?order=reverse"
iex(12)> user_url(Endpoint, :index,
order: "reverse")
"http://localhost:4000/users?order=reverse"
(C) Connettiva www.connettiva.eu 15
Cos'è un Endpoint?
lib/bologna_2015/endpoint.ex
defmodule Bologna_2015.Endpoint do
use Phoenix.Endpoint, otp_app: :bologna_2015
…
plug Plug.Session, # i plug modificano conn
store: :cookie,
key: "_bologna_2015_key",
signing_salt: "PgkCXiY6"
plug Bologna_2015.Router # definito in web/router.ex
end
(C) Connettiva www.connettiva.eu 16
Scoping delle rotte
scope "/admin" do
resources "/users", Admin.UserController
end
user_path GET /admin/users Bologna_2015.Admin.UserController :index
user_path GET /admin/users/:id/edit Bologna_2015.Admin.UserController :edit
user_path GET /admin/users/new Bologna_2015.Admin.UserController :new
user_path GET /admin/users/:id Bologna_2015.Admin.UserController :show
user_path POST /admin/users Bologna_2015.Admin.UserController :create
user_path PATCH /admin/users/:id Bologna_2015.Admin.UserController :update
PUT /admin/users/:id Bologna_2015.Admin.UserController :update
user_path DELETE /admin/users/:id Bologna_2015.Admin.UserController :delete
(C) Connettiva www.connettiva.eu 17
I controller
def show(conn, %{"id" => id}) do
user = Repo.get!(User, id)
render(conn, "show.html", user: user)
end
o anche: conn
|> assign(:user, user)
|> render("show.html")
(C) Connettiva www.connettiva.eu 18
Dev procedurali: attenzione!
Funziona: conn
|> assign(:user, user)
|> render("show.html")
Non funziona: assign(conn, :user, user)
render(conn, "show.html")
(C) Connettiva www.connettiva.eu 19
API JSON
def show(conn, %{"id" => id}) do
user = Repo.get!(User, id)
json conn, %{ id: user.id, email: user.email, inserted_at:
user.inserted_at, updated_at: user.updated_at }
end
GET /admin/users/1
{"updated_at":"2015-10-10T09:47:04.528266Z",
"inserted_at":"2015-10-10T09:47:04.528266Z",
"id":1,"email":"paolo.montrasio@connettiva.eu"}
(C) Connettiva www.connettiva.eu 20
Redirect
def delete(conn, %{"id" => id}) do
user = Repo.get!(User, id)
Repo.delete!(user)
conn
|> put_flash(:info, "User deleted successfully.")
|> redirect(to: user_path(conn, :index))
end
(C) Connettiva www.connettiva.eu 21
Cos'è un flash?
web/templates/layout/app.html.eex
<p class="alert alert-info" role="alert">
<%= get_flash(@conn, :info) %>
</p>
<p class="alert alert-danger" role="alert">
<%= get_flash(@conn, :error) %>
</p>
<%= @inner %>
(C) Connettiva www.connettiva.eu 22
Porting di una app a Phoenix
● Customers analytics per CheckBonus
http://checkbonus.it/
● Web app Rails
● Le pagine fanno richieste
a Rails per mostrare
tabelle e grafici
● Risposte JSON
(C) Connettiva www.connettiva.eu 23
Modelli
$ mix phoenix.gen.html Retailer retailers name:string internal_id:integer
* creating web/controllers/retailer_controller.ex
* creating web/templates/retailer/edit.html.eex
* creating web/templates/retailer/form.html.eex
* creating web/templates/retailer/index.html.eex
* creating web/templates/retailer/new.html.eex
* creating web/templates/retailer/show.html.eex
* creating web/views/retailer_view.ex
* creating test/controllers/retailer_controller_test.exs
* creating priv/repo/migrations/20150919101354_create_retailer.exs
* creating web/models/retailer.ex
* creating test/models/retailer_test.exs
(C) Connettiva www.connettiva.eu 24
Rotte e migrazioni
Aggiungere resources "/retailers", RetailerController
Altrimenti:
$ mix ecto.migrate
== Compilation error on file web/controllers/retailer_controller.ex ==
** (CompileError) web/controllers/retailer_controller.ex:25: function
retailer_path/2 undefined
(stdlib) lists.erl:1337: :lists.foreach/2
(stdlib) erl_eval.erl:669: :erl_eval.do_apply/6
(elixir) lib/kernel/parallel_compiler.ex:100: anonymous fn/4 in
Kernel.ParallelCompiler.spawn_compilers/8
(C) Connettiva www.connettiva.eu 25
Migrazioni con Ecto
$ mix ecto.migrate # up
$ mix ecto.rollback # down di uno
http://hexdocs.pm/ecto/Ecto.html
Adapter per PostgreSQL, MySQL, MariaDB,
MSSQL, MongoDB.
(C) Connettiva www.connettiva.eu 26
Il modello generato
defmodule Bologna_2015.Retailer do
use Bologna_2015.Web, :model
schema "retailers" do
field :name, :string
field :internal_id, :integer
timestamps
has_many :shops, Bologna_2015.Shop
has_many :visits, Bologna_2015.Visit
end
@required_fields ~w(name)
@optional_fields ~w(internal_id)
def changeset(model, params  :empty) do
model
|> cast(params, @required_fields,
@optional_fields)
end
end
(C) Connettiva www.connettiva.eu 27
Changeset e validazioni / 1
iex(5)> alias Bologna_2015.Retailer
iex(6)> changeset = Retailer.changeset(%Retailer{}, %{})
%Ecto.Changeset{action: nil, changes: %{}, constraints: [],
errors: [name: "can't be blank"], filters: %{},
model: %Bologna_2015.Retailer{__meta__: #Ecto.Schema.Metadata<:built>,
internal_id: nil, id: nil, inserted_at: nil, name: nil, updated_at: nil},
optional: [:internal_id], opts: nil, params: %{}, repo: nil,
required: [:name],
types: %{internal_id: :integer, id: :id, inserted_at: Ecto.DateTime,
name: :string, updated_at: Ecto.DateTime}, valid?: false, validations: []}
iex(7)> changeset.valid?
false
iex(8)> changeset.errors
[name: "can't be blank"]
(C) Connettiva www.connettiva.eu 28
Changeset e validazioni / 2
iex(10)> params = %{name: "Joe Example"}
%{name: "Joe Example"}
iex(11)> changeset = Retailer.changeset(%Retailer{}, params)
%Ecto.Changeset{action: nil, changes: %{name: "Joe Example"}, constraints: [],
errors: [], filters: %{},
model: %Bologna_2015.Retailer{__meta__: #Ecto.Schema.Metadata<:built>,
internal_id: nil, id: nil, inserted_at: nil, name: nil, updated_at: nil},
optional: [:internal_id], opts: nil, params: %{"name" => "Joe Example"},
repo: nil, required: [:name],
types: %{internal_id: :integer, id: :id, inserted_at: Ecto.DateTime,
name: :string, updated_at: Ecto.DateTime}, valid?: true, validations: []}
iex(12)> changeset.valid?
true
iex(13)> changeset.errors
[ ]
(C) Connettiva www.connettiva.eu 29
Validazioni
def changeset(model, params  :empty) do
model
|> cast(params, @required_fields, @optional_fields)
|> validate_confirmation(:password)
|> validate_length(:password, min: 12)
|> validate_number(:age)
|> validate_inclusion(:age, 18..130)
|> validate_format(:email, ~r/@/)
end
(C) Connettiva www.connettiva.eu 30
Registrazione e autenticazione
● L'ostacolo più grande all'adozione di Phoenix
● No framework con copertura di tutto lo use case
– Registrazione
– Invio mail di attivazione
– Non ho ricevuto il link di attivazione
– Ho perso la password
– Faccio login / faccio logout
– Mi autentico con FB / Tw / G+ / OAuth
(C) Connettiva www.connettiva.eu 31
Soluzioni
● Addict https://github.com/trenpixster/addict
– POST JSON per registrazione, login, logout, recupero e
reset password: OK per SPA.
– Mail via Mailgun
● Passport https://github.com/opendrops/passport
– No routes, no controllers: un SessionManager da usare
nel proprio codice
● Do it yourself
http://nithinbekal.com/posts/phoenix-authentication/
(C) Connettiva www.connettiva.eu 32
Do It Yourself: solo la login
/admin/users/5
/sessions/new
/admin/users/5
/sessions/create
(C) Connettiva www.connettiva.eu 33
I file necessari
resources "/sessions", SessionController,
only: [ :new, :create, :delete ]
web/models/user.ex
web/controllers/session_controller.ex
web/views/session_view.ex
web/templates/session/new.html.eex
lib/bologna_2015/authentication.ex
lib/bologna_2015/must_be_logged_in.ex
(C) Connettiva www.connettiva.eu 34
Modello e cifratura password
schema "users" do
field :email, :string
field :encrypted_password, :string
end
@required_fields ~w(email encryped_password)
def hash(plaintext) do
Base.encode16(:crypto.hash(:sha256, to_char_list(plaintext)))
end
https://www.djm.org.uk/cryptographic-hash-functions-elixir-gener
ating-hex-digests-md5-sha1-sha2/
(C) Connettiva www.connettiva.eu 35
Inserimento utenti / 1
Barando, direttamente nel db (PostgreSQL)
create extension pgcrypto;
insert into users (email, encrypted_password,
inserted_at, updated_at) values
('paolo.montrasio@connettiva.eu',
upper(encode(digest('password', 'sha256'),'hex')),
now(), now());
(C) Connettiva www.connettiva.eu 36
Inserimento utenti / 2
Correttamente, in Elixir
$ iex -S mix
alias Bologna_2015.User
changeset = User.changeset(%User{},
%{email: "paolo.montrasio@connettiva.eu",
encrypted_password: User.hash("password")})
alias Bologna_2015.Repo
Repo.insert(changeset)
(C) Connettiva www.connettiva.eu 37
Form di login
<form action="/sessions" method="post">
<input type="hidden" name="_csrf_token"
value="<%= get_csrf_token() %>">
Email
<input name="user[email]" type="email" value="" />
Password
<input name="user[password]" type="password" />
<input type="submit" value="Sign in" />
</form>
(C) Connettiva www.connettiva.eu 38
Controller per le sessioni
def create(conn, %{ "user" => %{ "email" => email, "password" => password }}) do
case User.find(email, password) do
[user] ->
fetch_session(conn)
|> put_session(:user_id, user.id) # user.id nella sessione per i controller
|> put_flash(:info, "Login successful")
|> redirect(to: page_path(conn, :index))
[ ] ->
fetch_session(conn)
|> put_flash(:error, "Login failed")
|> redirect(to: session_path(conn, :new))
end
end
def find(email, password) do
enc_pwd = hash(password)
query = from user in User,
where: user.email == ^email and
user.encrypted_password == ^enc_pwd,
select: user
Repo.all(query)
end
(C) Connettiva www.connettiva.eu 39
Plug di autenticazione
defmodule Bologna_2015.Plugs.Authentication do
import Plug.Conn
alias Bologna_2015.User
alias Bologna_2015.Repo
def init(default), do: default
def call(conn, _default) do
user = nil
user_id = get_session(conn, :user_id)
unless user_id == nil do
user = Repo.get(User, user_id)
end
assign(conn, :current_user, user)
end
end
# conn.assigns[:current_user]
web/router.ex
defmodule Bologna_2015.Router do
use Bologna_2015.Web, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
plug Bologna_2015.Plugs.Authentication
end
(C) Connettiva www.connettiva.eu 40
Plug di autorizzazione
defmodule Bologna_2015.Plugs.MustBeLoggedIn do
import Plug.Conn
import Phoenix.Controller
def init(default), do: default
def call(conn, _default) do
if conn.assigns[:current_user] == nil do
conn
|> put_flash(:info, "You must be logged in")
|> redirect(to: "/") |> halt
else
conn
end
end
end
web/controllers/admin/user_controller.ex
defmodule Bologna_2015.Admin.UserController do
use Bologna_2015.Web, :controller
plug Bologna_2015.Plugs.MustBeLoggedIn
(C) Connettiva www.connettiva.eu 41
Funziona? mix test
defmodule Bologna_2015.SessionControllerTest do
use Bologna_2015.ConnCase
alias Bologna_2015.User
@valid_attrs %{"email" => "paolo.montrasio@connettiva.eu", "password" => "password"}
setup do
conn = conn()
{:ok, conn: conn}
end
test "creates session and redirects when data is valid", %{conn: conn} do
changeset = User.changeset(%User{}, %{email: @valid_attrs["email"],
encrypted_password: User.hash(@valid_attrs[“password"])})
{:ok, user } = Repo.insert(changeset)
conn = post conn, session_path(conn, :create), user: @valid_attrs
assert redirected_to(conn) == page_path(conn, :index)
assert get_session(conn, :user_id) == user.id
end
end
(C) Connettiva www.connettiva.eu 42
API JSON – di nuovo e meglio
pipeline :api do
plug :accepts, ["json"]
scope "/api", Bologna_2015, as: :api do
resources "/retailers", API.RetailerController,
only: [:index] do
resources "/visits", API.VisitController,
only: [:index]
...
api_retailer_path GET /api/retailers Bologna_2015.API.RetailerController :index
api_retailer_visit_path GET /api/retailers/:retailer_id/visits Bologna_2015.API.VisitController :index
(C) Connettiva www.connettiva.eu 43
Visit: migrazione e modello
defmodule Bologna_2015.Repo.Migrations.CreateVisit do
use Ecto.Migration
def change do
create table(:visits) do
add :retailer_id, :integer
add :started_at, :timestamp
add :duration, :integer
end
end
end
defmodule Bologna_2015.Visit do
use Bologna_2015.Web, :model
schema "visits" do
belongs_to :retailer, Bologna_2015.Retailer
field :started_at, Ecto.DateTime
field :duration, :integer
end
@required_fields ~w(retailer_id, started_at,
duration)
@optional_fields ~w()
def changeset(model, params  :empty) do
model
|> cast(params, @required_fields,
@optional_fields)
end
end
(C) Connettiva www.connettiva.eu 44
Generazione controller
mix phoenix.gen.json API.Visit visits --no-model
* creating web/controllers/api/visit_controller.ex
* creating web/views/api/visit_view.ex
* creating test/controllers/api/visit_controller_test.exs
* creating web/views/changeset_view.ex
Sostituire alias Bologna_2015.API.Visit
Con alias Bologna_2015.Visit
(C) Connettiva www.connettiva.eu 45
Il controller
def index(conn, _params) do
retailer_id = conn.assigns[:retailer].id # da dove arriva?
query = from visit in Visit,
where: visit.retailer_id == ^retailer_id,
select: visit
visits = Repo.all(query)
render(conn, "index.json", visits: visits) # dov'è il template?
end
(C) Connettiva www.connettiva.eu 46
Assign del modello
plug :assign_retailer
defp assign_retailer(conn, _options) do
retailer = Repo.get!(Bologna_2015.Retailer,
conn.params["retailer_id"])
assign(conn, :retailer, retailer)
end
(C) Connettiva www.connettiva.eu 47
Il template / 1
# web/views/api/visit_view.ex
def render("index.json", %{visits: visits}) do
%{data: render_many(visits, Bologna_2015.API.VisitView,
"visit.json")}
end
# render_many? Circa equivalente a
Enum.map(visits, fn user ->
render(Bologna_2015.API.VisitView, "visit.json”, visit: visit)
end)
(C) Connettiva www.connettiva.eu 48
Il template / 2
# web/views/api/visit_view.ex
def render("visit.json", %{visit: visit}) do
%{id: visit.id}
end
- %{id: visit.id}
+ %{started_at: visit.started_at, duration: visit.duration}
(C) Connettiva www.connettiva.eu 49
La richiesta
GET /retailers/1/visits
{"data":[
{"started_at":"2015-09-29T20:11:00Z","duration":6},
{"started_at":"2015-09-29T20:41:00Z","duration":6},
…
]}
(C) Connettiva www.connettiva.eu 50
Benchmark Phoenix
query = from visit in Visit,
where: visit.retailer_id == ^retailer_id,
select: visit
visits = Repo.all(query)
(252), 147, 134, 145, 133, 142 → media 140 ms
per 5000+ visits
(C) Connettiva www.connettiva.eu 51
Benchmark Rails
visits = Visit.where(
retailer_id: params[:retailer_id]).
pluck(:started_at, :duration)
(149), 117, 112, 124, 109, 122 → media 116 ms
(C) Connettiva www.connettiva.eu 52
Benchmark Rails
visits = Visit.where(
retailer_id: params[:retailer_id]).
pluck(:started_at, :duration)
(149), 117, 112, 124, 109, 122 → media 116 ms
Ma è un confronto onesto?
select * vs select started_at, duration
(C) Connettiva www.connettiva.eu 53
Benchmark Rails select *
visits = Visit.where(
retailer_id: params[:retailer_id])
(265), 236, 233, 230, 259, 282 → media 248 ms
(C) Connettiva www.connettiva.eu 54
Benchmark Phoenix
query = from visit in Visit,
where: visit.retailer_id == ^retailer_id,
select: [visit.started_at, visit.duration]
visits = Repo.all(query)
(193), 85, 72, 79, 70, 68 → media 74 ms
(C) Connettiva www.connettiva.eu 55
Benchmark: riassunto
select * from visits
Phoenix 140 ms
Rails 248 ms x 1.71
select started_at, duration from visits
Phoenix 74 ms
Rails 116 ms x 1.56
(C) Connettiva www.connettiva.eu 56
Benchmark: riassunto
select * from visits
Phoenix 140 ms
Rails 248 ms x 1.71
Ruby senza AR 219 ms
PostgreSQL 2.97 ms
select started_at, duration from visits
Phoenix 74 ms
Rails 116 ms x 1.56
Ruby senza AR 88 ms
PostgreSQL 3.47 ms
(C) Connettiva www.connettiva.eu 57
Fastidi
● alias / import / require all'inizio di ogni file
● Mancanza di un framework di autenticazione
● Dover chiamare ogni tanto Erlang
● Dover scrivere due volte lo schema, nella
migrazione e nel modello
(C) Connettiva www.connettiva.eu 58
Delizie
● Hot reload
● iex -S mix
● Channels
– https://medium.com/@azzarcher/the-simplicity-and
-power-of-elixir-the-ws2048-case-b510eaa568c0
(C) Connettiva www.connettiva.eu 59
Domande e contatti
Paolo Montrasio
paolo.montrasio@connettiva.eu
Slide a
connettiva.eu/phoenix-per-principianti.pdf

Phoenix per principianti

  • 1.
    Phoenix per principianti diPaolo Montrasio [email protected] Slide a connettiva.eu/phoenix-per-principianti.pdf
  • 2.
    (C) Connettiva www.connettiva.eu2 Ovvero... 2005: Conoscenza(Ruby) == 0 → Rails 2014: Conoscenza(Elixir) == 0 → Phoenix Imparare Elixir mentre si impara Phoenix ● Le guide di Phoenix http://www.phoenixframework.org/docs/overview ● I generatori per i controller
  • 3.
    (C) Connettiva www.connettiva.eu3 Per ambientarsi ● Phoenix è MVC ● È giovane ma si innalza sulle spalle dei giganti ● Per chi conosce Rails: – Models e Controllers → Models e Controllers – Views → Template – Helpers → Views (circa) – Cables (Rails 5) → Channels (da sempre) – ActiveRecord → Ecto – Migrations → Migrations
  • 4.
    (C) Connettiva www.connettiva.eu4 Differenze ● Ecto – Lo schema va dichiarato – Changeset per modifiche e validazioni ● Controller singolari (UserController ma /users) ● app/ → web/ ● config/database.yml → config/<env>.exs ● config/routes.rb → web/router.ex
  • 5.
    (C) Connettiva www.connettiva.eu5 Creare una web app http://www.phoenixframework.org/docs/up-and-ru nning mix phoenix.new bologna_2015 git add .gitignore config/ lib/ mix.* package.json priv/ README.md test/ web/ git commit -a -m "Demo for today!"
  • 6.
    (C) Connettiva www.connettiva.eu6 config/dev.exs config :bologna_2015, Bologna_2015.Repo, adapter: Ecto.Adapters.Postgres, username: "bologna_2015", password: "RJP4Q1_2vPYX4UOR", database: "bologna_2015_dev", hostname: "localhost", pool_size: 10
  • 7.
    (C) Connettiva www.connettiva.eu7 Lancio della web app $ mix phoenix.run # rails s $ iex -S mix # rails c + rails s http://localhost:4000 $ mix -h # rake -T
  • 8.
    (C) Connettiva www.connettiva.eu8 Debug iex -S mix phoenix.server include IEx.pry IEx.pry nel punto dove ispezionare lo stato
  • 9.
    (C) Connettiva www.connettiva.eu9 web/router.ex defmodule Bologna_2015.Router do use Bologna_2015.Web, :router scope "/", Bologna_2015 do pipe_through :browser get "/", PageController, :index resources "/users", UserController end end
  • 10.
    (C) Connettiva www.connettiva.eu10 Restful routes $ mix phoenix.routes page_path GET / Bologna_2015.PageController :index user_path GET /users Bologna_2015.UserController :index user_path GET /users/:id/edit Bologna_2015.UserController :edit user_path GET /users/new Bologna_2015.UserController :new user_path GET /users/:id Bologna_2015.UserController :show user_path POST /users Bologna_2015.UserController :create user_path PATCH /users/:id Bologna_2015.UserController :update PUT /users/:id Bologna_2015.UserController :update user_path DELETE /users/:id Bologna_2015.UserController :delete
  • 11.
    (C) Connettiva www.connettiva.eu11 Path helpers iex(1)> Bologna_2015.Router.Helpers.user_path( Bologna_2015.Endpoint, :index) "/users" iex(2)> Bologna_2015.Router.Helpers.user_path( Bologna_2015.Endpoint, :show, 1) "/users/1"
  • 12.
    (C) Connettiva www.connettiva.eu12 Più compatto iex(3)> import Bologna_2015.Router.Helpers nil iex(4)> user_path(Bologna_2015.Endpoint, :index) "/users" iex(5)> user_path(Bologna_2015.Endpoint, :show, 1) "/users/1"
  • 13.
    (C) Connettiva www.connettiva.eu13 Ancora più compatto iex(8)> alias Bologna_2015.Endpoint nil iex(9)> user_path(Endpoint, :index) "/users" iex(10)> user_path(Endpoint, :show, 1) "/users/1"
  • 14.
    (C) Connettiva www.connettiva.eu14 URL con parametri iex(11)> user_path(Endpoint, :index, order: "reverse") "/users?order=reverse" iex(12)> user_url(Endpoint, :index, order: "reverse") "http://localhost:4000/users?order=reverse"
  • 15.
    (C) Connettiva www.connettiva.eu15 Cos'è un Endpoint? lib/bologna_2015/endpoint.ex defmodule Bologna_2015.Endpoint do use Phoenix.Endpoint, otp_app: :bologna_2015 … plug Plug.Session, # i plug modificano conn store: :cookie, key: "_bologna_2015_key", signing_salt: "PgkCXiY6" plug Bologna_2015.Router # definito in web/router.ex end
  • 16.
    (C) Connettiva www.connettiva.eu16 Scoping delle rotte scope "/admin" do resources "/users", Admin.UserController end user_path GET /admin/users Bologna_2015.Admin.UserController :index user_path GET /admin/users/:id/edit Bologna_2015.Admin.UserController :edit user_path GET /admin/users/new Bologna_2015.Admin.UserController :new user_path GET /admin/users/:id Bologna_2015.Admin.UserController :show user_path POST /admin/users Bologna_2015.Admin.UserController :create user_path PATCH /admin/users/:id Bologna_2015.Admin.UserController :update PUT /admin/users/:id Bologna_2015.Admin.UserController :update user_path DELETE /admin/users/:id Bologna_2015.Admin.UserController :delete
  • 17.
    (C) Connettiva www.connettiva.eu17 I controller def show(conn, %{"id" => id}) do user = Repo.get!(User, id) render(conn, "show.html", user: user) end o anche: conn |> assign(:user, user) |> render("show.html")
  • 18.
    (C) Connettiva www.connettiva.eu18 Dev procedurali: attenzione! Funziona: conn |> assign(:user, user) |> render("show.html") Non funziona: assign(conn, :user, user) render(conn, "show.html")
  • 19.
    (C) Connettiva www.connettiva.eu19 API JSON def show(conn, %{"id" => id}) do user = Repo.get!(User, id) json conn, %{ id: user.id, email: user.email, inserted_at: user.inserted_at, updated_at: user.updated_at } end GET /admin/users/1 {"updated_at":"2015-10-10T09:47:04.528266Z", "inserted_at":"2015-10-10T09:47:04.528266Z", "id":1,"email":"[email protected]"}
  • 20.
    (C) Connettiva www.connettiva.eu20 Redirect def delete(conn, %{"id" => id}) do user = Repo.get!(User, id) Repo.delete!(user) conn |> put_flash(:info, "User deleted successfully.") |> redirect(to: user_path(conn, :index)) end
  • 21.
    (C) Connettiva www.connettiva.eu21 Cos'è un flash? web/templates/layout/app.html.eex <p class="alert alert-info" role="alert"> <%= get_flash(@conn, :info) %> </p> <p class="alert alert-danger" role="alert"> <%= get_flash(@conn, :error) %> </p> <%= @inner %>
  • 22.
    (C) Connettiva www.connettiva.eu22 Porting di una app a Phoenix ● Customers analytics per CheckBonus http://checkbonus.it/ ● Web app Rails ● Le pagine fanno richieste a Rails per mostrare tabelle e grafici ● Risposte JSON
  • 23.
    (C) Connettiva www.connettiva.eu23 Modelli $ mix phoenix.gen.html Retailer retailers name:string internal_id:integer * creating web/controllers/retailer_controller.ex * creating web/templates/retailer/edit.html.eex * creating web/templates/retailer/form.html.eex * creating web/templates/retailer/index.html.eex * creating web/templates/retailer/new.html.eex * creating web/templates/retailer/show.html.eex * creating web/views/retailer_view.ex * creating test/controllers/retailer_controller_test.exs * creating priv/repo/migrations/20150919101354_create_retailer.exs * creating web/models/retailer.ex * creating test/models/retailer_test.exs
  • 24.
    (C) Connettiva www.connettiva.eu24 Rotte e migrazioni Aggiungere resources "/retailers", RetailerController Altrimenti: $ mix ecto.migrate == Compilation error on file web/controllers/retailer_controller.ex == ** (CompileError) web/controllers/retailer_controller.ex:25: function retailer_path/2 undefined (stdlib) lists.erl:1337: :lists.foreach/2 (stdlib) erl_eval.erl:669: :erl_eval.do_apply/6 (elixir) lib/kernel/parallel_compiler.ex:100: anonymous fn/4 in Kernel.ParallelCompiler.spawn_compilers/8
  • 25.
    (C) Connettiva www.connettiva.eu25 Migrazioni con Ecto $ mix ecto.migrate # up $ mix ecto.rollback # down di uno http://hexdocs.pm/ecto/Ecto.html Adapter per PostgreSQL, MySQL, MariaDB, MSSQL, MongoDB.
  • 26.
    (C) Connettiva www.connettiva.eu26 Il modello generato defmodule Bologna_2015.Retailer do use Bologna_2015.Web, :model schema "retailers" do field :name, :string field :internal_id, :integer timestamps has_many :shops, Bologna_2015.Shop has_many :visits, Bologna_2015.Visit end @required_fields ~w(name) @optional_fields ~w(internal_id) def changeset(model, params :empty) do model |> cast(params, @required_fields, @optional_fields) end end
  • 27.
    (C) Connettiva www.connettiva.eu27 Changeset e validazioni / 1 iex(5)> alias Bologna_2015.Retailer iex(6)> changeset = Retailer.changeset(%Retailer{}, %{}) %Ecto.Changeset{action: nil, changes: %{}, constraints: [], errors: [name: "can't be blank"], filters: %{}, model: %Bologna_2015.Retailer{__meta__: #Ecto.Schema.Metadata<:built>, internal_id: nil, id: nil, inserted_at: nil, name: nil, updated_at: nil}, optional: [:internal_id], opts: nil, params: %{}, repo: nil, required: [:name], types: %{internal_id: :integer, id: :id, inserted_at: Ecto.DateTime, name: :string, updated_at: Ecto.DateTime}, valid?: false, validations: []} iex(7)> changeset.valid? false iex(8)> changeset.errors [name: "can't be blank"]
  • 28.
    (C) Connettiva www.connettiva.eu28 Changeset e validazioni / 2 iex(10)> params = %{name: "Joe Example"} %{name: "Joe Example"} iex(11)> changeset = Retailer.changeset(%Retailer{}, params) %Ecto.Changeset{action: nil, changes: %{name: "Joe Example"}, constraints: [], errors: [], filters: %{}, model: %Bologna_2015.Retailer{__meta__: #Ecto.Schema.Metadata<:built>, internal_id: nil, id: nil, inserted_at: nil, name: nil, updated_at: nil}, optional: [:internal_id], opts: nil, params: %{"name" => "Joe Example"}, repo: nil, required: [:name], types: %{internal_id: :integer, id: :id, inserted_at: Ecto.DateTime, name: :string, updated_at: Ecto.DateTime}, valid?: true, validations: []} iex(12)> changeset.valid? true iex(13)> changeset.errors [ ]
  • 29.
    (C) Connettiva www.connettiva.eu29 Validazioni def changeset(model, params :empty) do model |> cast(params, @required_fields, @optional_fields) |> validate_confirmation(:password) |> validate_length(:password, min: 12) |> validate_number(:age) |> validate_inclusion(:age, 18..130) |> validate_format(:email, ~r/@/) end
  • 30.
    (C) Connettiva www.connettiva.eu30 Registrazione e autenticazione ● L'ostacolo più grande all'adozione di Phoenix ● No framework con copertura di tutto lo use case – Registrazione – Invio mail di attivazione – Non ho ricevuto il link di attivazione – Ho perso la password – Faccio login / faccio logout – Mi autentico con FB / Tw / G+ / OAuth
  • 31.
    (C) Connettiva www.connettiva.eu31 Soluzioni ● Addict https://github.com/trenpixster/addict – POST JSON per registrazione, login, logout, recupero e reset password: OK per SPA. – Mail via Mailgun ● Passport https://github.com/opendrops/passport – No routes, no controllers: un SessionManager da usare nel proprio codice ● Do it yourself http://nithinbekal.com/posts/phoenix-authentication/
  • 32.
    (C) Connettiva www.connettiva.eu32 Do It Yourself: solo la login /admin/users/5 /sessions/new /admin/users/5 /sessions/create
  • 33.
    (C) Connettiva www.connettiva.eu33 I file necessari resources "/sessions", SessionController, only: [ :new, :create, :delete ] web/models/user.ex web/controllers/session_controller.ex web/views/session_view.ex web/templates/session/new.html.eex lib/bologna_2015/authentication.ex lib/bologna_2015/must_be_logged_in.ex
  • 34.
    (C) Connettiva www.connettiva.eu34 Modello e cifratura password schema "users" do field :email, :string field :encrypted_password, :string end @required_fields ~w(email encryped_password) def hash(plaintext) do Base.encode16(:crypto.hash(:sha256, to_char_list(plaintext))) end https://www.djm.org.uk/cryptographic-hash-functions-elixir-gener ating-hex-digests-md5-sha1-sha2/
  • 35.
    (C) Connettiva www.connettiva.eu35 Inserimento utenti / 1 Barando, direttamente nel db (PostgreSQL) create extension pgcrypto; insert into users (email, encrypted_password, inserted_at, updated_at) values ('[email protected]', upper(encode(digest('password', 'sha256'),'hex')), now(), now());
  • 36.
    (C) Connettiva www.connettiva.eu36 Inserimento utenti / 2 Correttamente, in Elixir $ iex -S mix alias Bologna_2015.User changeset = User.changeset(%User{}, %{email: "[email protected]", encrypted_password: User.hash("password")}) alias Bologna_2015.Repo Repo.insert(changeset)
  • 37.
    (C) Connettiva www.connettiva.eu37 Form di login <form action="/sessions" method="post"> <input type="hidden" name="_csrf_token" value="<%= get_csrf_token() %>"> Email <input name="user[email]" type="email" value="" /> Password <input name="user[password]" type="password" /> <input type="submit" value="Sign in" /> </form>
  • 38.
    (C) Connettiva www.connettiva.eu38 Controller per le sessioni def create(conn, %{ "user" => %{ "email" => email, "password" => password }}) do case User.find(email, password) do [user] -> fetch_session(conn) |> put_session(:user_id, user.id) # user.id nella sessione per i controller |> put_flash(:info, "Login successful") |> redirect(to: page_path(conn, :index)) [ ] -> fetch_session(conn) |> put_flash(:error, "Login failed") |> redirect(to: session_path(conn, :new)) end end def find(email, password) do enc_pwd = hash(password) query = from user in User, where: user.email == ^email and user.encrypted_password == ^enc_pwd, select: user Repo.all(query) end
  • 39.
    (C) Connettiva www.connettiva.eu39 Plug di autenticazione defmodule Bologna_2015.Plugs.Authentication do import Plug.Conn alias Bologna_2015.User alias Bologna_2015.Repo def init(default), do: default def call(conn, _default) do user = nil user_id = get_session(conn, :user_id) unless user_id == nil do user = Repo.get(User, user_id) end assign(conn, :current_user, user) end end # conn.assigns[:current_user] web/router.ex defmodule Bologna_2015.Router do use Bologna_2015.Web, :router pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers plug Bologna_2015.Plugs.Authentication end
  • 40.
    (C) Connettiva www.connettiva.eu40 Plug di autorizzazione defmodule Bologna_2015.Plugs.MustBeLoggedIn do import Plug.Conn import Phoenix.Controller def init(default), do: default def call(conn, _default) do if conn.assigns[:current_user] == nil do conn |> put_flash(:info, "You must be logged in") |> redirect(to: "/") |> halt else conn end end end web/controllers/admin/user_controller.ex defmodule Bologna_2015.Admin.UserController do use Bologna_2015.Web, :controller plug Bologna_2015.Plugs.MustBeLoggedIn
  • 41.
    (C) Connettiva www.connettiva.eu41 Funziona? mix test defmodule Bologna_2015.SessionControllerTest do use Bologna_2015.ConnCase alias Bologna_2015.User @valid_attrs %{"email" => "[email protected]", "password" => "password"} setup do conn = conn() {:ok, conn: conn} end test "creates session and redirects when data is valid", %{conn: conn} do changeset = User.changeset(%User{}, %{email: @valid_attrs["email"], encrypted_password: User.hash(@valid_attrs[“password"])}) {:ok, user } = Repo.insert(changeset) conn = post conn, session_path(conn, :create), user: @valid_attrs assert redirected_to(conn) == page_path(conn, :index) assert get_session(conn, :user_id) == user.id end end
  • 42.
    (C) Connettiva www.connettiva.eu42 API JSON – di nuovo e meglio pipeline :api do plug :accepts, ["json"] scope "/api", Bologna_2015, as: :api do resources "/retailers", API.RetailerController, only: [:index] do resources "/visits", API.VisitController, only: [:index] ... api_retailer_path GET /api/retailers Bologna_2015.API.RetailerController :index api_retailer_visit_path GET /api/retailers/:retailer_id/visits Bologna_2015.API.VisitController :index
  • 43.
    (C) Connettiva www.connettiva.eu43 Visit: migrazione e modello defmodule Bologna_2015.Repo.Migrations.CreateVisit do use Ecto.Migration def change do create table(:visits) do add :retailer_id, :integer add :started_at, :timestamp add :duration, :integer end end end defmodule Bologna_2015.Visit do use Bologna_2015.Web, :model schema "visits" do belongs_to :retailer, Bologna_2015.Retailer field :started_at, Ecto.DateTime field :duration, :integer end @required_fields ~w(retailer_id, started_at, duration) @optional_fields ~w() def changeset(model, params :empty) do model |> cast(params, @required_fields, @optional_fields) end end
  • 44.
    (C) Connettiva www.connettiva.eu44 Generazione controller mix phoenix.gen.json API.Visit visits --no-model * creating web/controllers/api/visit_controller.ex * creating web/views/api/visit_view.ex * creating test/controllers/api/visit_controller_test.exs * creating web/views/changeset_view.ex Sostituire alias Bologna_2015.API.Visit Con alias Bologna_2015.Visit
  • 45.
    (C) Connettiva www.connettiva.eu45 Il controller def index(conn, _params) do retailer_id = conn.assigns[:retailer].id # da dove arriva? query = from visit in Visit, where: visit.retailer_id == ^retailer_id, select: visit visits = Repo.all(query) render(conn, "index.json", visits: visits) # dov'è il template? end
  • 46.
    (C) Connettiva www.connettiva.eu46 Assign del modello plug :assign_retailer defp assign_retailer(conn, _options) do retailer = Repo.get!(Bologna_2015.Retailer, conn.params["retailer_id"]) assign(conn, :retailer, retailer) end
  • 47.
    (C) Connettiva www.connettiva.eu47 Il template / 1 # web/views/api/visit_view.ex def render("index.json", %{visits: visits}) do %{data: render_many(visits, Bologna_2015.API.VisitView, "visit.json")} end # render_many? Circa equivalente a Enum.map(visits, fn user -> render(Bologna_2015.API.VisitView, "visit.json”, visit: visit) end)
  • 48.
    (C) Connettiva www.connettiva.eu48 Il template / 2 # web/views/api/visit_view.ex def render("visit.json", %{visit: visit}) do %{id: visit.id} end - %{id: visit.id} + %{started_at: visit.started_at, duration: visit.duration}
  • 49.
    (C) Connettiva www.connettiva.eu49 La richiesta GET /retailers/1/visits {"data":[ {"started_at":"2015-09-29T20:11:00Z","duration":6}, {"started_at":"2015-09-29T20:41:00Z","duration":6}, … ]}
  • 50.
    (C) Connettiva www.connettiva.eu50 Benchmark Phoenix query = from visit in Visit, where: visit.retailer_id == ^retailer_id, select: visit visits = Repo.all(query) (252), 147, 134, 145, 133, 142 → media 140 ms per 5000+ visits
  • 51.
    (C) Connettiva www.connettiva.eu51 Benchmark Rails visits = Visit.where( retailer_id: params[:retailer_id]). pluck(:started_at, :duration) (149), 117, 112, 124, 109, 122 → media 116 ms
  • 52.
    (C) Connettiva www.connettiva.eu52 Benchmark Rails visits = Visit.where( retailer_id: params[:retailer_id]). pluck(:started_at, :duration) (149), 117, 112, 124, 109, 122 → media 116 ms Ma è un confronto onesto? select * vs select started_at, duration
  • 53.
    (C) Connettiva www.connettiva.eu53 Benchmark Rails select * visits = Visit.where( retailer_id: params[:retailer_id]) (265), 236, 233, 230, 259, 282 → media 248 ms
  • 54.
    (C) Connettiva www.connettiva.eu54 Benchmark Phoenix query = from visit in Visit, where: visit.retailer_id == ^retailer_id, select: [visit.started_at, visit.duration] visits = Repo.all(query) (193), 85, 72, 79, 70, 68 → media 74 ms
  • 55.
    (C) Connettiva www.connettiva.eu55 Benchmark: riassunto select * from visits Phoenix 140 ms Rails 248 ms x 1.71 select started_at, duration from visits Phoenix 74 ms Rails 116 ms x 1.56
  • 56.
    (C) Connettiva www.connettiva.eu56 Benchmark: riassunto select * from visits Phoenix 140 ms Rails 248 ms x 1.71 Ruby senza AR 219 ms PostgreSQL 2.97 ms select started_at, duration from visits Phoenix 74 ms Rails 116 ms x 1.56 Ruby senza AR 88 ms PostgreSQL 3.47 ms
  • 57.
    (C) Connettiva www.connettiva.eu57 Fastidi ● alias / import / require all'inizio di ogni file ● Mancanza di un framework di autenticazione ● Dover chiamare ogni tanto Erlang ● Dover scrivere due volte lo schema, nella migrazione e nel modello
  • 58.
    (C) Connettiva www.connettiva.eu58 Delizie ● Hot reload ● iex -S mix ● Channels – https://medium.com/@azzarcher/the-simplicity-and -power-of-elixir-the-ws2048-case-b510eaa568c0
  • 59.
    (C) Connettiva www.connettiva.eu59 Domande e contatti Paolo Montrasio [email protected] Slide a connettiva.eu/phoenix-per-principianti.pdf