Le fonctionnement interne de Rails

Guirec Corbel

@GuirecCorbel

Pourquoi comprendre Rails ?

  • Devenir un expert
  • Se faire connaître
  • Expérience incroyable

Pourquoi apprendre Rails ?

Fils conducteur

Décrire le chemin fait par une requête de sa réception par l'application jusqu'au retour de la réponse

Rack

Interface pour développer des applications web en Ruby

 

Intercepte les requêtes HTTP et retourne un résultat

Rack

class ParisRb
  def call(env)
    [200, {}, ["Bonjour ParisRb"]]
  end
end

run ParisRb.new
➜  parisrb  rackup
Thin web server (v1.6.3 codename Protein Powder)
Maximum connections set to 1024
Listening on localhost:9292, CTRL+C to stop
➜  parisrb  curl http://localhost:9292/
Bonjour ParisRb% 

Rack

{"SERVER_SOFTWARE"=>"thin 1.6.3 codename Protein Powder", 
 "SERVER_NAME"=>"localhost", 
 "rack.input"=>#<Rack::Lint::InputWrapper:0x007f0c3fa46918 @input=#<StringIO:0x007f0c3fb9a2b0>>, 
 "rack.version"=>[1, 0], 
 "rack.errors"=>#<Rack::Lint::ErrorWrapper:0x007f0c3fa46710 @error=#<IO:<STDERR>>>, 
 "rack.multithread"=>false, 
 "rack.multiprocess"=>false, 
 "rack.run_once"=>false, 
 "REQUEST_METHOD"=>"GET", 
 "REQUEST_PATH"=>"/", 
 "PATH_INFO"=>"/", 
 "REQUEST_URI"=>"/", 
 "HTTP_VERSION"=>"HTTP/1.1", 
 "HTTP_USER_AGENT"=>"curl/7.35.0", 
 "HTTP_HOST"=>"localhost:9292", 
 "HTTP_ACCEPT"=>"*/*", 
 "GATEWAY_INTERFACE"=>"CGI/1.2", 
 "SERVER_PORT"=>"9292", 
 "QUERY_STRING"=>"", 
 "SERVER_PROTOCOL"=>"HTTP/1.1", 
 "rack.url_scheme"=>"http", 
 "SCRIPT_NAME"=>"", 
 "REMOTE_ADDR"=>"127.0.0.1", 
 "async.callback"=>#<Method: Thin::Connection#post_process>, 
 "async.close"=>#<EventMachine::DefaultDeferrable:0x007f0c3fad4ad8>, 
 "rack.tempfiles"=>[]}

Rack

class ParisRb
  def call(env)
    if env["REQUEST_PATH"] == "/bonsoir"
      [200, {}, ["Bonsoir ParisRb"]]
    elsif env["REQUEST_PATH"] == "/bonjour"
      [200, {}, ["Bonjour ParisRb"]]
    else
      [404, {}, ["Not Found"]]
    end
  end
end

run ParisRb.new
➜  parisrb  curl http://localhost:9292/bonsoir
Bonsoir ParisRb%
➜  parisrb  curl http://localhost:9292/bonjour
Bonjour ParisRb%
➜  parisrb  curl http://localhost:9292/salut
Not Found% 

Rack

#config.ru
# This file is used by Rack-based servers to start the application.

require ::File.expand_path('../config/environment', __FILE__)
run Rails.application
➜  parisrb  rackup
[2015-06-07 03:08:29] INFO  WEBrick 1.3.1
[2015-06-07 03:08:29] INFO  ruby 2.1.5 (2014-11-13) [x86_64-linux]
[2015-06-07 03:08:29] INFO  WEBrick::HTTPServer#start: pid=7646 port=9292
127.0.0.1 - - [07/Jun/2015:03:08:31 -0400] "GET / HTTP/1.1" 200 - 0.2457
127.0.0.1 - - [07/Jun/2015:03:08:39 -0400] "GET /products HTTP/1.1" 200 - 0.7318

Rack

Besoin d'un application simple ?

Utilisez Sinatra plutôt que Rack

Résumé

  • Réception d'un requête HTTP
  • ...
  • ...
  • ...
  • ...
  • Retour de la réponse

Middleware

require 'benchmark'

class TimeMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    puts Benchmark.measure { @response = @app.call(env) }
    @response
  end
end

Middleware

use Rack::Sendfile
use ActionDispatch::Static
use Rack::Lock
use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007f2f84717ba8>
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use WebConsole::Middleware
use ActionDispatch::DebugExceptions
use ActionDispatch::RemoteIp
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
run Parisrb::Application.routes

Point d'entré

module Parisrb
  class Application < Rails::Application
    # ...
  end
end
# railties/lib/rails/application.rb
module Rails
  class Application < Engine
    # ...
  end
end
# railties/lib/rails/engine.rb
module Rails
  class Engine < Railtie
    # ...
    def routes
      @routes ||= ActionDispatch::Routing::RouteSet.new
      @routes.append(&Proc.new) if block_given?
      @routes
    end
    # ...
  end
end

