El mecanismo de seguridad puede ser entendido como un filtro, por el que debe pasar cada petición antes de ejecutar la acción. Según las comprobaciones realizadas en el filtro, se puede modificar el procesamiento de la petición --por ejemplo, cambiando la acción ejecutada (default/secure en lugar de la acción solicitada en el caso del filtro de seguridad). Symfony extiende esta idea a clases de filtros. Se puede especificar cualquier número de clases de filtros a ser ejecutadas antes de que se procese la respuesta, y además hacerlo de forma sistemática para todas las peticiones. Se pueden entender los filtros como una forma de empaquetar cierto código de forma similar a preExecute() y postExecute(), pero a un nivel superior (para toda una aplicación en lugar de para todo un módulo).

6.7.1. La Cadena de Filtros

Symfony de hecho procesa cada petición como una cadena de filtros ejecutados de forma sucesiva. Cuando el framework recibe una petición, se ejecuta el primer filtro (que siempre es sfRenderingFilter). En algún punto, llama al siguiente filtro en la cadena, luego el siguiente, y asi sucesivamente. Cuando se ejecuta el último filtro (que siempre es sfExecutionFilter), los filtros anteriores pueden finalizar, y asi hasta el filtro de sfRenderingFilter. La Figura 6-3 ilustra esta idea con un diagrama de secuencias, utilizando una cadena de filtros simplificada (la cadena real tiene muchos más filtros).

Ejemplo de cadena de filtros

Figura 6.3 Ejemplo de cadena de filtros

Este proceso es la razón de la estructura de la clases de tipo filtro. Todas estas clases extienden la clase sfFilter y contienen un método execute() que espera un objeto de tipo $filterChain como parámetro. En algún punto de este método, el filtro pasa al siguiente filtro en la cadena, llamando a $filterChain->execute(). El listado 6-30 muestra un ejemplo. Por lo tanto, los filtros se dividen en dos partes:

  • El código que se encuentra antes de la llamada a $filterChain->execute() se ejecuta antes de que se ejecute la acción.
  • El código que se encuentra después de la llamada a $filterChain->execute() se ejecuta después de la acción y antes de producir la vista.

Listado 6-30 - Estructura de la clase filtro

class miFiltro extends sfFilter
{
  public function execute ($filterChain)
  {
    // Código que se ejecuta antes de la ejecución de la acción
    ...

    // Ejecutar el siguiente filtro de la cadena
    $filterChain->execute();

    // Código que se ejecuta después de la ejecuciñon de la acción y antes de que se genere la vista
    ...
  }
}

La cadena de filtros por defecto se define en el archivo de configurarcion de la aplicación filters.yml, y su contenido se muestra en el listado 6-31. Este archivo lista los filtros que se ejecutan para cada petición.

Listado 6-31 - Cadena de filtros por defecto, en miaplicacion/config/filters.yml

rendering: ~
web_debug: ~
security:  ~

# Normalmente los filtros propios se insertan aqui

cache:     ~
common:    ~
flash:     ~
execution: ~

Estas declaraciones no tienen parámetros (el caracter tilde, ~, significa null en YAML), porque heredan los parámetros definidos en el núcleo de Symfony. En su núcleo, Symfony define las opciones class y param para cada uno de estos filtros. Por ejemplo, el listado 6-32 muestra los parámetros por defecto para el filtro rendering.

Listado 6-32 - Parámetros por defecto del filtro sfRenderingFilter, en $sf_symfony_data_dir/config/filters.yml

rendering:
  class: sfRenderingFilter   # Clase del filtro
  param:                     # Parámetros del filtro
    type: rendering

Si se deja el valor vacío (~) en el archivo filters.yml de la aplicación, Symfony aplica el filtro con las opciones por defecto definidas en su núcleo.

Se pueden personalizar la cadenas de filtros en varias formas:

  • Desactivando algún filtro de la cadena agregando un parámetro enabled: off. Por ejemplo, para desactivar el filtro de depuración web (web_debug), se añade:
web_debug:
  enabled: off
  • No se deben borrar las entradas del archivo filters.yml para desactivar un filtro ya que Symfony lanzará una excepción.
  • Se pueden añadir declaraciones propias en cualquier lugar de la cadena (normalmente después del filtro security) para agregar un filtro propio (como se verá en la próxima sección). En cualquier caso, el filtro rendering debe ser siempre la primera entrada, y el filtro execution debe ser siempre la ultima entrada en la cadena de filtros.
  • Redefinir la clase y los parámetros por defecto del filtro por defecto (normalmente para modificar el sistema de seguridad y utilizar un filtro de seguridad propio).

Truco El parámetro enabled: off funciona correctamente para desactivar los filtros propios, pero se pueden desactivar los filtros por defecto a través del archivo settings.myl, modificando los valores de las opciones web_debug, use_security, cache, y use_flash. El motivo es que cada uno de los filtros por defecto posee un parámetro condition que comprueba el valor de estas opciones.

6.7.2. Construyendo Tu Propio Filtro

Construir un filtro propio es bastante sencillo. Se debe crear una definición de una clase similar a la demostrada en el listado 6-30, y se coloca en uno de los directorios lib/ del proyecto para aprovechar la carga automática de clases.

Como una acción puede pasar el control o redireccionar hacia otra acción y en consecuencia relanzar toda la cadena de filtros, quizás sea necesario restringir la ejecución de los filtros propios a la primera acción de la petición. El método isFirstCall() de la clase sfFilter retorna un valor booleano con este propósito. Esta llamada solo tiene sentido antes de la ejecución de una acción.

