#230 Inherited Resources
- Download:
- source codeProject Files in Zip (105 KB)
- mp4Full Size H.264 Video (16.4 MB)
- m4vSmaller H.264 Video (10.7 MB)
- webmFull Size VP8 Video (24.6 MB)
- ogvFull Size Theora Video (21.6 MB)
In questo episodio daremo un’occhiata al gem di José Valim chiamato Inherited Resources. Questo gem estrae funzionalità comuni dai controller RESTful e ci permette in tal modo di rimovere alcune duplicazioni dal codice dei nostri controller. Non si tratta del primo gem Rails a fornire questo genere di funzionalità: nell’episodio 92 abbiamo parlato di un gem chiamato make_resourceful analogo e esistono anche diversi plugin che fanno la stessa cosa. Ciascuno segue un approccio lievemente diverso, per cui, al solito, vale la pena cofrontarli fra loro prima di decidere quale usare. Noi abbiamo scelto Inherited Resources dal momento che funziona bene con Rails 3.0 e sembra anche quello più aggiornato.
L’applicazione Rails con cui lavoreremo in questo episodio sarà una semplice applicazione di e-commerce. Questa applicazione ha un elenco di prodotti e ciascuno di essi ha una serie di recensioni ad esso associate:
Usereme Inherited Resources per ripulire un po’ il codice di questa applicazione e vedere quanto codice dei controller potremo rimuovere senza intaccare le funzionalità applicative.
Installazione di Inherited Resources
Il codice del ProductController
attualmente appare così:
class ProductsController < ApplicationController def index @products = Product.all end def show @product = Product.find(params[:id]) end def new @product = Product.new end def create @product = Product.new(params[:product]) if @product.save flash[:notice] = "Successfully created product." redirect_to @product else render :action => 'new' end end def edit @product = Product.find(params[:id]) end def update @product = Product.find(params[:id]) if @product.update_attributes(params[:product]) flash[:notice] = "Successfully updated product." redirect_to @product else render :action => 'edit' end end def destroy @product = Product.find(params[:id]) @product.destroy flash[:notice] = "Successfully destroyed product." redirect_to products_url end end
Se avete diversi controller RESTful nella vostra applicazione, vi troverete a scrivere del codice tipo questo in ciascuno di essi ed è proprio in situazioni del genere che Inherited Resources si rivela molto utile. Se, tuttavia, in generale tendete a rendere molto personalizzate le logiche dei controller, allora un’astrazione come quella proposta da Inherited Resources potebbe non fare al caso vostro. Il controller riportato di sopra aderisce al pattern RESTful abbastanza bene, per cui possiamo usarlo per vedere ciò che è offerto dal gem Inherited Resources.
La nostra applicazione di e-commerce è scritta in Rails 3, per cui aggiungiamo Inherited Resources ad essa, includendo il gem nel nostroGemfile
:
source 'http://rubygems.org' gem 'rails', '3.0.0' gem 'sqlite3-ruby', :require => 'sqlite3' gem 'nifty-generators' gem 'inherited_resources'
Lo si installa al solito modo con:
$ bundle install
Ciò, come sempre, causerà l’installazione del gem e di un paio di dipendenze: has_scope
e responders
.
Una volta installati i gem, si può aggiornare il nostro ProductsController
per fargli usare le Inherited Resources. Per farlo, renderemo il controller specializzazione di InheritedResources::Base
anzichè di ApplicationController
. InheritedResources::Base
specializza ApplicationController
, per cui esporrà anche tutte le funzionalità del padre.
Poichè il ProductsController
è un controller RESTful abbastanza standard, possiamo sostituire tutti i suoi metodi con il codice ereditato da Inherited Resources, riducendo così il codice:
class ProductsController < InheritedResources::Base end
Dobbiamo riavviare il server per fare in modo che i nuovi gem vengano ricaricati, ma una volta fatto, vedremo che le pagine relative ai prodotti funzionano esattamente come prima. Possiamo persino creare un nuovo prodotto con successo e avere anche il messaggio flash di conferma:
Personalizzare una action
Quando abbiamo creato un nuovo prodotto, prima, siamo stati ridiretti alla pagina di dettaglio (show
) dello stesso al termine dell’operazione, ma come potremmo fare per far sì che l’applicazione ci mandi piuttosto alla action index
? Inherited Resources ci permette di ridefinire una qualunque delle proprie action di default, semplicemente ridefinando il metodo opportuno nel controller, per cui potremmo scrivere un metodo create
nel ProductsController
che crei il nuovo prodotto e che rimandi alla action index
.
Non c’è bisogno, tuttavia, di riscrivere completamente la action create solo per modificare il redirect. Possiamo includere il comportamento di Inherited Resource chiamando il metodo create!
e passandogli un blocco. Il cambio dell’URL di redirect alla creazione di un nuovo oggetto di modello è un’operazione piuttosto comune che si vorrebbe poter fare, per cui possiamo semplicemente restituire l’URL desiderato nel blocco:
class ProductsController < InheritedResources::Base def create create! { products_path } end end
Ci sono altre cose che potremmo fare nel blocco e sono tutte spiegate nella documentazione.
Ora, creando un nuovo prodotto, siamo ridiretti alla action index
proprio come volevamo:
Lavorare con formati differenti
Se vogliamo che il controller sia in grado di rispondere in diversi formati, tipo XML e HTML, è semplice. Basta aggiungere la respond_to
proprio come faremmo in un normale controller Rails 3:
class ProductsController < InheritedResources::Base respond_to :html, :xml def create create! { products_path } end end
Ciò funzionerà allo stesso modo mostrato nell’episodio 224 [guardalo, leggilo]. Se ora visitiamo /products.xml
otterremo una lista di prodotti in XML:
Nested Resource
Ora che abbiamo ripulito il ProductsController
, spostiamo l’attenzione sul ReviewsController
. Le opinioni sono annidate sotto i prodotti, per cui la review di un articolo sarà all’URL /products/1/reviews
per il prodotto di id
pari a 1
. Questo per quel che riguarda la action index
del ReviewsController
. Analogamente, se aggiungiamo una opinione, l’URL sarà sempre innestato sotto i prodotti:
Il codice per il ReviewsController
appare così:
class ReviewsController < ApplicationController def index @product = Product.find(params[:product_id]) @reviews = @product.reviews end def new @product = Product.find(params[:product_id]) @review = Review.new end def create @product = Product.find(params[:product_id]) @review = @product.reviews.build(params[:review]) if @review.save flash[:notice] = "Successfully created review." redirect_to product_reviews_path(@product) else render :action => 'new' end end end
La differenza più evidente fra questo controller e il ProductsController
è che il primo ha solo tre delle sette action RESTful. L’altra differenza è che, siccome gestisce l’innestamento, ciascuna action ottiene il prodotto di riferimento basandosi su un parametro nell’URL.
Anche se in questo contesto si tratta di una risorsa innestata, il comportamento è fondamentalmente lo stesso che quello nel ProductsController
e le Inherited Resources funzioneranno bene anche qui. Possiamo rimuovere il codice esistente nel controller e cambiare la tassonomia della classe, affinchè erediti da InheritedResources::Base
. Tutto quello che dobbiamo fare per gestire l’innestamento è di aggiungere belongs_to
, che è un metodo fornito da Inherited Resources e che può essere usato nella definizione di relazioni fra controller allo stesso modo in cui lo si fa fra modelli. Sistemato tutto ciò, Inherited Resources gestisce il recupero del prodotto corretto al posto nostro:
class ReviewsController < InheritedResources::Base belongs_to :product end
Per come è ora, il ReviewsController
esporrà tutte e sette le action di default, dal momento che questo è il comportamento di default di Inherited Resources, ma noi vogliamo che questo controller si limiti a rispondere solamente a index
, new
e create
. Possiamo usare il metodo actions
per limitare le action disponibili:
class ReviewsController < InheritedResources::Base belongs_to :product actions :index, :new, :create end
Come fatto già per il ProductsController
, vogliamo anche cambiare l’URL a cui rimandiamo a seguito della creazione di una nuova opinione. Ci sono molti metodi helper per gli URL forniti da Inherited Resources per ridirigere a varie action, che tornano utili in questo caso in cui abbiamo un innestamento. In questo caso possiamo usare un metodo chiamato collection_url
che ridirige alla action index
e gestisce l’innestamento per noi:
class ReviewsController < InheritedResources::Base belongs_to :product actions :index, :new, :create def create create! { collection_url } end end
Possiamo testare il tutto aggiungendo una opinione:
Dopo che abbiamo fatto il submit della nuova opinione verremo ridiretti alla pagina delle opinioni per tale prodotto proprio come volevamo:
Scope pubblici
Inherited Resources ha un’altra utile funzionalità chiamata has_scope
. Per usarla, dobbiamo solo aggiungere un riferimento al gem nel Gemfile
e poi rilanciare bundle install
:
source 'http://rubygems.org' gem 'rails', '3.0.0' gem 'sqlite3-ruby', :require => 'sqlite3' gem 'nifty-generators' gem 'inherited_resources' gem 'has_scope'
Installato il tutto, possiamo chiamare has_scope
in qualsiasi dei nostri controller passandogli il nome di uno scope presente sul modello associato. Per questo esempio, aggiungeremo lo scope limit
, che è fornito per default a tutti i modelli in Rails 3, al ProductsController
:
class ProductsController < InheritedResources::Base respond_to :html, :xml has_scope :limit def create create! { products_path } end end
Con questo codice, possiamo aggiungere scope come parametri all’URL, per cui se passiamo un parametro limit
, verrà chiamato tale scope e ridurremo il numero di prodotti mostrati:
Se volessimo applicare lo scope per tutte le query su quel controller, implicitamente, senza che sia necessario aggiungerlo come parametro alla stringa di query, potremmo dichiararlo con l’opzione default
e un valore per il parametro:
class ProductsController < InheritedResources::Base respond_to :html, :xml has_scope :limit, :default => 3 def create create! { products_path } end end
Ora, se non passiamo un parametro limit
verrà usato il valore di default e, in questo caso, vedremo solo tre prodotti:
Tutto ciò funziona naturalemente anche con gli scope personalizzati. Aggiungeremo uno scope al modello Review
che ci permetterà di filtrare le opinioni in base al loro punteggio:
class Review < ActiveRecord::Base belongs_to :product scope :rating, proc { |rating| where(:rating => rating) } end
Ora rendiamo lo scope pubblico, aggiungendolo al ReviewsController
:
class ReviewsController < InheritedResources::Base belongs_to :product actions :index, :new, :create has_scope :rating def create create! { collection_url } end end
Possiamo ora usare un parametro rating nell’URL, per limitare le opinioni in base al loro punteggio.
Il gem has_scope
può essere utilizzato al di fuori di Inherited Resources usando il metodo apply_scopes
all’interno della action index
. Ci sono ulteriori dettagli su tutto ciò nella documentazione su Github.
Personalizzare il messaggio flash
L’ultima cosa che tratteremo sarà come personalizzare il messaggio flash. Quando creiamo una nuova opinione, il messaggio di default è “Review was successfully created.”, ma noi vorremo modificarlo affinchè sia qualsiasi cosa che vogliamo, cambiando il file di internazionalizzazione. Anche se la vostra applicazione non supporta linguaggi multipli, questi file sono il posto migliore dove mettere le stringhe che verranno mostrate sull’interfaccia utente. Ogni applicazione Rails 3 ha un file di localizzazione inglese incluso al suo interno, sotto /config/locales/en.yml
.
Per ridefinire il messaggio flash di default di Inherited Resources, creiamo una chiave flash:
, sotto la quale avremo una chiave contenente il nome del controller, in questo caso reviews:
. Sotto di essa, aggiungiamo una chiave per la action e sotto quest’ultima, una per il nome del messaggio di flash. Per il nostro controller delle opinioni, il file di configurazione apparirà così:
# Sample localization file for English. Add more files in this directory for other locales. # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. en: flash: reviews: create: notice: "Your review has been created!"
Se non vogliamo dover configurare tutto ciò per ogni controller della nostra applicazione, possiamo sostituire il nome del controller con actions:
e di conseguenza i messaggi saranno applicati a tutti i singoli controller. Possiamo usare una variabile resource_name
a mo’ di placeholder, per attualizzare il nome del modello corrente.
# Sample localization file for English. Add more files in this directory for other locales. # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. en: flash: actions: create: notice: "Your {{resource_name}} has been created!"
Possiamo provare il tutto, creando una nuova opinione.. Al submit, verrà mostrato il messaggio flash così personalizzato:
E’ tutto per questo episodio. Se vi trovate a creare sempre lo stesso codice per i controller, vale la pena dare un’occhiata a Inherited Resources. Il file README è piuttosto esaustivo e tratta anche di parti che non abbiamo menzionato in questo episodio. Vale anche la pena consultare la wiki page.