Résumé

  • Réception d'un requête HTTP
  • Transformation par un stack de middleware
  • ...
  • ...
  • ...
  • Retour de la réponse

#resources

Rails.application.routes.draw do
  resources :products
end
# actionpack/lib/action_dispatch/routing/route_set.rb

def draw(&block)
  clear! unless @disable_clear_and_finalize
  eval_block(block)
  finalize! unless @disable_clear_and_finalize
  nil
end

def eval_block(block)
  if block.arity == 1
    raise "You are using the old router DSL which has been removed in Rails 3.1. " <<
      "Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/"
  end
  mapper = Mapper.new(self)
  if default_scope
    mapper.with_default_scope(default_scope, &block)
  else
    mapper.instance_exec(&block)
  end
end

#resources

#actionpack/lib/action_dispatch/routing/mapper.rb

def resource(*resources, &block)
  # ...
  resource_scope(:resource, SingletonResource.new(resources.pop, options)) do
    # ...
    set_member_mappings_for_resource
  end
  self
end

def set_member_mappings_for_resource
  member do
    get :edit if parent_resource.actions.include?(:edit)
    get :show if parent_resource.actions.include?(:show)
    if parent_resource.actions.include?(:update)
      patch :update
      put   :update
    end
    delete :destroy if parent_resource.actions.include?(:destroy)
  end
end

Bref :

  #resources = [#get, #post, #delete, #patch, #put]

#resources

#actionpack/lib/action_dispatch/routing/mapper.rb

def get(*args, &block)
  map_method(:get, args, &block)
end

def map_method(method, args, &block)
  options = args.extract_options!
  options[:via] = method
  match(*args, options, &block)
  self
end

Bref :

  [#get, #post, #delete, #patch, #put] = #match

Ajouter une route

match 'products', to: 'products#index', via: :get

match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] }, via: :get
match 'photos/:id', to: PeoductRackApp, via: :get
# Yes, controller actions are just rack endpoints
match 'photos/:id', to: ProductsController.action(:show), via: :get
def match(path, *rest)
  # ... manipule les options
  paths.each do |_path|
    # ... continue de manipuler les options
    decomposed_match(_path, route_options)
  end
end
def decomposed_match(path, options) # :nodoc:
  if on = options.delete(:on)
    send(on) { decomposed_match(path, options) }
  else
    case @scope.scope_level
    when :resources
      nested { decomposed_match(path, options) }
    when :resource
      member { decomposed_match(path, options) }
    else
      add_route(path, options)
    end
  end
end
def add_route(action, options) # :nodoc:
  # ... manipule les options

  mapping = Mapping.build(@scope, @set, URI.parser.escape(path), as, options)
  app, conditions, requirements, defaults, as, anchor = mapping.to_route
  # app.class == ActionDispatch::Routing::RouteSet::Dispatcher
  @set.add_route(app, conditions, requirements, defaults, as, anchor)
end

Ajouter une route

# actionpack/lib/action_dispatch/journey/routes.rb
# Add a route to the routing table.
def add_route(app, path, conditions, defaults, name = nil)
  route = Route.new(name, app, path, conditions, defaults)

  route.precedence = routes.length
  routes << route
  named_routes[name] = route if name && !named_routes[name]
  clear_cache!
  route
end

Ajouter une route

require ::File.expand_path('../config/environment', __FILE__)
run ProductsController.action(:index)

Point d'entré

module Parisrb
  class Application < Rails::Application
    # ...
  end
end
# railties/lib/rails/application.rb
module Rails
  class Application < Engine
    # ...
  end
end
# railties/lib/rails/engine.rb
module Rails
  class Engine < Railtie
    # ...
    def routes
      @routes ||= ActionDispatch::Routing::RouteSet.new
      @routes.append(&Proc.new) if block_given?
      @routes
    end
    # ...
  end
end

Trouver les routes

# actionpack/lib/action_dispatch/routing/route_set.rb
def call(env)
  req = request_class.new(env)
  req.path_info = Journey::Router::Utils.normalize_path(req.path_info)
  @router.serve(req)
end
# actionpack/lib/action_dispatch/journey/router.rb 
def serve
  find_routes(req).each do |match, parameters, route|
    # ... manipule la requête
    status, headers, body = route.app.serve(req)
    # ... manipule la requête
    return [status, headers, body]
  end
  return [404, {'X-Cascade' => 'pass'}, ['Not Found']] 
end

Trouver les routes

# actionpack/lib/action_dispatch/journey/router.rb
def find_routes req
  routes = filter_routes(req.path_info).concat custom_routes.find_all { |r|
    r.path.match(req.path_info)
  }

  # ... renseigne les informations sur le chemin et les paramêtres
end
# actionpack/lib/action_dispatch/journey/routes.rb
module ActionDispatch
  module Journey # :nodoc:
    # The Routing table. Contains all routes for a system. Routes can be
    # added to the table by calling Routes#add_route.
    class Routes # :nodoc:
      include Enumerable
      # ...
    end
  end
end

Trouver les routes

