homeASCIIcasts

224: Controladores en Rails 3 

(view original Railscast)

Other translations: En Pt De It

Other formats:

Written by Juan Lupión

A pesar de que la mayor parte de los cambios en los controladores de Rails 3 han tenido lugar entre bambalinas, sí que hay algunas novedades en la interfaz con la que como desarrolladores en Rails vamos a tener que trabajar cada día. En este episodio repasaremos parte de esta nueva funcionalidad.

Filtrado de parámetros

El primer cambio que veremos ha ocurrido en ApplicationController. En una aplicación Rails 2 podíamos filtrar algunos de los parámetros para evitar que aparezcan en claro en la traza de nuestra aplicación con el método filter_parameter_logging, como se muestra aquí:

/app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  protect_from_forgery
  filter_parameter_logging :password
end

Este método ya no existe en Rails 3, por lo que deberíamos eliminarlo. El filtrado de parámetros ahora se configura en /config/application.rb. Al final del archivo aparece el código que configura los parámetros que se deberían filtrar.

/config/application.rb

# Configure sensitive parameters which will be filtered from the log file.
config.filter_parameters += [:password]

La línea anterior viene por defecto en cualquier aplicación nueva de Rails 3. Esto es interesante porque resulta bastante común olvidarse de añadir este filtrado. Por supuesto, si lo dejamos tal cual sólo filtrará los parámetros de claves y tendremos que ampliarla línea anterior si queremos filtrar otros parámetros tales como números de tarjetas de crédito.

Atajos en las redirecciones

A continuación veremos el código de ProductsController en una aplicación de tienda que hemos escrito y cómo se controlan las redirecciones cuando se invoca la acción create o update en una aplicación Rails típica que tiene los siete métodos REST. Si miramos el código del método create veremos que una vez que se ha guardado un Product válido la acción establece un mensaje de flash y luego redirige a la acción show del producto recién creado.

/app/controllers/products_controller.rb

def create
  @product = Product.new(params[:product])
  if @product.save
    flash[:notice] = "Successfully created product."
    redirect_to @product
  else
    render :action => 'new'
  end
end

Se trata de un comportamiento muy común en los controladores por lo que en Rails 3 podemos combinar las dos líneas de forma que el mensaje flash se convierte en un parámetro del método redirect_to.

/app/controllers/products_controller.rb

def create
  @product = Product.new(params[:product])
  if @product.save
    redirect_to @product, :notice => "Successfully created product."
  else
    render :action => 'new'
  end
end

También podemos poner :notice o :alert (si queremos usar otro tipo de mensajetes tendremos que asignarlo como antes el hash de flash). Esta funcionalidad también ha sido añadida a Rails 2.3.6 por lo que si usamos la última versión de Rails 2 también podremos utilizar este tipo de atajos.

Otro atajo nuevo consiste en que el método redirect_to puede aceptar un objeto de modelo como parámetro. Por ejemplo, pasar una instancia de Product (@product en nuestro caso) es equivalente a utilizar product_path(@project) y se traduce en la ruta de la acción show de dicho producto. Si quisiésemos redirigir a la página de edición de dicho producto anteriormente usaríamos edit_product_path(@product), pero ahora podemos usar otro atajo, en lugar de escribir

redirect_to edit_product_path(@product)

podemos escribir:

redirect_to [:edit, @product]

Esto se traducirá a la misma ruta de edición de productos que antes, pero con menos código (esto funcionará también, al igual que la opción :notice anteriormente descrita, en la última versión de Rails 2). Este atajo también funciona con recursos anidados por lo que si un producto pertenece a una categoría podemos hacer la redirección a:

redirect_to [@category, @product]

Este nuevo parámetro en forma de lista funciona en cualquier sitio donde podamos generar una URL y podemos usarlo tanto en los controladores como en nuestra vistas en el método link_to.

Cookies permanentes

Lo siguiente que veremos son las cookies permanentes. Supongamos que cuando un usuario crea un nuevo producto queremos almacenar el id de dicho producto en una cookie. Para eso utilizaríamos el método cookies:

/app/controllers/proucts_controller.rb

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

Este código crea una cookie de sesión que perdurará sólo durante el tiempo que el usuario tenga su navegador abierto. Si queremos crear una cookie con una fecha explícita de expiración con Rails 2 tenemos que convertir el valor en un hash y establecer un tiempo de expiración así:

cookies[:last_product_id] = { :value => @product_id, :expires => 1.month.from_now }

En Rails 3 (o Rails 2.3.6 y superiores) podemos usar el nuevo método cookies.permanent para crear una cookie con un tiempo de expiración lejano.

cookies.permanent[:last_product_id] = @product.id

También pueden crearse con esta técnica cookies firmadas (no entraremos aquí en detalle, hay una gran anotación en el blog de Pratik Naik cuya lectura recomendamos). Básicamente podemos usar el método cookies.permanent.signed para generar una cookie cifrada y leerla más adelante. Nótese que para que esto funcione tenemos que configurar adecuadamente el valor de cookie_verifier_secret en nuestra aplicación.

Uso de respond_with

Lo último que nos queda por repasar en este episodio es posiblemente la más importante novedad en los controladores de Rails 3: respond_with. Vamos a demostrar esta funcionalidad añadiendo un servicio REST sobre una interfaz XML en nuestro controlador ProductsController. Esto quiere decir que cada una de las acciones de dicho controlador podrá devolver una respuesta XML además de su respuesta HTML normal.

