Neu: Rails Schulungen in Deutschland vom Autor dieses Buches.div class="para">Wenn wir zusätzlich zur URL /home/ping noch mit /home/apfelmus auf die gleiche Action im gleichen Controller zugreifen, so müssen wir die Datei config/routes.rb wie folgt verändern:
Shop::Application.routes.draw do
  get "home/index"

  get "home/ping"

  get "home/pong"

  root :to => "home#index"

  match "home/apfelmus" => "home#ping"
end

Damit stehen uns folgende Routen zur Verfügung:
MacBook:shop xyz$ rake routes
   home_index GET /home/index(.:format)    home#index
    home_ping GET /home/ping(.:format)     home#ping
    home_pong GET /home/pong(.:format)     home#pong
         root     /                        home#index
home_apfelmus     /home/apfelmus(.:format) home#ping
MacBook:shop xyz$ 
http://0.0.0.0:3000/home/apfelmus
Wenn Sie der Route /home/apfelmus einen anderen Namen geben wollen, können Sie dies mit der Einstellung :as => "name" in der config/routes.rb realisieren:
Shop::Application.routes.draw do
  get "home/index"

  get "home/ping"

  get "home/pong"

  root :to => "home#index"

  match "home/ping-ping" => "home#ping", :as => "apfelmus"
end
Jetzt stehen Ihnen im System folgende Routen-Namen zur Verfügung:
MacBook:shop xyz$ rake routes
home_index GET /home/index(.:format)    home#index
 home_ping GET /home/ping(.:format)     home#ping
 home_pong GET /home/pong(.:format)     home#pong
      root     /                        home#index
  apfelmus     /home/ping-ping(.:format) home#ping
MacBook:shop xyz$
Sie sehen dabei, dass eine URL ein Minuszeichen enthalten darf, aber der Routen-Name nicht (ein Unterstrich geht aber).

Wichtig

Die Routen in der Datei config/routes.rb werden immer von oben nach unten abgearbeitet. Der erste Treffer von oben gewinnt!

Parameter

Match kann nicht nur fixe Routen zuordnen, sondern auch noch Parameter übergeben. Ein typisches Beispiel dafür wären Angaben zu einem Datum. Wir erstellen dazu nachfolgend eine Mini-Blog-Applikation:
MacBook:~ xyz$ rails new blog
[...]
MacBook:~ xyz$ cd blog 
MacBook:blog xyz$ rails generate scaffold Post subject content published_at:date
[...]
MacBook:blog xyz$ rake db:migrate
[...]
MacBook:blog xyz$
Als Beispieldaten in der db/seeds.rb nehmen wir:
Post.create(:subject => 'Ein Test',            :published_at => '01.10.2011')
Post.create(:subject => 'Noch ein Test',       :published_at => '01.10.2011')
Post.create(:subject => 'Und wieder ein Test', :published_at => '02.10.2011')
Post.create(:subject => 'Letzter Test',        :published_at => '01.11.2011')
Post.create(:subject => 'Allerletzter Test',   :published_at => '01.11.2012')
Mit rake db:seed spielen wir diese Daten in die Datenbank:
MacBook:blog xyz$ rake db:seed
MacBook:blog xyz$
Wenn wir jetzt mit rails server den Rails-Server starten und mit dem Browser auf die Seite http://0.0.0.0:3000/posts surfen, bekommen wir folgende Anzeige:
Index-Ansicht aller Posts
Für ein solches Blog wäre es natürlich sehr praktisch, wenn man mit der URL http://0.0.0.0:3000/2010/ alle Einträge für das Jahr 2010 und mit http://0.0.0.0:3000/2010/10/01 alle Einträge für den 01.10.2010 anzeigen könnte. Das erreichen wir mit optionalen Parametern beim match-Eintrag. Bitte tragen Sie folgende Konfiguration in die config/routes.rb ein:
Blog::Application.routes.draw do
  resources :posts

  match "/:year(/:month(/:day))" => "posts#index"
end
Die runden Klammern stehen für optionale Parameter. In diesem Fall muss unbedingt das Jahr, aber nicht zwingend der Monat oder der Tag angegeben werden.
Wenn wir nichts anderes ändern, bekommen wir beim Aufruf von http://0.0.0.0:3000/2010/ und http://0.0.0.0:3000/2010/10/01 immer noch das gleiche Ergebnis wie beim Aufruf von http://0.0.0.0:3000/posts. Das ist ja auch logisch. Aber schauen Sie einmal in die Ausgabe von rails server:
Started GET "/2010/" for 127.0.0.1 at 2012-05-22 11:27:29 +0200
Processing by PostsController#index as HTML
  Parameters: {"year"=>"2010"}
  Post Load (0.2ms)  SELECT "posts".* FROM "posts" 
  Rendered posts/index.html.erb within layouts/application (5.5ms)
