Rails to Phoenix Guide (WIP)
30 Sep 2020
Main Components
Rails
- ActionCable - websocket
- ActionController - generates HTTP responses
- ActionDispatch - defines routes
- ActionView - renders templates
- ActionMailbox - handles incoming emails, not built into phoenix
- ActionMailer - sends emails, not built into Phoenix
- ActiveJob - background jobs
- ActiveModel - MVC model code (pulled from ActiveRecord in Rails 3)
- ActiveStorage - cloud storage wrapper
- ActiveSupport - helper methods
- ActiveRecord - ORM
Phoenix
- Phoenix.Channel - bidirectional websocket
- Phoenix.Controller - generates HTTP responses
- Phoenix.Router - defines routes
- Phoenix.Token - token generation and verification
- Phoenix.View - renders templates
- Phoenix.LiveView - real-time server-rendered HTML, no equivalent in Rails
- Phoenix.Endpoint - wrapper for the router and configuration, no Rails equivalent
- Phoenix.Naming - helper methods like ActiveSupport
- Ecto - ORM like ActiveRecord
Routes
Routes map URL patterns to controller actions.
Rails
Routes are stored in config/routes.rb.
Rails.application.routes.draw do
resources :users, only: [:index, :show] do
resources :comments, only: [:index, :show]
end
end
Available routes can be viewed with:
$ rake routes
Phoenix
Routes are stored in lib/project_name/router.ex.
In Phoenix routes, plug also provides the equivalent of before filters in Rails.
defmodule ExampleWeb.Router do
use ExampleWeb, :router
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
scope "/", ExampleWeb do
pipe_through :browser
get "/", PageController, :index
end
scope "/api", ExampleWeb do
pipe_through :api
resources "/users", UserController
end
end
Available routes can be viewed with:
$ mix phx.routes
Controllers
Rails
When rails receives a request, it creates an instance of a controller and runs the method associated with the action in the route. Action methods don't need to take any arguments because the instance state has a reference to the request and its parameters.
To have controller functionality, a class inherits from ApplicationController which is a wrapper for ActionController::Base. Rails generates wrapper classes to discourage developers from monkey-patching the core classes.
class UsersController < ApplicationController
def index
recent = params[:status] == "recent"
@users = recent ? User.recent : User.all
end
def create
@user = User.new(params[:user])
if @user.save
redirect_to @user
else
render "new"
end
end
end
Phoenix
When Phoenix receives a request, it calls the function on the module associated with the action in the route.
To have controller functionality, a module's defition invokes the use macro with the main module and :controller.
Because there is no state in Elixir, controller actions are passed the connection and the params.
defmodule MyAppWeb.UserController do
use MyAppWeb, :controller
def show(conn, %{"id" => id}) do
user = Repo.get(User, id)
render(conn, "show.html", user: user)
end
def index(conn, %{"recent" => recent}) do
users = Repo.all(User)
render(conn, "index.html", users: users, recent: recent)
end
def create(conn) do
user = Repo.create(User)
render(conn, "create.html", user: user)
end
end
Models
Rails
Rails uses the ActiveRecord library. It's based on the active record pattern.
A class gets model functionality by inheriting from ApplicationRecord which is a wrapper for ActiveRecord::Base
class User < ApplicationRecord has_many :comments end class Comment < ApplicationRecord belongs_to :user end
Phoenix
The main difference coming from ActiveRecord is that Ecto follows the Repository Pattern.
Since Elixir doesn't have objects or mutable data structures, changes are saved with change sets.
Ecto has 4 main components:
- Ecto.Repo - repositories are CRUD database wrappers
- Ecto.Schema - schemas map database entries to Elixir structs
- Ecto.Changeset - changesets contains changes to the database
- Ecto.Query - queries retrieve information from a repository
Ecto.Schema
defmodule Schema.User do
use Ecto.Schema
schema "user" do
field :username,:string
field :email, :string
end
end
Migrations
Migrations represent incremental changes to the database. They include logic for undoing the changes.
When Rails and Phoenix can infer the "undo" logic, a single change method/function can be used instead of defininig both up and down.
Rails
Migrations are stored as files in the db/migrate directory. Each is a class with instance methods containing logic for performing and undoing the migration.
class CreateUsers < ActiveRecord::Migration[5.0]
def change
create_table :users do |t|
t.string :username
t.string :email
t.text :bio
t.timestamps
end
end
end
$ rake db:migrate
Phoenix
defmodule MyRepo.Migrations.AddUsersTable do
use Ecto.Migration
def up do
create table("users") do
add :username, :string, size: 12
add :email, :string, size: 40
timestamps()
end
end
def down do
drop table("users")
end
end
$ mix ecto.migrate
ActiveRecord vs Ecto
The ActiveRecord library is named after the active record pattern that it follows. Ecto, on the other hand, uses the repository pattern.
Active Record Pattern
- A record object is created or returned from a query method on a table class.
- The object is modified in memory.
- A method is called on the object that writes its state to the database.
Repository Pattern
- A record hash is created or returned from a repository query with the table class passed in as an argument.
- The hash is modified in memory
- A function is called on the repository module with the hash as an argument that writes the change to the database
HTTP Servers
In the Rails marketplace you need to think about the Ruby implementation that you will be using and the servers it supports. Different implementations provide different types of concurrency and parallelism. CRuby, for example, has a global interpreter lock which prevents parallelism, but JRuby does not.
Elixir has one type of concurrency called erlang processes which combine a smaller memory footprint than threads with the memory isloation of operating system processes. Processes can communicate with each other via messages.
Rails
- Webrick - built-in server
- Unicorn - forked server
- Passenger - commercial server
- open-source version: multiprocessed
- enterprise-version: multi-processed and multi-threaded
- Puma -threaded server with clustered mode for forked processes
- Thin - evented server built on EventMachine
Phoenix
- Cowboy - small HTTP server that uses the Ranch TCP socket acceptor pool
Plug
According to its hex docs, Plug is
- A specification for composable modules between web applications
- Connection adapters for different web servers in the Erlang VM
Endpoints, Routers, and Controllers are all Plugs in Phoenix.
defmodule MyPlug do
import Plug.Conn
def init(options) do
# initialize options
options
end
def call(conn, _opts) do
conn
|> put_resp_content_type("text/plain")
|> send_resp(200, "Hello world")
end
end
Project Initialization
Rails
$ rails new <project_name>
Phoenix
$ mix phx.new <project_name>
Command-line Generators
Rails
$ rails generate resource User name username email
Phoenix
mix phx.gen.html Accounts User users name:string username:string email:string
Command-line Tasks
- Rails - rake
- Phoenix - mix
Dependencies
Unlike with Ruby, Elixir dependencies are installed in the project directory like in NodeJS.
- rubygems -> hex
- bundler -> mix
- Gemfile -> mix.exs
Rails
$ bundle install $ bundle update
Phoenix
$ mix deps.get
Project Environment REPL
Rails
$ rails console
Phoenix
$ iex -S mix
Running The Server
Rails
$ rails server
Phoenix
$ mix phx.server