# actionpack/lib/action_dispatch/journey/router.rb 
def serve
  find_routes(req).each do |match, parameters, route|
    # ... manipule la requête
    status, headers, body = route.app.serve(req)
    # ... manipule la requête
    return [status, headers, body]
  end
  return [404, {'X-Cascade' => 'pass'}, ['Not Found']] 
end
module ActionDispatch
  module Routing
    class RouteSet #:nodoc:
      class Dispatcher < Routing::Endpoint #:nodoc:
        def serve(req)
          req.check_path_parameters!
          params = req.path_parameters

          prepare_params!(params)

          # Just raise undefined constant errors if a controller was specified as default.
          unless controller = controller(params, @defaults.key?(:controller))
            return [404, {'X-Cascade' => 'pass'}, []]
          end

          dispatch(controller, params[:action], req.env)
        end

        def dispatch(controller, action, env)
          controller.action(action).call(env)
        end
      end
    end
  end
end

Ajouter une route

Résumé

  • Réception d'un requête HTTP
  • Transformation par un stack de middleware
  • Recherche de la route
  • ...
  • ...
  • Retour de la réponse

Controller

#app/controller/products_controller.rb
class ProductsController < ApplicationController
  def index
    @products = Product.all
  end
end
#app/views/products/index.html.erb
<p id="notice"><%= notice %></p>

<h1>Listing Products</h1>

<table>
  <thead>
    <tr>
      <th>Title</th>
      <th>Price</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @products.each do |product| %>
      <tr>
        <td><%= product.title %></td>
        <td><%= product.price %></td>
        <td><%= link_to 'Show', product %></td>
        <td><%= link_to 'Edit', edit_product_path(product) %></td>
        <td><%= link_to 'Destroy', product, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>

<%= link_to 'New Product', new_product_path %>

Controller

#app/controller/products_controller.rb
class ProductsController < ApplicationController
  def index
    @products = Product.all
    render
  end
end
#actionpack/lib/action_controller/metal/implicit_render.rb
def send_action(method, *args)
  ret = super
  default_render unless performed?
  ret
end

def default_render(*args)
  render(*args)
end

Controller

#app/controller/products_controller.rb
class ProductsController < ApplicationController
  def index
    @products = Product.all
    render
    render
  end
end
#actionpack/lib/action_controller/metal/rendering.rb
def render(*args) #:nodoc:
  raise ::AbstractController::DoubleRenderError if self.response_body
  super
end

Controller

#actionpack/lib/abstract_controller/rendering.rb
def render(*args, &block)
  options = _normalize_render(*args, &block)
  # => {:prefixes=>["products", "application"], :template=>"index", 
    :layout=>#<Proc:0x007fca6ca8dcc8@/home/dougui/workspace/rails/rails/actionview/lib/action_view/layouts.rb:386>

  self.response_body = render_to_body(options)
  # Cherche le bon système de template selon l'extension du fichier et 
  #  exécute celui par défaut si rien n'est trouvé

  _process_format(rendered_format, options) if rendered_format
  self.response_body
end

Résumé

  • Réception d'un requête HTTP
  • Transformation par tout un tas de middleware
  • Recherche de l'action en fonction du chemin
  • Exécution de l'action
  • ...
  • Retour de la réponse

Vues

@message = 'Bonjour ParisRb'
ERB.new(File.new("app/views/products/index.html.erb").read).result
# => "Bonjour ParisRb\n"
#app/views/products/index.html.erb
<%= @message %>

ERB (Embedded Ruby) : Système de template

@message = 'Bonjour ParisRb'
ERB.new("<%= @message %>").result
# => "Bonjour ParisRb\n"

Vues

#app/controllers/products_controller.rb
def index
  @message = 'Bonjour ParisRb'
end
#app/views/products/index.html.erb
<%= self.class == controller.class %> # => false
<%= @message %> # => 'Bonjour ParisRb'

?????

Vues

def view_context_class
  @view_context_class ||= begin
    supports_path = supports_path?
    routes  = respond_to?(:_routes)  && _routes
    helpers = respond_to?(:_helpers) && _helpers

    Class.new(ActionView::Base) do
      if routes
        include routes.url_helpers(supports_path)
        include routes.mounted_helpers
      end

      if helpers
        include helpers
      end
    end
  end
end

Vues

#actionview/lib/action_view/base.rb

def assign(new_assigns) # :nodoc:
  @_assigns = new_assigns.each { |key, value| instance_variable_set("@#{key}", value) }
end

Rails n'a probablement pas

le meilleur code du monde

Résumé

  • Réception d'un requête HTTP
  • Transformation par tout un tas de middleware
  • Recherche de l'action en fonction du chemin
  • Exécution de l'action
  • Construction du fichier HTML
  • Retour de la réponse

À retenir

  • Rails est compliqué
  • Rails n'est pas magique
  • Rails est très compliqué (mais avec du temps on y arrive)

Sources

  • Rebuilding Rails - Noah Gibbs
  • Crafting Rails Applications - José Valim
  • Railscasts Pro - Ryan Bates
  • Code source de Rails

Questions ???

Copy of ParisRb2015

By Guirec Corbel

Copy of ParisRb2015

  • 1,003