Understanding a Phoenix application and it's lifecycle
It is a post for beginners in Elixir and Phoenix. Has intention to help understanding how a Phoenix application works and a bit of its lifecycle.
I started the day thinking in write an chat application to apply what I have learned so far of Elixir. But even after reading the whole Elixir - Getting Started and Phoenix - Up And Running. I was not really feeling prepared to write an application on my own.
P.S. I didn't read the Phoenix framework documentation before start the application. I'm sure if I had read the docs, everything would makes sense. But I was just too excited to start coding :D.
So instead of writing a chat app from scratch. What we gonna do is understand how the chat app built by Chris McCord works.
Before understanding, lets see the application running:
- Clone the repo chrismccord/phoenix_chat_example, then cd to the new directory
- Install dependencies with
mix deps.get - (optional) Install npm dependencies to customize the ES6 js/Sass
npm install - Start Phoenix router with
mix phoenix.server
Now you can visit localhost:4000 from your browser.
mix.exs๐
Contains the definition for all dependencies and configures the OTP application.
Check out OTP Has Done It - by Nick DeMonner to understand what is an OTP application.
use Mix.Project
[app: :chat,
version: ,
elixir: ,
elixirc_paths: [, ],
compilers: [:phoenix] ++ Mix.compilers,
deps: deps]
end
# Configuration for the OTP application
#
# Type `mix help compile.app` for more information
[mod: ,
applications: [:phoenix, :phoenix_html, :cowboy, :logger]]
end
# Specifies your project dependencies
#
# Type `mix help deps` for examples and options
[,
,
,
,
,
]
end
end
In the application function is defined the Chat as the startup module. And also is defined all applications your application depends on at runtime.
lib/chat.ex๐
use Application
# See http://elixir-lang.org/docs/stable/elixir/Application.html
# for more information on OTP Applications
import Supervisor.Spec, warn: false
children = [
# Start the endpoint when the application starts
supervisor(Chat.Endpoint, []),
# Start the Ecto repository
worker(Chat.Repo, []),
# Here you could define other workers and supervisors as children
# worker(Chat.Worker, [arg1, arg2, arg3]),
]
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Chat.Supervisor]
Supervisor.start_link(children, opts)
end
# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
Chat.Endpoint.config_change(changed, removed)
:ok
end
end
In lib/chat.ex is defined the OTP Application. As we can see the Chat.Endpoint is started as supervisor. Which will start the endpoint when the application starts and it will be restarted in case it crashes.
And Chat.Repo is started as worker. Which will run the repository in a different process. Allowing this way to keep the state (e.g. connection pool) between different requests. Otherwise would be necessary establish a new DB connection for every request.
lib/chat/endpoint.ex๐
use Phoenix.Endpoint, otp_app: :chat
# Requests coming at "/socket" path will be handled by
# UserSocket (web/channels/user_socket.ex)
socket , Chat.UserSocket
# Serve at "/" the given assets from "priv/static" directory
plug Plug.Static,
at: , from: :chat,
only:
# Code reloading will only work if the :code_reloader key of
# the :phoenix application is set to true in your config file.
if code_reloading? do
socket , Phoenix.LiveReloader.Socket
plug Phoenix.CodeReloader
plug Phoenix.LiveReloader
end
# Log the requests
plug Plug.Logger
# Configure parsers
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: [],
json_decoder: Poison
plug Plug.MethodOverride
plug Plug.Head
plug Plug.Session,
store: :cookie,
key: ,
signing_salt: ,
encryption_salt:
# Only after passing through all the previous Plug
# the request will be handled by the Chat.Router (web/router.ex)
plug Chat.Router
end
In lib/chat/endpoint.ex is used a lot Plug. It allows compose modules between web applications. With Plug is possible to change the request and response data through the connection lifecycle. It is comparable to a middleware in Node JS.
web/router.ex๐
use Phoenix.Router
pipeline :browser do
plug :accepts, []
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
end
pipeline :api do
plug :accepts, []
end
scope , Chat do
pipe_through :browser # Use the default browser stack
get , PageController, :index
end
end
In Chat.Router we see the definition of pipeline. Which is a simple way to pipe a series of plug before passing the request ahead to a controller. That can be used for different type of requests. For example: an API request must be handled differently of a browser (page) request.
Once a request arrives at the Phoenix router, it performs a series of transformations through pipelines until the request is dispatched to a desired end-point.
Such transformations are defined via plugs, as defined in the Plug specification. Once a pipeline is defined, it can be piped through per scope.
http://hexdocs.pm/phoenix/Phoenix.Router.html
web/controllers/page_catroller.ex๐
use Chat.Web, :controller
render conn,
end
end
After a request passing through all previous plug. It will be handled by the controller. For instance a GET / will respond with index.html page.
channels/user_socket.ex๐
As we saw before in the Chat.Endpoint the socket connections will be handled by the Chat.UserSocket.
use Phoenix.Socket
channel , Chat.RoomChannel
transport :websocket, Phoenix.Transports.WebSocket
transport :longpoll, Phoenix.Transports.LongPoll
end
nil
end
Basically the Chat.UserSocket creates a channel for topics matching rooms:*. And add support for both web socket and log pool connections.
channels/room_socket.ex๐
use Phoenix.Channel
require Logger
@doc """
Authorize socket to subscribe and broadcast events on this channel & topic
Possible Return Values
`{:ok, socket}` to authorize subscription for channel for requested topic
`:ignore` to deny subscription/broadcast on this channel
for the requested topic
"""
# Exit signals arriving to a process are converted to {'EXIT', From, Reason} messages,
# which can be received as ordinary messages
Process.flag(:trap_exit, true)
:timer.send_interval(5000, :ping)
send(self, )
end
end
broadcast! socket, , %
push socket, , %
end
push socket, , %
end
Logger.debug
:ok
end
broadcast! socket, , %
end
end
Pretty simple, it handles:
- new users join the channel
- broadcast new users in the chat
- send ping messages
- broadcast a user's message
Check out Phoenix.Channel for a further explanation.
The rest is just a HTML page loading a CSS script (source in Sass) and a Javascript script (source in ES6) which consumes the socket provided by the Chat channel.
P.S. I have just started learning Elixir and Phoenix. Let me know if I had misunderstood something.