Completed 200 OK in 13ms (Views: 11.3ms | ActiveRecord: 0.2ms)
Die Route wurde erkannt und dem Hash params (in der Ausgabe irreführend als Parameters ausgeschrieben) ein Element "year" => "2010" zugewiesen. Ein Aufruf der URL http://0.0.0.0:3000/2010/12/24 ergibt erwartungsgemäß folgende Ausgabe:
Started GET "/2010/12/24" for 127.0.0.1 at 2012-05-22 11:30:49 +0200
Processing by PostsController#index as HTML
  Parameters: {"year"=>"2010", "month"=>"12", "day"=>"24"}
  Post Load (0.2ms)  SELECT "posts".* FROM "posts" 
  Rendered posts/index.html.erb within layouts/application (5.9ms)
Completed 200 OK in 13ms (Views: 11.5ms | ActiveRecord: 0.2ms)
Bei der URL http://0.0.0.0:3000/2010/12/24 wurden folgende Werte im Hash params gespeichert: "year"=>"2010", "month"=>"12", "day"=>"24".
Auf params[] haben wir im Controller Zugriff auf die in der URL definierten Werte. Wir müssen nur noch die index Methode in app/controllers/posts_controller.rb anpassen, um die für das entsprechende Datum, den entsprechenden Monat oder das entsprechende Jahr eingetragenen posts auszugeben:
def index
  if params[:day]
    @posts = Post.where(:published_at => Date.parse("#{params[:day]}.#{params[:month]}.#{params[:year]}"))
  elsif params[:month]
    @posts = Post.where(:published_at => ( Date.parse("01.#{params[:month]}.#{params[:year]}") .. Date.parse("01.#{params[:month]}.#{params[:year]}").end_of_month ))
  elsif params[:year]
    @posts = Post.where(:published_at => ( Date.parse("01.01.#{params[:year]}") .. Date.parse("31.12.#{params[:year]}") ))
  else
    @posts = Post.all
  end

  respond_to do |format|
    format.html # index.html.erb
    format.json { render json: @posts }
  end
end
Damit bekommen wir beim Aufruf von http://0.0.0.0:3000/2011/10/01 alle posts des 01.10.2011 angezeigt.
Index-View für den 01.10.2011

Constraints (Einschränkungen)

In „Parameter“ habe ich Ihnen gezeigt, wie man Parameter aus der URL auslesen und an den Controller weitergeben kann. Leider hat der dort definierte Eintrag in der config/routes.rb
match "/:year(/:month(/:day))" => "posts#index"
einen erheblichen Nachteil: Er überprüft die einzelnen Elemente nicht. Die URL http://0.0.0.0:3000/ein/beispiel/dafuer wird genauso gematcht und führt dann natürlich direkt zu einem Fehler:
Fehlermeldung
In der Log-Ausgabe in log/development.log sehen wir dabei folgenden Eintrag:
Started GET "/ein/beispiel/dafuer" for 127.0.0.1 at 2012-05-22 13:20:44 +0200
Processing by PostsController#index as HTML
  Parameters: {"year"=>"ein", "month"=>"beispiel", "day"=>"dafuer"}
Es ist klar, dass Date.parse( "dafuer.beispiel.ein") nicht funktionieren kann. Ein Datum besteht nun mal aus Zahlen und nicht aus Buchstaben.
Constraints können mittels Regular Expressions den Inhalt der URL besser definieren. In unserem Blog-Fall würde die config/routes.rb mit Constraints so aussehen:
Blog::Application.routes.draw do
  resources :posts

  match "/:year(/:month(/:day))" => "posts#index", :constraints => { :year => /\d{4}/, :month => /\d{2}/, :day => /\d{2}/ }
end

Warnung

Bitte beachten Sie, dass bei den Regular Expressions in einem Constraint keine Regex-Anchors wie "^" benutzt werden können.
Wenn wir mit dieser Konfiguration noch mal die URL aufrufen, bekommen wir einen Fehler „No route matches“ von Rails:
Routing Error

Advanced Constraints

