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

  1. A record object is created or returned from a query method on a table class.
  2. The object is modified in memory.
  3. A method is called on the object that writes its state to the database.

Repository Pattern

  1. A record hash is created or returned from a repository query with the table class passed in as an argument.
  2. The hash is modified in memory
  3. 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