Este concepto se puede entender fácilmente con un ejemplo. El listado 6-33 muestra un filtro utilizado para auto-loguear a los usuarios con una cookie MiSitioWeb, que se supone que se crea en la acción login. Se trata de una forma rudimentaria pero que funciona para incluir la característica Recuérdame de un formulario de login.

Listado 6-33 - Ejemplo de archivo de clase de filtro, en apps/miaplicacion/lib/rememberFilter.class.php

class rememberFilter extends sfFilter
{
  public function execute($filterChain)
  {
    // Ejecutar este filtro solo una vez
    if ($this->isFirstCall())
    {
      // Los filtros no tienen acceso directo a los objetos user y request.
      // Se necesita el contexto para obtenerlos
      $peticion = $this->getContext()->getRequest();
      $usuario  = $this->getContext()->getUser();

      if ($peticion->getCookie('MiSitioWeb'))
      {
        // logueado
        $usuario->setAuthenticated(true);
      }
    }

    // Ejecutar el proximo filtro
    $filterChain->execute();
  }
}

En ocasiones, en lugar de continuar con la ejecución de la cadena de filtros, se necesita pasar el control a una acción específica al final de un filtro. sfFilter no tiene un método forward(), pero sfController si, por lo que simplemente se puede llamar al siguiente método:

return $this->getContext()->getController()->forward('mimodulo', 'miAccion');

Nota La clase sfFilter tiene un método initialize(), ejecutado cuando se crea el objeto filtro. Se puede redefinir en el filtro propio si se necesita trabajar de forma personalizada con los parámetros de los filtros (definidos en filters.yml, como se describe a continuación).

6.7.3. Activación de Filtros y Parámetros

Crear un filtro no es suficiente para activarlo. Se necesita agregar el filtro propio a la cadena, y para eso, se debe declar la clase del filtro en el archivo filters.yml, localizado en el directorio config/de la aplicación o del módulo, como se muestra en el listado 6-34.

Listado 6-34 - Ejemplo de archivo de activación de filtro, en apps/miaplicacion/config/filters.yml

rendering: ~
web_debug: ~
security:  ~

remember:                 # Los filtros requieren un nombre único
  class: rememberFilter
  param:
    cookie_name: MiSitioWeb
    condition:   %APP_ENABLE_REMEMBER_ME%

cache:     ~
common:    ~
flash:     ~
execution: ~

Cuando se encuentra activo, el filtro se ejecuta en cada petición. El archivo de configuración de los filtros puede contener una o más definiciones de parámetros en la sección param. La clase filtro puede obtener estos parámetros con el método getParameter(). El listado 6-35 muestra como obtener los valores de los parámetros.

Listado 6-35 - Obteniendo el valor del parámetro, en apps/miaplicacion/lib/rememberFilter.class.php

class rememberFilter extends sfFilter
{
  public function execute($filterChain)
  {
      ...
      if ($request->getCookie($this->getParameter('cookie_name')))
      ...
  }
}

El parámetro condition se comprueba en la cadena de filtros para ver si el filtro debe ser ejecutado. Por lo que las declaraciones del filtro propio puede basarse en la configuración de la aplicación, como muestra el listado 6-34. El filtro remeber se ejecuta solo si el archivo app.yml incluye lo siguiente:

all:
  enable_remember_me: on

6.7.4. Filtros de Ejemplo

Los filtros son útiles para repetir cierto código en todas las acciones. Por ejemplo, si se utiliza un sistema remoto de estadísticas, puede ser necesario añadir un trozo de código que realice una llamada a un script de las estadísticas en cada página. Este código se puede colocar en el layout global, pero entonces estaría activo para toda la aplicación. Otra forma es colocarlo en un filtro, como se muestra el listado 6-36, y activarlo en cada módulo.

Listado 6-36 - Filtro para el sistema de estadísticas de Google Analytics

class sfGoogleAnalyticsFilter extends sfFilter
{
  public function execute($filterChain)
  {
    // No se hace nada antes de la acción
    $filterChain->execute();

    // Decorar la respuesta con el código de Google Analytics
    $codigoGoogle = '
<script src="http://www.google-analytics.com/urchin.js"  type="text/javascript">
</script>
<script type="text/javascript">
      _uacct="UA-'.$this->getParameter('google_id').'";urchinTracker();
</script>';
    $respuesta = $this->getContext()->getResponse();
    $respuesta->setContent(str_ireplace('</body>', $codigoGoogle.'</body>',$respuesta->getContent()));
   }
}

No obstante, este filtro no es perfecto, ya que no se debería añadir el código de Google si la respuesta no es de tipo HTML.

Otro ejemplo es el de un filtro que cambia las peticiones a SSL si no lo son, para hacer más segura la comunicación, como muestra el Listado 6-37.

Listado 6-37 - Filtro de comunicación segura

class sfSecureFilter extends sfFilter
{
  public function execute($filterChain)
  {
    $contexto = $this->getContext();
    $peticion = $context->getRequest();
    if (!$peticion->isSecure())
    {
      $urlSegura = str_replace('http', 'https', $peticion->getUri());
      return $contexto->getController()->redirect($urlSegura);
      // No se continúa con la cadena de filtros
    }
    else
    {
      // La petición ya es segura, asi que podemos continuar
      $filterChain->execute();
    }
  }
}

Los filtros se utilizan mucho en los plugins, porque permiten extender las características de una aplicación de forma global. El Capítulo 17 incluye más información sobre los plugins, y el wiki del proyecto Symfony (http://trac.symfony-project.org/) también tiene más ejemplos de filtros.