Share
Embed
Rails vs. Phoenix vs. Hanami
This is the deck for a https://www.rubyfuza.org 2019 talk by Stefan Wintermeyer about the three frameworks Ruby on Rails, Phoenix Framework and Hanami.
Stefan Wintermeyer
February 07, 2019
Transcript
80 slides
-
I am a software firefighter and architect for hire. I
see a lot of projects which became unmaintainable over the years. @wintermeyer -
I am a Rails Dinosaur My first Rails book. My
latest Rails book. @wintermeyer -
This talk is for normal people. Not for rock stars!
I will not dive into religious wars. BTW: vim is better than emacs. -
Frameworks to develop web applications. @wintermeyer Rails Phoenix Hanami https://rubyonrails.org
https://phoenixframework.org http://hanamirb.org -
Why use a Framework? • Structure and order within the
project • No need to reinvent the wheel every day. Let others do the heavy lifting. • Documentation • Easier on boarding of new team members @wintermeyer -
The framework is your hammer. All problems have to become
nails. And that’s a good thing because everybody in the team knows how to work with nails! @wintermeyer -
Inventor/Leader Sponsor Ruby Yukihiro Matsumoto Heroku Elixir José Valim Plataformatec
Ruby on Rails David Heinemeier Hansson Basecamp Phoenix Framework Chris McCord DockYard Hanami Luca Guidi - @wintermeyer -
Version Command Ruby 2.5.3p105 ruby -v Elixir 1.8.0 elixir -v
Ruby on Rails 5.2.2 rails -v Phoenix Framework 1.4.0 mix phx.new --version Hanami 1.3.1 hanami -v @wintermeyer -
@dhh @chris_mccord @jodosha They all worked with Rails in the
past. @josevalim -
Ruby was created in 1993 by Yukihiro Matsumoto for developers
happiness. @wintermeyer -
Elixir was created in 2011 by José Valim to be
a real concurrent language. @wintermeyer -
Don’t let the Elixir syntax fool you into believing it’s
easy to learn. @wintermeyer -
A Ruby programmer needs quite some time to learn Elixir.
Functional programming is a totally different ball game. @wintermeyer -
Hot Deployments! ZERO DOWNTIME! @wintermeyer No need to fire up
a million Docker instances and some fancy HA Proxy setup. -
Version How easy is an upgrade to the next version?
Ruby on Rails 5.2.2 Phoenix Framework 1.4.0 Hanami 1.3.1 2.0 is a big change @wintermeyer -
$ rails new my_blog $ cd my_blog $ rails g
scaffold post title body:text $ rails db:migrate $ rails server # Open http://0.0.0.0:3000/posts @wintermeyer -
├── app │ ├── assets │ │ ├── javascripts │
│ │ └── posts.coffee │ │ └── stylesheets │ │ ├── posts.scss │ │ └── scaffolds.scss │ ├── controllers │ │ └── posts_controller.rb │ ├── helpers │ │ └── posts_helper.rb │ ├── models │ │ └── post.rb │ └── views │ └── posts │ ├── _form.html.erb │ ├── _post.json.jbuilder │ ├── edit.html.erb │ ├── index.html.erb │ ├── index.json.jbuilder │ ├── new.html.erb │ ├── show.html.erb │ └── show.json.jbuilder ├── config │ └── routes.rb ├── db │ └── migrate │ └── 20190126153653_create_posts.rb └── test ├── controllers │ └── posts_controller_test.rb ├── fixtures │ └── posts.yml ├── models │ └── post_test.rb └── system └── posts_test.rb generated 20 files with 631 LoC -
class PostsController < ApplicationController before_action :set_post, only: [:show, :edit, :update,
:destroy] # GET /posts # GET /posts.json def index @posts = Post.all end # GET /posts/1 # GET /posts/1.json def show end # GET /posts/new def new @post = Post.new end # GET /posts/1/edit def edit end # POST /posts # POST /posts.json def create @post = Post.new(post_params) respond_to do |format| if @post.save format.html { redirect_to @post, notice: 'Post was successfully created.' } format.json { render :show, status: :created, location: @post } else format.html { render :new } format.json { render json: @post.errors, status: :unprocessable_entity } end end end # PATCH/PUT /posts/1 # PATCH/PUT /posts/1.json def update respond_to do |format| if @post.update(post_params) format.html { redirect_to @post, notice: 'Post was successfull format.json { render :show, status: :ok, location: @post } else format.html { render :edit } format.json { render json: @post.errors, status: :unprocessabl end end end # DELETE /posts/1 # DELETE /posts/1.json def destroy @post.destroy respond_to do |format| format.html { redirect_to posts_url, notice: 'Post was successfu format.json { head :no_content } end end private # Use callbacks to share common setup or constraints between actio def set_post @post = Post.find(params[:id]) end # Never trust parameters from the scary internet, only allow the w def post_params params.require(:post).permit(:title, :body) end end app/controllers/posts_controller.rb def create @post = Post.new(post_params) respond_to do |format| if @post.save format.html { redirect_to @post, notice: 'Post was successfully created.' } format.json { render :show, status: :created, location: @post } else format.html { render :new } format.json { render json: @post.errors, status: :unprocessable_entity } end end end -
def create @post = Post.new(post_params) respond_to do |format| if @post.save
format.html { redirect_to @post, notice: 'Post was successfully created.' } format.json { render :show, status: :created, location: @post } else format.html { render :new } format.json { render json: @post.errors, status: :unprocessable_entity } end end end @wintermeyer -
$ mix phx.new my_blog $ cd my_blog # configure database
in config/dev.exs (no SQLite ) $ mix ecto.create $ mix phx.gen.html Blog Post posts title body:text # add "resources "/posts", PostController" to router.ex $ mix ecto.migrate $ mix phx.server # Open http://0.0.0.0:4000/posts @wintermeyer -
├── lib │ ├── my_blog │ │ └── blog │
│ ├── blog.ex │ │ └── post.ex │ └── my_blog_web │ ├── controllers │ │ └── post_controller.ex │ ├── templates │ │ └── post │ │ ├── edit.html.eex │ │ ├── form.html.eex │ │ ├── index.html.eex │ │ ├── new.html.eex │ │ └── show.html.eex │ └── views │ └── post_view.ex ├── priv │ └── repo │ └── migrations ├── priv │ └── repo │ └── migrations │ └── 20190126174858_create_posts.exs └── test ├── my_blog │ └── blog │ └── blog_test.exs └── my_blog_web └── controllers └── post_controller_test.exs generated 12 files with 430 LoC -
defmodule MyBlogWeb.PostController do use MyBlogWeb, :controller alias MyBlog.Blog alias MyBlog.Blog.Post
def index(conn, _params) do posts = Blog.list_posts() render(conn, "index.html", posts: posts) end def new(conn, _params) do changeset = Blog.change_post(%Post{}) render(conn, "new.html", changeset: changeset) end def create(conn, %{"post" => post_params}) do case Blog.create_post(post_params) do {:ok, post} -> conn |> put_flash(:info, "Post created successfully.") |> redirect(to: Routes.post_path(conn, :show, post)) {:error, %Ecto.Changeset{} = changeset} -> render(conn, "new.html", changeset: changeset) end end def show(conn, %{"id" => id}) do post = Blog.get_post!(id) render(conn, "show.html", post: post) end |> put_flash(:info, "Post deleted successfully.") |> redirect(to: Routes.post_path(conn, :index)) end end def edit(conn, %{"id" => id}) do post = Blog.get_post!(id) changeset = Blog.change_post(post) render(conn, "edit.html", post: post, changeset: chang end def update(conn, %{"id" => id, "post" => post_params}) d post = Blog.get_post!(id) case Blog.update_post(post, post_params) do {:ok, post} -> conn |> put_flash(:info, "Post updated successfully.") |> redirect(to: Routes.post_path(conn, :show, post {:error, %Ecto.Changeset{} = changeset} -> render(conn, "edit.html", post: post, changeset: c end end def delete(conn, %{"id" => id}) do post = Blog.get_post!(id) {:ok, _post} = Blog.delete_post(post) conn |> put_flash(:info, "Post deleted successfully.") |> redirect(to: Routes.post_path(conn, :index)) end end lib/my_blog_web/controllers/post_controller.ex def create(conn, %{"post" => post_params}) do case Blog.create_post(post_params) do {:ok, post} -> conn |> put_flash(:info, "Post created successfully.") |> redirect(to: Routes.post_path(conn, :show, post)) {:error, %Ecto.Changeset{} = changeset} -> render(conn, "new.html", changeset: changeset) end end -
def create(conn, %{"post" => post_params}) do case Blog.create_post(post_params) do {:ok,
post} -> conn |> put_flash(:info, "Post created successfully.") |> redirect(to: Routes.post_path(conn, :show, post)) {:error, %Ecto.Changeset{} = changeset} -> render(conn, "new.html", changeset: changeset) end end @wintermeyer -
$ hanami generate model post create lib/hanami_blog/entities/post.rb create lib/hanami_blog/repositories/post_repository.rb create
db/migrations/20190124185552_create_posts.rb create spec/hanami_blog/entities/post_spec.rb create spec/hanami_blog/repositories/post_repository_spec.rb @wintermeyer -
$ hanami g model post create lib/hanami_blog/entities/post.rb create lib/hanami_blog/repositories/post_repository.rb create
db/migrations/20190124185552_create_posts.rb create spec/hanami_blog/entities/post_spec.rb create spec/hanami_blog/repositories/post_repository_spec.rb Hanami::Model.migration do change do create_table :posts do primary_key :id column :created_at, DateTime, null: false column :updated_at, DateTime, null: false end end end @wintermeyer -
$ hanami g model post create lib/hanami_blog/entities/post.rb create lib/hanami_blog/repositories/post_repository.rb create
db/migrations/20190124185552_create_posts.rb create spec/hanami_blog/entities/post_spec.rb create spec/hanami_blog/repositories/post_repository_spec.rb Hanami::Model.migration do change do create_table :posts do primary_key :id column :title, String column :body, String column :created_at, DateTime, null: false column :updated_at, DateTime, null: false end end end ⌨ @wintermeyer -
$ hanami new my_blog $ cd my_blog $ bundle $
hanami generate model post $ hanami generate action web posts#index --url="/posts" @wintermeyer -
$ hanami generate action web posts#index --url="/posts" create apps/web/controllers/posts/index.rb create
apps/web/views/posts/index.rb create apps/web/templates/posts/index.html.erb create spec/web/controllers/posts/index_spec.rb create spec/web/views/posts/index_spec.rb insert apps/web/config/routes.rb @wintermeyer -
$ hanami new my_blog $ cd my_blog $ bundle $
hanami generate model post $ hanami generate action web posts#index --url="/posts" $ hanami generate action web posts#show --url="/posts" $ hanami generate action web posts#new --url="/posts/new" $ hanami generate action web posts#create --url="/posts" $ hanami generate action web posts#edit --url="/posts/edit" $ hanami generate action web posts#update --url="/posts" $ hanami generate action web posts#destroy —url="/posts" @wintermeyer -
├── apps │ └── web │ ├── assets │ ├──
config │ │ └── routes.rb │ ├── controllers │ │ └── posts │ │ ├── create.rb │ │ ├── destroy.rb │ │ ├── edit.rb │ │ ├── index.rb │ │ ├── new.rb │ │ ├── show.rb │ │ └── update.rb │ ├── templates │ │ └── posts │ │ ├── create.html.erb │ │ ├── destroy.html.erb │ │ ├── edit.html.erb │ │ ├── index.html.erb │ │ ├── new.html.erb │ │ ├── show.html.erb │ │ └── update.html.erb │ └── views │ └── posts │ ├── create.rb │ ├── destroy.rb │ ├── edit.rb │ ├── index.rb │ ├── new.rb │ ├── show.rb │ └── update.rb ├── config ├── db │ └── migrations │ └── 20190125105614_create_posts.rb ├── lib │ └── blog │ ├── entities │ │ └── post.rb │ ├── mailers │ └── repositories │ └── post_repository.rb └── spec ├── blog │ ├── entities │ │ └── post_spec.rb │ └── repositories │ └── post_repository_spec.rb └── web ├── controllers │ └── posts │ ├── create_spec.rb │ ├── destroy_spec.rb │ ├── edit_spec.rb │ ├── index_spec.rb │ ├── new_spec.rb │ ├── show_spec.rb │ └── update_spec.rb └── views └── posts ├── create_spec.rb ├── destroy_spec.rb ├── edit_spec.rb ├── index_spec.rb ├── new_spec.rb ├── show_spec.rb └── update_spec.rb @wintermeyer -
$ hanami generate action web posts#index --url="/posts" create apps/web/controllers/posts/index.rb create
apps/web/views/posts/index.rb create apps/web/templates/posts/index.html.erb create spec/web/controllers/posts/index_spec.rb create spec/web/views/posts/index_spec.rb insert apps/web/config/routes.rb -
module Web module Controllers module Posts class Create include Web::Action
def call(params) end end end end end apps/web/controllers/posts/create.rb -
$ hanami new my_blog $ cd my_blog $ bundle $
hanami generate model post $ hanami generate action web posts#index --url="/posts" $ hanami generate action web posts#show --url="/posts" $ hanami generate action web posts#new --url="/posts/new" $ hanami generate action web posts#create --url="/posts" $ hanami generate action web posts#edit --url="/posts/edit" $ hanami generate action web posts#update --url="/posts" $ hanami generate action web posts#destroy —url="/posts" $ hanami db create $ hanami db migrate # put A LOT of Ruby code in the just generated files $ hanami server # Open http://localhost:2300 @wintermeyer -
The 15 Minute Blog @wintermeyer Time (hh:mm) generated LoC Ruby
on Rails 00:01 631 Phoenix Framework 00:02 430 Hanami 03:30 501 -
Easy and fast entry for newbies. @wintermeyer When you try
a new framework you want to see results within the first 20 minutes. -
For teams scaffolding is a big time saver and improves
code quality by creating a base line. @wintermeyer -
It’s a power tool for refactoring. @wintermeyer If you don’t
use it often you probably haven’t customized it yet. -
Over time Rails became easier and easier to use. That’s
a major factor for it’s success. @wintermeyer -
Stefan, tell us more about those customizable scaffold generators! "Never
send a human to do a machine’s job." by Agent Smith -
Download https://raw.githubusercontent.com/rails/rails/master/ activerecord/lib/rails/generators/active_record/ model/templates/model.rb.tt and copy it to lib/templates/active_record/model/model.rb.tt It
will be used for every "rails g model xyz" or "rails g scaffold xyz". -
<% module_namespacing do -%> class <%= class_name %> < <%=
parent_class_name.classify %> <% attributes.select(&:reference?).each do |attribute| -%> belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %><%= ', required: true' if attribute.required? %> <% end -%> <% attributes.select(&:token?).each do |attribute| -%> has_secure_token<% if attribute.name != "token" %> :<%= attribute.name %><% end %> <% end -%> <% if attributes.any?(&:password_digest?) -%> has_secure_password <% end -%> end <% end -%> lib/templates/active_record/model/model.rb.tt -
Example Enhancement <%- if attributes.map{ |a| a.name }.include?('position') -%> acts_as_list
<% end -%> <% if attributes.map{ |a| a.name }.include?('name') -%> validates :name, presence: true def to_s name end <% end -%> Should be customized for each project. -
lib/templates/ ├── active_record │ └── model │ └── model.rb.tt ├──
erb │ └── scaffold │ ├── _form.html.erb.tt │ ├── edit.html.erb.tt │ ├── index.html.erb.tt │ ├── new.html.erb.tt │ └── show.html.erb.tt └── rails └── scaffold_controller └── controller.rb.tt Just google for the file names to find the default ones on github.com/rails. -
def create(conn, %{<%= inspect schema.singular %> => <%= schema.singular %>_params})
do case <%= inspect context.alias %>.create_<%= schema.singular %>(<%= schema.singular %>_params) do {:ok, <%= schema.singular %>} -> conn |> put_flash(:info, "<%= schema.human_singular %> created successfully.") |> redirect(to: Routes.<%= schema.route_helper %>_path(conn, :show, <%= schema.singular %>)) {:error, %Ecto.Changeset{} = changeset} -> render(conn, "new.html", changeset: changeset) end end https://github.com/phoenixframework/phoenix/blob/master/priv/templates/ phx.gen.html/controller.ex -
Stackoverflow Questions GitHub Contributors Ruby on Rails 302,236 3.742 Phoenix
Framework 3,030 693 Hanami 60 140 @wintermeyer -
Use Phoenix if your team doesn’t have (much?) Ruby knowledge
yet. Technically it is the best choice! -
Don’t use Hanami (for now). Let’s see what the future
brings. "Nobody ever got fired for buying IBM".gsub(/buying IBM/, 'using Rails')