Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rails vs. Phoenix vs. Hanami

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
Tweet

More Decks by Stefan Wintermeyer

Other Decks in Programming

Transcript

  1. I am a software firefighter and architect for hire. I

    see a lot of projects which became unmaintainable over the years. @wintermeyer
  2. I am a Rails Dinosaur My first Rails book. My

    latest Rails book. @wintermeyer
  3. This talk is for normal people. Not for rock stars!

    I will not dive into religious wars. BTW: vim is better than emacs.
  4. 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
  5. 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
  6. 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
  7. 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
  8. Elixir was created in 2011 by José Valim to be

    a real concurrent language. @wintermeyer
  9. A Ruby programmer needs quite some time to learn Elixir.

    Functional programming is a totally different ball game. @wintermeyer
  10. Hot Deployments! ZERO DOWNTIME! @wintermeyer No need to fire up

    a million Docker instances and some fancy HA Proxy setup.
  11. 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
  12. $ 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
  13. ├── 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
  14. 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
  15. 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
  16. $ 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
  17. ├── 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
  18. 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
  19. 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
  20. $ 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
  21. $ 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
  22. $ 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
  23. $ hanami new my_blog $ cd my_blog $ bundle $

    hanami generate model post $ hanami generate action web posts#index --url="/posts" @wintermeyer
  24. $ 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
  25. $ 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
  26. ├── 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
  27. $ 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
  28. 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
  29. $ 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
  30. 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
  31. Easy and fast entry for newbies. @wintermeyer When you try

    a new framework you want to see results within the first 20 minutes.
  32. For teams scaffolding is a big time saver and improves

    code quality by creating a base line. @wintermeyer
  33. It’s a power tool for refactoring. @wintermeyer If you don’t

    use it often you probably haven’t customized it yet.
  34. Over time Rails became easier and easier to use. That’s

    a major factor for it’s success. @wintermeyer
  35. Stefan, tell us more about those customizable scaffold generators! "Never

    send a human to do a machine’s job." by Agent Smith
  36. <% 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
  37. 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.
  38. 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.
  39. 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
  40. Use Phoenix if your team doesn’t have (much?) Ruby knowledge

    yet. Technically it is the best choice!
  41. 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')