Le fonctionnement interne de Rails
https://slides.com/guireccorbel/parisrb2015/
Guirec Corbel
@GuirecCorbel

guirec@optik360.com


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ée
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.class == ActionDispatch::Routing::RouteSet
@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ée
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(req)
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(req)
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)
# {:controller=>"products", :action=>"index"}
# 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
Trouver les routes
Résumé
- Réception d'un requête HTTP
- Transformation par un stack de middleware
- Recherche de la route
- ...
- Retour de la réponse

À retenir
- Rails est basé sur Rack
- 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 ???

ParisRb2015
By Guirec Corbel
ParisRb2015
- 1,900