Getting Started with Phoenix (as a Rails Developer) - Part 2
Elizabeth Karst, Former Developer
Article Categories:
Posted on
Comparing routing and the MVC architecture of Elixir-Phoenix and Rails apps
Welcome back to this three-part series, which provides an overview of building Elixir apps with Phoenix and how that compares to building Rails apps. Part 2 compares Routing and the Model-View-Controller Architecture for each framework. Be sure to check out Part 1 and Part 3 as well:
Routing #
Phoenix routing has a lot in common with Rails routing, but makes controlling how a request is processed easier and more explicit. Before mapping a request URL to a controller and action, the conn
is run through some additional plug pipelines (see Part 1 for an overview of conn
).
Here’s what a typical Phoenix Router looks like:
# Phoenix Router: lib/my_book_app_web/router.ex
defmodule MyBookApp.Router do
# Make Phoenix router functions available
use MyBookApp.Web, :router
# Plug Pipelines
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
# Scopes
scope "/", MyBookApp do
pipe_through :browser
resources "/books", BookController
get "/books/new_releases", BookController, :new_releases
end
scope "/api", MyBookApp do
pipe_through :api
end
end
The router contains scopes (which define an apps URLs and their associated controller actions) and pipelines (which are used to modify the conn
struct before passing it to a controller).
When a request is sent with the url /books
, the router will first pipe the request through the :browser
pipeline per the pipe_through :browser
line in the /
scope. The :browser
pipeline will fetch flash, fetch session data and run through any other plugs added to that pipeline before the request is dispatched to the BooksController
books
action.
A separate plug pipeline for api
requests can be similarly configured. You can add as many plugs and plug pipelines as you want for a given scope. A user authentication pipeline is a common addition.
In a Rails app, this pipeline functionality is handled in Rack middleware or the controllers themselves. For example, the controller may check user authentication with a before_action
, or respond to an API call with render :json
. While this allows the Rails Router to be extremely simple (see below), it also means that the logic for determining what data to surface to the user in a view (from auth, to flash, to grabbing the session) is a bit more spread out in the app.
# Rails Router: config/routes.rb
Rails.application.routes.draw do
resources :books
get '/books/new_releases', to: 'books#new_releases'
end
Further Reading:
Model-View-Controller Architecture #
Rails and Phoenix both embody a Model-View-Controller design pattern, but with slightly different approaches.
Model - ActiveRecord vs. Ecto
While Rails is designed around object-oriented models, it’s somewhat of a misnomer to say Phoenix has models at all. It has a functional data layer most commonly accessed through a domain-specific language called Ecto. Ecto ‘models’ are often referred to simply as ‘structs’ or ‘schemas’.
ActiveRecord allows Rails developers to build objects that handle data persistence, validation and relationships, and automatically infers a schema from the underlying database that maps database tables and columns to objects and their properties.
Ecto also handles data persistence, validation and relationships, but in a different way. Ecto consists of the following:
Schema - a struct used to map fields from a data source to an Elixir struct and create virtual fields that are not persisted. Schemas are built by the developer, not inferred like with ActiveRecord.
Changeset - provides a way to filter and whitelist external parameters, as well as define rules for data validation and transformation. Developers can create different changesets for different scenarios in lieu of using helpers like
before_create
in Rails.Repo - a wrapper around a data store that handles data persistence by providing an interface to create, update, delete and query data. Ecto Query provides an Elixir query syntax for retrieving data from a Repo.
Let’s look at an example of what a books
model might look like in Rails and Phoenix. To create a ‘Books’ model, you start with a migration in both frameworks (these can be created through simple migration generators - see ‘Development Tools’ in Part 3). The migrations look and behave similarly overall, with a few syntactic differences and the use of a module instead of a class in Phoenix (classes don’t exist in Elixir).
# Rails Migration: db/migrate/XXXX_create_books.rb
class CreateBooks < ActiveRecord::Migration[5.0]
def change
create_table :books do |t|
t.string :title
t.integer :number_of_pages
t.references :author
t.timestamps
end
end
end
# Phoenix Migration: priv/repo/migrations/XXXX_create_books.exs
defmodule MyBookApp.Migrations.AddBooksTable do
use Ecto.Migration
def change do
create table("books") do
add :title, :string
add :number_of_pages, :integer
add :author_id, references(:authors)
timestamps()
end
end
end
The big difference between Rails and Phoenix migrations is really what happens after you have run them. Rails automatically creates or updates a schema
file to map a model to the underlying database table. In Phoenix a Book
schema is explicitly built by the developer. While it can feel cumbersome to build schemas manually, it does allow for creating virtual fields that aren’t mapped to the database. A common virtual field is a raw “password” string that can be collected from a user and passed through a changeset, where it is used to create a separate encrypted “password_hash” that is stored in the database.
The Phoenix schema lives in a Book
module with a changeset and no other business logic. This module is often referred to as a ‘model’, but it’s really just a module that houses schema and changeset structs - it does not work like a Rails model in any way.
# Phoenix ‘Model’ Struct: lib/my_book_app/books/book.ex
defmodule MyBookApp.Books.Book do
use Ecto.Schema
import Ecto.Changeset
schema "books" do
field :title, :string
field :number_of_pages, :integer
belongs_to :author, MyBookApp.Author
timestamps
end
@required_fields ~w(title author_id)
@optional_fields ~w(number_of_pates)
def changeset(user, params \\ %{}) do
user
|> cast(params, @required_fields ++ @optional_fields)
|> validate_required(@required_fields)
|> unique_constraint(:author_book_constraint, name: :author_book_index)
|> some_other_validation
end
defp some_other_validation(changeset) do
# ...
end
end
Business logic is defined in Phoenix contexts (modules with groups of related functions). Phoenix controllers and views rely on contexts to gather and update business data via the Repo. Model structs are typically namespaced under the context they are most associated with. Here's an example of what a Phoenix context might look like. It includes functions with business logic around books, like new_releases
or top_rated
.
# Phoenix Context: lib/my_book_app/books/books.ex
defmodule MyBookApp.Books
def new_releases do
# Repo query
end
end
In summary, a complete book data layer in Phoenix is the combination of the above Book
model and Books
context. It is common to create contexts that map directly to a ‘model’ like this, but in some cases multiple models may nest under the same context. For example, a single Auth
context might support User
, Account
and Session
model structs.
In Rails, a single Book
model handles data validations, associations and business logic in one place. The schema that maps this book model to a database table lives in an auto-generated schema.rb
file that stores the schema for the entire app. Here’s what the above example might look like in a Rails book model:
# Rails Model: app/models/book.rb
class Book
belongs_to :author
validates :title, :author, presence: true
validates_uniqueness_of :title, scope: [:author_id]
validate :some_other_validation
private
def some_other_validation
# custom validation
end
def self.new_releases
# ActiveRecord query - find all new releases
end
end
The division of models and contexts can take a bit of getting used to in Phoenix, but it can be freeing to organize business logic independently of models. It feels a bit like writing a Rails app where all of the business logic lives in shared modules instead of individual models.
Further Reading:
Controller
Controllers are responsible for converting an incoming request into a response by passing data from the model layer to a view. Phoenix and Rails controllers are fairly similar at a high level, but there are a few notable differences:
- Everything in Phoenix is handled through plugs. Phoenix controllers are plugs, as are the actions they are composed of. Plugs are also used in place of filters you might use in Rails (e.g.
before_action
), and can be integrated at any point in the control flow for additional functionality. - Rendering is explicit in Phoenix. Each controller action ends with a
redirect
orrender
call that takes theconn
, a template and data the template needs. Though you specify a template in a controller action, the controller does not render the template. The controller uses this information to find a corresponding view, which is responsible for surfacing any additional business data and rendering the template. More on this in the “View” section below. - Naming is singular. A
BooksController
in Rails is aBookController
in Phoenix.
Here’s how a simple Phoenix controller for rendering a books
index might look:
# Phoenix Controller: lib/my_book_app_web/controllers/book_controller.ex
defmodule MyBookApp.BookController do
use MyBookApp.Web, :controller
plug :authenticate_resource_class
def index(conn, _params) do
books = Books.list_books(conn)
render(conn, :index, books: books)
end
end
The corresponding controller in Rails appears a bit simpler because there is a good amount of implicit rendering magic going on behind the scenes.
# Rails Controller: app/controllers/books_controller.ex
class BooksController < ApplicationController
before_action :authorize_resource_class
def index
@books = Books.list_books(conn)
end
end
Notice that the authorize_resource_class
validation is handled via a plug in Phoenix rather than a before_action
like in Rails. Any number of plugs can be added in a similar way depending on how you need to manipulate your conn
struct before passing it to a controller action. It’s also worth mentioning that Rails controllers inherit controller functionality through ApplicationController. Elixir doesn’t have a concept of inheritance, so you explicitly include a controller module at the top of each controller (e.g. use MyBookApp.web, :controller
).
Further Reading:
View
Rails views != Phoenix views.
- In Rails, views are HTML files with embedded Ruby code. Rails views are rendered by the controller and are supported by methods in helper modules that make model data easier for the view to use.
- Phoenix has both views and templates. Phoenix templates are most akin to Rails views - they are HTML files with embedded Elixir code. Phoenix views stand between the controller and the template. Views render templates and provide helper functions to make raw data easier for those templates to use.
In the below example, we have a Phoenix BookView
and books index
template.
# Phoenix View: lib/my_book_app_web/views/book_view.ex
defmodule MyBookApp.BookView do
use MyBookApp.Web, :view
def title_and_author(book) do
"#{book.title}, by #{book.author.full_name}"
end
end
# Phoenix Template: lib/my_book_app_web/templates/books/index.html.eex
<ul>
<%= for book <- @books do %>
<li><%= title_and_author %></li>
<% end %>
</ul>
The view (BookView
) defines functions like title_and_author
that the index
template uses. Any additional helper functions needed for other book templates (e.g. show
or new
) would be added to the BookView
as well. You specify the template you want the BookView
to render in the BookController
render call (e.g. render(conn, :index)
).
In Rails, a books index view is rendered directly by the BooksController
, and helper methods that support that view live in a BookHelper
module. Rails will automatically load a BookHelper
for any views in a books
view directory based on naming conventions.
# Rails View: app/views/books/index.html.erb
<ul>
<% @books.each do |book| %>
<li><%= title_and_author %></li>
<% end %>
</ul>
# Rails Book Helper: app/helpers/book_helper.rb
module BookHelper
def title_and_author(book)
"#{book.title}, by #{book.author.full_name}"
end
end
Structurally, a Rails helper module + view feels fairly similar to a Phoenix view + template. The biggest difference to keep in mind is that the controller is responsible for rendering views in Rails (helpers don't play a role in rendering), while the view is responsible for rendering templates in Phoenix.
Further Reading:
Thanks for reading! You can read more on building Elixir apps with Phoenix vs. Rails in Part 1 and Part 3 of this series: