#224 Controllers in Rails 3
- Download:
- source codeProject Files in Zip (95.2 KB)
- mp4Full Size H.264 Video (19.7 MB)
- m4vSmaller H.264 Video (12.4 MB)
- webmFull Size VP8 Video (30.9 MB)
- ogvFull Size Theora Video (28.7 MB)
Während die wichtigsten Änderungen an den Rails 3-Controllern im Hintergrund passiert sind, gibt es auch einige Änderungen an der öffentlichen API, mit welcher wir als Rails-Entwickler interagieren. Diese Episode wird einige der neuen Features behandeln.
Parameter Logging
Die erste Änderung betrifft den ApplicationController
. In einer Rails 2 Applikation kann man bestimmte Parameter filtern, damit sie nicht in Klartext im Logfile stehen, indem man die filter_parameter_logging
-Methode benutzt:
class ApplicationController < ActionController::Base protect_from_forgery filter_parameter_logging :password end
Diese Methode wurde in Rails 3 als deprecated markiert und sollte aus Anwendungen entfernt werden. Der Parameterfilter wird nun in der neuen config/application.rb
-Datei konfiguriert. Der Parameterfiltercode, befindet sich am Ende der Datei.
# Configure sensitive parameters which will be filtered from the log file. config.filter_parameters += [:password]
Diese Zeile wird standardmäßig zu einer neuen Rails 3 Anwendung hinzugefügt, da man leicht vergessen kann, dieses Feature zu aktivieren. Natürlich wird es nur Passwortparameter filtern und muss abgeändert werden, wenn andere Parameter, zum Beispiel Kreditkartennummern, gefiltert werden sollen.
Redirect-Kurzformen
Als nächstes werfen wir ein Blick auf den ProductsController
einer Store-Anwendung. Wenn wir uns den Code für die create
-Methode anschauen, sehen wir, dass, sobald ein valides Produkt gespeichert wurde, die Action eine Flash-Nachricht erstellt und dann zur show
-Action für das neu erstellte Produkt weiterleitet.
def create @product = Product.new(params[:product]) if @product.save flash[:notice] = "Successfully created product." redirect_to @product else render :action => 'new' end end
Dies ist das übliche Verhalten eines Controllers und in Rails 3 können wir diese zwei Zeilen kombinieren, sodass die Flash-Nachricht ein Parameter der redirect_to
-Methode wird.
def create @product = Product.new(params[:product]) if @product.save redirect_to @product, :notice => "Successfully created product." else render :action => 'new' end end
Sowohl :notice
als auch :alert
funktionieren hier, jeder andere Flash-Nachrichtentyp muss in einem :flash
-Hash eingebettet werden, was man aber eher selten benötigt. Dieses Feature wurde auch zu Rails 2.3.6 hinzugefügt, damit man es auch mit einer aktuellen Rails 2 Version nutzen kann.
Die redirect_to
-Methode nimmt auch ein Model-Objekt als Parameter. Ein Produkt-Model-Objekt als @product
zu übergeben ist eine Kurzform für product_path(@product)
und wird in den Pfad der show
-Action für das Produkt übersetzt. Wenn wir stattdessen zur edit-Seite dieses' Produkts weiterleiten wollen, können wir edit_product_path(@product)
nutzen. Es gibt allerdings noch eine weitere nützliche Kurzform. Anstatt
redirect_to edit_product_path(@product)
können wir es als Array schreiben:
redirect_to [:edit, @product]
Dies wird in denselben edit_product_path übersetzt, aber mit etwas weniger Code. Wie die obige :notice
-Option, funktioniert das auch in einer neueren Version von Rails 2. Diese Kurzform funktioniert auch mit verschachtelten Ressourcen, somit können wir, wenn ein Produkt zu einer Kategorie gehört, folgendermaßen weiterleiten:
redirect_to [@category, @product]
Diese Array-Technik funktioniert überall, wo man eine URL generieren kann und kann genauso gut in dem View-Code einer link_to
-Methode oder in Controllern benutzt werden.
Permanente Cookies
Das nächste neue Feature sind permanente Cookies. Angenommen ein Benutzer erstellt ein neues Produkt, dann wollen wir die Produkt-ID in einem Cookie speichern. Wir können die cookies
-Methode dafür benutzen:
def create @product = Product.new(params[:product]) if @product.save cookies[:last_product_id] = @product.id redirect_to @product, :notice => "Successfully created product." else render :action => 'new' end end
Dieser Code erstellt ein Session Cookie, welcher nur solange hält, wie der Benutzer sein Browserfenster offen hält. Um einen Cookie mit einem speziellem Ablauf-Datum in Rails 2 zu erstellen, müssten wir den Wert in einen Hash umwandeln und ein explizites Ablaufdatum setzen:
cookies[:last_product_id] = { :value => @product_id, :expires => 1.month.from_now }
In Rails 3 (und ab Rails 2.3.6) können wir die neue cookies.permanent
-Methode benutzen um einen Cookie mit langer Ablaufzeit zu erstellen.
cookies.permanent[:last_product_id] = @product.id
Wir können auf diese Art außerdem gesignte Cookies erstellen, doch darauf gehen wir jetzt nicht im Detail ein. Es gibt einen sehr lesenswerten Blogeintrag von Pratik Naik zu diesem Thema. Im Wesentlichen nutzt man die cookies.permanent.signed
-Methode um einen verschlüsselten Cookie zu generieren und ihn später zu lesen. Zu beachten ist, dass man ein cookie_verifier_secret
brauchen, damit die Anwendung funktioniert.
respond_with
benutzen
Die letzte neue Sache in dieser Episode, ist die wahrscheinlich bedeutendste Ergänzung an Rails 3 Controllern: respond_with
. Wir werden das demonstrieren, indem wir ein REST-Service über ein XML-Interface zu unserem ProductsController
hinzufügen. Das bedeutet, dass jede Action des Controllers zusätzlich zu seiner HTML Antwort eine XML-Antwort zurückgeben können muss.
In einer Rails 2 Applikation würden wir dies mit der respond_to
-Methode in jeder Action implementieren:
def index @products = Product.all respond_to do |format| format.html format.xml { render :xml => @products } end end
In dem respond_to
-Block legen wir für alle Formate fest, wie die Action reagieren soll. Für eine XML-Anfrage soll sie, zum Beispiel, den @products
-Array als XML darstellen. Diese Vorgehensweise kann schnell unhandlich werden, wenn wir dies für jede Action im Controller machen müssen.
In Rails 3 können wir diesen Code mit einem Aufruf von respond_with
ersetzen, dem wir das dazugehörige Objekt mitgeben, in diesem Fall das Array von Produkten. Damit respond_with
funktioniert, müssen wir die "Schwester"-Klassenmethode respond_to
benutzen, um festzulegen, auf welche MIME-Typen der Controller reagieren soll und damit respond_with
weiß, welche Formate es in jeder Action zurückgeben kann.
class ProductsController < ApplicationController respond_to :html, :xml def index @products = Product.all respond_with @products end # Other methods end
Wenn wir unsere Änderungen testen wollen und uns die Produkt-Indexseite im Browser anschauen, sehen wir, das die HTML-index-Action so angezeigt wird, wie wir es erwartet haben.
Und wenn wir .xml
an die URL anhängen, bekommen wir die XML-Antwort.
Wie funktioniert nun die respond_with
-Methode genau? Für einen einfachen GET-Request (z.B. index
) sucht es nach einer View-Datei zur Darstellung des passenden MIME-Typs, zum Beispiel eine .html.erb
-Datei, und rendert sie. Falls respond_with
keinen View finden kann, versucht es eine Methode wie to_xml
(oder z.B. to_json
) aufzurufen, um das übergebene Objekt im passenden Format darzustellen.
Mit diesem Wissen können wir diese respond_with-Zeile einfach in die show
- und die new
-Methode kopieren.
def show @product = Product.find(params[:id]) respond_with @product end def new @product = Product.new respond_with @product end
Die create
-Action ist etwas komplizierter, denn sie enthält HTML-spezifischen Code, wie redirect_to
und render
. Wir wollen diese Methoden nicht in einem XML-Response verwenden. Und wir können beide vermeiden, indem wir sie einfach entfernen und stattdessen einen Aufruf zu respond_with
hinzufügen. Die Action sieht jetzt wie folgt aus:
def create @product = Product.new(params[:product]) if @product.save cookies[:last_product_id] = @product.id flash[:notice] = "Successfully created product." end respond_with(@product) end
Wir können das machen, weil respond_with
überprüft, ob das übergebene Objekt valide ist oder nicht, und je nachdem rendert oder redirectet. Jetzt können wir respond_with
zu den anderen Actions im Controller hinzufügen und dabei wieder jeden HTML-spezifischen Code entfernen. Danach sieht unsere Controller-Code etwa so aus:
class ProductsController < ApplicationController respond_to :html, :xml def index @products = Product.all respond_with @products end def show @product = Product.find(params[:id]) respond_with @product end def new @product = Product.new respond_with @product end def create @product = Product.new(params[:product]) if @product.save cookies[:last_product_id] = @product.id flash[:notice] = "Successfully created product." end respond_with(@product) end def edit @product = Product.find(params[:id]) respond_with(@product) end def update @product = Product.find(params[:id]) if @product.update_attributes(params[:product]) flash[:notice] = "Successfully updated product." end respond_with(@product) end def destroy @product = Product.find(params[:id]) @product.destroy flash[:notice] = "Successfully destroyed product." respond_with(@product) end end
Die respond_with
-Methode kann etwas verwirrend sein, deswegen hier eine kleine Zusammenfassung:
Für einen GET-Request sucht respond_with
zuerst nach einem View, welcher zu diesem spezifischen Format passt. Wenn es einen findet, wird der View gerendert; wenn nicht, ruft es to_xml
(oder was auch immer für ein Format angefordert wurde) auf dem übergebenen Objekt auf.
Für jeden anderen Request-Typ prüft respond_with
, ob das übergebene Objekt Errors beinhaltet. Wenn es welche gibt, wird der passende View gerendert (z.B. new
für die create
-Action und edit
für update
). Falls es keine gibt, redirectet es zu der objektspezifischen Seite (z.B. die show
-Action).
Wird können das ausprobieren, indem wir eines unserer Produkte bearbeiten.
Wenn wir den "Update Product"-Button drücken, wird die update
-Action aufgerufen und respond_with
führt eine Weiterleitung zur Seite des Produktes durch. Das Produkt wurde erfolgreich aktualisiert.
Genauso klappt der "Destroy"-Link, wir werden zur Indexseite weitergeleitet.
Wenn das Standardverhalten von respond_with
für einige Teile unserer Anwendung nicht mehr ausreicht, können wir es für bestimmte Actions anpassen. Zum Beispiel, wenn wir wollen, dass bei dem Update eines Produktes, die Anwendung nicht zur show
-Action, sondern zur index
-Action weiterleiten soll. In diesem Fall können wir respond_with
eine location-Option übergeben:
def update @product = Product.find(params[:id]) if @product.update_attributes(params[:product]) flash[:notice] = "Successfully updated product." end respond_with(@product, :location => products_url) end
Wir können auch die jeweiligen Formate überschreiben, indem wir respond_with
einen Block übergeben, genauso wie wir es mit der respond_to
-Methode machen würden. Wenn wir zum Beispiel einfach nur etwas Text für das XML-Format der edit-Action darstellen wollen, können wir das so machen:
def edit @product = Product.find(params[:id]) respond_with(@product) do |format| format.xml { render :text => "I'm XML!" } end end
Wenn wir die XML-Version der edit-Seite besuchen, sehen wir den Text.
Sollte man letztendlich eine komplett eigene Anpassung benötigen, kann man eine eigene Responder-Klasse übergeben. Auf diese Weise wird der Controller alles an diese Klasse weiterreichen und wir können das Verhalten des Responses frei anpassen.
respond_with(@product, :responder => MyResponder)
Wenn man wissen möchte, wie eine Responder-Klasse erstellte wird, sollte man sich die in Rails 3 eingebaute Responder-Klasse anschauen. Der Code ist recht einfach zu lesen und gut dokumentiert und man bekommt eine bessere Vorstellung davon, wie der Responder funktioniert.
Das ist alles für diese Episode. Wie man gesehen hat, ist respond_with
eine wirklich zweckmäßige Herangehensweise, viele Formate zu bewältigen. Sollte man aber feststellen, dass man es zu stark anpasst, ist es wahrscheinlich einfacher, respond_with
nicht zu verwenden, und den Code direkt einzubetten, oder eine eigene Responder-Klasse zu schreiben.