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