Mit der Route match "/:year(/:month(/:day))" => "posts#index", :constraints => { :year => /\d{4}/, :month => /\d{2}/, :day => /\d{2}/ } haben wir zwar die URL syntaktisch auf ein Datum überprüft, aber es kann natürlich ein User immer noch die URL http://0.0.0.0:3000/2011/02/31 aufrufen. Da es keinen 31. Februar gibt, dürfte es diese Route logisch nicht geben. Wir brauchen also eine Möglichkeit, die ein angegebenes syntaktisch komplettes Datum darauf überprüft, ob es auch ein korrektes Datum nach dem Kalender ist.
Dazu müssen wir eine eigene Klasse definieren, mit der Objekte mit der Methode matches?() definiert werden. Innerhalb von matches?() können wir die gewünschte eigene Validierung vornehmen, um dann mit true oder false zu antworten. Bitte erstellen Sie dazu die Datei lib/valid_date_contraint.rb mit folgendem Inhalt:
class ValidDateConstraint
  def matches?(request)
    begin
      Date.parse("#{request.params[:day]}.#{request.params[:month]}.#{request.params[:year]}")
      true
    rescue
      false
    end
  end
end
Diese Klasse müssen wir jetzt laden. Bitte erstellen Sie dazu die Datei config/initializers/load_extensions.rb mit diesem Inhalt:
require 'valid_date_constraint'
Jetzt splitten wir die Datums-Route in der config/routes.rb auf:
Blog::Application.routes.draw do
  resources :posts

  match "/:year/:month/:day" => "posts#index", :constraints => { :year => /\d{4}/, :month => /\d{2}/, :day => /\d{2}/ }, :constraints => ValidDateConstraint.new

  match "/:year(/:month)" => "posts#index", :constraints => { :year => /\d{4}/, :month => /\d{2}/ }
end
Das erste match reagiert auf alle URLs mit drei Parametern, die syntaktisch den gegebenen Contraints entsprechen. Zusätzlich wird noch das Datum überprüft. Erst wenn das Datum an sich valide ist, wird diese Route benutzt. Sie wird also nicht bei der URL http://0.0.0.0:3000/2011/02/31 aufgerufen. Hingegen würde die URL http://0.0.0.0:3000/2011/10/01 funktionieren.

Redirects

Ich kann mit einem match auch auf eine andere Seite umleiten (redirect). Wenn ich die Eingabe der unsinnigen URL http://0.0.0.0:3000/2010/02/31 auf /2010/02 redirecten will, dann geht das folgendermaßen:
match "/:year/02/31" => redirect("/%{year}/02")
Damit könnte man auch die Eingabe eines einstelligen Monats auf einen zweistelligen Monat umleiten:
match "/:year/:month/:day" => redirect("/%{year}/0%{month}/%{day}"), :constraints => { :year => /\d{4}/, :month => /\d{1}/, :day => /\d{2}/ }
Das Gleiche ginge natürlich auch für einen einstelligen Tag. Unter Berücksichtigung aller Kombinationen sähe unsere config/routes.rb dann so aus:
Blog::Application.routes.draw do
  resources :posts

  match "/:year/:month/:day" => redirect("/%{year}/0%{month}/%{day}" ), :constraints => { :year => /\d{4}/, :month => /\d{1}/, :day => /\d{2}/ }
  match "/:year/:month/:day" => redirect("/%{year}/0%{month}/0%{day}"), :constraints => { :year => /\d{4}/, :month => /\d{1}/, :day => /\d{1}/ }
  match "/:year/:month/:day" => redirect("/%{year}/%{month}/0%{day}" ), :constraints => { :year => /\d{4}/, :month => /\d{2}/, :day => /\d{1}/ }

  match "/:year/:month" => redirect("/%{year}/0%{month}"), :constraints => { :year => /\d{4}/, :month => /\d{1}/ }

  match "/:year(/:month(/:day))" => "posts#index", :constraints => { :year => /\d{4}/, :month => /\d{2}/, :day => /\d{2}/ }
end
Mit diesem Redirect-Regelwerk wäre sichergestellt, dass ein Benutzer der Seite auch einstellige Monate und Tage eingeben kann und trotzdem an der richtigen Stelle landet bzw. zum richtigen Format umgeleitet wird.

Anmerkung

Redirects in der config/routes.rb sind per Default Redirects mit dem Code 301 ("Moved Permanetly").

Autor

Stefan Wintermeyer