#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)
En este episodio se presenta una gema desarrollada por José Valim llamada Inherited Resources. Esta gema extrae la funcionalidad más habitual de los controladores REST para evitar la duplicación del código. No se trata de la primera gema de Rails que trata de ayudarnos con esto, en el episodio 92 vimos una gema llamada make_resourceful y hay otros plugins disponibles. Cada uno tiene un enfoque distinto, y vale la pena echarles un vistazo antes de decidirnos por uno u otro. Nosotros hemos escogido Inherited Resources porque funciona bien con Rails 3.0 y parece estar un poco más actualizado.
La aplicación Rails sobre la que vamos a trabajar en este episodio es una sencilla tienda electrónica. La aplicación tiene un listado de productos y cada producto tiene varias reseñas.
Con Inherited Resources vamos a reorganizar el código de la aplicación, y veremos que podemos eliminar una gran cantidad de código de los controladores sin afectar a la funcionalidad.
Instalación de Inherited Resources
El código de ProductController
tiene el siguiente aspecto:
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
Estaremos acostumbrados a escribir código como este si en nuestras aplicaciones solemos usar controladores REST, que es la situación en la que Inherited Resources nos resultará más útil. Si por el contrario solemos personalizar mucho nuestros controladores, una abstracción como Inherited Resources no se adaptará a nuestras necesidades. El código de arriba sigue el patrón REST con bastante fidelidad por lo que nos servirá para ver qué nos puede dar Inherited Resources.
Nuestra aplicación de comercio electrónico está escrita con Rails 3 por lo que añadiremos Inherited Resources a nuestra aplicación incluyendo la gema en el GemFile
.
source 'http://rubygems.org' gem 'rails', '3.0.0' gem 'sqlite3-ruby', :require => 'sqlite3' gem 'nifty-generators' gem 'inherited_resources'
Y la instalaremos, como es habitual, con
$ bundle install
Este comando instalará la gema junto a un par de dependencias: has_scope
y responders
.
Con las gemas instaladas ya podemos actualizar ProductsController
para utilizar Inherited Resources. Para ello hacemos que el controlador herede de InheritedResources::Base
en ApplicationController
. InheritedResources::Base
hereda de ApplicationController
por lo que tiene toda su funcionalidad.
Dado que el controlador ProductsController
es un controlador REST normal podemos reemplazar sus métodos con el código heredado de Inherited Resources, con lo que el controlador queda mucho más escueto.
class ProductsController < InheritedResources::Base end
Tendremos que reiniciar el servidor para que se carguen las nuevas gemas, una vez hecho esto veremos que las páginas relacionadas con productos funcionan exactamente igual que antes. Podemos incluso crear un nuevo producto y veremos que al hacerlo se muestra el mensaje flash correspondiente
Personalización de acciones
Cuando creamos antes un nuevo producto fuimos redirigidos a la página show
de dicho producto, ¿cómo hacer que en lugar de eso la página nos lleve a la acción index
? Con Inherited Resources podemos puentear cualquiera de las acciones por defecto simplemente redefiniendo el método correspondiente del controlador, por lo que podemos escribir un método create
en ProductsController
que cree el nuevo producto y luego haga la redirección a la acción index
.
No tenemos, sin embargo, que reescribir por completo la acción create
sólo para cambiar la redirección. Podemos incluir el comportamiento de Inherited Resources simplemente llamando a create!
pero pasándole un bloque. Como cambiar la URL a la que se realizar la redirección tras crear un nuevo objeto es algo tan común, podemos simplemente devolver la URL que queramos en el bloque.
class ProductsController < InheritedResources::Base def create create! { products_path } end end
En el bloque podemos hacer más cosas que podemos consultar en la documentación.
<o>Cuando ahora creemos un nuevo producto seremos llevados a la acciónindex
tal y como queríamos.</o>
Soporte de múltiples formatos
Es fácil hacer que nuestro controlador sea capaz de responder a diferentes formatos (por ejemplo para trabajar con XML además de HTML). Lo único que hay que hacer es añadir respond_to
como haríamos con cualquier otro controlador de Rails 3.
class ProductsController < InheritedResources::Base respond_to :html, :xml def create create! { products_path } end end
Esto funciona exactamente igual que en el episodio 224 [verlo, leerlo], si visitamos /products.xml
recibiremos el listado de productos en formato XML.
Recursos anidados
Ahora que ProductsController
ha quedado más organizado pasemos a ReviewsController
. Las reseñas están anidadas dentro de los productos, lo que quiere decir que las reseñas de un producto con id
igual a 1
estarán en la URL /products/1/review
, que se corresponderá con la acción index
de ReviewsController
. De la misma manera, si queremos añadir una reseña, ésta deberá estar anidada dentro de un producto.
El código de ReviewsController
es así:
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 diferencia obvia entre este controlador y ProductsController
es que en este sólo tenemos tres en lugar de las siete acciones REST estándar. La otra diferencia es que, como tiene que gestionar el anidamiento, cada acción recibe el producto a partir de un parámetro de la URL.
Aunque estemos tratando con un recurso anidado aquí el comportamiento de Inherited Resources es fundamentalmente el mismo que en ProductsController
. Podemos eliminar el código existente en el controlador y cambiar la clase para que herede de InheritedResources::Base
. Lo único que hay que hacer para controlar el anidamiento es utilizar belongs_to
, que es un método proporcionado por Inherited Resources para definir relaciones entre los controladores de la misma manera que se definen entre modelos con lo que Inherited Resources gestionará por nosotros la recuperación del producto correcto sobre el que se definen las reseñas.
class ReviewsController < InheritedResources::Base belongs_to :product end
Tal y como está el controlador ReviewsController
tendrá las siete famosas acciones REST dado que se trata del comportamiento por defecto de Inherited Resources pero queremos que el controlador tan sólo responda a las acciones index
, new
y create
. Podemos utilizar el método actions
para restringir las acciones disponibles:
class ReviewsController < InheritedResources::Base belongs_to :product actions :index, :new, :create end
Al igual que hicimos en ProducsController
queremos cambiar la URL a la que se nos redirige tras crear una nueva reseña. Dado que en nuestro caso se trata de un recurso anidado podremos usar uno de los métodos de ayuda que ofrece Inherited Resources para generar URLs de recursos. En este caso escogeremos un método llamado collection_url
que nos redirigirá a la acción index
y gestionará el anidamiento por nosotros.
class ReviewsController < InheritedResources::Base belongs_to :product actions :index, :new, :create def create create! { collection_url } end end
Podemos hacer la prueba y añadir una reseña.
Tras el envío de la nueva reseña seremos llevados a la página de reseñas del producto, tal y como queríamos.
Ámbitos públicos
Otra característica útil de Inherited Resources se llama has_scope
. Para usarla tan sólo tenemos que añadir una referencia a su gema en el Gemfile
y luego ejecutar 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'
Con esto instalado podemos llamar a has_scope
en cualquiera de nuestros controladores y pasar el nombre de un ámbito sobre el modelo relacionado. En nuestro ejemplo añadiremos el ámbito limit
, que Rails 3 proporciona en todos los modelos, a ProductsController
.
class ProductsController < InheritedResources::Base respond_to :html, :xml has_scope :limit def create create! { products_path } end end
Con esto podemos añadir ámbitos como párametros a la URL por lo que si pasamos el parámetro limit
se invocará dicho ámbito y se restringirá el número de productos mostrados.
Si queremos que siempre se aplique un ámbito sin necesidad de hacer referencia en la URL de la petición, podemos pasar un valor por defecto.
class ProductsController < InheritedResources::Base respond_to :html, :xml has_scope :limit, :default => 3 def create create! { products_path } end end
Si ahora no pasamos el parámetro limit
se utilizará el valor por defecto y veremos tres productos.
Por supuesto esto también funciona con el resto de ámbitos de la aplicación. Añadamos un ámbito al modelo Review
para filtrar reseñas según su valoración.
class Review < ActiveRecord::Base belongs_to :product scope :rating, proc { |rating| where(:rating => rating) } end
Ahora haremos que dicho ámbito sea público añadiéndolo a ReviewsController
.
class ReviewsController < InheritedResources::Base belongs_to :product actions :index, :new, :create has_scope :rating def create create! { collection_url } end end
Con esto ya podemos usar un parámetro de valoración en la URL para restringir a las reseñas que tengan esa valoración.
La gema has_scope
también se puede usar de forma independiente de Inherited Resources utilizando el método apply_scopes
en la acción index
. Hay más detalles en la documentación en Github.
Personalización del mensaje flash
Terminaremos este episodio viendo cómo personalizar los mensajes flash. Cuando se crea una nueva reseña el mensaje por defecto es “Review was successfully created.” pero podemos cambiarlo a lo que queramos modificando los ficheros de internacionalización. Estos archivos son un excelente lugar para almacenar cadenas que se mostrarán en la interfaz de usuario aunque nuestra aplicación no esté dando soporte a múltiples lenguajes. Todas las aplicaciones Rails 3 vienen con un fichero de internacionalización en inglés en /config/locales/en.yml
.
Para modificar los mensajes flash por defecto de Inherited Resources crearemos una clave flash:
bajo la que tendremos otra clave que contendrá el nombre del controlador (en nuestro caso reviews
) y en la que anidaremos una clave para la acción y dentro de esta otra para el nombre del mensaje de flash. Para nuestro controlador de reseñas el fichero de configuración quedará así:
# 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!"
Si no queremos tener que configurar esto en todos los controladores de nuestra aplicación podemos cambiar el nombre del controlador por actions:
para aplicar dichos mensajes a todos los controladores, teniendo en cuenta que la cadena resource_name
será reemplazada por el nombre del modelo.
# 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!"
Para probarlo crearemos una nueva reseña. Al enviarla se mostrará el mensaje flash personalizado.
Con esto concluimos el episodio. Si acabamos escribiendo una y otra vez el mismo código en nuestros controladores nos interesará considerar el uso de Inherited Resources. El fichero README es bastante completo y cubre casos que no hemos cubierto aquí. La página del wiki también es interesante.