Slide 1

Slide 1 text

Rails vs. Phoenix vs. Hanami by Stefan Wintermeyer @wintermeyer

Slide 2

Slide 2 text

I am a software firefighter and architect for hire. I see a lot of projects which became unmaintainable over the years. @wintermeyer

Slide 3

Slide 3 text

I am a Rails Dinosaur My first Rails book. My latest Rails book. @wintermeyer

Slide 4

Slide 4 text

This talk is for normal people. Not for rock stars! I will not dive into religious wars. BTW: vim is better than emacs.

Slide 5

Slide 5 text

Frameworks to develop web applications. @wintermeyer Rails Phoenix Hanami https://rubyonrails.org https://phoenixframework.org http://hanamirb.org

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

@dhh @chris_mccord @jodosha They all worked with Rails in the past. @josevalim

Slide 11

Slide 11 text

Ruby vs. Elixir

Slide 12

Slide 12 text

Ruby was created in 1993 by Yukihiro Matsumoto for developers happiness. @wintermeyer

Slide 13

Slide 13 text

Elixir was created in 2011 by José Valim to be a real concurrent language. @wintermeyer

Slide 14

Slide 14 text

Don’t let the Elixir syntax fool you into believing it’s easy to learn. @wintermeyer

Slide 15

Slide 15 text

A Ruby programmer needs quite some time to learn Elixir. Functional programming is a totally different ball game. @wintermeyer

Slide 16

Slide 16 text

Elixir’s biggest advantages are speed, scalability and hot deployments. @wintermeyer

Slide 17

Slide 17 text

Hot Deployments! ZERO DOWNTIME! @wintermeyer No need to fire up a million Docker instances and some fancy HA Proxy setup.

Slide 18

Slide 18 text

Let’s compare the Frameworks.

Slide 19

Slide 19 text

Continuity for Developers Do you remember how painful Rails upgrades used to be?

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

15 Minute Blog in 2019 @wintermeyer The 2005 grandfather: https://www.youtube.com/watch?v=Gzj723LkRJY

Slide 22

Slide 22 text

Ruby on Rails @dhh

Slide 23

Slide 23 text

$ 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

Slide 24

Slide 24 text

├── 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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

Phoenix Framework @chris_mccord

Slide 29

Slide 29 text

$ 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

Slide 30

Slide 30 text

├── 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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

No content

Slide 34

Slide 34 text

Hanami @jodosha

Slide 35

Slide 35 text

$ hanami new my_blog $ cd blog $ bundle @wintermeyer

Slide 36

Slide 36 text

$ 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

Slide 37

Slide 37 text

$ 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

Slide 38

Slide 38 text

$ 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

Slide 39

Slide 39 text

$ hanami new my_blog $ cd my_blog $ bundle $ hanami generate model post $ hanami generate action web posts#index --url="/posts" @wintermeyer

Slide 40

Slide 40 text

$ 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

Slide 41

Slide 41 text

$ 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

Slide 42

Slide 42 text

├── 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

Slide 43

Slide 43 text

$ 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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

No content

Slide 47

Slide 47 text

$ 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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

Luca Guidi considers scaffolding an anti-pattern. @jodosha

Slide 50

Slide 50 text

I disagree! @wintermeyer

Slide 51

Slide 51 text

Why is scaffolding important? @wintermeyer

Slide 52

Slide 52 text

Easy and fast entry for newbies. @wintermeyer When you try a new framework you want to see results within the first 20 minutes.

Slide 53

Slide 53 text

For teams scaffolding is a big time saver and improves code quality by creating a base line. @wintermeyer

Slide 54

Slide 54 text

It’s a power tool for refactoring. @wintermeyer If you don’t use it often you probably haven’t customized it yet.

Slide 55

Slide 55 text

Over time Rails became easier and easier to use. That’s a major factor for it’s success. @wintermeyer

Slide 56

Slide 56 text

Hanami is not there. (yet?) @wintermeyer

Slide 57

Slide 57 text

But Phoenix is. @wintermeyer

Slide 58

Slide 58 text

Stefan, tell us more about those customizable scaffold generators! "Never send a human to do a machine’s job." by Agent Smith

Slide 59

Slide 59 text

Ruby on Rails @dhh

Slide 60

Slide 60 text

Example: model generator

Slide 61

Slide 61 text

No content

Slide 62

Slide 62 text

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".

Slide 63

Slide 63 text

<% 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

Slide 64

Slide 64 text

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.

Slide 65

Slide 65 text

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.

Slide 66

Slide 66 text

Phoenix Framework @chris_mccord

Slide 67

Slide 67 text

The same concept as with Rails. Just done with Elixir.

Slide 68

Slide 68 text

No content

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

Hanami @jodosha

Slide 71

Slide 71 text

You can’t.

Slide 72

Slide 72 text

How big is the community/visibility? How easy can I get help?

Slide 73

Slide 73 text

Stackoverflow
 Questions GitHub
 Contributors Ruby on Rails 302,236 3.742 Phoenix Framework 3,030 693 Hanami 60 140 @wintermeyer

Slide 74

Slide 74 text

Twitter Followers @dhh 324 K @chris_mccord 13.6 K @jodosha 2.672 @wintermeyer

Slide 75

Slide 75 text

What should I use?

Slide 76

Slide 76 text

Use Phoenix if speed is paramount. Technically it is the best choice!

Slide 77

Slide 77 text

Use Phoenix if your team doesn’t have (much?) Ruby knowledge yet. Technically it is the best choice!

Slide 78

Slide 78 text

Use Rails for all other cases.

Slide 79

Slide 79 text

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')

Slide 80

Slide 80 text

Thank you! Stefan Wintermeyer @wintermeyer www.wintermeyer-consulting.de