En una aplicación Rails 2 lo implementaríamos utilizando el método respond_to en cada acción, así:

/app/controllers/products_controller.rb

def index
  @products = Product.all
  respond_to do |format|
    format.html
    format.xml { render :xml => @products }
  end
end

Dentro del bloque respond_to especificaremos cada uno de los formatos a los que puede responder la acción, mostrando la versión XML del listado de productos si la petición solicita el formato XML, lo que se complica rápidamente si tenemos que hacerlo para todas y cada una de las acciones de un controlador.

En Rails 3 podemos reemplazar este código por una llamada a respond_with pasándole el objeto adecuado, en este caso el array con productos. Para que respond_with funcione también tenemos que usar el método de clase respond_to para especificar los tipos MIME a los que el controlador debería responder de forma que respond_with sepa qué hacer en cada acción.

/app/controllers/products_controller.rb

class ProductsController < ApplicationController
  respond_to :html, :xml
  
  def index
    @products = Product.all
    respond_with @products
    end
  end

  # Other methods
end

Una vez que hemos cambiado ProductsController para que utilice respond_with podremos ver si los cambios han surtido efecto. Si visitamos la página del listado de productos en un navegador veremos la acción index devolviendo, como sería de esperar, HTML.

La página del listado de productos.

Y si en la URL ponemos .xml obtendremos la respuesta en XML.

La página de índice de productos en XML.

¿Cómo funciona el método respond_with? Para una petición GET sencilla como index busca un fichero de vista con el tipo MIME adecuado, por ejemplo .html.erb, y lo mostrará. Si respond_with no es capaz de encontrar esa plantilla intentará llamar al método to_xml o to_json para mostrar el objeto recibido en el formato apropiado.

Sabiendo esto podemos actualizar los métodos show y new en el controlador ProductsController para que funcione con respond_with.

/app/controller/products_controller.rb

def show
  @product = Product.find(params[:id])
  respond_with @product
end

def new
  @product = Product.new
  respond_with @product
end

La acción create es un poco más compleja porque tiene algo de código específico de HTML, por ejemplo invoca redirect_to y render, cosa que no querremos hacer en una respuesta XML. Podemos resolver esto simplemente eliminándolos del método y añadiendo una llamada a respond_with con lo que el método quedaría así:

/app/controllers/products_controller.rb

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

Esto es posible porque respond_with hará un redirección o un render dependiendo de si el objeto que recibe es válido. Con esto podemos añadir respond_with a los otros métodos del controlador elinando todo código que sea específico de HTML. Cuando hayamos acabado nuestro controlador tendrá el siguiente aspecto:

/app/controllers/products_controller.rb

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

El método respond_with puede ser un poco confuso, así que lo resumiremos aquí.

Para una petición GET respond_with en primer lugar buscará una vista que responda a ese formato específico, y si la encuentra la mostrará. Si no, llamará a to_xml (o cualquier formato que se haya solicitado) en el objeto que reciba.

Para cualquier otro tipo de petición respond_with comprueba primero que el objeto recibido tiene errores. Si los tiene entonces se mostrará la vista apropiada (por ejemplo new para la acción create y edit para update). Si no hay errores entonces se redirigirá a la página de dicho objeto (es decir, a la accion show).

Esto se puede probar editando uno de nuestros productos.

Edición de un producto.

Cuando hacemos clic en el botón “Update Product” se invoca la acción update y respond_to ejecutará la redirección a la página de dicho producto.

Se ha actualizado correctamente el producto

De la misma forma, si hacemos clic en el enlace “destroy” acabaremos siendo redirigidos a la página de índice.

respond_with redirige a la página de índice después de borrar un producto

Si el comportamiento por defecto de respond_with no se adapta a ciertas partes de nuestra aplicación se puede personalizar. Por ejemplo, supongamos que cuando actualizamos un producto queremos que la aplicación nos redirija a la acción index en lugar de a la acción show. En este caso, podemos pasar una opción a respond_with de forma que redirija a una acción diferente, por ejemplo para redirigir a la página de índice podemos cambiar la acción update así:

/app/controllers/products_controller.rb

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

También podemos puentear ciertos formatos pasando un bloque a respond_with exactamente igual que haríamos con respond_to. Así que si para el formato XML de la acción edit tan sólo queremos mostrar texto podemos hacerlo:

/app/controllers/products_controller.rb

def edit
  @product = Product.find(params[:id])
  respond_with(@product) do |format|
    format.xml { render :text => "I'm XML!" }
  end
end

Si visitamos la versión XML de la página de edición veremos que sale dicha cadena.

La página de edición ahora muestra nuestro XML personalizado.

Por último si necesitamos personalizar por completo la vista podemos pasar nuestra propia clase respondedora, sobre la que el controlador delegará todo.

respond_with(@product, :responder => MyResponder)

Para inspirarnos a la hora de crear una clase respondedora podemos mirar el código fuente de la clase Responder de Rails 3. El código es bastante fácil de leer y está bien documentado, y por lo menos nos dará una idea de cómo funciona una clase respondedora.

Y con esto acabamos el episodio de hoy. Como hemos visto respond_with es una manera muy cómoda de gestionar múltiples formatos pero si necesitamos un alto grado de personalización nos será más fácil crear nuestra propia clase respondedora.