Caution: You are browsing the legacy symfony 1.x part of this website.

Día dieciocho del calendario de symfony: Filtros

1.0
Language

Anteriormente en symfony

Ayer vimos cómo utilizar el servicio de askeet vía una API XML. El programa de hoy se centra en los filtros, y mostraremos su uso con la creación de subdominios para askeet. Por ejemplo, 'php.askket.com' mostrará sólo preguntas con la etiqueta PHP, y todas las preguntas nuevas entradas en este dominio serán etiquetadas con 'php'.. Vamos a llamar esta nueva funcionalidad 'universo askeet' y vamos a desarrollarla enseguida.

Funcionalidad configurable

Primero, esta nueva funcionalidad tiene que ser opcional. Se supone que askeet es un software que se puede instalar con cualquier configuración, y quizá uno no quiera permitir subdominios en, por ejemplo, una Intranet corporativa.

Por lo tanto vamos a añadir un nuevo parámetro en la configuración de la aplicación. Para activar la funcionalidad universo, deberá tener el valor on. Para añadir un parámetro personalizado, abre el fichero askeet/apps/frontend/config/app.yml y añade:

   all:      .global:        universe: on

Este parámetro es ahora disponible para todas las acciones de la aplicación. Para obtener su valor, utiliza la función sfConfig::get('app_universe').

Encontrarás más información acerca de los parámetros personalizados en el capítulo de configuración del libro de symfony.

Crear un filtro

Un filtro es un trozo de código ejecutado antes de cada acción. Tenemos que examinar el nombre de host para todas las acciones en busca de un nombre de etiqueta en el domino.

Los filtros tiene que ser declarados en un fichero de configuración especial para ser ejecutados, el askeet/apps/frontend/config/filters.yml. Este fichero es creado por defecto cuando se inicializa una aplicación y además está vació. Abrelo y añádele:

    myTagFilter:       class: myTagFilter

Eso declara un nuevo filtro llamado myTagFilter. Vamos a crear un fichero de clase llamado myTagFilter.class.php en el directorio askeet/apps/frontend/lib/ para hacerla disponible a toda la aplicación frontend

    [php]     isFirstCall())         {           // hacer algo         }              // ejecuta siguiente filtro         $filterChain->execute();       }     }          ?>

Ésta es la estructura general de un filtro. Si el parámetro app_universe no tiene el valor on, el filtro no se va a ejecutar. Como queremos que se ejecute sólo una vez por petición (aunque probablemente haya más de una acción por llamada, porque usamos forwards), comprobamos el método ->isFirstCall(). Si es verdadero el filtro sólo se ejecutará una vez en una llamada.

Una cosa acerca del objeto filterChain: Todos los pasos de ejecución de una petición (configuración, controlador frontal, acción, vista) son una cadena de filtros. Un filtro personalizado entra pronto en esta cadena (antes de la ejecución de una acción), y no tiene que romper la ejecución de los otros pasos de la cadena de filtros. Es por eso que un filtro personalizado siempre tiene que acabar con el método $filterChain->execute();.

note

La clase sfFilter tiene un método initialize(), ejecutado cuando un objeto filtro es creado. Puedes sobreescribirlo en tu filtro personalizado si necesitas tratar con tus propios parámetros de filtrado.

Obtener una etiqueta permanente del nombre de dominio

Queremos inspeccionar el nombre del host para ver si contiene un subdominio que pueda ser una etiqueta. Etiquetas como 'www' o 'askeet' tendrán que ser ignoradas. Además, queremos que nos sea posible modificar las reglas de los subdominios a ignorar, por si usamos técnicas de distribución de carga con nombres de dominio alternativos como 'www1', 'www2', etc. Para eso hemos decidido poner las reglas de los universos a ignorar (expresiones regulares) en una parámetro del fichero de configuración filters.yml:

    myTagFilter:       class: myTagFilter       param:         host_exclude_regex: /^(www|askeet)/      Ahora es el momento de mirar el contenido de la acción del filtro execute() (reemplazando el comentario // hacer algo):

    [php]     // hay alguna etiqueta en el nombre de host?     $hostname = $this->getContext()->getRequest()->getHost();     if (!preg_match($this->getParameter('host_exclude_regex'), $hostname) && $pos = strpos($hostname, '.'))     {       $tag = Tag::normalize(substr($hostname, 0, $pos));

      // añadir una etiqueta permanente a un parámetro de configuración personalizado       sfConfig::set('app_permanent_tag', $tag);

      // añadir hoja de estilo personalizada       $this->getContext()->getResponse()->addStylesheet($tag);     }      El filtro mira si encuentra una etiqueta permanente en el URI. Si se encuentra, se añade como parámetro personalizado y una hoja de estilos personalizada se añade a la vista. Por tanto:

    // acceder a esta URI para mostrar el universo PHP     http://php.askeet.com

    // crear constante     sfConfig::set('app_permanent_tag', 'php');

    // añadir hoja de estilo personalizada en la vista      

note

Como la ejecución de un filtro personalizado ocurre muy pronto en la cadena de filtrado, y aún más pronto en el parsing de la configuración de la vista, la hoja de estilos personalizada aparecerá en el fichero HTML resultante antes que las otras hojas de estilo. Por lo tanto si tienes que sobreescribir algún valor de un estilo de la hoja de estilos principal de askeet, estos valores se tienen que declarar !important.

Modificaciones del Modelo

Ahora tenemos que modificar los métodos de las acciones y el modelo para tengan en cuenta las etiquetas permanentes. Como queremos mantener la lógica del modelo en la capa del Modelo y porque además 'refactorizar' es realmente necesario, nos ayudamos de los cambios de las etiquetas permanentes para sacar las peticiones a Propel de las acciones y ponerlas en el modelo. Si hechas un vistazo a la lista de modificaciones de la entrega de hoy en el trac de askeet, verás que se han creado unos cuantos métodos nuevos del modelo y que las acciones llaman a esos métodos en vez de hacer doSelect() por ellas mismas:

    [php]     Answer->getRecent()     Question->getPopularAnswers()     QuestionPeer::getPopular()     QuestionPeer::getRecent()     QuestionTagPeer::getForUserLike()

Lista de filtrado según las etiquetas permanentes

Cuando una lista de preguntas, etiquetas o respuestas son mostradas en un universo askeet, todas las peticiones tienen que tener en cuenta un nuevo parámetro de búsqueda. En symfony, los parámetros de búsqueda son llamadas a los métodos ->add()del objeto Criteria.

Por lo tanto añade el siguiente método a las clases QuestionPeer y AnswerPeer:

    [php]     private static function addPermanentTagToCriteria($criteria)     {       if (sfConfig::get('app_permanent_tag'))       {         $criteria->addJoin(self::ID, QuestionTagPeer::QUESTION_ID, Criteria::LEFT_JOIN);         $criteria->add(QuestionTagPeer::NORMALIZED_TAG, sfConfig::get('app_permanent_tag'));         $criteria->setDistinct();       }              return $criteria;     }  

Ahora necesitamos comprobar todos los métodos del modelo que devuelven una lista que tiene que estar filtrada en un universo y añadir en la definición del Criteria la siguiente línea:

    [php]     $c = self::addPermanentTagToCriteria($c);

Por ejemplo, la QuestionPeer:getHomepagePager() tiene que parecerse a:

    [php]     public static function getHomepagePager($page)     {       $pager = new sfPropelPager('Question', sfConfig::get('app_pager_homepage_max'));       $c = new Criteria();       $c->addDescendingOrderByColumn(self::INTERESTED_USERS);              // añadir esta línea       $c = self::addPermanentTagToCriteria($c);              $pager->setCriteria($c);       $pager->setPage($page);       $pager->setPeerMethod('doSelectJoinUser');       $pager->init();            return $pager;     }   La misma modificación tiene que repetirse unas cuantas veces en los siguientes métodos:

    [php]     QuestionPeer::getHomepagePager()     QuestionPeer::getPopular()     QuestionPeer::getPopular()     QuestionPeer::getRecentPager()     QuestionPeer::getRecent()     AnswerPeer::getPager()     AnswerPeer::getRecentPager()     AnswerPeer::getRecent()

Para peticiones complejas que no usan el objeto Criteria necesitamos añadir etiquetas permanentes como condiciones WHERE en el código SQL. Mira como lo hicimos con los métodos QuestionTagPeer::getPopularTags()QuestionTagPeer:getPopularTagsFor() en el trac de askeet.

Lista de etiquetas de una pregunta o un usuario

Todas las preguntas del universo 'PHP' son etiquetadas con 'php'. Pero si un usuario está navegando por las preguntas del universo 'PHP', la etiqueta 'php' no debe de mostrarse en la lista de etiquetas ya que se presupone. Cuando se muestra una lista de etiquetas para una pregunta o un usuario en un universo, la etiqueta permanente debe de ser omitida. Eso puede hacerse fácilmente haciéndolas pasar por un bucle, como por ejemplo en el método Question-getTags():

    [php]     public function getTags()     {       $c = new Criteria();       $c->add(QuestionTagPeer::QUESTION_ID, $this->getId());       $c->addGroupByColumn(QuestionTagPeer::NORMALIZED_TAG);       $c->setDistinct();       $c->addAscendingOrderByColumn(QuestionTagPeer::NORMALIZED_TAG);            $tags = array();       foreach (QuestionTagPeer::doSelect($c) as $tag)       {         if (sfConfig::get('app_permanent_tag') == $tag)         {           continue;         }              $tags[] = $tag->getNormalizedTag();       }            return $tags;     }

La misma técnica es usada en los siguientes métodos:

    [php]     Question->getTags()     Question->getPopularTags()     User->getTagsFor()     User->getPopularTags()

Añadir la lista de etiquetas permanentes a una pregunta nueva

Cuando una pregunta es creada en un universo askeet, debe de etiquetarse con la etiqueta permanente además de las etiquetas elegidas por el usuario. Como recordatorio, en el método question/add, el método Question->addTagsForUser() es llamado:

    [php]     $question->addTagsForUser($this->getRequestParameter('tag'), $sf_user->getId());

...dónde el parámetro pedido, tag, contiene las etiquetas introducidas por el usuario separadas por espacios en blanco (a eso se le llama 'frase'). Por lo tanto sólo tenemos que añadir la etiqueta permanente a la frase en la primera línea del método addTagsForUser:

    [php]     public function addTagsForUser($phrase, $userId)     {       // separar la frase en etiquetas individuales       $tags = Tag::splitPhrase($phrase.(sfConfig::get('app_permanent_tag') ? ' '.sfConfig::get('app_permanent_tag') : ''));            // añadir etiquetas       foreach ($tags as $tag)       {         $questionTag = new QuestionTag();         $questionTag->setQuestionId($this->getId());         $questionTag->setUserId($userId);         $questionTag->setTag($tag);         $questionTag->save();       }     }

Eso es: si el usuario no ha añadido la etiqueta permanente, se añade a la lista de etiquetas para la nueva pregunta.

Configuración del servidor

Para conseguir hacer disponibles los nuevos dominios, tienes que modificar la configuración de tu servidor web.

En local, i.e. si no tienes control de la entrada DNS de tu sitio askeet, añade un nuevo host por cada universo que quieras añadir (en el fichero /etc/hosts en Linux, o en el fichero C:\WINDOWS\system32\drivers\etc\hosts en Windows):

    127.0.0.1         php.askeet     127.0.0.1         senseoflife.askeet     127.0.0.1         women.askeet

note

Necesitas permisos de administrador para hacer esto

En todos los casos, necesitas añadir un servidor alias en la configuración del virtual host (en el fichero de configuración de Apache httpd.conf):

          ServerName askeet       ServerAlias *.askeet       DocumentRoot "/home/sfprojects/askeet/web"       DirectoryIndex index.php       Alias /sf /usr/local/lib/php/data/symfony/web/sf                   AllowOverride All          

Después de reiniciar el servidor web, puedes probar si los universos funcionan, por ejemplo:

    http://php.askeet/    

Nos vemos mañana

Los filtros tienen mucha potencia y pueden ser usados para todo tipo de cosas. Las etiquetas nos han permitido personalizar el contenido según un tema específico. Combinar etiquetas y filtros nos ha ayudado a dividir askeet en varios universos, y las posibilidades de tener sitios askeet especializados (por ejemplo music.askeet.com, programming.askeet.com o hazlotumismo.askeet.com) son interminables. Como todos los sitios pueden tener un aspecto distinto, y además el contenido de los sitios especializados también saldrá en el sito global de askeet, se obtiene lo mejor de las aplicaciones web basadas en las comunidades de usuarios.

Mañana nos concentraremos en el rendimiento y veremos como la cache HTML puede acelerar el tiempo de entrega de páginas complejas. En tres días sabremos la misteriosa funcionalidad, todavía tienes tiempo para votar la mejor idea. Puedes visitar el fórum de askeet y ver cómo el sitio web de askeet se comporta online.