You are on page 1of 273

Introduccin

El framework Symfony ha sido un proyecto de Cdigo Abierto por ms de cuatro aos y se ha convertido en uno de los framework PHP ms populares gracias a sus excelentes caractersticas y gran documentacin. Esto ha sido una gran tradicin desde sus inicios. Este Libro describe la creacin de una aplicacin webcon el framework, symfony paso a paso desde las especificaciones hasta la implementacin. Este esta dirijido a principaintes quienes desean aprender symfony, entender como funciona, y tambin sobre las mejores prcticas del desarrollo web. La aplicacin a disear podra haber sido otro motor de blogs, pero queremos usar Symfony en algo ms til. El objetivo es demostrar que Symfony puede ser usado para desarrollar aplicaciones con estilo y poco esfuerzo. Vamos a mantener el contenido del proyecto en secreto por un das ms ya que tenemos mucho para hacer por hoy. De todas formas, ya sabes el nombre de la aplicacin: Jobeet. Cada captulo de este ibro est destinado a durar entre una a dos horas, y ser la ocasin de aprender Symfony mediante la codificacin de un sitio web real, de principio a fin. Cada da, sern agregadas nuevas caractersticas a la aplicacin, y nos valdremos de las ventajas de este desarrollo para introducirte a las nuevas funcionalidades de Symfony as como a las mejores prcticas de desarrollo web con Symfony.

Este Libro es diferente


Recuerdo los primeros das de PHP4 Ah, la Belle Epoque! PHP era uno de los primeros lenguages dedicados a la web y uno de los ms fciles de aprender. Pero con la rpida evolucin de las tecnologas web, los desarrolladores web deben mantenerse al tanto de las ltimas herramientas y mejores prcticas de desarrollo. Por supuesto que la mejor manera de aprender es leyendo blogs, tutoriales y libros. Hemos ledo un montn de estos, sea que estn escritos para PHP, Python, Java, Ruby o Perl, y muchos se quedan cortos cuando el autor comienza a mostrar fragmentos de cdigo como ejemplos. Probablemente ests acostumbrado a leer adevertencias de este tipo: "Para una aplicacin real, no te olvides de agregar validacin y un manejo de errores adecuado." o "La Seguridad es dejada como un ejercicio para el lector." o "Por supuesto que vas a necesitar escribir tests"

Cmo? Estos temas son asuntos importantes. Son quizs la parte ms importante de cualquier trozo de cdigo. Como lector, te dejan solo. Si no nos preocupamos por proveer dicha informacin, los ejemplos resultan mucho menos tiles, no pudiendo utilizarlos como un buen punto de inicio. Eso no puede ser as, ya que la seguridad, la validacin, el manejo de errores y las tests, por nombrar algunos, es lo que nos lleva a programar de forma correcta. En este libro nunca vas a ver oraciones de ese tipo, vamos a escribir tests, manejo de errores, cdigo para validacin, y nos aseguraremos de desarrollar una aplicacin segura. Todo ello porque Symfony es sinnimo de programacin, de buenas prcticas y de como desarrollar aplicaciones profesionales para la empresa. Podemos darnos dicho lujo porque Symfony provee todas las herramientas necesarias para programar esos aspectos de forma fcil y sin la necesidad de escribir mucho cdigo. Debido a que la validacin, el manejo de errores, la seguridad y los tests son ciudadanos de primera clase en Symfony, no nos va a llevar mucho tiempo explicarlos. Esta es una de las tantas razones por las cuales hay que utilizar un framework para proyectos de la vida real. Todo el cdigo que leers en este libro es cdigo que puedes usar en un proyecto de la vida real. Te invitamos a que copies y pegues el cdigo en tus proyectos o que simplemente robes trozos completos del mismo.

Qu hacemos hoy?
Hoy no vamos a escribir cdigo PHP. Pero incluso sin escribir una sola linea de cdigo vas a entender los beneficios de utilizar un framework como Symfony simplemente al configurar un nuevo proyecto. El objetivo de este captulo es configurar un entorno de desarrollo y mostrar una pgina web de la aplicacin en el navegador web. Esto incluye la instalacin de Symfony, la creacin de una aplicacin y la configuracin del servidor web.

Pre requisitos.
Primero que nada, asegrate de que cuentas con un entorno de desarrollo web funcionando: un servidor web (Apache por ejemplo), un motor de bases de datos (MySQL, PostgreSQL, o SQLite), y PHP 5.2.4 o superior. Como utilizaremos la lnea de comandos constantemente, es preferible utilizar un sistema operativo tipo Unix, si utilizas Windows, va a funcionar de todas formas, simplemente vas a tener que ingresar comandos en la consola cmd.
Los comandos de Unix pueden serte tiles dentro de un entorno Windows. Si quisieras utilizar herramientas como tar, gzip, or grepen Windows puedes instalar Cygwin. La documentacin oficial puede ser un poco escasa pero una gua interesante puede encontrarseaqu. Los aventureros tambin pueden probar con los Servicios Windows para Unix. de Microsoft.

Como este libro se concentra en el framework Symfony, asumimos que ya cuentas con un conocimiento slido de PHP 5 y de Programacin Orientada a Objetos.

Instalacin de Symfony
Primero crea un directorio donde albergar los archivos relacionados al proyecto Jobeet:
$ mkdir -p /home/sfprojects/jobeet $ cd /home/sfprojects/jobeet

En Windows:
c:\> mkdir c:\development\sfprojects\jobeet c:\> cd c:\development\sfprojects\jobeet Se recomienda que los usuarios Windows configuren sus proyectos en una ruta sin espacios. Intenta no utilizar el directorioDocuments and Settings, incluyendo cualquier ruta bajo Mis Documentos.

Crea un directorio para alojar los archivos del framework Symfony:


$ mkdir -p lib/vendor

La pgina de instalacin en el sitio web de Symfony listas y compara todas las versiones disponibles de Symfony. Ya que este libro fue escrito para Symfony 1.4, ve a la pgina de instalacin de Symfony 1.4. Bajo la seccin "Source Download", vas a encontrar el archivo en formato .tgz o en formato .zip. Descarga el archivo y colocarlo bajo el directorio lib/vendor que acabas de crear y descomprimelo:
$ $ $ $ cd lib/vendor tar zxpf symfony-1.4.0.tgz mv symfony-1.4.0 symfony rm symfony-1.4.0.tgz

En Windows puedes descomprimirlo en el explorador de archivos. Una vez que hayas renombrado el directorio a symfony, la ruta debera ser la siguiente: c:\development\sfprojects\jobeet\lib\vendor\symfony. Como las configuraciones PHP varan mucho de una distribucin a otra, tenemos que comprobar que tu configuracin PHP cumple los requisitos mnimos de Symfony. Inicia el script de comprobacin de la configuracin que viene con Symfony desde la lnea de comandos:
$ cd ../.. $ php lib/vendor/symfony/data/bin/check_configuration.php

Si hay un problema, la salida te dar consejos sobre cmo solucionarlo. Tambin debes ejecutar el script desde un navegador ya que la configuracin PHP puede ser diferente. Copia el archivo en algn lugar bajo el directorio raz del servidor web y accede al archivo. No te olvides de quitar el archivo del directorio raz web despus:
$ rm web/check_configuration.php

Si el script no muestra ningn error, comprueba que Symfony se ha instalado correctamente usando la lnea de comandos para mostrar la versin (nota la letra V mayscula):
$ php lib/vendor/symfony/data/bin/symfony -V

En Windows:
c:\> cd ..\.. c:\> php lib\vendor\symfony\data\bin\symfony -V

Si eres curioso sobre lo que esta herramienta de lnea de comandos puede hacer por t, escribe symfony para ver una lista de las opciones y las tareas disponibles:
$ php lib/vendor/symfony/data/bin/symfony

En Windows:
c:\> php lib\vendor\symfony\data\bin\symfony

La lnea de comandos de Symfony es la mejor amiga del programador. Te brinda un montn de utilidades para aumentar tu productividad en las actividades del da a da como limpiar el cache, generar cdigo y mucho ms.

Configuracin del Proyecto


En Symfony, las aplicaciones que comparten el mismo modelo de datos se agrupan en proyectos. Para el proyecto Jobeet, vamos a tener dos aplicaciones diferentes: un frontend y un backend. Creacin del Proyecto Desde el directorio jobeet, ejecuta la tarea symfony generate:project para realmente crear el proyecto symfony:
$ php lib/vendor/symfony/data/bin/symfony generate:project jobeet

En Windows:
c:\> php lib\vendor\symfony\data\bin\symfony generate:project jobeet

La tarea generate:project genera por defecto la estructura de directorios y archivos necesarios para un proyecto symfony: Directorio Descripcin apps/ cache/ config/ lib/ log/ plugins/ test/ web/ Hospeda todas las aplicaciones del proyecto Los archivos en cach Los archivos de configuracin del proyecto Las bibliotecas y clases del proyecto Los archivos de registro Los plugins instalados Los archivos de pruebas unitarias y funcionales El directorio raz web (vase ms adelante)

Por qu Symfony genera tantos archivos? Uno de los principales beneficios de la utilizacin de un completo framework es normalizar tus desarrollos. Gracias a estructura predeterminada de archivos y directorios de Symfony, cualquier desarrollador con algunos conocimientos de Symfony puede asumir el mantenimiento de cualquier proyecto symfony. En cuestin de minutos, ser capaz de bucear en el cdigo, corregir los errores, y aadir nuevas funciones.

La tarea generate:project tambin ha creado un atajo symfony en el directorio raz del proyecto Jobeet para reducir el nmero de caracteres que tienes que escribir cuando se ejecuta una tarea. As, a partir de ahora, en lugar de utilizar la ruta completa al programa de Symfony, vamos a utilizar el atajo symfony. Creacin de una Aplicacin Ahora, crear la aplicacin frontend ejecutando la tarea generate:app:
$ php symfony generate:app frontend Como el archivo symfony es ejecutable, los usuarios de Unix puede reemplazar todas las apariciones de 'php symfony' por './symfony' de ahora en adelante. En Windows puedes copiar el archivo 'symfony.bat' a tu proyecto y usar 'symfony' en lugar de 'php symfony': c:\> copy lib\vendor\symfony\data\bin\symfony.bat .

Una vez ms, la tarea generate:app crea por defecto la estructura necesaria de directorios para una aplicacin bajo el directorioapps/frontend:

Directorio Descripcin config/ lib/ modules/ templates/ Los archivos de configuracin de la aplicacin Las bibliotecas y clases de la aplicacin El cdigo de la aplicacin (MVC) La plantilla global

Todos los comandos symfony debe ser ejecutados en el directorio raz del proyecto a menos que se diga expresamente otra cosa. Security Por defecto, la tarea generate:app ha asegurado nuestro futuro desarrollo de las dos vulnerabilidades ms extendidas se encuentran en la web. As es, Symfony automticamente toma las medidas de seguridad por nosotros. Para evitar ataques XSS, el escapar la salida ha sido habilitado; y para prevenir ataque CSRF, un aleatorio y secreto CSRF ha sido generado. Por supuesto, puedes establecer estas configuraciones gracias a las siguientes opciones: --escaping-strategy: Permite escapar la salida para evitar ataques XSS --csrf-secret: Permite tokens de sesin en los formularios para prevenir los ataques CSRF Si no sabes nada acerca de XSS o CSRF, date un tiempo para aprender ms de estas vulnerabilidades de seguridad.

La Ruta de Symfony Puede obtener la versin utilizada de Symfony por su proyecto escribiendo:
$ php symfony -V

La opcin -V tambin muestra la ruta de acceso al directorio de instalacin de Symfony, se almacena enconfig/ProjectConfiguration.class.php:
// config/ProjectConfiguration.class.php require_once '/Users/fabien/work/symfony/dev/1.2/lib/autoload/sfCoreAutoload.class.php ';

Para mejorar la portabilidad, cambia la ruta absoluta a la instalacin de Symfony por una relativa:
// config/ProjectConfiguration.class.php require_once dirname(__FILE__).'/../lib/vendor/symfony/lib/autoload/sfCoreAutoload.cla ss.php';

De esta manera, puedes mover el directorio del proyecto Jobeet a cualquier lugar de tu mquina u a otra, y ste funcionar.
Autoloading La clase sfCoreAutoload es un autoloader|Autoload~ de PHP usado para cargar el nuclo de clases de Symfony a pedido (on demand). Por defecto, Symfony tambin registra otros autoloader para automticamente cargar las clases guardadas en el directorio lib/. Esto significa que nunca necesitars usar un require en tu cdigo. Este es uno de las numerosas cosas que Symfony automatiza por el desarrollador.

Los Entornos
Si revisas el directorio web/, encontrars dos archivos PHP: index.php y frontend_dev.php. Estos archivos son llamados controladores frontales: todas las peticiones a la aplicacin se hacen a travs de ellos. Pero, por qu tenemos dos controladores frontales si hemos definido slo una aplicacin? Ambos archivos apuntan a la misma aplicacin pero para distintos entornos. Cuando desarrollas una aplicacin, excepto si desarrollas directamente en el servidor de produccin, necesitas varios entornos:

El entorno de desarrollo: Este es el ambiente utilizado por desarrolladores web para aadir nuevas funciones, corregir los errores, ... El entorno de prueba: Este entorno se utiliza para probar automticamente la aplicacin. El entorno staging: Este entorno es utilizado por el cliente para poner a prueba la aplicacin e informar errores o caractersticas faltantes. El entorno de produccin: Este es el entorno donde un usuario final interacta.

Qu hace que un entorno sea nico? En el entorno de desarrollo, la aplicacin necesita registrar todos los detalles de una peticin para facilitar la depuracin, debe mostrar la excepcin en el navegador, pero la cache debe ser deshabilitada para que todos los cambios realizados al cdigo se tengan en cuenta de inmediato. El entorno de desarrollo debe ser optimizado para el desarrollador:

En el entorno de la produccin, la aplicacin deber mostrar mensajes de error personalizados en lugar de excepciones, y por supuesto, la capa del cache debe estar activada. El entorno de produccin debe ser optimizado para el rendimiento y la experiencia del usuario final.

Un entorno symfony es un conjunto nico de ajustes de configuracin. El framework Symfony incluye tres de ellos: dev, test, y prod. En el captulo 21, aprenders como crear nuevos ambientes, tales como el staging. Si abres los archivos de los controladores frontales, vers que la nica diferencia es el ajuste del entorno:
// web/index.php <?php require_once(dirname(__FILE__).'/../config/ProjectConfiguration.class.php '); $configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'prod', false); sfContext::createInstance($configuration)->dispatch(); Definir un nuevo entorno symfony es tan simple como crear un nuevo controlador frontal. Veremos ms adelante cmo cambiar la configuracin de un entorno.

Configuracin del Servidor Web: La Forma Fea


En la seccin anterior, un directorio se ha creado para alojar el proyecto Jobeet. Si lo creaste bajo el directorio "raz web" de tu servidor web, ya puedes acceder al proyecto en un navegador web. Por supuesto, como no hay ninguna configuracin, es muy rpido para establecer, pero intenta tener acceso al archivoconfig/databases.yml en tu navegador y compreders las malas consecuencias de esta actitud perezosa. Si el usuario conoce que tu sitio web esta desarrollado con Symfony, l tendr acceso a un montn de archivos delicados.

Configuracin del Servidor Web: La forma segura


Una buena prctica web es poner bajo el directorio raz web slo los archivos a los que necesita tener acceso el navegador web: las hojas de estilo, JavaScripts, o imgenes. Te recomendamos almacenar estos archivos en el subdirectorio web de un proyecto symfony. Si echas un vistazo a este directorio, encontrars algunos sub-directorios para los recursos web y los dos archivos de los controladores frontales. Los controladores frontales son los nicos archivos PHP que necesitan estar bajo el directorio raz web. Todos los dems archivos PHP se pueden ocultar del navegador, la cual es una buena idea en lo que respecta a seguridad. La configuracin del Servidor Web Ahora es el momento de cambiar tu configuracin de Apache para que el nuevo proyecto sea accesible para el mundo. Busca y abre el archivo de configuracin httpd.conf y aade la siguiente configuracin al final:
# Asegrate de tener slo una vez esta lnea en su configuracin NameVirtualHost 127.0.0.1:8080 # Esta es la configuracin de Jobeet Listen 127.0.0.1:8080 <VirtualHost 127.0.0.1:8080> DocumentRoot "/home/sfprojects/jobeet/web" DirectoryIndex index.php <Directory "/home/sfprojects/jobeet/web"> AllowOverride All Allow from All </Directory> Alias /sf /home/sfprojects/jobeet/lib/vendor/symfony/data/web/sf <Directory "/home/sfprojects/jobeet/lib/vendor/symfony/data/web/sf"> AllowOverride All Allow from All

</Directory> </VirtualHost> El alias /sf te da acceso a las imgenes y los archivos JavaScript necesarios para adecuadamente mostrar las pginas symfony por defecto y la barra de herramientas de depuracin web. En Windows, es necesario sustituir la linea Alias con algo como: Alias /sf "c:\development\sfprojects\jobeet\lib\vendor\symfony\data\web\sf" Y /home/sfprojects/jobeet/web debera ser sustituida por: c:\development\sfprojects\jobeet\web

En esta configuracin, Apache escucha en el puerto 8080 de tu mquina, por lo que el sitio web de Jobeet ser accesible en la siguiente URL:
http://localhost:8080/

Puedes cambiar 8080 por cualquier nmero mayor que 1024, ya que no se requieren derechos de administrador en esos puertos.
Configurar un nombre dedicado de dominio para Jobeet Si eres el administrador de tu equipo, es mejor configurar un virtual host en lugar de aadir un nuevo puerto cada vez que se inicia un nuevo proyecto. En lugar de aadir un puerto y agregar una declaracin Listen, elige un nombre de dominio y aade la declaracinServerName: # This is the configuration for Jobeet <VirtualHost 127.0.0.1:80> ServerName jobeet.localhost <!-- same configuration as before --> </VirtualHost> El nombre de dominio jobeet.localhost tiene que ser declarado localmente. Si se ejecuta un sistema Linux, que tiene que hacerse en el archivo /etc/hosts. Si ejecuta Windows XP, este archivo se encuentra en el directorio C:\WINDOWS\system32\drivers\etc\. Aade la siguiente lnea: 127.0.0.1 jobeet.localhost

Tip Nota del Traductor Cuando


se complican con estos pasos, los usuarios de Distribuciones Linux, como Ubuntu, pueden usar una herramienta grfica que simplique an ms esta configuracin. Rapache, es un software para sistemas Gnome que hace la configuracin bsica y necesaria, en los

archivos /etc/hosts y httpd.conf, en un solo paso adems de permitir el reinicio de Apache con un clic.

Probar la nueva configuracin Reinicia Apache, y comprueba que ahora tienes acceso a la nueva aplicacin abriendo un navegador y escribiendo http://localhost:8080/index.php/, o http://jobeet.localhost/index.php/ dependiendo de la configuracin de Apache que has elegido en la seccin anterior.

Si tienes el mdulo Apache mod_rewrite instalado, puedes remover la parte /index.php/ de todas las URL. Esto es posible gracias a las reglas de reescritura configuradas en el archivo web/.htaccess.

Deberas tratar de acceder a la aplicacin en el entorno de desarrollo. Escribe la siguiente URL:


http://jobeet.localhost/frontend_dev .php/

La web debug toolbar o barra de herramientas de depuracin web debera mostrarse en la esquina superior derecha, incluidos los iconos, demostrando que tu configuracin alias sf/ es correcta.

Subversion
Es una buena prctica utilizar control de versiones de cdigo fuente en el desarrollo de una aplicacin web. Usar el control de versiones de cdigo fuente nos permitir:

trabajar con confianza volver a una versin anterior si un cambio rompe algo permitir a ms de una persona para trabajar eficientemente en el proyecto tener acceso a todas las versiones sucesivas de la aplicacin

En esta seccin, vamos a describir cmo utilizar Subversion con Symfony. Si utilizas otra herramienta de control de cdigo fuente, debe ser muy fcil de adaptar segn lo descripto para Subversion. Suponemos que ya tienes acceso a un servidor Subversion.
Si no tienes un servidor Subversion a tu disposicin, puedes crear uno gratis en Google Code o solo escribir "free subversion repository" en Google para tener muchas ms opciones.

En primer lugar, crea un nuevo repositorio para el proyecto jobeet:


$ svnadmin create /path/to/jobeet/repository

En tu mquina, crea la estructura bsica de directorios:


$ svn mkdir -m "created default directory structure" http://svn.example.com/jobeet/trunk http://svn.example.com/jobeet/tags http://svn.example.com/jobeet/branches

Y comprueba el directorio vaco trunk/ :


$ cd /home/sfprojects/jobeet $ svn co http://svn.example.com/jobeet/trunk/ .

Entonces, elimina el contenido de los directorios cache/ y log/ porque no queremos ponerlos en el repositorio.
$ rm -rf cache/* log/*

Ahora, asegrate de poner los permisos de escritura sobre los directorios cache y logs a los niveles apropiados a fin de que tu servidor web pueda escribir en ellos:
$ chmod 777 cache/ log/

Ahora, importa todos los archivos y directorios:


$ svn add *

Como nunca queremos enviar los archivos situados en los directorios cache/ y /log, es necesario especificar una lista de ignorados:
$ svn propedit svn:ignore cache

El editor de texto por defecto configurado para SVN debera ejecutarse. Subversion debe hacer caso omiso de todo el contenido de este directorio:
*

Guardar y cerrar. Has terminado. Repite el procedimiento para el directorio log/:


$ svn propedit svn:ignore log

Y escribe:
*

Finalmente, enviamos (commit) estos cambios al repositorio:


$ svn import -m "made the initial import" . http://svn.example.com/jobeet/trunk Los usuarios de Windows pueden utilizar el excelente cliente TortoiseSVN para gestionar sus repositorio de Subversion. Los usuarios de Linux tienen muchas opciones y una de ellas es utilizar el excelente cliente Rapidsvn para gestionar sus repositorio de Subversion.

El Proyecto
o hemos escrito una sola lnea de PHP an, pero ayer, configuramos el entorno, creamos un proyecto symfony vaco, y nos aseguramos de iniciar con una buena seguridad por defecto. Si seguiste todo, tienes que buscar en la pantalla de entonces, ya que se muestra la hermosa pgina por defecto de symfony para las nuevas aplicaciones.

Pero quieres ms. Quieres saber todos los detalles de symfony para el desarrollo de aplicaciones? Por lo tanto, vamos a reanudar nuestro viaje al nirvana del desarrollo symfony. Hoy, nos tomaremos el tiempo para describir los requisitos del proyecto Jobeet con algunos bsicos mockups (diseos).

El Foco del Proyecto


Todo el mundo est hablando de la crisis hoy en da. El desempleo est aumentando de nuevo. Lo s, los desarrolladores symfony no estn realmente interesados y esto es porque quieren aprender symfony en primer lugar. Pero tambin es bastante difcil encontrar desarrolladores symfony buenos. Dnde puedes encontrar un desarrollador symfony? Dnde puedes anunciar tus habilidades symfony? Necesitas encontrar una buena Bolsa de Trabajo. Monster dices? Piensa de nuevo. Necesitas una Bolsa especializada. Una donde puedas encontrar a las mejores personas, los expertos. Una donde sea fcil, rpido y divertido buscar un puesto de trabajo, u ofrecer uno. No busques ms. Jobeet es el lugar. Jobeet es un software Open-Source para Bolsas de Trabajo que slo hace una cosa, pero la hace bien. Es fcil de usar, personalizar, ampliar, e incluir en tu sitio web. Soporta mltiples idiomas, y utiliza las ltimas tecnologas Web 2.0 para mejorar la experiencia del usuario. Tambin proporciona feeds y una API para interactuar con l programticamente. Ya existe? Como usuario, encontrars un montn de Bolsas de Trabajo como Jobeet en Internet. Pero trata de encontrar una que sea Open-Source (Cdigo Abierto), y con especiales caractersticas como las que proponemos aqu.
Si realmente ests buscando un empleo con symfony o quieres convertirte en un desarrollador symfony, puedes ir al sitio websymfonians.

Los Casos de Uso del Proyecto


Antes de meternos en el cdigo de cabeza, vamos a describir el proyecto un poco ms. Las secciones siguientes describen las caractersticas que queremos aplicar en la primera versin / iteracin del proyecto con algunos Casos de Uso sencillos. El sitio web Jobeet tiene cuatro tipo de usuarios:

administrador: l es el propietario de la pgina web y tiene poderes mgicos user: Visita la pgina web para buscar un puesto de trabajo y se postula para uno poster: Visita la pgina web para envar/ofrecer un puesto de trabajo affiliate: El re-publica algunos trabajos en su pgina web El proyecto tiene dos aplicaciones: el frontend (Casos de Uso F1 a F7, que estn ms abajo), donde los usuarios interactan con el sitio web, y el backend (Casos de Uso B1 a B3), donde los administradores gestionan el sitio web. La aplicacin backend tiene seguridad y requiere de credenciales para acceder. Caso de Uso F1: En la pgina principal, los usuarios ven los ltimos puestos de trabajo activos.

Cuando un usuario entra a la pgina web Jobeet, ve una lista de los puestos de trabajo activos. Los puestos de trabajo se clasifican por categora y a continuacin, por fecha de publicacin (los nuevos puestos de trabajo primero). Para cada puesto de trabajo, slo la ubicacin, la posicin, y la empresa se muestran. Para cada categora, la lista slo muestra los primeros 10 puestos de trabajo y un enlace permite listar todos los puestos de trabajo para una categora determinada (Caso de Uso F2). En la pgina principal, el usuario puede refinar la lista de puestos (Caso de Uso F2), o enviar un nuevo puesto de trabajo (Caso de Uso F5).

Caso de Uso F2: Un usuario puede solicitar todos los puestos de trabajo de una categora determinada Cuando un usuario hace clic en el nombre de una categora o en un enlace "more jobs" (ms trabajos) en la pgina de inicio, ver todos los puestos de trabajo para esta categora ordenados por fecha.
Nota del Traductor Trataremos de hacer la traducciones pertinentes de los textos de las plantillas (por ejemplo, ms trabajos), para as poder mantener stas identicas a las originales del ingls.

La lista est paginada, con 20 puestos de trabajo por pgina.

Caso de Uso F3: Un usuario refina la lista con algunas palabras clave El usuario puede introducir algunas palabras clave para refinar su bsqueda. Las palabras clave pueden ser palabras se encuentra en los campos de la ubicacin, la posicin, la categora, y de la compana. Caso de Uso F4: Un usuario hace clic en un puesto de trabajo para ver informacin ms detallada El usuario puede seleccionar un trabajo de la lista para ver informacin ms detallada.

Caso de Uso F5: Un usuario enva un puesto de trabajo Un usuario puede envar un puesto de trabajo. Un puesto de trabajo est formado por varias partes de informacin:

Compaa

Tipo (full-time, part-time, o freelance)

Logo (opcional) URL (opcional) Posicin Ubicacin Categora (el usuario elige una de

una lista de posibles categoras)


Descripcin del trabajo (URL y correos electrnicos son enlazados de forma automtica) Cmo aplicar (URL y correos electrnicos son enlazados de forma automtica) Pblico (si el trabajo tambin pueden ser publicados en sitios web afiliados) Email (email del oferente)

No hay necesidad de crear una cuenta para crear un puesto de trabajo. El proceso es sencillo con slo dos pasos: en primer lugar, el usuario rellena el formulario con toda la informacin necesaria para describir el trabajo y, a continuacin, se valida la informacin con una vista previa de la pgina de empleo final. Incluso si el usuario no tiene cuenta, un puesto de trabajo pueden ser modificado despus, gracias a un URL concreto (protegido por un token dado al usuario cuando crea el puesto de trabajo). Cada puesto de trabajo est en lnea durante 30 das (esto es configurable por el administrador - ver (Caso de Uso B2). Un usuario puede volver a activar y extender la validez de un puesto de trabajo por 30 das extra pero slo cuando el trabajo expira y entonces tiene menos de 5 das para hacerlo.

Caso de Uso F6: Un usuario se registra para ser un afiliado Un usuario para re-publicar necesita convertirse en un afiliado y ser autorizado a utilizar la API de Jobeet. Para afiliarse, debe dar la siguiente informacin:

Nombre Email URL del sitio web

La cuenta de afiliado debe ser activada por el administrador (Caso de Uso B3). Una vez activada, el afiliado recibe un token via email para poder usar la API. Cuando se registra, el afiliado puede tambin elegir los puestos de trabajo a obtener de un sub-conjunto de las categoras disponibles. Caso de Uso F7: Un afiliado recupera la lista activa de puestos de trabajo Un afiliado puede recuperar la actual lista de puestos de trabajo llamando a la API con su token de afiliado. La lista puede ser devuelta en formato XML, JSON o YAML. La lista contiene la informacin pblica disponible para un puesto de trabajo. El afiliado tambin puede limitar el nmero de puestos de trabajo a ser devueltos, y refinar su consulta especificando una categora.

Caso de Uso B1: Un administrador configura el sitio web Un administrador puede modificar las categoras disponibles en el sitio web. Tambin puede hacer algunos ajustes:

El nmero mximo de puestos de trabajo que figura en la pgina de inicio Idioma de la pgina web Nmero de das que un trabajo est en lnea

Caso de Uso B2: Un administrador gestiona los puestos de trabajo Un administrador puede editar y eliminar cualquier puesto de trabajo publicado. Caso de Uso B3: Un administrador gestiona los afiliados El administrador puede crear o editar los afiliados. l es el responsable de la activacin de un afiliado y tambin puede desactivarlo. Cuando el administrador activa un nuevo afiliado, el sistema crea un nico token para ser utilizado por ese afiliado.

El Modelo y la base de datos


Aquellos de ustedes locos por abrir el editor de texto y hacer algo de PHP estarn encantados de saber que el tutorial de hoy nos introduce algo en el desarrollo. Vamos a definir el modelo de datos Jobeet, utilizaremos un ORM para interactuar con la base de datos, y construiremos el primer mdulo de la aplicacin. Pero como Symfony hace una gran parte del trabajo por nosotros, vamos a tener un mdulo web totalmente funcional sin tener que escribir mucho cdigo PHP.

El Modelo Relacional
Los casos de uso que escribimos ayer describen los principales objetos de nuestro proyecto: puestos de trabajo, afiliados, y las categoras. Aqu est el correspondiente diagrama de entidad relacin:

Adems de las columnas descritas en los casos de uso, tambin hemos aadido un campo created_at a algunas tablas. Symfony reconoce estos campos y establece el valor de la hora actual de sistema cuando un registro es creado. Esto es lo mismo para el campoupdated_at: su valor se establece siempre a la hora del sistema en que el registro se actualiza.

El Esquema
Para almacenar los puestos de trabajo, los afiliados, y las categoras, es evidente que necesitamos una base de datos relacional. Pero como Symfony es un Framework Orientado a Objetos, nos gustara manipular los objetos cada vez que podamos. Por ejemplo, en lugar de escribir sentencias SQL para recuperar los registros de la base de datos, preferimos ms usar los objetos. La informacin de la base de datos relacional debe ser mapeada a un modelo de objetos. Esto se puede hacer con una herramienta ORM, o Mapeador, y afortunadamente, Symfony tiene incluido dos de ellas: Propel y Doctrine. En este tutorial, usaremos Doctrine.

El ORM necesita una descripcin de las tablas y sus relaciones para crear las clases relacionadas. Hay dos maneras de crear este esquema de descripcin: ingeniera reversa de una base de datos existente o crendolo a mano. Como la base de datos no existe todava, y como queremos mantener la base de datos Jobeet agnstica, vamos a crear el archivo de esquema a mano editando el archivo vaco config/doctrine/schema.yml:
# config/doctrine/schema.yml --JobeetCategory: actAs: { Timestampable: ~ } columns: name: { type: string(255), notnull: true, unique: true } JobeetJob: actAs: { Timestampable: ~ } columns: category_id: { type: integer, notnull: true } type: { type: string(255) } company: { type: string(255), notnull: true } logo: { type: string(255) } url: { type: string(255) } position: { type: string(255), notnull: true } location: { type: string(255), notnull: true } description: { type: string(4000), notnull: true } how_to_apply: { type: string(4000), notnull: true } token: { type: string(255), notnull: true, unique: true } is_public: { type: boolean, notnull: true, default: 1 } is_activated: { type: boolean, notnull: true, default: 0 } email: { type: string(255), notnull: true } expires_at: { type: timestamp, notnull: true } relations: JobeetCategory: { onDelete: CASCADE, local: category_id, foreign: id, foreignAlias: JobeetJobs } JobeetAffiliate: actAs: { Timestampable: ~ } columns: url: { type: string(255), notnull: true } email: { type: string(255), notnull: true, unique: true } token: { type: string(255), notnull: true } is_active: { type: boolean, notnull: true, default: 0 } relations: JobeetCategories: class: JobeetCategory refClass: JobeetCategoryAffiliate local: affiliate_id foreign: category_id foreignAlias: JobeetAffiliates

JobeetCategoryAffiliate: columns: category_id: { type: integer, primary: true affiliate_id: { type: integer, primary: true relations: JobeetCategory: { onDelete: CASCADE, local: } JobeetAffiliate: { onDelete: CASCADE, local: id }

} } category_id, foreign: id affiliate_id, foreign:

Si decidiste crear las tablas escribiendo las sentencias SQL, puedes generar el archivo de configuracin del esquema correspondienteschema.yml, ejecutando la tarea doctrine:build-schema: $ php symfony doctrine:build-schema La anterior tarea requiere que tengas la base de datos configurada en el databases.yml. Te mostraremos como configurar la base de datos en un paso posterior. Si tratas de ejecutar esta tarea ahora no funcionar ya que no sabe que base de datos contruir para el esquema.

El esquema es la traduccin directa de un diagrama de entidad relacin en formato YAML.


El Formato YAML De acuerdo con el sitio web oficial YAML, YAML es "es una serializacin de datos estndar muy amigable para todos los lenguajes de programacin" Dicho de otra manera, YAML es un lenguaje sencillo para describir los datos (strings, integers, dates, arrays, y hashes). En YAML, la estructura se muestra a travs de la sangra, la secuencia de elementos se denotan por un guin, y los pares clave/valor estn separados por dos puntos. YAML tambin tiene una sintaxis abreviada para describir la misma estructura con menos lneas, donde los arrays explcitamente se muestran con [] y los hashes o array asociativos con {}. Si todava no estn familiarizados con YAML, es hora de empezar con l pues el framework Symfony lo utiliza ampliamente para sus archivos de configuracin. Un buen punto de partida es la documentacin del componente YAML de Symfony. Hay una cosa importante que necesitas recordar cuando ests editando un archivo YAML: la indentacin debe hacerse con uno o mas espacios en blanco, pero nunca con tabulaciones.

El archivo schema.yml contiene la descripcin de todas las tablas y sus columnas. Cada columna se describe con la siguiente informacin:
type: El tipo de columna (boolean, integer, float, decimal, string, array, object, blob, clob, time stamp, time, date, enum,gzip) notnull: Es true si deseas que la columna sea obligadoria

unique: Es true si deseas crear un ndice nico para la columna.

El atributo onDelete define el comportamiento ON DELETE para claves forneas, y Doctrine da soporte para CASCADE, SETNULL, yRESTRICT. Por ejemplo, cuando un registro de job es borrado, todos los registros jobeet_category_affiliate relacionados sern automticamente eliminados de la base de datos Nota del Traductor Si bien no es probable, es posible que se quiera eliminar una categora completa del sistema. El campo category_id de la entidad JobeetJob, tiene al final aadida la opcin onDelete. La misma est establecida como CASCADE, para determinar que cuando una categora sea eliminada, tambien se eliminen los registros job (los puestos de trabajo). Esto se debe a que los registros job tienen establecido como campo obligatorio a la categora asociada (a la que pertenece), por lo que no sera correcto dejar huerfano a un registro, ni establecer ste valor a null.

La Base De Datos
El framework Symfony soporta todas las Base De Datos soportadas por PDO (MySQL, PostgreSQL, SQLite, Oracle, MSSQL, ...). PDO es la capa de abstraccin de base de datos que viene con PHP. Vamos a usar MySQL para este tutorial:
$ mysqladmin -uroot -p create jobeet Enter password: mYsEcret ## La clave se mostrar como ******** Eres libre de elegir otro motor de base de datos si lo deseas. No ser difcil adaptar el cdigo que vamos a escribir ya que vamos a utilizar el ORM quien ser quien escriba el SQL por nosotros.

Tenemos que decirle a Symfony que base de datos usar para el proyecto Jobeet:
$ php symfony configure:database "mysql:host=localhost;dbname=jobeet" root mYsEcret

La tarea configure:database emplea tres argumentos: el PDO DSN, el nombre de usuario, y la clave de acceso a la base de datos. Si no tienes ninguna contrasea de la base en el servidor de desarrollo, basta con omitir el tercer argumento.
La tarea configure:database almacena la configuracin de la base de datos en el archivo de configuracin config/databases.yml. En lugar de utilizar la tarea, puedes editar este archivo manualmente. Pasar la clave de la base de datos por linea de comandos es conveniente pero inseguro. Dependiendo de quienes tiene acceso a tu entorno, podra ser mejor editar el config/databases.yml para cambiar la clave. Desde luego, para mantener la clave a salvo, el acceso al archivo de configuracin debera tambin ser restringido.

El ORM
Gracias al archivo de descripcin de la base de datos schema.yml, podemos utilizar algunas de las tareas de Doctrine para generar los comandos SQL necesarios para crear tablas de la base de datos: Primero para generar el SQL debes contruir el modelo apartir de tus archivos esquema.
$ php symfony doctrine:build --model

Ahora que tus modelos existen puedes generar e insertar el SQL.


$ php symfony doctrine:build --sql

La tarea doctrine:build --sql genera comandos SQL en el directorio data/sql/, optimizado para el motor de base de datos que hemos configurado:
# snippet from data/sql/schema.sql CREATE TABLE jobeet_category (id BIGINT AUTO_INCREMENT, name VARCHAR(255) NOT NULL COMMENT 'test', created_at DATETIME, updated_at DATETIME, slug VARCHAR(255), UNIQUE INDEX sluggable_idx (slug), PRIMARY KEY(id)) ENGINE = INNODB;

Para realmente crear las tablas en la base de datos, necesitas ejecutar la tarea doctrine:insert-sql:
$ php symfony doctrine:insert-sql Como con cualquier herramienta de lnea de comandos, las tareas symfony pueden tener argumentos y opciones. Cada tarea viene con un mensaje de ayuda que se puede mostrar ejecutando la tarea help: $ php symfony help doctrine:insert-sql El mensaje de ayuda lista de todos los posibles argumentos y opciones, da los valores por defecto para cada uno de ellos, y proporciona algunos ejemplos tiles.

El ORM tambin genera las clases PHP que mapea los registros de la tabla con los objetos:
$ php symfony doctrine:build --model

La tarea doctrine:build --model genera archivos PHP en el directorio lib/model/ que se pueden utilizar para interactuar con la base de datos. Navegando por los archivos generados, probablemente habrs notado que Doctrine genera cuatro clases por tabla. Para la tablajobeet_job:
JobeetJob: Un objeto de esta clase representa un nico registro de la tabla jobeet_job. La clase est vaca por defecto. BaseJobeetJob: La clase padre de JobeetJob. Cada vez que ejecutas doctrine:build --model, esta clase es sobreescrita, por lo que todas las personalizaciones se deben hacer en la clase JobeetJob. JobeetJobTable: La clase define los mtodos que mayormente devuelve colecciones de objetos JobeetJob. La clase est vaca por defecto.

Los valores de las columnas de un registro se pueden manipular con el modelo de objetos mediante el uso de algunos mtodos get*()y mtodos set*():
$job = new JobeetJob(); $job->setPosition('Web developer'); $job->save(); echo $job->getPosition(); $job->delete();

Tambin puedes definir claves forneas directamente por la vinculacin de objetos:


$category = new JobeetCategory(); $category->setName('Programming'); $job = new JobeetJob(); $job->setCategory($category);

La tarea doctrine:build --all es un acceso directo para las tareas que han ejecutado en esta seccin y algunas ms. Por lo tanto, ejecuta esta tarea ahora para generar formularios y validadores para el modelo de clases de Jobeet:
$ php symfony doctrine:build --all --no-confirmation

Vers los validadores en accin al final del da y los formularios se explicarn en gran detalle en el da 10.

Los Datos Iniciales


Los tablas se han creado en la base de datos, pero no hay datos en ellas. Para cualquier aplicacin web, hay tres tipos de datos:

Datos Iniciales: Los datos iniciales son necesarios para que la aplicacin funcione. Por ejemplo, Jobeet necesita algunas categoras iniciales. Si no, nadie ser capaz de envar un puesto de trabajo. Tambin necesitamos un administrador de usuarios para poder acceder al backend. Datos de Prueba: Los datos de prueba son necesarios para que la aplicacin sea probada. Como desarrollador, escribes pruebas para asegurarte que Jobeet se comporta como se describe en los casos de uso, y la mejor manera es escribir pruebas automticas. Por lo tanto, cada vez que ejecutes tus pruebas, necesitas una base de datos limpia con algunos datos nuevos de prueba en ella. Los Datos del Usuario: Los datos del usuario son creados por los usuarios durante la vida normal de la aplicacin.

Cada vez que Symfony crea las tablas en la base de datos, todos los datos se pierden. Para rellenar la base de datos con algunos los datos iniciales, podriamos crear un script PHP, o ejecutar sentencias SQL con algun programa mysql. Pero como la necesidad es bastante comn, hay una mejor manera con Symfony: crear

archivos YAML en el directorio data/fixtures/ y usar la tareadoctrine:dataload para cargarlos en la base de datos. Primero, crea los siguientes archivos de datos:
# data/fixtures/categories.yml JobeetCategory: design: name: Design programming: name: Programming manager: name: Manager administrator: name: Administrator # data/fixtures/jobs.yml JobeetJob: job_sensio_labs: JobeetCategory: programming type: full-time company: Sensio Labs logo: sensio-labs.gif url: http://www.sensiolabs.com/ position: Web Developer location: Paris, France description: | You've already developed websites with symfony and you want to work with Open-Source technologies. You have a minimum of 3 years experience in web development with PHP or Java and you wish to participate to development of Web 2.0 sites using the best frameworks available. how_to_apply: | Send your resume to fabien.potencier [at] sensio.com is_public: true is_activated: true token: job_sensio_labs email: job@example.com expires_at: '2008-10-10' job_extreme_sensio: JobeetCategory: design type: part-time company: Extreme Sensio logo: extreme-sensio.gif url: http://www.extreme-sensio.com/ position: Web Designer location: Paris, France description: | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut

enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in. Voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. how_to_apply: | Send your resume to fabien.potencier [at] sensio.com is_public: true is_activated: true token: job_extreme_sensio email: job@example.com expires_at: '2008-10-10' El archivo de datos de puestos de trabajo hace referencia a dos imgenes. Puedes descargarlas de (http://www.symfony-project.org/get/jobeet/sensiolabs.gif, http://www.symfony-project.org/get/jobeet/extreme-sensio.gif) y colocarlas en el directorio uploads/jobs/.

Esta escrito en YAML, y define el modelo de objetos, etiquetados con un nombre nico (por ejemplo, hemos definido dos puestos de trabajo etiquetados con job_sensio_labs y job_extreme_sensio). Estas etiquetas son de gran utilidad para vincular objetos relacionados sin tener que definir claves primarias (que a menudo son auto-incrementales y no se pueden establecer). Por ejemplo, la categora de job_sensio_labs es programming, que es la etiqueta dada a la categora 'Programming'.
En un archivo YAML, cuado una cadena tiene saltos de linea (como la columna description en el archivo de datos de puestos de trabajo), puedes usar la barra vertical (|) para indicar que la cadena aparecer en varias lineas.

A travs de un archivo de datos se puede tener objetos de uno o varios modelos, hemos decidido crear un archivo por cada modelo para los datos de Jobeet.
Propel require que los archivos de datos tengan un prefijo nmerico que determine el orden en el cual los archivos sern cargados. Con Doctrine esto no es necesario ya que todos los archivos sern cargados y guardados en el correcto orden para asegurar que las claves foraneas sean adecuadas.

En un archivo de datos, no es necesario definir todos los valores de las columnas. Si no que, Symfony utilizar el valor predeterminado definido en el esquema de base de datos. Y como Symfony usa Doctrine para cargar los datos en la base de datos, todos los comportamientos incorporados (como el seteo automtico a created_at o updated_at), o los comportamientos personalizados que puedes haber agregado al modelo de las clases son activados. La carga de los datos iniciales en la base de datos es tan simple como ejecutar la tarea doctrine:data-load:

$ php symfony doctrine:data-load La tarea doctrine:build --all --and-load es un atajo para la tarea doctrine:build --all seguida de la tarea doctrine:data-load.

Ejecuta la tarea doctrine:build --all --and-load para asegurarte que todo es generado desde tu esquema. Esto generar tus formularios, filtros, modelos, vaciar tu base de datos y la re-crear de nuevo con todas las tablas.
$ php symfony doctrine:build --all --and-load

Miralo en accin en el Navegador


Hemos utilizado la interfaz de lnea de comandos mucho, pero eso no es realmente emocionante, especialmente para un proyecto web. Ahora tenemos todo lo que necesitamos para crear pginas Web que interactan con la base de datos. Vamos a ver cmo mostrar la lista de puestos de trabajo, cmo editar un trabajo, y cmo eliminar un puesto de trabajo. Como se explic durante el da 1, un proyecto symfony se hace de las aplicaciones. Cada aplicacin est dividida en mdulos. Un mdulo es un conjunto de cdigo PHP auto-contenido que representa una caracterstica de la aplicacin (el mdulo API, por ejemplo), o un conjunto de manipulaciones que el usuario puede hacer sobre un objeto del modelo (un mdulo job, por ejemplo). Symfony es capaz de generar automticamente un mdulo para un determinado modelo que proporciona las caractersticas bsicas de manipulacin:
$ php symfony doctrine:generate-module --with-show --non-verbosetemplates frontend job JobeetJob

La tarea doctrine:generate-module genera mdulo job en la aplicacin frontend para el modelo JobeetJob. Como con la mayora de las tareas symfony, algunos archivos y directorios se han creado para ti bajo el directorio apps/frontend/modules/job/: Directorio Descripcin
actions/

Acciones del mdulo

templates/ Plantillas del mdulo

El archivo actions/actions.class.php define todas las acciones disponibles para el mdulo job: Nombre de la accin
index show

Descripcin

Muestra los registros de la tabla Muestra los campos de un registro

Nombre de la accin
new create edit

Descripcin

Muestra un formulario para crear un nuevo registro Crea un nuevo registro Muestra un formulario para crear editar un registro existente

update delete

Actualiza un registro con los valores que envi el usuario Borra un registro de la tabla

Ahora puedes probar el mdulo job en un navegador:


http://www.jobeet.com.localhost/frontend_dev.php/job

Si intentas editar un puesto de trabajo, te dars cuenta que el combo Category id tiene una lista de todos los nombres de las categoras. El valor de cada opcin esta dado por el mtodo __toString(). Doctrine tratar de dar un mtodo base __toString() adivinandolo de una columna descriptiva de nombre como ser, title, name,subject, etc. Si quieres algo distinto entonces necesitas agregar tus propios mtodos __toString() como se ve ms abajo. El modeloJobeetCategory esta listo para adivinar el mtodo __toString() usando la columna name de la tabla jobeet_category.

// lib/model/doctrine/JobeetJob.class.php class JobeetJob extends BaseJobeetJob { public function __toString() { return sprintf('%s at %s (%s)', $this->getPosition(), $this>getCompany(), $this->getLocation()); } } // lib/model/doctrine/JobeetAffiliate.class.php class JobeetAffiliate extends BaseJobeetAffiliate { public function __toString() { return $this->getUrl(); } }

Ahora puedes editar y crear puestos de trabajo. Trata de dejar un campo obligatorio en blanco, o tratar de dar una fecha no vlida. As es, Symfony ha creado bsicas reglas de validacin analizando el esquema de base de datos.

El Controlador y la Vista
hemos explorado cmo Symfony simplifica la gestin de bases de datos por abstraccin entre los diferentes motores de bases de datos, y mediante la conversin de elementos relacionales con tiles clases orientadas a objetos. Tambin hemos jugado con Doctrine para describir el esquema de base de datos, crear las tablas, y llenar la base de datos con algunos datos iniciales. Hoy, vamos a personalizar el mdulo bsico job creado ayer. El mdulo job existente tiene todo el cdigo que necesitamos para Jobeet:

Una pgina que lista todos los puestos de trabajo Una pgina para crear un nuevo puesto de trabajo Una pgina para actualizar un puesto de trabajo existente Una pgina para eliminar un puesto de trabajo

Aunque el cdigo est listo para ser utilizado como esta, vamos a refactorizar las plantillas para adaptarlas lo ms cerca a los mockups Jobeet.

La arquitectura MVC
Si ests desarrollando con PHP sitios web sin ningn framework, probablemente uses el paradigma de un archivo PHP por pgina HTML. Estos archivos PHP probablemente contengan el mismo tipo de estructura: inicializacin y configuracin global, lgica de negocio relacionada con la pgina solicitada, busqueda de registros en la base, y finalmente el cdigo HTML que arma la pgina. Puedes utilizar un motor de plantillas para separar la lgica del HTML. Tal vez utilizas una capa de abstraccin de la base de datos para separar el modelo de la lgica de negocio. Sin embargo, la mayora de las veces, terminas con un montn de cdigo que es una pesadilla para mantener. Es rpido para construir, pero con el tiempo, es ms y ms difcil de hacer cambios, especialmente porque nadie, excepto t entiende cmo se construye y cmo funciona. Al igual que con todos los problemas, hay soluciones agradables. Para desarrollo web, la solucin ms comn para la organizacin de su cdigo de hoy en da es el patrn de diseo MVC. En resumen, el patrn de diseo MVC define una manera de organizar el cdigo de acuerdo a su naturaleza. Este patrn separa el cdigo en tres capas:

La capa Modelo define la lgica de negocio (la base de datos pertenece a esta capa). Ya sabes que Symfony guarda todas las clases y archivos relacionados con el modelo en el directorio lib/model/.

La Vista es con lo que el usuario interacta (un motor de plantillas es parte de esta capa). En Symfony, la vista es principalmente la capa de plantillas PHP. Estas son guardadas en varios directorios templates/ como veremos ms adelante en el da de hoy.
El Controlador es la pieza de cdigo que llama al Modelo para obtener algunos datos que le pasa a la Vista para la presentacin al cliente. Cuando instalamos Symfony el primer da, vimos que todas las solicitudes son gestionadas por un controlador frontal (index.php y frontend_dev.php). Estos controladores frontales delegadan la verdadera labor a las acciones. Como vimos ayer, estas acciones son, lgicamente, agrupadas en mdulos.

Hoy, usaremos el mockup definido el da 2 para personalizar la pgina principal y la pgina de puestos de trabajos. Vamos hacerlas dinmicas. A lo largo del camino, vamos a modificar un montn de cosas en diferentes archivos para demostrar la estructura de directorios symfony y la forma de separar el cdigo entre las capas.

El diseo
En primer lugar, si miraste de cerca los mockups, te dars cuenta de que gran parte de cada una de las pginas tiene el mismo aspecto. Ya sabes que la duplicacin de cdigo esta mal, ya sea si estamos hablando de cdigo HTML o PHP, por lo que necesitamos encontrar una manera de prevenir estos elementos de vista comn resultantes de la duplicacin de cdigo. Una forma de resolver el problema es definir un encabezado y un pie de pgina y lo incluyes en cada plantilla:

Pero los archivos de la cabecera y el pie de pgina no contienen HTML vlido. Debe haber una mejor manera. En lugar de reinventar la rueda, vamos a utilizar otro patrn de diseo para resolver este problema: el patrn de diseo decorador. El patrn de diseo decorador resuelve el problema al revs: la plantilla es decorada

despus de que el contenido es mostrado por una plantilla global, llamada layout en Symfony:

El layout de una aplicacin se llama layout.php y se puede encontrar en el directorio apps/frontend/templates/. Este directorio contiene todas las plantillas globales para una aplicacin. Reemplaza el layout por defecto de Symfony por el siguiente cdigo:
<!-- apps/frontend/templates/layout.php --> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <title>Jobeet - Your best job board</title> <link rel="shortcut icon" href="/favicon.ico" /> <?php include_javascripts() ?> <?php include_stylesheets() ?> </head> <body> <div id="container"> <div id="header"> <div class="content"> <h1><a href="<?php echo url_for('job/index') ?>"> <img src="/images/logo.jpg" alt="Jobeet Job Board" /> </a></h1> <div id="sub_header"> <div class="post"> <h2>Ask for people</h2> <div> <a href="<?php echo url_for('job/index') ?>">Post a Job</a> </div> </div> <div class="search"> <h2>Ask for a job</h2> <form action="" method="get"> <input type="text" name="keywords" id="search_keywords" /> <input type="submit" value="search" /> <div class="help"> Enter some keywords (city, country, position, ...) </div>

</form> </div> </div> </div> </div> <div id="content"> <?php if ($sf_user->hasFlash('notice')): ?> <div class="flash_notice"> <?php echo $sf_user->getFlash('notice') ?> </div> <?php endif; ?> <?php if ($sf_user->hasFlash('error')): ?> <div class="flash_error"> <?php echo $sf_user->getFlash('error') ?> </div> <?php endif; ?> <div class="content"> <?php echo $sf_content ?> </div> </div> <div id="footer"> <div class="content"> <span class="symfony"> <img src="/images/jobeet-mini.png" /> powered by <a href="http://www.symfony-project.org/"> <img src="/images/symfony.gif" alt="symfony framework" /> </a> </span> <ul> <li><a href="">About Jobeet</a></li> <li class="feed"><a href="">Full feed</a></li> <li><a href="">Jobeet API</a></li> <li class="last"><a href="">Affiliates</a></li> </ul> </div> </div> </div> </body> </html>

Una plantilla symfony es slo un simple archivo PHP. En la plantilla layout, vers llamadas a funciones PHP y referencias a variables PHP.$sf_content es la variable ms interesante: la define el mismo framework y contiene el cdigo HTML generado por la accin.

Si navegas el mdulo job (http://www.jobeet.com.localhost/frontend_dev.php/job), vers que todas las acciones ahora son decoradas por el layout.

Las Hojas de Estilo, Imgenes, y JavaScripts


Ya que este tutorial no es acerca de diseo web, tenemos ya preparado todo lo necesario que usaremos para Jobeet: descarga los archivo grficos y copiarlos dentro del directorio web/images/; descarga los archivo de hojas de estilo y copiarlos dentro del directorioweb/css/.
En el layout, hemos includo un favicon. Puedes descargarlo Jobeet y ponerlo bajo el directorio web/.

de

Por defecto, la tarea generate:project ha creado tres directorios para los recusos de project: web/images/ para las imgenes,web/css/ para las hojas de estilo, y web/js/ para los Javascripts. Esta es una de las muchas convenciones definidas por Symfony, pero por supuesto puedes almacenar en otro lugar bajo el directorio web/.

El astuto lector habr notado que, incluso si el archivo main.css no es mencionado en cualquier lugar del layout por defecto, est sin duda presentes en el cdigo HTML generado. Pero no los otros. Cmo es esto posible? El archivo de estilo se ha incluido por la llamada a la funcin include_stylesheets() que encuentra la etiqueta <head>. La funcin include_stylesheets() es llamada un helper. Un helper es una funcin, definida por Symfony, que puede tener parmetros y devolver cdigo HTML. La mayora de las veces, los helpers te ahorran tiempo, ellos empaquetan cdigo en snippets (porciones de cdigo) utilizados con frecuencia en las plantillas. El helperinclude_stylesheets() genera una etiqueta <link> para las hojas de estilo. Pero, cmo hace el helper para saber que hojas de estilo incluir? La capa de la Vista se puede configurar editando el archivo de configuracin view.yml de la aplicacin. Aqu est el archivo por defecto generado por la tarea generate:app:
# apps/frontend/config/view.yml default: http_metas:

content-type: text/html metas: #title: #description: #keywords: #language: #robots: stylesheets: javascripts: has_layout: layout:

symfony project symfony project symfony, project en index, follow [main.css] [] true layout

El archivo view.yml establece la configuracin por defecto para todas las plantillas de la aplicacin. Por ejemplo, para las hojas de estilo define un array de archivos de estilo para incluir en todas las pginas de la aplicacin (la inclusin se hace por el helperinclude_stylesheets()).
En el archivo de configuracin view.yml por defecto, se hace referencia al archivo main.css, y no al /css/main.css. Como cuestin de hecho, ambas definiciones son equivalentes pues el prefijo relativo symfony es /css/.

Si muchos archivos se definen, Symfony los incluye en el mismo orden que la definicin:
stylesheets: [main.css, jobs.css, job.css]

Tambin puedes cambiar el atributo media y omitir el sufijo .css:


stylesheets: [main.css, jobs.css, job.css, print: { media: print }]

Esta configuracin se presentar as:


<link rel="stylesheet" type="text/css" media="screen" href="/css/main.css" /> <link rel="stylesheet" type="text/css" media="screen" href="/css/jobs.css" /> <link rel="stylesheet" type="text/css" media="screen" href="/css/job.css" /> <link rel="stylesheet" type="text/css" media="print" href="/css/print.css" /> El archivo de configuracin view.yml tambin define el layout por defecto usado por la aplicacin. Por defecto, el nombre es layout, y as Symfony decora cada pgina con el archivo layout.php. Puedes tambin deshabilitar el proceso de decoracin completo cambiando el valor de has_layout a false.

Funciona como est pero el archivo jobs.css es solo necesario en la pgina principal y el job.css slo es necesario para la pgina job. El archivo view.yml se

puede personalizar partiendo de una base por-mdulo. Cambia el archivo view.yml de la aplicacin slo para tener el archivo main.css:
# apps/frontend/config/view.yml stylesheets: [main.css]

Para personalizar la vista del mdulo job, crea un nuevo archivo view.yml en el directorio apps/frontend/modules/job/config/:
# apps/frontend/modules/job/config/view.yml indexSuccess: stylesheets: [jobs.css] showSuccess: stylesheets: [job.css]

Bajo las secciones indexSuccess y showSuccess (ellas son las plantillas asociadas a las acciones index y show, como veremos ms adelante), puedes personalizar cualquiera de los items encontrados bajo la seccin default del view.yml de la aplicacin. Todos los items se fusionan con la configuracin de la aplicacin. Tambin puedes definir algunas configuraciones para todas las acciones de un mdulo con la seccin especial all.
Principios de configuracin en Symfony Para los muchos archivos de configuracin de Symfony, la misma configuracin se puede definir en diferentes niveles: La configuracin por defecto se encuentra en el framework La configuracin global para el proyecto (en config/) La configuracin local de una aplicacin (en apps/APP/config/) La configuracin local limitada a un mdulo (en apps/APP/modules/MODULE/config/)

En tiempo de ejecucin, la configuracin del sistema combina todos los valores de los diferentes archivos si existen y guarda en la memoria cache el resultado para un mejor rendimiento.

Como regla emprica, cuando algo es configurable a travs de un archivo de configuracin, la misma puede realizarse con cdigo PHP. En lugar de crear un archivo view.yml para el mdulo job por ejemplo, tambin puedes utilizar el helper use_stylesheet() a fin de incluir una hoja de estilos a partir de una plantilla:
<?php use_stylesheet('main.css') ?>

Tambin puedes utilizar este helper en el layout a fin de incluir una hoja de estilo globalmente. Elegir entre un mtodo u otro es realmente una cuestin de gusto. El archivo view.yml proporciona una manera de definir las cosas para todas las acciones de un mdulo, las cuales no son posible en una plantilla, pero la

configuracin es bastante esttico. Por otro lado, utilizando el helper use_stylesheet() es ms flexible y adems, todo est en el mismo lugar: la definicin de estilos y el cdigo HTML. Para Jobeet, vamos a utilizar el helper use_stylesheet(), osea puedes quitar el view.yml que recin creamos y actualiza la plantilla job con la llamada a use_stylesheet():
<!-- apps/frontend/modules/job/templates/indexSuccess.php --> <?php use_stylesheet('jobs.css') ?> <!-- apps/frontend/modules/job/templates/showSuccess.php --> <?php use_stylesheet('job.css') ?> En consecuencia, la configuracin de JavaScript se realiza a travs de la linea javascripts del archivo de configuracin view.yml y el helper use_javascript() define los archivos JavaScript a incluir para una plantilla.

La Pgina Principal de Puesto de Trabajo


Como se observa en el da 3, la pgina principal de puestos de trabajo (job) es generada por la accin index del mdulo job. La accinindex es la parte del Controlador de la pgina y la plantilla asociada, indexSuccess.php, en la parte de la Vista:
apps/ frontend/ modules/ job/ actions/ actions.class.php templates/ indexSuccess.php

La Accin Cada accin est representada por un mtodo de una clase. Para la pgina principal de puestos de trabajo, la clase es jobActions (el nombre del mdulo seguido de Actions) y el mtodo es executeIndex() (execute seguido por el nombre de la accin). Esto recupera todos los puestos de trabajo de la base de datos:
// apps/frontend/modules/job/actions/actions.class.php class jobActions extends sfActions { public function executeIndex(sfWebRequest $request) { $this->jobeet_jobs = Doctrine::getTable('JobeetJob') ->createQuery('a') ->execute(); } // ... }

Echemos un vistazo ms de cerca al cdigo: el mtodo executeIndex() (el Controlador) llama a la Tabla JobeetJob para crear una consulta que recupere todos los puestos de trabajo. Devuelve una Doctrine_Collection de objetos JobeetJob que se asignan a la propiedad jobeet_jobs del objeto. Todas las propiedades del objeto luego se pasan automticamente a la plantilla (la Vista). Para pasar los datos del Controlador a la Vista, solo crea una nueva propiedad:
public function executeFooBar(sfWebRequest $request) { $this->foo = 'bar'; $this->bar = array('bar', 'baz'); }

Este cdigo har que las variables $foo y $bar sean accesibles en la plantilla. La Plantilla De forma predeterminada, el nombre de plantilla asociado con una accin se deduce por Symfony gracias a un convencin (el nombre de la accin seguida por Success). La plantilla indexSuccess.php genera una tabla HTML para todos los puestos de trabajo. Aqu esta el cdido de la plantilla actual:
<!-- apps/frontend/modules/job/templates/indexSuccess.php --> <?php use_stylesheet('jobs.css') ?> <h1>Job List</h1> <table> <thead> <tr> <th>Id</th> <th>Category</th> <th>Type</th> <!-- more columns here --> <th>Created at</th> <th>Updated at</th> </tr> </thead> <tbody> <?php foreach ($jobeet_jobs as $jobeet_job): ?> <tr> <td> <a href="<?php echo url_for('job/show?id='.$jobeet_job->getId()) ?>"> <?php echo $jobeet_job->getId() ?> </a> </td> <td><?php echo $jobeet_job->getCategoryId() ?></td>

<td><?php echo $jobeet_job->getType() ?></td> <!-- more columns here --> <td><?php echo $jobeet_job->getCreatedAt() ?></td> <td><?php echo $jobeet_job->getUpdatedAt() ?></td> </tr> <?php endforeach; ?> </tbody> </table> <a href="<?php echo url_for('job/new') ?>">New</a>

En el cdigo de plantilla, el foreach itera a travs de la lista de objetos Job ($jobeet_jobs), y para cada puesto de trabajo, cada valor de la columna es mostrado. Recuerda, el acceso a un valor de una columna es tan simple como una llamada al mtodo de acceso cuyo nombre comienza con get y el nombre de la columna en formato CamelCase (por ejemplo, el mtodo getCreatedAt() para la columnacreated_at). Vamos a limpiar esto un poco para mostrar slo un subconjunto de las columnas disponibles:
<!-- apps/frontend/modules/job/templates/indexSuccess.php --> <?php use_stylesheet('jobs.css') ?> <div id="jobs"> <table class="jobs"> <?php foreach ($jobeet_jobs as $i => $job): ?> <tr class="<?php echo fmod($i, 2) ? 'even' : 'odd' ?>"> <td class="location"><?php echo $job->getLocation() ?></td> <td class="position"> <a href="<?php echo url_for('job/show?id='.$job->getId()) ?>"> <?php echo $job->getPosition() ?> </a> </td> <td class="company"><?php echo $job->getCompany() ?></td> </tr> <?php endforeach; ?> </table> </div>

La funcin url_for() en esta plantilla es un helper symfony que vamos a discutir maana.

La Plantilla para los Puestos de Trabajo


Ahora vamos a personalizar la plantilla de la pgina de puestos de trabajo. Abre el archivo showSuccess.php y reemplaza su contenido con el siguiente cdigo:
<!-- apps/frontend/modules/job/templates/showSuccess.php --> <?php use_stylesheet('job.css') ?> <?php use_helper('Text') ?> <div id="job"> <h1><?php echo $job->getCompany() ?></h1> <h2><?php echo $job->getLocation() ?></h2> <h3> <?php echo $job->getPosition() ?> <small> - <?php echo $job->getType() ?></small> </h3> <?php if ($job->getLogo()): ?> <div class="logo"> <a href="<?php echo $job->getUrl() ?>"> <img src="/uploads/jobs/<?php echo $job->getLogo() ?>" alt="<?php echo $job->getCompany() ?> logo" /> </a> </div> <?php endif; ?> <div class="description"> <?php echo simple_format_text($job->getDescription()) ?> </div> <h4>How to apply?</h4> <p class="how_to_apply"><?php echo $job->getHowToApply() ?></p> <div class="meta"> <small>posted on <?php echo $job->getDateTimeObject('created_at')>format('m/d/Y') ?></small> </div> <div style="padding: 20px 0"> <a href="<?php echo url_for('job/edit?id='.$job->getId()) ?>"> Edit </a> </div> </div>

Esta plantilla utiliza la variable $job pasada por la accin para mostrar la informacin del puesto de trabajo. Como hemos rebautizado el nombre de variable

pasada a la plantilla de $jobeet_job a $job, es necesario tambin realizar este cambio en la accin show (tener cuidado, hay dos ocurrencias de la variable):
// apps/frontend/modules/job/actions/actions.class.php public function executeShow(sfWebRequest $request) { $this->job = Doctrine::getTable('JobeetJob')-> find($request>getParameter('id')); $this->forward404Unless($this->job); } La descripcin del trabajo usa el helper simple_format_text() par a formatearlo como HTML, sustituyendo los retornos de carro con<br /> por ejemplo. Como este helper pertenece al grupo de helper Text, que no se carga por defecto, tenemos que cargarlo manualmente utilizando el helper use_helper().

Los Slots
Ahora, el ttulo de todas las pginas se define en la etiqueta <title> del layout:
<title>Jobeet - Your best job board</title>

Sin embargo, para la pgina de puestos de trabajo, queremos darle ms informacin til, como el nombre de la empresa y el puesto de trabajo a ocupar. En Symfony, cuando una zona del layout depende de la plantilla para mostrarse, necesitas definir un slot:

Aade un slot al layout para permitir que el ttulo sea dinmico:


// apps/frontend/templates/layout.php <title><?php include_slot('title') ?></title>

Cada slot es definido por un nombre (title) y se pueden visualizar mediante el uso del helper include_slot(). Ahora, al comienzo de la plantilla showSuccess.php, usa el helper slot() para definir el contenido del slot para la pgina de puestos de trabajo:
// apps/frontend/modules/job/templates/showSuccess.php <?php slot( 'title',

sprintf('%s is looking for a %s', $job->getCompany(), $job>getPosition())) ?>

Si el ttulo es complejo de generar, el helper slot() tambin se puede utilizar con un bloque de cdigo:
// apps/frontend/modules/job/templates/showSuccess.php <?php slot('title') ?> <?php echo sprintf('%s is looking for a %s', $job->getCompany(), $job>getPosition()) ?> <?php end_slot(); ?>

Para algunas pginas, como la pgina de inicio, slo necesitamos un ttulo genrico. En lugar de repetir el mismo ttulo una y otra vez en las plantillas, podemos definir un ttulo predeterminado en el layout:
// apps/frontend/templates/layout.php <title> <?php if (!include_slot('title')): ?> Jobeet - Your best job board <?php endif; ?> </title>

El helper include_slot() regresa true si el slot se ha definido. Por lo tanto, cuando defines el contenido del slot title en una plantilla, ste es usado; sino, el ttulo predeterminado ser utilizado.
Ya hemos visto bastantes helpers comenzando con include_. Estos helpers generan el HTML y en la mayora de los casos tienen un helper get_ como contrapartida solo para devolver el contenido: <?php include_slot('title') ?> <?php echo get_slot('title') ?> <?php include_stylesheets() ?> <?php echo get_stylesheets() ?>

La Accin de la Pgina de Puestos de Trabajo


La pgina de puestos de trabajo es generada por la accin show, definida en el mtodo executeShow() del mdulo job:
class jobActions extends sfActions { public function executeShow(sfWebRequest $request) { $this->job = Doctrine::getTable('JobeetJob')-> find($request>getParameter('id')); $this->forward404Unless($this->job); } // ... }

Como en la accin index, la clase JobeetJob es usada para obtener un puesto de trabajo, esta vez es mediante el uso del mtodofind(). El parmetro de este mtodo es el identificador nico de un puesto de trabajo, su clave principal. La siguiente seccin explicar el por qu la sentencia $request>getParameter('id') devuelve la clave principal del puesto de trabajo. Si el puesto de trabajo no existe en la base de datos, queremos llevar al usuario a una pgina 404, que es exactamente lo que hace el mtodo forward404Unless(). Este toma un Booleano como primer argumento y, a menos que sea true, detiene el flujo de la actual ejecucin. Como los mtodos forward detienen la ejecucin de la accin enseguida lanzando un sfError404Exception, no necesitas despus volver atrs. En cuanto a las excepciones, la pgina que aparece al usuario es diferente en el entorno prod del entorno dev:

Antes de implementar el sitio web Jobeet para el servidor de produccin, aprenders cmo personalizar la pgina 404 por defecto. La Familia de Mtodos "forward" El forward404Unless es en realidad equivalente a: $this->forward404If(!$this->job); que tambin es equivalente a: if (!$this->job) { $this->forward404(); } El mtodo forward404() en s mismo es slo un atajo para: $this->forward('default', '404'); El mtodo forward() hace un forward a otra accin de la misma aplicacin; en el ejemplo anterior, para la accin 404 del mdulodefault. El mdulo default es includo con Symfony y da acciones predeterminadas para mostrar pginas 404, de seguridad, y de login.

La Peticin y la Respuesta
Cuando navegas por las pginas /job o /job/show/id/1 en tu navegador, ests iniciando un viaje de ida y vuelta al servidor web. El navegador est enviando una Peticin y el servidor devuelve una Respuesta. Ya hemos visto que Symfony encapsula la peticin en un object sfWebRequest (mir el mtodo executeShow()). Y como Symfony es un Framework Orientado a Objetos, la respuesta es tambin un objeto, de la clase sfWebResponse. Puedes acceder al objeto respuesta en la accin llamando a $this->getResponse(). Estos objetos proporcionan una gran cantidad de mtodos convenientes para acceder a la informacin de funciones PHP y variables globales PHP.
Por qu Symfony envuelve funcionalidades PHP existentes? En primer lugar, porque Symfony y sus mtodos son ms poderosos que su homlogo PHP. Luego, porque cuando se prueba una aplicacin, es mucho ms fcil para simular un request o un response con Objetos que tratar de ver alrededor de variables globales o trabajar con funciones PHP como header() la cuales hacen demasiado magia por detrs.

La Peticin La clase sfWebRequest envuelve a los arrays PHP globales $_SERVER, $_COOKIE, $_GET, $_POST, y $_FILES: Nombre del mtodo PHP equivalente
getMethod() getUri() getReferer() getHost() getLanguages() getCharsets() isXmlHttpRequest() getHttpHeader() getCookie() isSecure() getFiles() getGetParameter() getPostParameter() $_SERVER['REQUEST_METHOD'] $_SERVER['REQUEST_URI'] $_SERVER['HTTP_REFERER'] $_SERVER['HTTP_HOST'] $_SERVER['HTTP_ACCEPT_LANGUAGE'] $_SERVER['HTTP_ACCEPT_CHARSET'] $_SERVER['X_REQUESTED_WITH'] == 'XMLHttpRequest' $_SERVER $_COOKIE $_SERVER['HTTPS'] $_FILES $_GET $_POST

Nombre del mtodo PHP equivalente


getUrlParameter() getRemoteAddress() $_SERVER['PATH_INFO'] $_SERVER['REMOTE_ADDR']

Ya hemos accedido a parmetros de solicitud usando el mtodo getParameter(). Devuelve un valor de la variable global $_GET o $_POST, o de la variable PATH_INFO. Si deseas asegurarte de que un parmetro de la peticin procede de uno particular de estas variables, necesitas utilizar los mtodosgetGetParameter(), getPostParameter(), y getUrlParameter() respectivamente.
Si deseas restringir la accin de un mtodo especfico, por ejemplo, cuando que deseas asegurarte de que un formulario es enviado como POST, puede utilizar el mtodo isMethod(): $this->forwardUnless($request->isMethod('POST'));.

La Respuesta La clase sfWebResponse envuelve a los mtodos PHP header() y setrawcookie(): Nombre del mtodo
setCookie() setStatusCode() setHttpHeader() setContentType() addVaryHttpHeader()

PHP equivalente
setrawcookie() header() header() header() header()

addCacheControlHttpHeader() header()

Por supuesto, que la clase sfWebResponse tambin proporciona una manera de configurar el contenido de la respuesta (setContent()) y enviar la respuesta al navegador (send()). Hoy hemos visto cmo gestionar hojas de estilo y JavaScripts tanto en el view.yml como en las plantillas. Al final, ambas tcnicas utilizan el objeto Response y sus mtodos addStylesheet() y addJavascript().
Las clases sfAction, sfRequest, y sfResponse dan muchos otros mtodos tiles. No dudes en navegar por la API documentationpara aprender ms acerca de todas las clases internas de Symfony. TIP Las clases sfAction, sfRequest, y sfResponse dan muchos otros mtodos tiles. No dudes en navegar por la documentacin API para aprender ms acerca de todas las clases internas de Symfony.

El Enrutamiento
ahora deberas estar familiarizado con el patrn MVC y deberas sentir ms y ms natural esta forma de codificacin. Dedica un poco ms de tiempo con esto para no tener que volver y mirar hacia atrs. Para practicar un poco el da de ayer, hemos personalizado la pginas Jobeet y en el proceso, tambin examinamos varios conceptos de Symfony, como el layout, los helpers, y los slots. Hoy nos sumergiremos en el maravilloso mundo del Framework de Enrutamiento de Symfony .

Las URLs
Si haces clic en un puesto de trabajo en la pgina principal Jobeet, la URL se parece a esto: /job/show/id/1. Si ya has desarrollado sitios web PHP, probablemente ests ms acostumbrados a las URL como /job.php?id=1. Cmo Symfony hace para que funcione? Cmo Symfony determina la accin a llamar basndose en esta URL? Porqu el id de un job se obtiene con $request>getParameter('id')? Hoy, vamos a responder a todas estas preguntas. Pero primero, vamos a hablar acerca de las URL y exactamente que son ellas. En un contexto web, una URL es el identificador nico de un recurso web. Cuando accedes a una URL, ests pidiendo al navegador obtener un recurso identificado por esa URL. Por lo tanto, como la direccin URL es la interfaz entre la pgina web y el usuario, debe transmitir informacin significativa sobre algn recurso al que hace referencia. Pero las "tradicionales" URLs realmente no describen al recurso, sino que exponen la estructura interna de la aplicacin. Al usuario no le importa que tu sitio web sea desarrollado con el lenguaje PHP o que el puesto de trabajo tiene un cierto identificador en la base de datos. Exponer el funcionamiento interno de tu aplicacin es tambin es bastante malo en lo que medida de seguridad se refiere: Qu pasa si el usuario intenta adivinar la direccin URL de los recursos que no tienen acceso? As es, el desarrollador debe asegurarlos de la manera adecuada, pero ms te vale ocultar la informacin sensible. Las URL son tan importantes en Symfony que tiene todo un framework dedicado a su gestin: el framework de enrutamiento. El enrutamiento gestiona el URI interno y la URL externa. Cuando una peticin llega, el enrutamiento analiza la URL y la convierte en un URI interno. Ya has visto el URI interno de la pgina de puestos de trabajo en la plantilla indexSuccess.php:
'job/show?id='.$job->getId()

El helper url_for() convierte ste URI interno a una correcta URL:


/job/show/id/1

El URI interno est hecho de varias partes: job es el mdulo, show es la accin y la cadena de consulta aade los parmetros a pasar a la accin. El modelo genrico para los URIs internos es:
MDULO/ACCIN?clave=valor&clave_1=valor_1&...

Como el enrutamiento de Symfony es un proceso bidireccional, puedes cambiar las URLs sin cambiar la implementacin tcnica. Esta es una de las principales ventajas del patrn de diseo sobre controlador frontal.

La Configuracin del Enrutamiento


El mapeo entre los URIs internos y las URLs externas esta listo en el archivo de configuracin routing.yml:
# apps/frontend/config/routing.yml homepage: url: / param: { module: default, action: index } default_index: url: /:module param: { action: index } default: url: /:module/:action/*

El archivo routing.yml describe las rutas. Una ruta tiene un nombre (homepage), un patrn (/:module/:action/*), y algunos parmetros (bajo la clave param). Cuando una peticin llega, el Enrutamiento trata de hacerla coincidir la URL con un patrn dado. La primera ruta que coincida gana, por lo tanto el orden en routing.yml es importante. Echemos un vistazo a algunos ejemplos para comprender mejor cmo funciona esto. Cuando solicitas la pgina de inicio Jobeet, la cual tiene la URL /job, la primera ruta que coincide es con default_index. En un patrn, una palabra con un prefijo dos puntos (:) es una variable, por eso el patrn /:module significa: Concidir con un / seguida por cualquier cosa. En nuestro ejemplo, la variable module ser job como su valor. Este valor puede ser obtenido con $request->getParameter('module'). Esta ruta tambin define un valor por defecto para la variable action. Por eso, para todas las URLs que coincidan con esta ruta, la peticin tambin tendr un parmetro action con index como su valor. Si solicitas la pgina /job/show/id/1, Symfony coincidir con el ltimo patrn: /:module/:action/*. En un patrn, un asterisco (*) coincide con una coleccin de pares variable/valor separados por una barra (/):

Parmetro de peticin Valor mdulo accin id job show 1

Las variables mdulo y accin son especiales ya que son utilizados por Symfony para determinar la accin a ejecutar.

La URL /job/show/id/1 se pueden crear desde una plantilla utilizando la siguiente llamada al helper url_for():
url_for('job/show?id='.$job->getId())

Tambin puedes usar el nombre de la ruta gracias al prefijo @:


url_for('@default?module=job&action=show&id='.$job->getId())

Ambas llamadas son equivalentes, pero esta ltima es mucho ms rpido ya que el enrutamiento no tiene que analizar todas las rutas para encontrar el mejor patrn coincidente, y es menos complicado su implementacin (los nombres del mdulo y la accin no estn presentes en el URI interno).

Personalizaciones del Enrutamiento


Por el momento, cuando la solicitas la URL / en un navegador, tienes por defecto la pgina de felicitaciones de Symfony. Esto se debe a que esta URL coincide con la ruta homepage. Pero tiene sentido cambiarla para que no sea la pgina de inicio de Jobeet. Para hacer el cambio, modifica la variable module de la ruta homepage a job:
# apps/frontend/config/routing.yml homepage: url: / param: { module: job, action: index }

Puedes ahora cambiar el enlace al logo de Jobeet en el layout para usar la ruta homepage:
<!-- apps/frontend/templates/layout.php --> <h1> <a href="<?php echo url_for('@homepage') ?>"> <img src="/images/jobeet.gif" alt="Jobeet Job Board" /> </a> </h1>

Eso fue fcil!


Cuando se actualiza la configuracin del enrutamiento, los cambios son immediatamente tomados en cuenta en el entorno de desarrollo. Pero para hacerlos que tambien funcionen en el entorno de produccin, necesitas limpiar el cache ejecutando la tareacache:clear.

Para algo un poco ms aplicado, vamos a cambiar el la URL de la pgina job a algo ms significativo:
/job/sensio-labs/paris-france/1/web-developer

Sin saber nada acerca de Jobeet, y sin mirar en la pgina, se puede entender a partir de la URL que Sensio Labs est buscando un Web developer para trabajar en Paris, France.
Las URLs amigables son importantes porque ellas transmiten informacin al usuario. Es tambin til cuando copias y pegas la URL en un email o para optimizar tu sitio web para los motores de bsqueda.

El siguiente patrn coincide con esta URL:


/job/:company/:location/:id/:position

Edita el archivo routing.yml y agrega la ruta job_show_user al principio del archivo:


job_show_user: url: /job/:company/:location/:id/:position param: { module: job, action: show }

Si actualizas la pgina de inicio Jobeet, los enlaces a jobs no han cambiado. Esto se debe a que para generar una ruta, necesitas pasar todas las variables requeridas. Por eso, necesitas cambiar la llamada a url_for() en indexSuccess.php a:
url_for('job/show?id='.$job->getId().'&company='.$job->getCompany(). '&location='.$job->getLocation().'&position='.$job->getPosition())

Un URI interno tambin puede ser expresado como un array:


url_for(array( 'module' => 'action' => 'id' => 'company' => 'location' => 'position' => )) 'job', 'show', $job->getId(), $job->getCompany(), $job->getLocation(), $job->getPosition(),

Los Requisitos
Durante el primer da de tutora, hemos hablado de la validacin y manejo de errores por una buena razn. El sistema de enrutamiento tiene incorporada una funcin de validacin. Cada variable patrn pueda ser validada por una expresin regular definida utilizando la linea requirements en la definicin de la ruta:
job_show_user: url: /job/:company/:location/:id/:position param: { module: job, action: show } requirements: id: \d+

La anterior linea requirements fuerza a que el id sea un valor numrico. Sino lo es, la ruta no coincide.

La Clase Route
Cada ruta definida en routing.yml es internamente convertida en un objecto de la clase sfRoute. Esta clase se puede cambiar mediante una linea class en la definicin de la ruta. Si ests familiarizado con el protocolo HTTP, sabes que ste define varios "mtodos", comoGET, POST, HEAD, DELETE, y PUT. Los tres primeros cuentan con soporte en todos los navegadores, mientras que los otros dos no. Para restringir una ruta a slo la coincidencia con algunos mtodos, puedes modificar la clase de enrutamiento a sfRequestRoute y aadir un requisito para la variable virtual sf_method:
job_show_user: url: /job/:company/:location/:id/:position class: sfRequestRoute param: { module: job, action: show } requirements: id: \d+ sf_method: [get] Exigir una ruta para solo algunos mtodos HTTP no es totalmente equivalente a usar sfWebRequest::isMethod() en tus acciones. Eso es porque el enrutamiento continuar buscando por una ruta coincidente si el mtodo no coincide con el esperado

El Objecto de la Clase Route


La nuevo URI interno para un puesto de trabajo es bastante largo y tedioso de escribir, (url_for('job/show?id='.$job->getId().'&company='.$job>getCompany().'&location='.$job->getLocation().'&position='.$job>getPosition())), pero como acabamos de aprender en la seccin anterior, la clase route puede ser modificada. Para la ruta job_show_user, es mejor utilizarsfDoctrineRoute ya que la clase est optimizada para las rutas que

representan objetos Doctrine o colecciones de objetos Doctrine:


job_show_user: url: /job/:company/:location/:id/:position class: sfDoctrineRoute options: { model: JobeetJob, type: object } param: { module: job, action: show } requirements: id: \d+ sf_method: [get]

La linea options personaliza el comportamiento de la ruta. Aqu, la opcin model define la clase del mdelo Doctrine (JobeetJob) relacionada a la ruta, y la opcin type define que esta ruta est vinculada a un objeto (tambin puedes utilizar list si una ruta representa una coleccin de objetos).

La ruta job_show_user es ahora consciente de su relacin con JobeetJob y as podemos simplificar la llamada url_for() a:
url_for(array('sf_route' => 'job_show_user', 'sf_subject' => $job))

o solo:
url_for('job_show_user', $job) El primer ejemplo es muy til cuando necesitas pasar ms argumentos que slo un objeto.

Funciona porque todas las variables en la ruta tiene su mtodo correspondiente en la clase JobeetJob (por ejemplo, la variable companyes reemplazada con el valor de getCompany()). Si hechas una mirada a las URL generadas, no son todava bastantes amigables como queremos que sean:
http://www.jobeet.com.localhost/frontend_dev.php/job/Sensio+Labs/Paris%2C +France/1/Web+Developer

Tenemos que "slugear" los valores de columna mediante la sustitucin de todos los caracteres no ASCII por un -. Abre el archivoJobeetJob y aade los siguientes mtodos para la clase:
// lib/model/doctrine/JobeetJob.class.php public function getCompanySlug() { return Jobeet::slugify($this->getCompany()); } public function getPositionSlug() { return Jobeet::slugify($this->getPosition()); } public function getLocationSlug() { return Jobeet::slugify($this->getLocation()); }

A continuacin, crea el archivo lib/Jobeet.class.php y aadir el mtodo slugify en l:


// lib/Jobeet.class.php class Jobeet { static public function slugify($text) { // replace all non letters or digits by $text = preg_replace('/\W+/', '-', $text); // trim and lowercase $text = strtolower(trim($text, '-'));

return $text; } } En este tutorial, nunca mostramos la sentencia de apertura <?php en los ejemplos de cdigo que solo tienen cdigo PHP puro para optimizar el espacio. Deberas obviamente recordar agregarlos cuando creas un nuevo archivo PHP.

Tenemos definidos tres nuevos mtodos "virtuales" : getCompanySlug(), getPositionSlug(), y getLocationSlug(). Ellos devuelven sus correspondiente valor de columna despus de pasalos por el mtodo slugify(). Ahora, puedes sustituir los nombres de las columas reales por sus virtuales en la ruta job_show_user:
job_show_user: url: /job/:company_slug/:location_slug/:id/:position_slug class: sfDoctrineRoute options: { model: JobeetJob, type: object } param: { module: job, action: show } requirements: id: \d+ sf_method: [get]

Tendrs ahora las URLs esperadas:


http://www.jobeet.com.localhost/frontend_dev.php/job/sensio-labs/parisfrance/1/web-developer

Pero esa es slo la mitad de la historia. La ruta es capaz de generar una URL sobre la base de un objeto, pero tambin es capaz de encontrar el objeto en relacin con una determinada URL. Los objetos pueden ser recuperados con el mtodo getObject() de la ruta. Al analizar una peticin, el enrutamiento guarda la ruta del objeto coincidentes para que la uses en las acciones. Por lo tanto, cambia el mtodo executeShow() para utilizar el objeto route para obtener el objeto Jobeet:
class jobActions extends sfActions { public function executeShow(sfWebRequest $request) { $this->job = $this->getRoute()->getObject(); $this->forward404Unless($this->job); } // ... }

Si intentas obtener un job para un desconocido id, vers una pgina de error

404 pero el mensaje de error ha cambiado:

Esto es as porque el error 404 ha sido lanzado de forma automtica por el mtodo getRoute(). As, podemos simplificar el mtodoexecuteShow an ms:
class jobActions extends sfActions { public function executeShow(sfWebRequest $request) { $this->job = $this->getRoute()->getObject(); } // ... } Si no deseas que la ruta genere un error 404, puede establecer la opcin allow_empty a true. El objeto relacionado de una ruta es cargado ligeramente. Este es obtenido desde la base de datos solo si llamas al mtodogetRoute().

El Enrutamiento en Acciones y Plantillas


En una plantilla, el helper url_for() convierte una URI interna a una URL external. Algunos otros helpers symfony tambin toman una URI interna como un argumento, como el helper link_to() el cual genera una etiqueta <a>:
<?php echo link_to($job->getPosition(), 'job_show_user', $job) ?>

Genera el siguiente cdigo HTML:


<a href="/job/sensio-labs/paris-france/1/web-developer">Web Developer</a>

Para ambos url_for() y link_to() tambin pueden generar URL absoluta:


url_for('job_show_user', $job, true); link_to($job->getPosition(), 'job_show_user', $job, true);

Si deseas generar una URL desde una accin, puedes usar el mtodo generateUrl():
$this->redirect($this->generateUrl('job_show_user', $job)); La Familia de Mtodos "redirect" En el tutorial de ayer, hablamos acerca del mtodo "forward". Estos mtodos envan l apeticin actual a otra accin sin regresar al navegador. El mtodo "redirect" redirige al usuario a otra URL. Al igual que con forward, puedes utilizar el mtodo redirect(), o los mtodosredirectIf() y redirectUnless().

La Clase de Coleccin de Rutas


Para el mdulo job, ya tenemos personalizado la ruta de la accin show, pero las URLs para los otros mtodos (index, new, edit,create, update, and delete) estn aun gestionadas por la ruta default:
default: url: /:module/:action/*

La ruta default es una gran manera de comenzar la codificacin sin definir demasiadas rutas. Pero como la ruta acta como un "catch-all", no puede ser configurada para necesidades especficas. Como todas las acciones job estn relacionadas con el modelo de la clase JobeetJob, podemos facilmente definir una ruta particularsfDoctrineRoute para cada uno de ellas como ya lo hemos hecho para la accin show. Sin embargo, como el mdulo job define las clsicas siete posibles acciones para un modelo, tambin podemos utilizar la clase sfDoctrineRouteCollection. Abre el archivorouting.yml y modificalo para que se lea como sigue:
# apps/frontend/config/routing.yml job: class: sfDoctrineRouteCollection options: { model: JobeetJob } job_show_user: url: /job/:company_slug/:location_slug/:id/:position_slug class: sfDoctrineRoute options: { model: JobeetJob, type: object } param: { module: job, action: show } requirements: id: \d+ sf_method: [get] # default rules homepage: url: / param: { module: job, action: index } default_index: url: /:module param: { action: index } default: url: /:module/:action/*

La ruta job anterior es en realidad un acceso directo que generar automticamente las siguientes siete rutas sfDoctrineRoute:
job: url: /job.:sf_format

class: sfDoctrineRoute options: { model: JobeetJob, type: list } param: { module: job, action: index, sf_format: html } requirements: { sf_method: get } job_new: url: /job/new.:sf_format class: sfDoctrineRoute options: { model: JobeetJob, type: object } param: { module: job, action: new, sf_format: html } requirements: { sf_method: get } job_create: url: /job.:sf_format class: sfDoctrineRoute options: { model: JobeetJob, type: object } param: { module: job, action: create, sf_format: html } requirements: { sf_method: post } job_edit: url: /job/:id/edit.:sf_format class: sfDoctrineRoute options: { model: JobeetJob, type: object } param: { module: job, action: edit, sf_format: html } requirements: { sf_method: get } job_update: url: /job/:id.:sf_format class: sfDoctrineRoute options: { model: JobeetJob, type: object } param: { module: job, action: update, sf_format: html } requirements: { sf_method: put } job_delete: url: /job/:id.:sf_format class: sfDoctrineRoute options: { model: JobeetJob, type: object } param: { module: job, action: delete, sf_format: html } requirements: { sf_method: delete } job_show: url: /job/:id.:sf_format class: sfDoctrineRoute options: { model: JobeetJob, type: object } param: { module: job, action: show, sf_format: html } requirements: { sf_method: get } Algunas rutas generadas por sfDoctrineRouteCollection tienen la misma URL. El enrutamiento est an disponible para usarlas porque todas ellas tienen diferentes parmetro en el mtodo HTTP.

Las rutas job_delete y job_update require mtodos HTTP que no son compatibles con los navegadores (DELETE y PUTrespectivamente). Esto funciona porque los simula Symfony. Abre la plantilla _form.php Para ver un ejemplo:
// apps/frontend/modules/job/templates/_form.php <form action="..." ...> <?php if (!$form->getObject()->isNew()): ?> <input type="hidden" name="sf_method" value="PUT" /> <?php endif; ?> <?php echo link_to( 'Delete', 'job/delete?id='.$form->getObject()->getId(), array('method' => 'delete', 'confirm' => 'Are you sure?') ) ?>

Todos los helpers symfony pueden ser invocados para simular cualquier mtodo HTTP que desea pasando el parmetro especialsf_method.
Symfony tiene otros parmetros especiales como sf_method, todo lo que inicie con el prefijo sf_. En las rutas generadas antes, se puede ver otro: sf_format, que se explicar en los prximos das.

Debugeando la Ruta
Cuando se utiliza una coleccin de rutas, a veces es til listar de rutas generadas. La tarea app:routes muestra todas las rutas para una aplicacin determinada:
$ php symfony app:routes frontend

Tambin puedes tener una gran cantidad de informacin de depuracin para una ruta pasando su nombre como un argumento adicional:
$ php symfony app:routes frontend job_edit

Las Rutas por defecto


Es una buena prctica definir las rutas para todas tus URLs. Ya que la ruta job define todas las rutas necesarias para describir la aplicacin Jobeet, sigue adelante y quita o comenta las rutas por defecto del archivo de configuracin routing.yml:
# apps/frontend/config/routing.yml #default_index: # url: /:module # param: { action: index } # #default: # url: /:module/:action/*

La aplicacin Jobeet debe seguir funcionando como antes.

Mas cerca del Modelo yer fue un gran da. Aprendiste como crear URLs amigables y como usar el framework Symfony para automatizar un montn de cosas por ti. Hoy, mejoraremos el sitio web Jobeet afinando el cdigo aqu y all. En el proceso, aprenders ms acerca de todas las caractersticas que hemos introducido durante los primeros cinco dias de este tutorial.

El Objeto Query de Doctrine


De los requisitos del da 2: "Cuando un usuario llega al sitio de Jobeet, ver una lista de los puestos de trabajos activos." Pero hasta ahora, todos los puestos de trabajo sern mostrados, sea que estn activos o no:
// apps/frontend/modules/job/actions/actions.class.php class jobActions extends sfActions { public function executeIndex(sfWebRequest $request) { $this->jobeet_jobs = Doctrine::getTable('JobeetJob') ->createQuery('a') ->execute(); } // ... }

Un puesto de trabajo activo es uno que fue envado hace menos de 30 das. El mtodo Doctrine_Query::execute() ejecutar una peticin contra la base de datos. En el cdigo anterior, no hemos especificado ninguna condicin lo que significa que todos los registros son obtenidos de la base de datos. Cambiemos para que solo seleccione los puestos de trabajo activos:
public function executeIndex(sfWebRequest $request) { $q = Doctrine_Query::create() ->from('JobeetJob j') ->where('j.created_at > ?', date('Y-m-d h:i:s', time() - 86400 * 30)); $this->jobeet_jobs = $q->execute(); }

Depurando por Doctrine el SQL generado


Como no escribiste ninguna sentencia SQL a mano, el Doctrine cuidar de las diferencias que hay entre los motores de base de datos y generar las sentencias

SQL optimizadas para el motor de la base de datos que elejste el da 3. Pero algunas veces, es de gran ayuda para ver el SQL generado por el Doctrine; por ejemplo, para depurar una consulta que no funciona como esperamos. En el entorno dev, symfony registra esas consultas (junto a otras muchas ms) en el directorio log/. Hay un archivo log para cada combinacion de aplicacin y entorno. El archivo que estmos buscando es frontend_dev.log:
# log/frontend_dev.log Dec 04 13:58:33 symfony [info] {sfDoctrineLogger} executeQuery : SELECT j.id AS j__id, j.category_id AS j__category_id, j.type AS j__type, j.company AS j__company, j.logo AS j__logo, j.url AS j__url, j.position AS j__position, j.location AS j__location, j.description AS j__description, j.how_to_apply AS j__how_to_apply, j.token AS j__token, j.is_public AS j__is_public, j.is_activated AS j__is_activated, j.email AS j__email, j.expires_at AS j__expires_at, j.created_at AS j__created_at, j.updated_at AS j__updated_at FROM jobeet_job j WHERE j.created_at > ? (2008-11-08 01:13:35)

Puedes ver por t mismo que Doctrine tiene una clasula where para la columna created_at (WHERE j.created_at > ?).
La cadena ? en la consulta indica que Doctrine genera una sentencia preparada. El valor actual de ? ('2008-11-08 01:13:35' en el ejemplo anterior) es pasado durante la ejecucin de la consulta y escapado apropiadamente por el motor de la base de datos. El uso de sentencias preparadas dramticamente reduce tu exposicin a los ataques de inyecciones SQL.

Esto esta bueno, pero es bastante molesto tener que cambiar del navegador , al IDE, y el archivo log cada vez que necesitas probar un cambio. Gracias a la barra web de depuracin de symfony, toda la informacin que necesitas esta tambin disponible dentro de la comodidad de tu navegador:

Serializacin de Objetos
An si el cdigo anterior funciona, esta lejos de ser perfecto ya que no toma en cuenta algunos requisitos del da 2: "Un usuario puede volver a re-activar y extender la validez de un puesto de trabajo por 30 das extra..." Pero ya que el cdigo anterior solo se basa en el valor de created_at, y porque esta columna almacena el da de creacin, no podemos satisfacer el requisito anterior. Pero si recuerdas el esquema de la base de datos que describimos durante el da 3, tambin tenemos definido una columna expires_at. Actualmente este valor esta siempre vaco ya que este no se establece en el archivo de datos. Pero

cuando un puesto de trabajo es creado, puede ser automticamente establecido a 30 das del da actual. Cuando necesitas hacer algo automticamente antes que un objeto Doctrine sea guardado en la base de datos, puedes sobreescribir el mtodo save() de la clase del modelo:
// lib/model/doctrine/JobeetJob.class.php class JobeetJob extends BaseJobeetJob { public function save(Doctrine_Connection $conn = null) { if ($this->isNew() && !$this->getExpiresAt()) { $now = $this->getCreatedAt() ? $this>getDateTimeObject('created_at')->format('U') : time(); $this->setExpiresAt(date('Y-m-d H:i:s', $now + 86400 * 30)); } return parent::save($conn); } // ... }

El mtodo isNew() devuelve true cuando el objeto no ha sido serializado an en la base de datos, y false de lo contratio. Ahora, vamos a cambiar la accin para usar la columna expires_at en lugar de created_at para seleccionar los puestos de trabajo activos:
public function executeIndex(sfWebRequest $request) { $q = Doctrine_Query::create() ->from('JobeetJob j') ->where('j.expires_at > ?', date('Y-m-d h:i:s', time())); $this->jobeet_jobs = $q->execute(); }

Restringimos la consulta para solo seleccionar los puestos de trabajo con un da expires_at en el futuro.

Con Datos
Actualizando la pgina de inicio de Jobeet en tu navegador vemos que no cambiamos ningn puesto de trabajo en la base de datos que habamos dejado hace unos pocos das atrs. Vamos a cambiar el archivo fixtures para agregar un puesto de trabajo que ya haya expirado:
# data/fixtures/jobs.yml JobeetJob: # other jobs

expired_job: JobeetCategory: programming company: Sensio Labs position: Web Developer location: Paris, France description: Lorem ipsum dolor sit amet, consectetur adipisicing elit. how_to_apply: Send your resume to lorem.ipsum [at] dolor.sit is_public: true is_activated: true created_at: '2005-12-01 00:00:00' token: job_expired email: job@example.com Ten cuidado cuando copies y pegues cdifo en un archivo de datos para no romper la indentacin. El expired_job debe solo tener dos espacios en blanco despus de si.

Recarga los datos y actualiza tu navegador para asegurarte que los viejos puestos de trabajo no se muestran ms:
$ php symfony doctrine:data-load

Tambin puedes ejecutar la siguiente consulta para asegurarte que la columna expires_at es automticamente completada por el mtodo save(), basado en el valor de created_at:
SELECT `position`, `created_at`, `expires_at` FROM `jobeet_job`;

Configuracin Personalizada
En el mtodo JobeetJob::save(), hemos tenido que hardcodear el nmero de das para que los puestos de trabajo expiren. Podra mejorarse haciendo que los 30 das sean configurables. El framework Symfony trae includo un archivo de configuracin para la configuracin especfica de una aplicacin, el archivo app.yml. Este archivo de formato YAML puede contener cualquier configuracin de desees:
# apps/frontend/config/app.yml all: active_days: 30

En la aplicacin, esas configuraciones estn disponibles a travs de la clase global sfConfig:


sfConfig::get('app_active_days')

El parmetro tiene un prefijo app_ porque la clase sfConfig tambin da acceso a la configuracin de symfony como veremos ms tarde. Vamos a actualizar el cdigo para tomar esta nueva configuracin en cuenta:
public function save(Doctrine_Connection $conn = null) { if ($this->isNew() && !$this->getExpiresAt())

{ $now = $this->getCreatedAt() ? $this>getDateTimeObject('created_at')->format('U') : time(); $this->setExpiresAt(date('Y-m-d H:i:s', $now + 86400 * sfConfig::get('app_active_days'))); } return parent::save($conn); }

El archivo de configuracin app.yml e una gran forma de centralizar configuraciones globales para tu aplicacin.

Refactorizando
Todo el cdigo escrito funciona bien, pero an no esta del todo bien. Puedes ver el problema? El cdigo Doctrine_Query no pertenece a la accin (capa del Controlador), sino que pertenece a la capa del Modelo. En el modelo MVC, el modelo define toda la lgicas de negocios, y el Controlador solo invoca al modelo para obtener los datos de ste. Como el cdigo devuelve una coleccin de puestos de trabajo, vamos a mover el cdigo a la clase JobeetJobTable y crear un mtodo getActiveJobs():
// lib/model/doctrine/JobeetJobTable.class.php class JobeetJobTable extends Doctrine_Table { public function getActiveJobs() { $q = $this->createQuery('j') ->where('j.expires_at > ?', date('Y-m-d h:i:s', time())); return $q->execute(); } }

Ahora el cdigo de la accin puede usar este nuevo mtodo para obtener los puestos de trabajo activos:
public function executeIndex(sfWebRequest $request) { $this->jobeet_jobs = Doctrine_Core::getTable('JobeetJob')>getActiveJobs(); }

Esta refactorizacin tiene varios beneficios sobre el anterior cdigo:


La lgica para obtener los puestos de trabajo activos est ahora en el modelo, donde pertenerce El cdigo en el controlador es mucho mas legible El mtodo getActiveJobs() es re-usable (por ejemplo en otra accin) El cdigo del modelo ahora puede ser probado con pruebas unitarias

Vamos a ordenar los puestos de trabajo por la columna expires_at:


public function getActiveJobs() { $q = $this->createQuery('j') ->where('j.expires_at > ?', date('Y-m-d h:i:s', time())) ->orderBy('j.expires_at DESC'); return $q->execute(); }

El mtodo orderBy aade una clasula ORDER BY al SQL generado (addOrderBy() tambin existe).

Categoras en la Pgina de Inicio


De los requisitos del da 2: "Los puestos de trabajo son ordenados por categora y entonces por la fecha de publicacin (los nuevos primeros)." Hasta ahora, no tenamos la categora en cuenta. De los requisitos, la pgina de inicio debe mostrar los puestos de trabajo por categora. Primero, necesitamos obtener todas las categoras con al menos un puesto de trabajo activo. Abre la clase JobeetCategoryTable y agregale el mtodo getWithJobs():
// lib/model/doctrine/JobeetCategoryTable.class.php class JobeetCategoryTable extends Doctrine_Table { public function getWithJobs() { $q = $this->createQuery('c') ->leftJoin('c.JobeetJobs j') ->where('j.expires_at > ?', date('Y-m-d h:i:s', time())); return $q->execute(); } }

Cambia la accin index adecuadamente:


// apps/frontend/modules/job/actions/actions.class.php public function executeIndex(sfWebRequest $request) { $this->categories = Doctrine_Core::getTable('JobeetCategory')>getWithJobs(); }

En la plantilla, necesitamos iterar a travs de todas las categoras y mostrar los puestos de trabajo activos:
// apps/frontend/modules/job/indexSuccess.php <?php use_stylesheet('jobs.css') ?>

<div id="jobs"> <?php foreach ($categories as $category): ?> <div class="category_<?php echo Jobeet::slugify($category->getName()) ?>"> <div class="category"> <div class="feed"> <a href="">Feed</a> </div> <h1><?php echo $category ?></h1> </div> <table class="jobs"> <?php foreach ($category->getActiveJobs() as $i => $job): ?> <tr class="<?php echo fmod($i, 2) ? 'even' : 'odd' ?>"> <td class="location"> <?php echo $job->getLocation() ?> </td> <td class="position"> <?php echo link_to($job->getPosition(), 'job_show_user', $job) ?> </td> <td class="company"> <?php echo $job->getCompany() ?> </td> </tr> <?php endforeach; ?> </table> </div> <?php endforeach; ?> </div> Para mostrar el nomre de la categora en la plantilla, usamos echo $category. Te suena raro? $category es un objeto, Cmo puedeecho mgicamente mostrar el nombre de la categora? La respuesta fue dada durante el da 3 cuando tenamos que definir el mtodo mgico __toString() para todas las clasese del modelo.

Para que funcione, necesitamos agregar el mtodo getActiveJobs() a la clase JobeetCategory que devuelve los puestos de trabajo activos para el objeto categora:
// lib/model/doctrine/JobeetCategory.class.php public function getActiveJobs() { $q = Doctrine_Query::create() ->from('JobeetJob j') ->where('j.category_id = ?', $this->getId()); return Doctrine_Core::getTable('JobeetJob')->getActiveJobs($q); }

El mtodo JobeetCategory::getActiveJobs() usa al mtodo Doctrine::getTable('JobeetJob')->getActiveJobs() para obtener los puestos de trabajo activos para una categora dada. Cuando llamamos al Doctrine::getTable('JobeetJob')->getActiveJobs(), lo queremos para restringir la condicin an ms para una categora dada. En lugar de pasar el objeto categora, tenemos decidido pasar el objeto Doctrine_Query ya que este es la mejor forma de encapsular una condicin genrica. El mtodo getActiveJobs() necesita combinar este objeto Doctrine_Query con su propio consulta. Ya que Doctrine_Query es un objeto, esto es bastante simple:
// lib/model/doctrine/JobeetJobTable.class.php public function getActiveJobs(Doctrine_Query $q = null) { if (is_null($q)) { $q = Doctrine_Query::create() ->from('JobeetJob j'); } $q->andWhere('j.expires_at > ?', date('Y-m-d h:i:s', time())) ->addOrderBy('j.expires_at DESC'); return $q->execute(); }

Limitar los Resultados


An queda un requisito por implementar para la lista de puestos de trabajo de la pgina de inicio: "Por cada categora, la lista solo muestra los primeros 10 puestos de trabajo y un enlace que permite listar todos los puestos de una categora dada." Es tn simple de agregar al mtodo getActiveJobs():
// lib/model/doctrine/JobeetCategory.class.php public function getActiveJobs($max = 10) { $q = Doctrine_Query::create() ->from('JobeetJob j') ->where('j.category_id = ?', $this->getId()) ->limit($max); return Doctrine_Core::getTable('JobeetJob')->getActiveJobs($q); }

La apropiada clasula LIMIT es ahora hardcodeada dentro del Modelo, pero es mejor que este valor sea configurable. Cambia la plantilla para pasar el nmero mximo de puestos de trabajo establecido en app.yml:
<!-- apps/frontend/modules/job/indexSuccess.php -->

<?php foreach ($category>getActiveJobs(sfConfig::get('app_max_jobs_on_homepage')) as $i => $job): ?>

y agrega esta nueva configuracin en app.yml:


all: active_days: 30 max_jobs_on_homepage: 10

Datos Dinmicos
A menos que bajes el max_jobs_on_homepage, no vers ninguna diferencia. Necesitamos agregar un paquete de puestos de trabajo a los archivos fixtures de datos. Por eso, puedes copiar y pegar uno existente, diez, o veinte veces a mano... pero hay una mejor manera. La duplicacin esta mal, an es archivos fixture. Symfony al rescate! Los archivos YAML en symfony pueden tener cdigo PHP que ser evaluado justo antes de ser analizado. Edita el archivo de datos jobs.yml y aade el siguiente cdigo al final:
JobeetJob:

# Starts at the beginning of the line (no whitespace before) <?php for ($i = 100; $i <= 130; $i++): ?> job_<?php echo $i ?>: JobeetCategory: programming company: Company <?php echo $i."\n" ?> position: Web Developer location: Paris, France description: Lorem ipsum dolor sit amet, consectetur adipisicing elit. how_to_apply: | Send your resume to lorem.ipsum [at] company_<?php echo $i ?>.sit is_public: true is_activated: true token: job_<?php echo $i."\n" ?> email: job@example.com <?php endfor ?>

Ten cuidado, al analizar YAML no olvides ninguna indentacin. Manten en mente los siguientes simples tips cuando aadas cdigo PHP a un archivo YAML

La declaracin <?php ?> debe siempre empezar la linea o ser incrustada en un valor. Si una declaracin <?php ?> finaliza una linea, necesitars explcitamente agregar una nueva linea ("\n").

Puedes ahora recargar los archivos de datos con la tarea doctrine:data-load y ver si solo 10 puestos de trabajo son mostrados en la pgina de inicio para la categora Programming. En la siguiente captura de pantalla, tenemos modificado el nmero mximo de puestos de trabajo a cinco para hacer una imgen mas pequea:

Asegurar la Pgina
Cuando un puesto de trabajo expira, aun sabiendo la URL, no debera ser posible acceder a l nunca ms. Prueba con la URL para el puesto de trabajo expirado (reemplaza el id con el actual id en tu base de datos - SELECT id, token FROM jobeet_job WHERE expires_at < NOW()):
/frontend_dev.php/job/sensio-labs/paris-france/ID/web-developer-expired

En lugar de mostrar la informacin, necesitars redirigir al usuario a una pgina 404. Perp, Cmo puedo hacer esto cuando la info es cargada automaticamente va la ruta?
# apps/frontend/config/routing.yml job_show_user: url: /job/:company_slug/:location_slug/:id/:position_slug class: sfDoctrineRoute options: model: JobeetJob type: object method_for_query: retrieveActiveJob param: { module: job, action: show } requirements: id: \d+ sf_method: [GET]

El mtodo retrieveActiveJob() recibir el objeto Doctrine_Query ya listo por parte de la ruta:


// lib/model/doctrine/JobeetJobTable.class.php class JobeetJobTable extends Doctrine_Table { public function retrieveActiveJob(Doctrine_Query $q) { $q->andWhere('a.expires_at > ?', date('Y-m-d h:i:s', time())); return $q->fetchOne(); } // ... }

Ahora, si tratas de ontener un puesto de trabajo expirado, sers enviadoa una pgina 404.

Enlazar a la Pgina de la Categora


Ahora, vamos a agregar un enlace a la pgina de la

categoras en la pgina de inicio y crear dicha pgina. Pero, aguarda un minuto. La hora no termin aun y ya hemos trabajado mucho. Por eso, ests libre y con suficiente conocimientos para hacer esto por t mismo.! Vamos hacer el ejecicio. Revisa maana nuestra implementcin.

Jugando con la pagina de Categorias

La Ruta Category
Primero, necesitamos agregar una ruta para definir una URL amigable para la pgina de la categora. Agrgalo al inicio del archivo de enrutamiento:
# apps/frontend/config/routing.yml category: url: /category/:slug class: sfDoctrineRoute param: { module: category, action: show } options: { model: JobeetCategory, type: object } Si vas a comenzar la implementacin de una nueva funcionalidad, es una buena prctica primero pensar acerca de la URL y crear la ruta asociada. Y esto es obligatorio si quitas las reglas de enrutamiento por defecto.

Como slug no es una columna de la tabla category, necesitamos para agregar un mtodo virtual en JobeetCategory para que la ruta funcione: Un ruta puede usar cualquier columna de su objeto asociado como parmetro. Tambin puede usa cualquier otro valor si hay un mtodo asociado definido en la clase del objeto. Debido a que el parmetro slug no tiene una columna correspondiente en la tablacategory, necesitamos agregar un mtodo de acceso virtual en JobeetCategory para que la ruta funcione:
// lib/model/doctrine/JobeetCategory.class.php public function getSlug() { return Jobeet::slugify($this->getName()); }

El Enlace Categora
Ahora, edita la plantilla indexSuccess.php del mdulo job para agregar el enlace a la pgina de la categora:
<!-- some HTML code --> <h1> <?php echo link_to($category, 'category', $category) ?> </h1> <!-- some HTML code --> </table> <?php if (($count = $category->countActiveJobs() sfConfig::get('app_max_jobs_on_homepage')) > 0): ?> <div class="more_jobs">

and <?php echo link_to($count, 'category', $category) ?> more... </div> <?php endif; ?> </div> <?php endforeach; ?> </div>

Solo agregamos el enlace si hay ms de 10 puestos de trabajo a mostrar para la categora actual. El enlace tiene el nmero de puestos de trabajo no mostrados. Para que esta plantilla funcione, necesitamos agregar el mtodo countActiveJobs() a JobeetCategory:
// lib/model/doctrine/JobeetCategory.class.php public function countActiveJobs() { $q = Doctrine_Query::create() ->from('JobeetJob j') ->where('j.category_id = ?', $this->getId()); return Doctrine_Core::getTable('JobeetJob')->countActiveJobs($q); }

El mtodo countActiveJobs() emplea un mtodo countActiveJobs() que an no existe en JobeetJobTable. Reemplaza el contenido del archivo JobeetJobTable.php con el siguiente cdigo: [php] // lib/model/doctrine/JobeetJobTable.class.php class JobeetJobTable extends Doctrine_Table { public function retrieveActiveJob(Doctrine_Query $q) { return $this->addActiveJobsQuery($q)->fetchOne(); }
public function getActiveJobs(Doctrine_Query $q = null) { return $this->addActiveJobsQuery($q)->execute(); } public function countActiveJobs(Doctrine_Query $q = null) { return $this->addActiveJobsQuery($q)->count(); } public function addActiveJobsQuery(Doctrine_Query $q = null) { if (is_null($q)) { $q = Doctrine_Query::create() ->from('JobeetJob j'); } $alias = $q->getRootAlias(); $q->andWhere($alias . '.expires_at > ?', date('Y-m-d h:i:s', time()))

->addOrderBy($alias . '.expires_at DESC'); return $q; } }

Como puedes ver por ti mismo, tenemos que refactorizar todo el cdigo de JobeetJobTable para introducir un nuevo mtodo compartido addActiveJobsQuery() para hacer el cdigo ms DRY (Don't Repeat Yourself).
La primera vez, un trozo de cdigo es re-usado, copiando el cdigo puede ser suficiente. Pero si encuentras otra funcin para l necesitas refactorizar para reutilizar la funcin o mtodo, como hemos hecho aqu.

En el mtodo countActiveJobs(), en vez de usar execute() y recin contar el nmero de resultados, usamos el mtodo mas rpidocount(). Hemos cambiado un montn de archivos, recin para esta simple funcionalidad. Pero cada vez que debas agregar algn cdigo, tenemos que tratar de ponerlo en la capa correcta de la aplicacin y tambin tratar de hace el cdigo ms reusable. En el proceso, tenemos que rectorizar algn cdigo existente. Ese es el tpico workflow cuando trabajamos en un proyecto symfony.

Creacin del Mdulo Category


Es hora de crear el mdulo category:
$ php symfony generate:module frontend category

Si has creado un mdulo, probablemente has utilizado el doctrine:generatemodule. Eso est bien, pero como no es necesario el 90% del cdigo generado, usa generate:module para crear un mdulo vaco.
Por qu no aadir una accin category al mdulo job? Podramos, pero como el tema principal de la pgina de categora es una categora, se siente ms natural crear un mdulo dedicado category.

Al acceder a la pgina de categora, la ruta category tendr que encontrar la categora asociada con la variable slug de la peticin. Pero como slug no se almacena en la base de datos, y porque no podemos deducir el nombre de la categora del slug, no hay forma de encontrar la categora asociada con el slug.

Actualizar la Base de Datos


Tenemos que aadir una columna slug para la tabla category: Esta columna slug puede ser tomada con cuidado por un comportamiento Doctrine llamado Sluggable. Simplemente necesitamos habilitar el comportamiento sobre nuestro modelo JobeetCategory y este se encargar de todo por t.
# config/doctrine/schema.yml JobeetCategory: actAs: Timestampable: ~ Sluggable: fields: [name] columns: name: type: string(255) notnull: true

Ahora que slug es una columna real, es necesario eliminar el mtodo getSlug() de JobeetCategory.
La configuracin de la columna slug es tomada automticamente cuando guardas un registro. El slug es armado usando el valor del campo name y se lo da al objeto.

Usa la tarea doctrine:build --all --and-load para actualizar las tablas de la base de datos, y llenar la base de datos con nuestros datos:
$ php symfony doctrine:build --all --and-load --no-confirmation

Tenemos ahora todo en su lugar para crear el mtodo executeShow(). Reemplaza el contenido del archivo de acciones category con el siguiente cdigo:

// apps/frontend/modules/category/actions/actions.class.php class categoryActions extends sfActions { public function executeShow(sfWebRequest $request) { $this->category = $this->getRoute()->getObject(); } } Debido a que quitamos el mtodo generado executeIndex(), tambin puedes quitar la platilla automticamente generadaindexSuccess.php (apps/frontend/modules/category/templates/indexSu ccess.php).

El ltimo paso es crear la plantilla showSuccess.php:


// apps/frontend/modules/category/templates/showSuccess.php <?php use_stylesheet('jobs.css') ?> <?php slot('title', sprintf('Jobs in the %s category', $category>getName())) ?> <div class="category"> <div class="feed"> <a href="">Feed</a> </div> <h1><?php echo $category ?></h1> </div> <table class="jobs"> <?php foreach ($category->getActiveJobs() as $i => $job): ?> <tr class="<?php echo fmod($i, 2) ? 'even' : 'odd' ?>"> <td class="location"> <?php echo $job->getLocation() ?> </td> <td class="position"> <?php echo link_to($job->getPosition(), 'job_show_user', $job) ?> </td> <td class="company"> <?php echo $job->getCompany() ?> </td> </tr> <?php endforeach; ?> </table>

Elementos Parciales o Partials


Nota del Traductor Los Partials o Parciales, son elementos que se usan en las plantillas, haciendo uso del helperinclude_partial(), es por eso que su traduccin literal no es muy amigable, ya que debemos pensar no en parcial sino en porcin (snippet de cdigo de la capa Vista)

Observa que hemos copiado y pegado la etiqueta <table> que crear una lista de puestos de trabajo en la plantilla indexSuccess.php. Eso esta mal. Es tiempo para aprender un nuevo truco. Cuando necesites volver a utilizar una parte de una plantilla, lo que necesitas es crear un partial. Un partial es un snippet de cdigo de plantilla que puede ser compartido entre varias plantillas. Un partial es slo otra plantilla que comienza con un guin bajo (_). Crea el archivo _list.php:
// apps/frontend/modules/job/templates/_list.php <table class="jobs"> <?php foreach ($jobs as $i => $job): ?> <tr class="<?php echo fmod($i, 2) ? 'even' : 'odd' ?>"> <td class="location"> <?php echo $job->getLocation() ?> </td> <td class="position"> <?php echo link_to($job->getPosition(), 'job_show_user', $job) ?> </td> <td class="company"> <?php echo $job->getCompany() ?> </td> </tr> <?php endforeach; ?> </table>

Puedes incluir un partial utilizando el helper include_partial():


<?php include_partial('job/list', array('jobs' => $jobs)) ?>

El primer argumento de include_partial() es el nombre del partial (hecho del nombre del mdulo, una /, y el nombre del partial sin el_). El segundo argumento es un array de las variables a pasar al partial.
Por qu no utilizar el mtodo include() includo en PHP en lugar del helper include_partial()? La principal diferencia entre los dos es el soporte de cache includo del helper include_partial().

Reemplaza el HTML <table> de ambas plantillas con la llamada a include_partial():


// in apps/frontend/modules/job/templates/indexSuccess.php <?php include_partial('job/list', array('jobs' => $category>getActiveJobs(sfConfig::get('app_max_jobs_on_homepage')))) ?> // in apps/frontend/modules/category/templates/showSuccess.php <?php include_partial('job/list', array('jobs' => $category>getActiveJobs())) ?>

Lista Paginada
De los requisitos del da 2:

"La lista es paginada, con 20 puestos de trabajo por pgina." Para paginar una lista de un Objetos Doctrine, symfony proporciona una clase dedicada a ello: sfDoctrinePager. En la accin categoryen lugar de pasar los objetos (jobs) de los puestos de trabajo a la plantilla, pasamos un paginador:
// apps/frontend/modules/category/actions/actions.class.php public function executeShow(sfWebRequest $request) { $this->category = $this->getRoute()->getObject(); $this->pager = new sfDoctrinePager( 'JobeetJob', sfConfig::get('app_max_jobs_on_category') ); $this->pager->setQuery($this->category->getActiveJobsQuery()); $this->pager->setPage($request->getParameter('page', 1)); $this->pager->init(); } El mtodo sfRequest::getParameter() toma un valor por defecto como segundo argumento. En la accin anterior, si el parmetro de la peticin page no existe, entonces getParameter() devolver 1.

El constructor de sfDoctrinePager tiene una clase del modelo y el nmero mximo de elementos a regresar por pgina. Aade este ltimo valor a tu archivo de configuracin:
# apps/frontend/config/app.yml all: active_days: 30 max_jobs_on_homepage: 10 max_jobs_on_category: 20

El mtodo sfDoctrinePager::setQuery() toma un objeto Doctrine_Query para utilizarlo a la hora de seleccionar los elementos de la base de datos. Agrega el mtodo getActiveJobsCriteria():
// lib/model/doctrine/JobeetCategory.class.php public function getActiveJobsQuery() { $q = Doctrine_Query::create() ->from('JobeetJob j') ->where('j.category_id = ?', $this->getId()); return Doctrine_Core::getTable('JobeetJob')->addActiveJobsQuery($q); }

Ahora que tenemos el mtodo getActiveJobsQuery(), podemos refactorizar otros mtodos de JobeetCategory para usarlos:
// lib/model/doctrine/JobeetCategory.class.php public function getActiveJobs($max = 10)

{ $q = $this->getActiveJobsQuery() ->limit($max); return $q->execute(); } public function countActiveJobs() { return $this->getActiveJobsQuery()->count(); }

Por ltimo, vamos a actualizar la plantilla:


<!-- apps/frontend/modules/category/templates/showSuccess.php --> <?php use_stylesheet('jobs.css') ?> <?php slot('title', sprintf('Jobs in the %s category', $category>getName())) ?> <div class="category"> <div class="feed"> <a href="">Feed</a> </div> <h1><?php echo $category ?></h1> </div> <?php include_partial('job/list', array('jobs' => $pager->getResults())) ?> <?php if ($pager->haveToPaginate()): ?> <div class="pagination"> <a href="<?php echo url_for('category', $category) ?>?page=1"> <img src="/images/first.png" alt="First page" title="First page" /> </a> <a href="<?php echo url_for('category', $category) ?>?page=<?php echo $pager->getPreviousPage() ?>"> <img src="/images/previous.png" alt="Previous page" title="Previous page" /> </a> <?php foreach ($pager->getLinks() as $page): ?> <?php if ($page == $pager->getPage()): ?> <?php echo $page ?> <?php else: ?> <a href="<?php echo url_for('category', $category) ?>?page=<?php echo $page ?>"><?php echo $page ?></a> <?php endif; ?> <?php endforeach; ?>

<a href="<?php echo url_for('category', $category) ?>?page=<?php echo $pager->getNextPage() ?>"> <img src="/images/next.png" alt="Next page" title="Next page" /> </a> <a href="<?php echo url_for('category', $category) ?>?page=<?php echo $pager->getLastPage() ?>"> <img src="/images/last.png" alt="Last page" title="Last page" /> </a> </div> <?php endif; ?> <div class="pagination_desc"> <strong><?php echo $pager->getNbResults() ?></strong> jobs in this category <?php if ($pager->haveToPaginate()): ?> - page <strong><?php echo $pager->getPage() ?>/<?php echo $pager>getLastPage() ?></strong> <?php endif; ?> </div>

La mayor parte de este cdigo se refiere a los enlaces a otras pginas. Aqu est la lista de mtodos sfDoctrinePager usados en esta plantilla:
getResults(): Devuelve un array objetos Doctrine para la pgina actual getNbResults(): Devuelve el nmero total de resultados haveToPaginate(): Devuelve true si hay ms de una pgina getLinks(): Devuelve una lista de los enlaces de la pgina a mostrar getPage(): Devuelve el nmero de la pgina actual getPreviousPage(): Devuelve el nmero de la pgina anterior getNextPage(): Devuelve el nmero de la siguiente pgina getLastPage(): Devuelve el nmero de la ltima pgina

Pruebas Unitarias
Durante las ltimas dos semanas hemos revisado todas las funciones aprendidas durante los cinco primeros das del calendario de Jobeet para personalizar y aadir nuevas funciones. En el proceso, tambin hemos tocado otras funciones ms avanzadas de symfony. Hoy, vamos a empezar hablando de algo completamente diferente: pruebas automticas. Como el tema es bastante grande, nos llevar dos das completos para cubrir todo.

Las Pruebas en Symfony


Existen dos tipos de pruebas automticas en symfony: Pruebas Unitarias y Pruebas Funcionales. Las Pruebas Unitarias verificar que cada mtodo y funcin est trabajando correctamente. Cada pruebas deber ser lo ms independiente posible de las dems. Por otro lado, Pruebas Funcionales verifican que la aplicacin resultante se comporta correctamente en todo su conjunto. Todas las pruebas en symfony estn ubicadas bajo el directorio test/ del proyecto. Este tiene a su vez dos sub-directorios, uno para pruebas unitarias (test/unit/) y otro para las pruebas funtionales (test/functional/). Las Pruebas Unitarias se cubrirn en el tutorial de hoy, mientras que en el de maana se dedicar a las Pruebas Funcionales .

Pruebas Unitarias
Escribir pruebas unitarias es quizs una de las mejores prcticas de desarrollo web, ms difciles de poner en prctica. Como los desarrolladores web realmente no las utilizan para poner a prueba su trabajo, muchas preguntas surgen: Tengo que escribir las pruebas antes de la implementacin de una funcin? Qu necesito para hacer la prueba? Mis pruebas necesitan cubrir todos y cada uno de los casos de uso? Cmo puedo estar seguro de que todo est bien probado? Pero frecuentemente, la primer pregunta es la ms bsica: Donde empezar? Incluso si eres un fervoroso partidario de las pruebas, el enfoque de symfony es pragmtico: siempre es mejor disponer de algunas pruebas que no tener ninguna. Ya tienes un montn de cdigo sin ningn tipo de prueba? No hay problema. No es necesario disponer de un completo conjunto de pruebas para beneficiarse de las ventajas de ellas. Empieza por agregar pruebas cada vez que encuentras un fallo en el cdigo. Con el tiempo, el cdigo ser mejor, el cdigo aumentar, y sers un desarrollador con mayor confianza en t mismo. Empezando con un enfoque ms pragmtico, te sentirs ms cmodo con las pruebas con el paso del tiempo. El siguiente paso es escribir las pruebas de las nuevas caractersticas. En breve tiempo, te convertirs en un adicto a las pruebas.

El problema con la mayora de las bibliotecas de pruebas es su empinada curva de aprendizaje. Es por eso que symfony proporciona una muy simple librera para pruebas, lime, para hacer la escritura de pruebas increblemente fcil.
An si este tutorial describe extensamente la librera lime que viene incorporada en Symfony, puedes utilizar cualquier librera para pruebas, como la excelente librera PHPUnit.

El Framework de Pruebas lime


Todas las pruebas unitarias escritas con el framework lime comienzan con el mismo cdigo:
require_once dirname(__FILE__).'/../bootstrap/unit.php'; $t = new lime_test(1);

Primero, el archivo includo unit.php hace la inicializacin de un par de cosas. A continuacin, un nuevo objeto lime_test se crea y el nmero de pruebas que planeamos lanzar se pasa como argumento.
El plan permite a lime mostrar un mensaje de error an en caso de que sean pocas las pruebas que se ejecutan (por ejemplo, cuando una prueba genera un error fatal de PHP). Las Pruebas implican llamar a un mtodo o a una funcin con un conjunto predefinido de argumentos y, a continuacin, comparar la salida con los resultados esperados. Esta comparacin determina si una prueba pasa (aprueba) o no.

Para facilitar la comparacin, el objeto lime_test proporciona varios mtodos: Mtodo


ok($test) is($value1, $value2) isnt($value1, $value2) like($string, $regexp) unlike($string, $regexp)

Descripcin Prueba una condicin y pasa si es true Compara dos valores y pasa si son iguales (==) Compara dos valores y pasa si son distintos Prueba una cadena contra una expresin regular Comprueba que la cadena difiera de la expresin regular

is_deeply($array1, $array2)

Comprueba que dos arrays tienen los mismos valores

Puedes preguntarte por qu lime define tantos mtodos de prueba, si todas las pruebas se pueden escribir solo usando el mtodook(). El beneficio de los mtodos alternativos estan en los mensajes de error mucho ms explcitos en caso de que una prueba falle y en la mejora de la legibilidad de las pruebas.

El objeto lime_test tambin ofrece otros convenientes mtodos de prueba:

Mtodo
fail() pass() skip($msg, $nb_tests) todo()

Descripcin Siempre falla -til para probar las excepciones Siempre pasa -til para probar las excepciones Cuenta como $nb_tests pruebas -para pruebas condicionales Cuenta como una prueba -til para pruebas aun no escritas

Por ltimo, el mtodo comment($msg) muestra un comentario o mensaje pero no realiza ninguna prueba.

Ejecutando Pruebas Unitarias


Todas las pruebas unitarias son guardadas en el directorio test/unit/. Por convencin, las pruebas son nombradas con el nombre de la clase que ellas prueban ms el sufijo Test. Puedes organizar los archivos en el directorio test/unit/ de la forma que deseas, te recomendamos replicar la estructura de directorios del directorio lib/. Crea un archivo test/unit/JobeetTest.php y copia en l, el siguiente cdigo:
// test/unit/JobeetTest.php require_once dirname(__FILE__).'/../bootstrap/unit.php'; $t = new lime_test(1); $t->pass('This test always passes.');

Para lanzar las pruebas, puedes ejecutar el archivo directamente:


$ php test/unit/JobeetTest.php

O usar la tarea test:unit:


$ php symfony test:unit Jobeet

Los comandos de linea de Windows desafortunadamente no pueden resaltar los resultados de la prueba en colores rojo ni verde. Pero su utilizas Cygwin, puedes forzar a Symfony a usar colores pasando la opcin --color a la tarea.

Probando slugify
Vamos a comenzar nuestro viaje al maravilloso mundo de las pruebas unitarias escribiendo las pruebas para el mtodoJobeet::slugify().

Creamos el mtodo slugify() durante el da 5 para limpiar una cadena para que pueda ser seguro inclurla en una URL. La conversin consiste en algunas bsicas transformaciones como la de convertir todos los carcteres no-ASCII en un guin (-) o convertir la cadena a minsculas: Entrada Sensio Labs Salida sensio-labs

Paris, France paris-france Reemplaza el contenido del archivo de pruebas con el siguiente cdigo:
// test/unit/JobeetTest.php require_once dirname(__FILE__).'/../bootstrap/unit.php'; $t = new lime_test(6); $t->is(Jobeet::slugify('Sensio'), 'sensio'); $t->is(Jobeet::slugify('sensio labs'), 'sensio-labs'); $t->is(Jobeet::slugify('sensio labs'), 'sensio-labs'); $t->is(Jobeet::slugify('paris,france'), 'paris-france'); $t->is(Jobeet::slugify(' sensio'), 'sensio'); $t->is(Jobeet::slugify('sensio '), 'sensio');

Si miras de cerca las pruebas que hemos escrito, notars que cada linea solo prueba un sola cosa. Esto es algo que necesitas mantener en mente cuando desarrollas pruebas unitarias. Prueba una sola cosa a la vez. Puedes ahora ejecutar el archivo de pruebas. Si todas pruebas pasan, como esperamos que sea, te alegrar ver una "barra verde". Sino, la infame "barra roja" te alertar que algunas pruebas no pasaron y que necesitas arreglar.

Si una prueba falla, la salida te dar alguna informacin acerca del porque sta fall; pero si tienes cientos de pruebas en un archivo, puede ser difcil identificar rpidamente cual fall. Todos los mtodos de prueba lime toman una cadena como su ltimo argumento que sirve como descripcin para la prueba. Esto es muy conveniente pues te fuerza a describir que es lo que deseas realmente probar. Tambin te puede servir como una forma de documentacin para el comportamiento esperado del mtodo. Vamos agregar algunos mensajes para el archivo de pruebas slugify:
require_once dirname(__FILE__).'/../bootstrap/unit.php';

$t = new lime_test(6); $t->comment('::slugify()'); $t->is(Jobeet::slugify('Sensio'), 'sensio', '::slugify() converts all characters to lower case'); $t->is(Jobeet::slugify('sensio labs'), 'sensio-labs', '::slugify() replaces a white space by a -'); $t->is(Jobeet::slugify('sensio labs'), 'sensio-labs', '::slugify() replaces several white spaces by a single -'); $t->is(Jobeet::slugify(' sensio'), 'sensio', '::slugify() removes - at the beginning of a string'); $t->is(Jobeet::slugify('sensio '), 'sensio', '::slugify() removes - at the end of a string'); $t->is(Jobeet::slugify('paris,france'), 'paris-france', '::slugify() replaces non-ASCII characters by a -');

La descripcin de la prueba es tambin una herramienta importante cuando tratas de mostrar qu vamos a probar. Puedes ver un patrn en las cadenas de pruebas: ellas son sentencias que describen como el mtodo se comporta y ellas siempre comienzan con el nombre del mtodo a probar.
Cobertura del Cdigo Al escribir pruebas, es fcil olvidar una porcin del cdigo. Para ayudarte a comprobar que todo el cdigo est bien probado, symfony proporciona la tarea test:coverage. Para esta tarea pasale un archivo o directorio test y un archivo o directorio lib un directorio como argumentos y te dir el porcentaje de cdigo de tu sistema que la prueba cubre: $ php symfony test:coverage test/unit/JobeetTest.php lib/Jobeet.class.php Si quieres saber que lneas no estn cubiertos por tus pruebas, usa la opcin -detailed: $ php symfony test:coverage --detailed test/unit/JobeetTest.php lib/Jobeet.class.php Ten en cuenta que cuando la tarea te indica que el cdigo esta completamente probado, simplemente significa que cada lnea ha sido ejecutada, no que todos los casos de uso han sido probados. Como test:coverage depende de XDebug para recoger esta informacin, necesitas instalarlo y habilitarlo.

Agregando Pruebas para las nuevas Caractersticas

El slug de una cadena vaca es una cadena vaca. Puedes probarlo, va a funcionar. Pero una cadena vaca en una URL no es que una gran idea. Vamos a cambiar el mtodo slugify() para que devuelva la cadena "n-a" en caso de una cadena vaca. Puedes escribir la prueba primero, entonces actualiza el mtodo, o al revs. Es realmente una cuestin de gusto, pero escribir la prueba primero te da la confianza de que tu cdigo se ajusta en realidad lo que previste:
$t->is(Jobeet::slugify(''), 'n-a', '::slugify() converts the empty string to n-a');

Si lanzas las pruebas ahora, debes obtener una barra roja. Si no es as, significa que la caracterstica ya est implementada o tu prueba no est probando lo que debera estar probando. Ahora, edita la clase Jobeet y aade la siguiente condicin al inicio:
// lib/Jobeet.class.php static public function slugify($text) { if (empty($text)) { return 'n-a'; } // ... }

La prueba debe pasar ahora segn lo esperado, y puedes disfrutar de la barra verde, pero slo si has recordado actualizar el plan de pruebas. Si no es as, tendrs un mensaje que te dice planeaste seis pruebas y se ejecut una extra. Despus de haber planificado las pruebas debes contar con ellas hasta la fecha y esto es importante, ya que te mantendr informado si la secuencia de comandos de prueba termina antes de lo deseado.

Agregar Pruebas a causa de un fallo


Digamos que el tiempo ha pasado y uno de tus usuarios informa de un extrao error: algunos vnculos a los puestos de trabajo apuntan a una Pgina de error 404. Despus de algunas investigaciones, vas encontrar que por alguna razn, esos puestos de trabajo no tienen company, position, o location slug. Cmo es posible? Ves a travs de los registros en la base de datos y las columnas no estn vacas. Lo piensas por un rato, y bingo, encuentras la causa. Cuando una cadena slo contiene caracteres no ASCII, el mtodoslugify() lo convierte a una cadena vaca. Tan feliz de haber encontrado la causa, abres la clase Jobeet y solucionas el problema de inmediato. Eso es una mala idea. En primer lugar, vamos a aadir una prueba:
$t->is(Jobeet::slugify(' - '), 'n-a', '::slugify() converts a string that only contains non-ASCII characters to n-a');

Despus de comprobar que la prueba no pasa, edita la clase Jobeet y pasa la cadena vaca a comprobar al final del mtodo:
static public function slugify($text) { // ... if (empty($text)) { return 'n-a'; } return $text; }

La nueva prueba ahora pasa, al igual que todas los dems. El slugify() tena un error a pesar de nuestra cobertura 100%. No se puede pensar en todos casos de uso al escribir pruebas, y eso est bien. Pero cuando descubres uno, tienes que escribir una prueba antes de arreglar tu cdigo. Tambin significa que tu cdigo va mejorar con el tiempo, lo que siempre es algo bueno.
Hacia un mejor Mtodo slugify Probablemente sabes que Symfony ha sido creado por Franceses, as que vamos a agregar una prueba con una palabra francesa que contiene un "acento": $t->is(Jobeet::slugify('Dveloppeur Web'), 'developpeur-web', '::slugify() removes accents'); La prueba debe fallar. En lugar de sustituir por e, el Mtodo slugify() lo ha sustituido por un guin (-). Eso es un problema difcil, llamado Transliteracin. Esperemos que, si tiene "iconv" instalado, haga el trabajo para nosotros. Reemplaza el cdigo del mtodo slugifycon lo siguiente: // code derived from http://php.vrana.cz/vytvoreni-pratelskeho-url.php static public function slugify($text) { // replace non letter or digits by $text = preg_replace('#[^\\pL\d]+#u', '-', $text); // trim $text = trim($text, '-'); // transliterate if (function_exists('iconv')) { $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text); }

// lowercase $text = strtolower($text); // remove unwanted characters $text = preg_replace('#[^-\w]+#', '', $text); if (empty($text)) { return 'n-a'; } return $text; } No olvides guardar todos tus archivos PHP con la codificacin UTF-8, ya que esta es la codificacin por defecto de Symfony, y la utilizada por "iconv" para hacer la Transliteracin. Tambin cambia el archivo de prueba para el funcionamiento de la prueba slo si "iconv" est disponible: if (function_exists('iconv')) { $t->is(Jobeet::slugify('Dveloppeur Web'), 'developpeur-web', '::slugify() removes accents'); } else { $t->skip('::slugify() removes accents - iconv not installed'); }

Pruebas Unitarias y Doctrine


Configuracin de la Base de datos Probar en forma unitaria una clase Doctrine del modelo es un poco ms complejo ya que requiere una conexin de base de datos. Ya tienes la que utilizas para el desarrollo, pero es un buen hbito crear una base de datos especial para las pruebas. Durante el da 1, se presentaron los entornos como una forma de variar la configuracin de una aplicacin. Por defecto, todas las pruebas de symfony se ejecutan en el entorno test, as que vamos a configurar una base de datos para el entorno test:
$ php symfony configure:database --name=doctrine -class=sfDoctrineDatabase --env=test "mysql:host=localhost;dbname=jobeet_test" root mYsEcret

La opcin env le dice a la tarea que la configuracin de la base de datos es slo para el entorno test. Cuando usamos esta tarea durante el da 3, no pas ninguna opcin env, por lo que la configuracin se aplica a todos los entornos.
Si eres curioso, abre el archivo de configuracin config/databases.yml para ver como Symfony hace que sea fcil de cambiar la configuracin en funcin del entorno.

Ahora que hemos configurado la base de datos, podemos iniciarla usando la tarea doctrine:insert-sql:
$ mysqladmin -uroot -pmYsEcret create jobeet_test $ php symfony doctrine:insert-sql --env=test Principios de Configuracin en Symfony Durante el da 4, vimos los ajustes procedentes de los archivos de configuracin puede ser definido a diferentes niveles. Estos valores tambin pueden ser dependientes del entorno. Esto es verdad para la mayora de los archivos de configuracin que hemos utilizado hasta ahora: databases.yml, app.yml, view.yml, y settings.yml. En todos los archivos, la clave principal es el entorno, la clave all est indicando que los ajustes son para todos los entornos: # config/databases.yml dev: doctrine: class: sfDoctrineDatabase test: doctrine: class: sfDoctrineDatabase param: dsn: 'mysql:host=localhost;dbname=jobeet_test' all: doctrine: class: sfDoctrineDatabase param: dsn: 'mysql:host=localhost;dbname=jobeet' username: root password: null

Datos de Prueba Ahora que ya tenemos una base de datos slo para pruebas, tenemos que llenarla con datos de prueba. Durante el da 3 aprendimos a utilizar la tarea doctrine:data-load, pero en las pruebas es necesario volver a cargar los datos cada vez que ejecutamos las pruebas para conocer el estado inicial de la base de datos. La tarea doctrine:data-load internamente utiliza el mtodo Doctrine_Core::loadData() para cargar los datos:

Doctrine_Core::loadData(sfConfig::get('sf_test_dir').'/fixtures'); El objeto sfConfig puede ser utilizado para obtener la ruta completa de un subdirectorio del proyecto. Uso que permite a la estructura de directorio por defecto ser personalizada.

El mtodo loadData() toma un directorio o un archivo como primer argumento. Tambin puede tomar un array directorios y/o archivos. Ya hemos creado algunos datos iniciales en el directorio data/fixtures/. Para las pruebas, pondremos los datos en el directoriotest/fixtures/. Estos datos se utilizarn para pruebas unitarias y pruebas funcionales con objetos Doctrine. Por el momento, copiar los archivos de data/fixtures/ al directorio test/fixtures/. Probando JobeetJob Vamos a crear algunas de las pruebas unitarias para la clase del modelo, JobeetJob. Como todos nuestros objetos Doctrine harn las pruebas unitarias comenzarn con el mismo cdigo, crea un archivo Doctrine.php en el directorio bootstrap/ con el siguiente cdigo:
// test/bootstrap/Doctrine.php include(dirname(__FILE__).'/unit.php'); $configuration = ProjectConfiguration::getApplicationConfiguration( 'frontend', 'test', true); new sfDatabaseManager($configuration); Doctrine_Core::loadData(sfConfig::get('sf_test_dir').'/fixtures');

El script se explica bastante por s mismo:

Como pasa en todos los controladores frontales, los inicializamos con un objeto de configuracin para el entorno test :
$configuration = ProjectConfiguration::getApplicationConfiguration( 'frontend', 'test', true);

Creamos un gestor de bases de datos e inicializamos la conexin Doctrine cargando el archivo de configuracin databases.yml.
new sfDatabaseManager($configuration);

Cargamos nuestros datos de prueba mediante el uso de Doctrine_Core::loadData():


Doctrine_Core::loadData(sfConfig::get('sf_test_dir').'/fixtures');

Doctrine se conecta a la base de datos slo si tiene algunas sentencias SQL para ejecutar.

Ahora que todo est en su lugar, podemos empezar a probar la clase JobeetJob. En primer lugar, tenemos que crear el archivo JobeetJobTest.php en test/unit/model:
// test/unit/model/JobeetJobTest.php include(dirname(__FILE__).'/../../bootstrap/Doctrine.php'); $t = new lime_test(1);

Entonces, vamos a empezar por agregar una prueba para el mtodo getCompanySlug():
$t->comment('->getCompanySlug()'); $job = Doctrine_Core::getTable('JobeetJob')->createQuery()->fetchOne(); $t->is($job->getCompanySlug(), Jobeet::slugify($job->getCompany()), '>getCompanySlug() return the slug for the company');

Observe que slo prueba el mtodo getCompanySlug() y no si el slug es correcto o no, ya que lo estamos probando a ste en otros lugares. Escribir pruebas para el mtodo save() es ligeramente ms complejo:
$t->comment('->save()'); $job = create_job(); $job->save(); $expiresAt = date('Y-m-d', time() + 86400 * sfConfig::get('app_active_days')); $t->is($job->getDateTimeObject('expires_at')->format('Y-m-d'), $expiresAt, '->save() updates expires_at if not set'); $job = create_job(array('expires_at' => '2008-08-08')); $job->save(); $t->is($job->getDateTimeObject('expires_at')->format('Y-m-d'), '2008-0808', '->save() does not update expires_at if set'); function create_job($defaults = array()) { static $category = null; if (is_null($category)) { $category = Doctrine_Core::getTable('JobeetCategory') ->createQuery() ->limit(1) ->fetchOne(); } $job = new JobeetJob(); $job->fromArray(array_merge(array( 'category_id' => $category->getId(), 'company' => 'Sensio Labs', 'position' => 'Senior Tester',

'location' 'description' 'how_to_apply' 'email' 'token' 'is_activated' ), $defaults));

=> => => => => =>

'Paris, France', 'Testing is fun', 'Send e-Mail', 'job@example.com', rand(1111, 9999), true,

return $job; } Cada vez que aadas pruebas, no te olvides de actualizar el nmero de pruebas previsto (el plan) en el mtodo constructorlime_test. Para el archivo JobeetJobTest es necesario cambiar de 1 a 3.

Prueba otras Clases Doctrine Ahora puedes aadir pruebas para todas las dems clases de Doctrine. Como ahora te acostumbraste al proceso de la escritura de pruebas unitarias, debera ser bastante fcil. Comprueba el repositorio para el da de hoy si quieres ver los archivos de datos que hemos creado, y los pruebas unitarias asociadas (bajo la etiqueta release_day_08).

Set de Pruebas Unitarias


La tarea test:unit tambin se puede utilizar para poner en marcha todas las pruebas unitarias para un proyecto:
$ php symfony test:unit

Esta tarea muestra si ha pasado o ha fallado cada uno de los archivos de pruebas:

Si la tarea test:unit devuelve un estado "dubious" para un archivo, esto indica que el script se detuvo/muri antes de llegar al final. Ejecutando un archivo de pruebas en forma individual te dar el mensaje de error exacto.

Las Pruebas Funcionales


Las Pruebas Funcionales son una gran herramienta para probar tus aplicaciones de principio a fin: desde la peticin hecha en el navegador hasta la respuesta que enva el servidor. Ellas prueban todas las capas de una aplicacin: El enrutamiento, el modelo, las acciones, y las plantillas. Ellas son muy similares a lo que probablemente ya haces manualmente: cada vez que vamos a aadir o modificar una accin, es necesario ir al navegador y comprobar que todo funciona como se esperaba, haciendo clic en los enlaces y controlando que los elementos se muestran en la pgina. En otras palabras, corres un escenario correspondiente al caso de uso que acabas de implementar. Como el proceso es manual, es tedioso y propenso a errores. Cada vez que cambias algo en el cdigo, debes pasar a travs de todos los escenarios para asegurarte de que no rompiste algo. Eso es una locura. Las Pruebas Funcionales en Symfony proporcionar un mtodo sencillo para describir escenarios. Cada escenario puede ser ejecutado automticamente una y otra vez simulando la experiencia de lo que un usuario ha hecho en su navegador. Al igual que pruebas unitarias, ellas te dan la confianza para que el cdigo quede en paz.
El framework de pruebas funcionales no reemplaza las herramientas como "Selenium". Selenium funciona directamente en el navegador para automatizar las pruebas a travs de muchas plataformas y navegadores y, as, habilitar la prueba del JavaScript de tu aplicacin.

La Clase sfBrowser
En Symfony, las pruebas funcionales se ejecutan a travs de un navegador especial, ejecutadas por la clase sfBrowser. Esta acta como un navegador adaptado para tu aplicacin y directamente conectada a ella, sin la necesidad de un servidor web. Te da acceso a todos los objetos Symfony antes y despus de cada peticin, dndote la oportunidad de inspeccionarlos y hacer los controles que deseas programaticamente.
sfBrowser brinda mtodos de navegacin que simula lo que hace el clsico

navegador: Mtodo
get() post() call() back()

Descripcin Obtiene una direccin URL Enva a una URL Pide una URL (utilizado para los mtodos PUT y DELETE) Se remonta a una pgina atrs en el historial

Mtodo
forward() reload() click() select()

Descripcin Va adelante una pgina en el historial Recarga la pgina actual Hace clic en un enlace o un botn Selecciona una casilla de verificacin o de opcin

deselect() Deselecciona una casilla de verificacin o de opcin restart()

Reinicia el navegador

He aqu algunos ejemplos de uso de los mtodos de sfBrowser :


$browser = new sfBrowser(); $browser-> get('/')-> click('Design')-> get('/category/programming?page=2')-> get('/category/programming', array('page' => 2))-> post('search', array('keywords' => 'php')) ; sfBrowser contiene mtodos adicionales para configurar el comportamiento del

navegador: Mtodo
setHttpHeader() setAuth() setCookie() removeCookie() clearCookie()

Descripcin Establece una cabecera HTTP Establece las credenciales de autenticacin bsica Establecer una cookie Removes a cookie Borra todas las cookies

Mtodo

Descripcin

followRedirect() Sigue un redireccionamiento

La Clase sfTestFunctional
Tenemos un navegador, pero necesitamos una forma de inspeccionar los objetos Symfony para hacer la prueba real. Se puede hacer con lime y algunos mtodos de sfBrowser como getResponse() y getRequest() pero Symfony proporciona una mejor manera. Los mtodos de pruebas son proporcionados por otra clase, sfTestFunctional que toma una instancia de sfBrowser en su constructor. La clasesfTestFunctional delega las pruebas a objetos tester. Varios testers son empaquetados con Symfony, y tambin puedes crear el tuyo propio. Como vimos ayer, las pruebas funcionales se guardan en el directorio test/functional/. Para Jobeet, la pruebas se encuentran en el subdirectoriotest/functional/frontend/ ya que cada aplicacin tiene su propio subdirectorio. Este directorio ya contiene dos archivos:categoryActionsTest.php, y jobActionsTest.php como todas las tareas que generan un mdulo crean automticamente un archivo base de prueba funcional:
// test/functional/frontend/categoryActionsTest.php include(dirname(__FILE__).'/../../bootstrap/functional.php'); $browser = new sfTestFunctional(new sfBrowser()); $browser-> get('/category/index')-> with('request')->begin()-> isParameter('module', 'category')-> isParameter('action', 'index')-> end()-> with('response')->begin()-> isStatusCode(200)-> checkElement('body', '!/This is a temporary page/')-> end() ;

En primer lugar, el script anterior puede parecer un poco raro. Esto se debe a que los mtodos de sfBrowser y sfTestFunctional implementan unainterfaz fluda que siempre devuelve un objeto $this. Te permite encadenar llamadas a mtodos para la mejor legibilidad. El snippet anterior es equivalente a:
// test/functional/frontend/categoryActionsTest.php include(dirname(__FILE__).'/../../bootstrap/functional.php');

$browser = new sfTestFunctional(new sfBrowser()); $browser->get('/category/index'); $browser->with('request')->begin(); $browser->isParameter('module', 'category'); $browser->isParameter('action', 'index'); $browser->end(); $browser->with('response')->begin(); $browser->isStatusCode(200); $browser->checkElement('body', '!/This is a temporary page/'); $browser->end();

Las Pruebas se ejecutan dentro de un bloque de contexto de prueba. Un bloque de contexto de prueba comienza con with('TESTER NAME')->begin() y finaliza con end():
$browser-> with('request')->begin()-> isParameter('module', 'category')-> isParameter('action', 'index')-> end() ;

El cdigo prueba que el parmetro de la peticin module es igual a category y action es igual a index.
Cuando slo necesitas llamar a un solo mtodo en el tester, no es necesario crear un bloque: with('request')->isParameter('module', 'category').

El Tester de la Peticin El request tester proporciona mtodos tester para probar e inspeccionar el objeto sfWebRequest: Mtodo Descripcin

isParameter() Comprueba un valor de un parmetro isFormat() isMethod() hasCookie() isCookie()

Verifica el formato de una peticin Verifica el mtodo Chequea si la peticin tiene una cookie con el nombre dado Verifica el valor de una cookie

El Tester de la Respuesta Tambin hay una clase response tester que proporciona mtodos tester contra el objeto sfWebResponse: Mtodo Descripcin

checkElement() Comprueba si un selector CSS de la respuesta coincide con

algunos criterios
checkForm() debug() matches() isHeader()

Checks an sfForm form object Prints the response output to ease debug Tests a response against a regexp Verifica el valor de una cabecera

isStatusCode() Verifica el cdigo de estado de la respuesta isRedirected() Verifica si la respuesta actual es un redireccionamiento Vamos a describir ms clases testers en los prximos das (para forms, user, cache, ...).

Ejecutando las pruebas funcionales


As como para las pruebas unitarias, ejecutar las pruebas funcionales se puede hacer directamente desde un archivo de pruebas:
$ php test/functional/frontend/categoryActionsTest.php

O usando la tarea test:functional:


$ php symfony test:functional frontend categoryActions

Probar los Datos


Asi como para Doctrine en las pruebas unitarias, tenemos que cargar los datos de ensayo cada vez que un lanzemos una prueba funcional. Podemos reutilizar el cdigo que hemos escrito ayer:
include(dirname(__FILE__).'/../../bootstrap/functional.php'); $browser = new JobeetTestFunctional(new sfBrowser()); Doctrine_Core::loadData(sfConfig::get('sf_test_dir').'/fixtures');

Cargar datos en una prueba funcional es un poco ms fcil que en pruebas unitarias pues la base de datos ya ha sido inicializadas por el script bootstrapping. Asi como para pruebas unitarias, no vamos a copiar y pegar este snippet de cdigo en cada archivo de prueba, pero nos vamos a crear nuestra propia clase funcional que hereda de sfTestFunctional:
// lib/test/JobeetTestFunctional.class.php class JobeetTestFunctional extends sfTestFunctional { public function loadData() { Doctrine_Core::loadData(sfConfig::get('sf_test_dir').'/fixtures'); return $this; } }

Escribiendo las pruebas funcionales


Escribir las pruebas funcionales es como usar un escenario en el navegador. Ya tenemos por escrito todos los escenarios que necesitamos para poner a prueba como parte del da 2. En primer lugar, vamos a probar la pgina principal Jobeet editando el jobActionsTest.php. Reemplace el cdigo con el siguiente: Expired jobs are not listed
// test/functional/frontend/jobActionsTest.php include(dirname(__FILE__).'/../../bootstrap/functional.php'); $browser = new JobeetTestFunctional(new sfBrowser()); $browser->loadData(); $browser->info('1 - The homepage')-> get('/')-> with('request')->begin()-> isParameter('module', 'job')-> isParameter('action', 'index')-> end()-> with('response')->begin()->

info(' 1.1 - Expired jobs are not listed')-> checkElement('.jobs td.position:contains("expired")', false)-> end() ;

Con lime, un mensaje informativo puede ser insertadao llamando al mtodo info() para hacer la salida ms legible. Para verificar la exclusin de los puestos de trabajo expirados en la pgina de inicio, comprobamos que el selector CSS .jobs td.position:contains("expired") no coincide con ninguna parte en la respuesta y su contenido HTML (Recuerdo que en los archivos de datos, el nico puesto vencido que tenemos tiene "expired" en la posicin). Cuando el segundo argumento del mtodo checkElement() es un Boolean, el mtodo prueba la existencia de nodos que coincidan con el selector CSS.
El mtodo checkElement() es capaz de interpretar la mayora de los selectores vlidows CSS3.

Solo n puestos se listan para una categora Agrega el cdigo siguiente al final del archivo de prueba:
// test/functional/frontend/jobActionsTest.php $max = sfConfig::get('app_max_jobs_on_homepage'); $browser->info('1 - The homepage')-> get('/')-> info(sprintf(' 1.2 - Only %s jobs are listed for a category', $max))-> with('response')-> checkElement('.category_programming tr', $max) ;

El mtodo checkElement() tambin puede comprobar que un selector CSS coincide 'n' nodos en el documento pasando un entero como su segundo argumento. Una categora tiene un enlace a la pgina de categora slo si tiene muchos puestos de trabajo
// test/functional/frontend/jobActionsTest.php $browser->info('1 - The homepage')-> get('/')-> info(' 1.3 - A category has a link to the category page only if too many jobs')-> with('response')->begin()-> checkElement('.category_design .more_jobs', false)-> checkElement('.category_programming .more_jobs')-> end() ;

En estas pruebas, comprueba que no hay un enlace "more jobs" para la categora de design (.category_design .more_jobs no existe), y que existe un enlace "more jobs" para la categora programming (.category_programming .more_jobs existe).

Puestos de trabajo estn ordenados por fecha


$q = Doctrine_Query::create() ->select('j.*') ->from('JobeetJob j') ->leftJoin('j.JobeetCategory c') ->where('c.slug = ?', 'programming') ->andWhere('j.expires_at > ?', date('Y-m-d', time())) ->orderBy('j.created_at DESC'); $job = $q->fetchOne(); $browser->info('1 - The homepage')-> get('/')-> info(' 1.4 - Jobs are sorted by date')-> with('response')->begin()-> checkElement(sprintf('.category_programming tr:first a[href*="/%d/"]', $job->getId()))-> end() ;

Para probar si un puesto de trabajo son en realidad ordenados por fecha, necesitamos comprobar que el primer puesto de trabajo aparece en la pgina principal como esperamos. Esto puede hacerse comprobando que la URL contenga la clave primaria esperada. Como la clave principal puede cambiar entre ejecuciones, tenemos que obtener el objeto Doctrine a partir de la primera base de datos. Incluso si la prueba funciona como debe, tenemos que refactorizar el cdigo un poco, ya que conseguir el primer trabajo de la categora programming pueden ser reutilizados en otras partes de nuestras pruebas. No vamos a mover el cdigo a la capa del Modelo que que es el cdigo de una prueba especfica. En lugar de ello, vamos a mover el cdigo a la clase JobeetTestFunctional que hemos creado anteriormente. Esta clase acta como una clase tester funcional para un Dominio Especfico de Jobeet:
// lib/test/JobeetTestFunctional.class.php class JobeetTestFunctional extends sfTestFunctional { public function getMostRecentProgrammingJob() { $q = Doctrine_Query::create() ->select('j.*') ->from('JobeetJob j') ->leftJoin('j.JobeetCategory c') ->where('c.slug = ?', 'programming'); $q = Doctrine_Core::getTable('JobeetJob')->addActiveJobsQuery($q); return $q->fetchOne(); }

// ... }

Puedes ahora reemplazar el cdigo de la prueba anterior con el siguiente:


// test/functional/frontend/jobActionsTest.php $browser->info('1 - The homepage')-> get('/')-> info(' 1.4 - Jobs are sorted by date')-> with('response')->begin()-> checkElement(sprintf('.category_programming tr:first a[href*="/%d/"]', $browser->getMostRecentProgrammingJob()->getId()))-> end() ;

Cada puesto de trabajo en la pgina principal es cliqueable


$job = $browser->getMostRecentProgrammingJob(); $browser->info('2 - The job page')-> get('/')-> info(' 2.1 - Each job on the homepage is clickable and give detailed information')-> click('Web Developer', array(), array('position' => 1))-> with('request')->begin()-> isParameter('module', 'job')-> isParameter('action', 'show')-> isParameter('company_slug', $job->getCompanySlug())-> isParameter('location_slug', $job->getLocationSlug())-> isParameter('position_slug', $job->getPositionSlug())-> isParameter('id', $job->getId())-> end() ;

Para probar el vnculo de un puesto en la pgina de inicio, simularemos un clic en el texto "Web Developer". Como hay muchos de ellos en la pgina, hemos pedido explcitamente al navegador que haga clic en el primero (array('position' => 1)). Cada parmetro de la peticin se prueba para asegurarte que la ruta ha hecho su trabajo correctamente.

Aprender con el Ejemplo


En esta seccin, hemos proporcionado todo el cdigo necesario para poner a prueba la pginas de puestos de trabajo y categora. Lee el cdigo cuidadosamente, ya que puedes aprender algunos trucos nuevos:
// lib/test/JobeetTestFunctional.class.php class JobeetTestFunctional extends sfTestFunctional {

public function loadData() { Doctrine_Core::loadData(sfConfig::get('sf_test_dir').'/fixtures'); return $this; } public function getMostRecentProgrammingJob() { $q = Doctrine_Query::create() ->select('j.*') ->from('JobeetJob j') ->leftJoin('j.JobeetCategory c') ->where('c.slug = ?', 'programming'); $q = Doctrine_Core::getTable('JobeetJob')->addActiveJobsQuery($q); return $q->fetchOne(); } public function getExpiredJob() { $q = Doctrine_Query::create() ->from('JobeetJob j') ->where('j.expires_at < ?', date('Y-m-d', time())); return $q->fetchOne(); } } // test/functional/frontend/jobActionsTest.php include(dirname(__FILE__).'/../../bootstrap/functional.php'); $browser = new JobeetTestFunctional(new sfBrowser()); $browser->loadData(); $browser->info('1 - The homepage')-> get('/')-> with('request')->begin()-> isParameter('module', 'job')-> isParameter('action', 'index')-> end()-> with('response')->begin()-> info(' 1.1 - Expired jobs are not listed')-> checkElement('.jobs td.position:contains("expired")', false)-> end() ; $max = sfConfig::get('app_max_jobs_on_homepage'); $browser->info('1 - The homepage')-> info(sprintf(' 1.2 - Only %s jobs are listed for a category', $max))->

with('response')-> checkElement('.category_programming tr', $max) ; $browser->info('1 - The homepage')-> get('/')-> info(' 1.3 - A category has a link to the category page only if too many jobs')-> with('response')->begin()-> checkElement('.category_design .more_jobs', false)-> checkElement('.category_programming .more_jobs')-> end() ; $browser->info('1 - The homepage')-> info(' 1.4 - Jobs are sorted by date')-> with('response')->begin()-> checkElement(sprintf('.category_programming tr:first a[href*="/%d/"]', $browser->getMostRecentProgrammingJob()->getId()))-> end() ; $job = $browser->getMostRecentProgrammingJob(); $browser->info('2 - The job page')-> get('/')-> info(' 2.1 - Each job on the homepage is clickable and give detailed information')-> click('Web Developer', array(), array('position' => 1))-> with('request')->begin()-> isParameter('module', 'job')-> isParameter('action', 'show')-> isParameter('company_slug', $job->getCompanySlug())-> isParameter('location_slug', $job->getLocationSlug())-> isParameter('position_slug', $job->getPositionSlug())-> isParameter('id', $job->getId())-> end()-> info(' 2.2 - A non-existent job forwards the user to a 404')-> get('/job/foo-inc/milano-italy/0/painter')-> with('response')->isStatusCode(404)-> info(' 2.3 - An expired job page forwards the user to a 404')-> get(sprintf('/job/sensio-labs/paris-france/%d/web-developer', $browser>getExpiredJob()->getId()))-> with('response')->isStatusCode(404) ; // test/functional/frontend/categoryActionsTest.php include(dirname(__FILE__).'/../../bootstrap/functional.php');

$browser = new JobeetTestFunctional(new sfBrowser()); $browser->loadData(); $browser->info('1 - The category page')-> info(' 1.1 - Categories on homepage are clickable')-> get('/')-> click('Programming')-> with('request')->begin()-> isParameter('module', 'category')-> isParameter('action', 'show')-> isParameter('slug', 'programming')-> end()-> info(sprintf(' 1.2 - Categories with more than %s jobs also have a "more" link', sfConfig::get('app_max_jobs_on_homepage')))-> get('/')-> click('22')-> with('request')->begin()-> isParameter('module', 'category')-> isParameter('action', 'show')-> isParameter('slug', 'programming')-> end()-> info(sprintf(' 1.3 - Only %s jobs are listed', sfConfig::get('app_max_jobs_on_category')))-> with('response')->checkElement('.jobs tr', sfConfig::get('app_max_jobs_on_category'))-> info(' 1.4 - The job listed is paginated')-> with('response')->begin()-> checkElement('.pagination_desc', '/32 jobs/')-> checkElement('.pagination_desc', '#page 1/2#')-> end()-> click('2')-> with('request')->begin()-> isParameter('page', 2)-> end()-> with('response')->checkElement('.pagination_desc', '#page 2/2#') ;

Depurando las Pruebas Funcionales


A veces una prueba funcional falla. Como Symfony simula un navegador sin ningn tipo de interfaz grfica, puede ser difcil de diagnosticar el problema. Afortunadamente, Symfony proporciona el mtodo debug() para mostrar la cabecera dela respuesta y su contenido:
$browser->with('response')->debug();

El mtodo debug() se puede insertar en cualquier lugar de un bloque tester response y detener el script de ejecucin.

Grupo de Pruebas Funcionales


La tarea test:functional tambin se puede utilizar para poner en marcha todas las pruebas funcionales para una aplicacin:
$ php symfony test:functional frontend

La tarea muestra una sola linea por cada archivo de prueba:

Grupo de Pruebas
Como puedes esperar, tambin hay una tarea para poner en marcha todas las pruebas para un proyecto (unitarias y funcionales):
$ php symfony test:all

Los Formularios
La segunda semana de Jobeet pas volando con la introduccin del framework de pruebas de Symfony. Vamos a continuar hoy con el framework de formularios.

El Framework de Formularios
Cualquier sitio web tiene formularios; desde el simple formulario de contacto hasta los complejos con decenas de campos. Crear formularios es tambin una de las ms complejas y tediosas tareas de un desarrollador web: necesitas crear el HTML del formulario, implementar las reglas de validacin para cada campo, procesar los valores para luego guardarlos en la base de datos, mostrar mensajes de error, rellenar los campos en caso de errores, y mucho ms ... Por supuesto, en lugar de reinventar la rueda una y otra vez, Symfony proporciona una framework para facilitar la administracin de un formulario. El framework de formularios esta hecho de tres partes:

validacin: El sub-framework validation ofrece clases para validar las entradas (entero, cadenas, direccin de correo electrnico, ...) widgets: El sub-framework de widgets ofrece clases para la salida de los campos HTML (input, textarea, select, ...) forms: La clases form representan formularios hechos de widgets y validadores y dan mtodos para ayudar a gestionar el formulario. Cada campo del formulario tiene su propio validador y su widget.

Formularios
En Symfony un formulario es una clase hecha de campos. Cada campo tiene un nombre, un validador, y un widget. Un simple ContactForm puede definirse con la siguiente clase:
class ContactForm extends sfForm { public function configure() { $this->setWidgets(array( 'email' => new sfWidgetFormInputText(), 'message' => new sfWidgetFormTextarea(), )); $this->setValidators(array( 'email' => new sfValidatorEmail(), 'message' => new sfValidatorString(array('max_length' => 255)), )); } }

Los campos del formulario se configuran en el mtodo configure(), usando los mtodos setValidators() y setWidgets().

El framework de formularios trae incluida una gran cantidad de widgets y validators. La API los describe muy ampliamente con todas las opciones, los errores, y mensajes de error por defecto.

Los nombres de las clases de los widgets y validadores son muy explcitos: el campo email se muestra como una etiqueta HTML <input>(sfWidgetFormInputText) y validado como una direccin de correo electrnico (sfValidatorEmail). El campo message se muestra como una etiqueta HTML <textarea> (sfWidgetFormTextarea), y debe ser una cadena de no ms de 255 caracteres (sfValidatorString). Por defecto, todos los campos son obligatorios, as el valor por defecto para required es true. Por lo tanto, la definicin de la validacin para emailes equivalente a new sfValidatorEmail(array('required' => true)).
Es posible combinar un formulario en otro usando el mtodo mergeForm(), o incluir un formulario dentro de otro mediante el mtodo embedForm(): $this->mergeForm(new AnotherForm()); $this->embedForm('name', new AnotherForm());

Formularios Doctrine
La mayora de las veces, un formulario tiene que ser serializado (guardado) para la base de datos. Como Symfony ya sabe todo acerca de su modelo de base de datos, puede generar automticamente formularios basados sobre esta informacin. De hecho, cuando se puso en marcha la tarea doctrine:build -all durante el da 3, Symfony automticamente llam a la tarea doctrine:build -forms:
$ php symfony doctrine:build --forms

La tarea doctrine:build --forms genera las clases form en lib/form/. La organizacin de estos archivos generados es similar a la de lib/model/. Cada modelo de clase tiene una clase form relacionada (por ejemplo JobeetJob tiene JobeetJobForm), que est vaco por defecto, ya que hereda de una clase base:
// lib/form/doctrine/JobeetJobForm.class.php class JobeetJobForm extends BaseJobeetJobForm { public function configure() { } } Navegando por los archivos generados en el subdirectorio lib/form/doctrine/base/ vers un montn de ejemplos de uso de widgets symfony incluidos y de validadores.

Personalizacin del Job Form

El formulario Job es un ejemplo perfecto para aprender la personalizacin de un formulario. Vamos a ver cmo personalizarlo, paso a paso. En primer lugar, cambia el enlace "Post a Job" en el layout para poder comprobar los cambios directamente en tu navegador:
<!-- apps/frontend/templates/layout.php --> <a href="<?php echo url_for('@job_new') ?>">Post a Job</a>

De manera predeterminada, un formulario Doctrine muestra todos los campos de la columnas de la tabla. Sin embargo, para el job form, algunos de ellos no deben ser editables por el usuario final. Eliminar los campos del formulario es simple:
// lib/form/doctrine/JobeetJobForm.class.php class JobeetJobForm extends BaseJobeetJobForm { public function configure() { unset( $this['created_at'], $this['updated_at'], $this['expires_at'], $this['is_activated'] ); } }

La desconexin de un campo significa que tanto el widget como validador se eliminan. La configuracin del formulario a veces tienen que ser ms precisa que lo que se puede inspeccionar desde el esquema de base de datos. Por ejemplo, la columna email es un varchar en el esquema, pero necesitamos que esta columna sea validada como un email. Vamos a cambiar el valor por defecto sfValidatorString a sfValidatorEmail:
// lib/form/doctrine/JobeetJobForm.class.php public function configure() { // ... $this->validatorSchema['email'] = new sfValidatorEmail(); }

Reemplar el validador por defecto no siempre es la mejor solucin, ya que las reglas de validacin por defecto inferidas del esquema de la base de datos se pierden (new sfValidatorString(array('max_length' => 255))). Es casi siempre mejor para agregar un nuevo validador a uno existente usar el validador especial sfValidatorAnd:
// lib/form/doctrine/JobeetJobForm.class.php public function configure() { // ...

$this->validatorSchema['email'] = new sfValidatorAnd(array( $this->validatorSchema['email'], new sfValidatorEmail(), )); }

El validador sfValidatorAnd toma un arreglo o array de validadores que deben pasarse para que el aor sea vlido. El truco aqu es referenciar al actual validador ($this->validatorSchema['email']), y aadirle uno nuevo.
Tambin puede usar el validador sfValidatorOr para forzar un valor a pasar al menos un validador. Y por supuesto, puedes mezclar y coinicidir los validadores sfValidatorAnd y sfValidatorOr para crear complejos validadores basados en boleanos.

Incluso si el type de la columna es tambin un varchar en el esquema, queremos que su valor este restringido a una lista de opciones: full time, part time, o freelance. En primer lugar, vamos a definir los posibles valores en JobeetJobTable:
// lib/model/doctrine/JobeetJobTable.class.php class JobeetJobTable extends Doctrine_Table { static public $types = array( 'full-time' => 'Full time', 'part-time' => 'Part time', 'freelance' => 'Freelance', ); public function getTypes() { return self::$types; } // ... }

A continuacin, utiliza sfWidgetFormChoice para el type del widget:


$this->widgetSchema['type'] = new sfWidgetFormChoice(array( 'choices' => Doctrine_Core::getTable('JobeetJob')->getTypes(), 'expanded' => true, )); sfWidgetFormChoice representa un widget de opciones el cual mostrar un diferente widget de acuerdo a las opciones de configuracin (expanded ymultiple):

Lista desplegable (<select>): array('multiple' => false, 'expanded' =>


false)

Combo Lista (<select multiple="multiple">): array('multiple' => true,


'expanded' => false)

Lista de botones radio: array('multiple' => false, 'expanded' => true) Lista de checkboxes: array('multiple' => true, 'expanded' => true)

Si quieres que uno de los radio button este seleccionado por defecto (full-time por ejemplo), puedes cambiar el valor por defecto en el esquema de base de datos.

Incluso si piensas que nadie pueda enviar un valor no-vlido, un hacker fcilmente puede pasar por alto las opciones del widget usando herramientas como curl o la Firefox Web Developer Toolbar. Vamos a cambiar el validador para restringir a las opciones posibles:
$this->validatorSchema['type'] = new sfValidatorChoice(array( 'choices' => array_keys(Doctrine_Core::getTable('JobeetJob')>getTypes()), ));

Como la columna logo almacenar el nombre del archivo del logotipo relacionados con el puesto de trabajo, tenemos que cambiar el widget a file input tag:
$this->widgetSchema['logo'] = new sfWidgetFormInputFile(array( 'label' => 'Company logo', ));

Para cada uno de los campos, Symfony automticamente genera un label (que se utilizarn al mostrar la etiqueta <label>). Esto puede ser cambiado con la opcin label. Tambin puedes cambiar labels en un batch con el mtodo setLabels() del widget array:
$this->widgetSchema->setLabels(array( 'category_id' => 'Category', 'is_public' => 'Public?', 'how_to_apply' => 'How to apply?', ));

Tambin tenemos que cambiar el valor por defecto del validador:


$this->validatorSchema['logo'] = new sfValidatorFile(array( 'required' => false, 'path' => sfConfig::get('sf_upload_dir').'/jobs', 'mime_types' => 'web_images', )); sfValidatorFile es muy interesante ya que hace una serie de cosas:

Valida que el archivo subido es una imagen en un formato web (mime_types) Cambia el nombre del archivo a algo nico Almacena el archivo en un path dado Actualiza la columna logo con el nombre generado

Necesitas crear el directorio logo (web/uploads/jobs/) y comprobar que tenga permisos de escritura por el servidor web.

Como el validador guarda la ruta relativa en la base de datos, cambia la ruta de acceso utilizada en la plantilla showSuccess:
// apps/frontend/modules/job/templates/showSuccess.php <img src="/uploads/jobs/<?php echo $job->getLogo() ?>" alt="<?php echo $job->getCompany() ?> logo" /> Si un mtodo generateLogoFilename() existe en el modelo, este ser llamado por el validador y el resultado sobreescribir el nombre del archivo generado por defecto logo. El mtodo tiene un objeto sfValidatedFile como un argumento.

As como puedes sobreescribir el label generado de cualquier campo, puedes tambien definir un mensaje de ayuda. Vamos agregar uno para la columna is_public para explicar mejor su significado:
$this->widgetSchema->setHelp('is_public', 'Whether the job can also be published on affiliate websites or not.');

La clase final JobeetJobForm se lee como sigue:


// lib/form/doctrine/JobeetJobForm.class.php class JobeetJobForm extends BaseJobeetJobForm { public function configure() { unset( $this['created_at'], $this['updated_at'], $this['expires_at'], $this['is_activated'] ); $this->validatorSchema['email'] = new sfValidatorAnd(array( $this->validatorSchema['email'], new sfValidatorEmail(), )); $this->widgetSchema['type'] = new sfWidgetFormChoice(array( 'choices' => Doctrine_Core::getTable('JobeetJob')->getTypes(), 'expanded' => true, )); $this->validatorSchema['type'] = new sfValidatorChoice(array( 'choices' => array_keys(Doctrine_Core::getTable('JobeetJob')>getTypes()), )); $this->widgetSchema['logo'] = new sfWidgetFormInputFile(array( 'label' => 'Company logo', )); $this->widgetSchema->setLabels(array( 'category_id' => 'Category',

'is_public' 'how_to_apply' ));

=> 'Public?', => 'How to apply?',

$this->validatorSchema['logo'] = new sfValidatorFile(array( 'required' => false, 'path' => sfConfig::get('sf_upload_dir').'/jobs', 'mime_types' => 'web_images', )); $this->widgetSchema->setHelp('is_public', 'Whether the job can also be published on affiliate websites or not.'); } }

La Plantilla del Formulario Ahora que la clase form ha sido personalizada, necesitamos mostrarla. La plantilla para el formulario es la misma si quieres crear un nuevo puesto de trabajo o editar uno existente. De hecho, ambas plantilla newSuccess.php y editSuccess.php son bastante similares:
<!-- apps/frontend/modules/job/templates/newSuccess.php --> <?php use_stylesheet('job.css') ?> <h1>Post a Job</h1> <?php include_partial('form', array('form' => $form)) ?> Si no has agregado la hoja de estilo job an, es tiempo de hacerlo para ambas plantillas (<?php use_stylesheet('job.css') ?>).

El formulario en s mismo es mostrado en el partial _form. Reemplaza el contenido del partial generado _form con el siguiente cdigo:
<!-- apps/frontend/modules/job/templates/_form.php --> <?php use_stylesheets_for_form($form) ?> <?php use_javascripts_for_form($form) ?> <?php echo form_tag_for($form, '@job') ?> <table id="job_form"> <tfoot> <tr> <td colspan="2"> <input type="submit" value="Preview your job" /> </td> </tr> </tfoot> <tbody> <?php echo $form ?> </tbody> </table>

</form>

Los helpers use_javascripts_for_form() y use_stylesheets_for_form() incluyen el JavaScript y hoja de estilo necesarios para los form widgets.
Incluso si el formulario job no necesita ningn JavaScript o hoja de estilo, se trata de un buen hbito mantener estos helper llamandolos "por las dudas". Puede salvar tu da si decides cambiar un widget que necesite de algunos JavaScript o una hoja de estilo especfica.

El helper form_tag_for() genera una etiqueta <form> para un formulario y ruta dado y cambia los mtodos HTTP a POST o PUT dependiendo de si el objeto es nuevo o no. Tambin se ocupa del atributo multipart si el formulario tiene algun file input. Eventualmente, <?php echo $form ?> muestra los form widgets.
Personalizar el Look and Feel de un Form Por defecto, <?php echo $form ?> muestra los form widgets como una tabla. La mayora de las veces, tendrs que personalizar el diseo de tus formularios. El objeto form ofrece muchos mtodos tiles para esta personalizacin:

Mtodo
render()

Descripcin Muestra el formulario (equivalente a la salida de echo


$form)

renderHiddenFields() Muestra los campos ocultos hasErrors() hasGlobalErrors() getGlobalErrors()

Devuelve true si el form tiene algunos errores Devuelve true si el form tiene errores globales Devuelve un array de errores globales

renderGlobalErrors() Muestra errores globales El formulario tambin se comporta como un array de campos. Puedes acceder al campo company con $form['company']. El objeto devuelto proporciona mtodos para mostrar cada uno de los elementos del campo:

Mtodo
renderRow()

Descripcin Muestra el campo fila

Mtodo
render()

Descripcin Muestra el campo widget

renderLabel() Muestra el campo label renderError() Muestra el campo mensajes de error en caso de haber renderHelp()

Muestra el campo de mensajes de ayuda

La sentencia echo $form es equivalente a: <?php foreach ($form as $widget): ?> <?php echo $widget->renderRow() ?> <?php endforeach ?>

La Acci del Formulario Tenemos una clase form y una plantilla que la muestra. Ahora, es el momento de realmente hacer que funcione con algunas acciones. El formulario job es gestionado por cinco mtodos en el mdulo job:

new: Muestra un formulario en blanco para crear un nuevo puesto de trabajo edit: Muestra un formulario para editar uno existente create: Crea un nuevo puesto de trabajo con los valores enviados por el usuario update: Actualiza uno existente con los valores enviados processForm: Invocado por create y update, este procesa el form (validacin, rellena el formulario, y serializacin a la base de datos)

Todos los formularios tienen el siguieten ciclo de vida:

Como hemos creado una coleccin de rutas Doctrine 5 das atras para el mdulo job, podemos simplificar el cdigo del formulario y sus mtodos de gestin:
// apps/frontend/modules/job/actions/actions.class.php public function executeNew(sfWebRequest $request) { $this->form = new JobeetJobForm(); } public function executeCreate(sfWebRequest $request) { $this->form = new JobeetJobForm(); $this->processForm($request, $this->form); $this->setTemplate('new'); } public function executeEdit(sfWebRequest $request) { $this->form = new JobeetJobForm($this->getRoute()->getObject()); } public function executeUpdate(sfWebRequest $request) { $this->form = new JobeetJobForm($this->getRoute()->getObject()); $this->processForm($request, $this->form); $this->setTemplate('edit');

} public function executeDelete(sfWebRequest $request) { $request->checkCSRFProtection(); $job = $this->getRoute()->getObject(); $job->delete(); $this->redirect('job/index'); } protected function processForm(sfWebRequest $request, sfForm $form) { $form->bind( $request->getParameter($form->getName()), $request->getFiles($form->getName()) ); if ($form->isValid()) { $job = $form->save(); $this->redirect($this->generateUrl('job_show', $job)); } }

Cuando navegamos a la pgina /job/new, una nueva instancia de form es creada y pasada a la plantilla (accin new). Cuando el usuario enva el formulario (accin create), el formulario es atado (mtodo bind()) con los valores envados por el usuario y la validacin es desencadenada. Una vez que el formulario est, es posible de comprobar su validez usando el mtodo isValid(): Si el formulario es vlido (regresa true), el job es guardado en la base de datos ($form->save()), y el usuario es redirigido a la pgina de vista previa; si no, la plantilla newSuccess.php es mostrada de nuevo con los valores envados por el usuario y los mensajes de error asociados.
El mtodo setTemplate() cambia la plantilla empleada para una accin dada. Si el formulario envado no es vlido, los mtodos create y updateusan la misma plantilla para las acciones new y edit respectivamente, para mostrar el formulario con mensajes de error.

La modificacin de un puesto de trabajo existente es bastante similar. La nica diferencia entre la accin new y edit es que el objeto job para ser modificado es pasado como el primer argumento del constructor de form. Este objeto se utilizar por defecto en la plantilla (los valores por defecto son un objeto para los formularios Doctrine, un simple array de simples formularios).

Tambien puedes definir los valores por defecto para la creacin. Una forma es declarar los valores en el esquema de base de datos. Otra es pasar un premodificado objeto Job al constructor de form. Cambia el mtodo executeNew() para definir full-time como el valor por defecto para la columna type:
// apps/frontend/modules/job/actions/actions.class.php public function executeNew(sfWebRequest $request) { $job = new JobeetJob(); $job->setType('full-time'); $this->form = new JobeetJobForm($job); } Cuando el formulario es tomado, los valores por defecto se sustituirn por los del usuario. El usuario enva valores que se utilizarn para rellenar el formulario cuando el formulario se devuelve en caso de errores de validacin.

Proteger el formulario Job con un Token Todo debe funcionar bien por ahora. A partir de ahora, el usuario debe ingresar el token para el puesto de trabajo (job). Pero el token del puesto de trabajo debe ser generado automticamente cuando un nuevo puesto de trabajo es creado, ya que no queremos proporcionarle al usuario un nico token. Actualiza el mtodo save() de JobeetJob para agregar la lgica que genera los token antes de que un nuevo puesto de trabajo sea guardado:
// lib/model/doctrine/JobeetJob.class.php public function save(Doctrine_Connection $conn = null) { // ... if (!$this->getToken()) { $this->setToken(sha1($this->getEmail().rand(11111, 99999))); } return parent::save($conn); }

Puedes ahora eliminar el campo token del form:


// lib/form/doctrine/JobeetJobForm.class.php class JobeetJobForm extends BaseJobeetJobForm { public function configure() { unset( $this['created_at'], $this['updated_at'], $this['expires_at'], $this['is_activated'],

$this['token'] ); // ... } // ... }

Si recuerdas los casos de uso del da 2, un puesto de trabajo o job puede ser editado solo si el usuario conoce el token asociado. En este momento, es bastante fcil de editar o eliminar cualquier puesto de trabajo, simplemente adivinando la URL. Esto se debe a que la edicin del URL es como /job/ID/edit, donde ID es la clave principal del puesto de trabajo (job). Por defecto, una ruta sfDoctrineRouteCollection genera URLs con la clave primaria, pero puede ser cambiado a una nica columna pasando la opcin column:
# apps/frontend/config/routing.yml job: class: sfDoctrineRouteCollection options: { model: JobeetJob, column: token } requirements: { token: \w+ }

Nota que tenemos tambin que cambiar el parmetro de requirement token para que coincida con cualquier cadena ya que el requirements por defecto de Symfony es \d+ para la clave nica. Ahora, todas las rutas, excepto la job_show_user, tienen un token. Por ejemplo, la ruta para editar un job es ahora:
http://www.jobeet.com.localhost/job/TOKEN/edit

Tambin tendrs que cambiar el enlace "Edit" en la plantilla showSuccess:


<!-- apps/frontend/modules/job/templates/showSuccess.php --> <a href="<?php echo url_for('job_edit', $job) ?>">Edit</a> Tambin hemos cambiado los requisitos para la columna token ya que para Symfony y su requirements por defecto es \d+ para la clave principal.

La Pgina de Vista Previa


La pgina de vista previa es la misma que para la visualizacin de la pgina de los puestos de trabajo. Gracias a la ruta, el usuario viene con el correcto token, accesible en el parametro token. Si el usuario entra con una URL con token, vamos a aadir una barra de admin en la parte superior. Al comienzo de la plantilla showSuccess, aadir un partial para mostrar la barra de administrador y eliminar el enlace edit en la parte inferior:
<!-- apps/frontend/modules/job/templates/showSuccess.php --> <?php if ($sf_request->getParameter('token') == $job->getToken()): ?> <?php include_partial('job/admin', array('job' => $job)) ?>

<?php endif ?>

Entonces, crea el partial _admin:


<!-- apps/frontend/modules/job/templates/_admin.php --> <div id="job_actions"> <h3>Admin</h3> <ul> <?php if (!$job->getIsActivated()): ?> <li><?php echo link_to('Edit', 'job_edit', $job) ?></li> <li><?php echo link_to('Publish', 'job_edit', $job) ?></li> <?php endif ?> <li><?php echo link_to('Delete', 'job_delete', $job, array('method' => 'delete', 'confirm' => 'Are you sure?')) ?></li> <?php if ($job->getIsActivated()): ?> <li<?php $job->expiresSoon() and print ' class="expires_soon"' ?>> <?php if ($job->isExpired()): ?> Expired <?php else: ?> Expires in <strong><?php echo $job->getDaysBeforeExpires() ?></strong> days <?php endif ?> <?php if ($job->expiresSoon()): ?> - <a href="">Extend</a> for another <?php echo sfConfig::get('app_active_days') ?> days <?php endif ?> </li> <?php else: ?> <li> [Bookmark this <?php echo link_to('URL', 'job_show', $job, true) ?> to manage this job in the future.] </li> <?php endif ?> </ul> </div>

Hay un montn de cdigo, pero la mayor parte del cdigo es fcil de entender. Para hacer la plantilla ms fcil de leer, hemos aadido un montn de mtodos de acceso directo en la clase JobeetJob:
// lib/model/doctrine/JobeetJob.class.php public function getTypeName() { $types = Doctrine_Core::getTable('JobeetJob')->getTypes(); return $this->getType() ? $types[$this->getType()] : ''; } public function isExpired() { return $this->getDaysBeforeExpires() < 0; }

public function expiresSoon() { return $this->getDaysBeforeExpires() < 5; } public function getDaysBeforeExpires() { return ceil(($this->getDateTimeObject('expires_at')->format('U') time()) / 86400); }

La barra de admin muestra diferentes acciones, dependiendo del estado del puesto de trabajo:

Activacin y Publicacin del Puesto de Trabajo


En la seccin anterior, hay un enlace para publicar el trabajo. El enlace debe ser cambiado para que apunte a una nueva accin publish. En lugar de crear una nueva ruta, podemos configurar la actual ruta job:
# apps/frontend/config/routing.yml job: class: sfDoctrineRouteCollection options: model: JobeetJob column: token object_actions: { publish: put } requirements: token: \w+

El object_actions toma un array de acciones adicionales para el objeto dado. Ahora podemos cambiar el vnculo del enlace "Publish":

<!-- apps/frontend/modules/job/templates/_admin.php --> <li> <?php echo link_to('Publish', 'job_publish', $job, array('method' => 'put')) ?> </li>

El ltimo paso es crear la accin publish:


// apps/frontend/modules/job/actions/actions.class.php public function executePublish(sfWebRequest $request) { $request->checkCSRFProtection(); $job = $this->getRoute()->getObject(); $job->publish(); $this->getUser()->setFlash('notice', sprintf('Your job is now online for %s days.', sfConfig::get('app_active_days'))); $this->redirect($this->generateUrl('job_show_user', $job)); }

El lector astuto se habr dado cuenta que el enlace "Publish" es enviado con el mtodo HTTP put. Para simular el mtodo put, el enlace es automticamente convertido a un formulario cuando haces clic en l. Y debido a que hemos activado la proteccin CSRF, el helper link_to() tiene un token CSRF en el enlace y el mtodo checkCSRFProtection() del objeto comprueba la validez del envo. El mtodo executePublish() usa un nuevo mtodo publish() que puede definirse como sigue:
// lib/model/doctrine/JobeetJob.class.php public function publish() { $this->setIsActivated(true); $this->save(); }

Ahora puedes probar la nueva caracterstica para publicar desde tu navegador. Pero todava tenemos algo que arreglar. Los puestos de trabajo inactivos no deben ser accesibles, lo que significa que no debe aparecer en la pgina principal Jobeet, y no deben ser accesible por sus URL. Como hemos creado un mtodo addActiveJobsCriteria() para restringir unDoctrine_Query a puestos de trabajo activos, podemos editarlo y aadir la nueva exigencia al final:
// lib/model/doctrine/JobeetJobTable.class.php public function addActiveJobsQuery(Doctrine_Query $q = null) { // ... $q->andWhere($alias . '.is_activated = ?', 1);

return $q; }

Eso es todo. Puedes probar ahora en tu navegador. Todos los puestos de trabajo no activos han desaparecido de la pgina principal, incluso si conoces su URL, ya no son accesibles. Sin embargo, son accesibles si se conoce el token URL del puesto de trabajo. En ese caso, el puesto de trabajo se mostrar con una vista previa y el admin bar. Esta es una de las grandes ventajas del modelo MVC y la refactorizacin que hemos hecho a lo largo del camino. Un solo cambio en un mtodo fue necesario para aadir el nuevo requisito.
Cuando creamos el mtodo getWithJobs(), hemos olvidado de utilizar el mtodo addActiveJobsQuery(). Por lo tanto, tenemos que editar y aadir el nuevo requisito: class JobeetCategoryTable extends Doctrine_Table { public function getWithJobs() { // ... $q->andWhere('j.is_activated = ?', 1); return $q->execute(); }

Probando el Formulario
Eso es lo que haremos el da de hoy. A lo largo del camino, tambin vamos a aprender ms sobre el framework de formularios.
Usando el Framework de Formularios sin Symfony Los componentes del Framework Symfony estn bastante desacoplados. Esto significa que la mayora de ellos se pueden utilizar sin necesidad de utilizar todo el Framework MVC. Ese es el caso del Framework de Formularios, el cual no dependen de Symfony. Puedes utilizarlo en cualquier aplicacin PHP obteniendo los directorios lib/form/, lib/widgets/, ylib/validators/ . Otro componente reusable es el framework de enrutamiento. Copia el directorio lib/routing/ en tu proyecto non-symfony, y beneficiate URLs ricas sin costo alguno. Los componentes symfony-independentes de la Plataforma Symfony son:

Enviando un Formulario
Vamos a abrir el archivo jobActionsTest para agregar pruebas funcionales para el proceso de creacin y validacin de un puesto de trabajo. Al final del archivo, agrega el cdigo siguiente para obtener la pgina de creacin del puesto de trabajo:
// test/functional/frontend/jobActionsTest.php $browser->info('3 - Post a Job page')-> info(' 3.1 - Submit a Job')-> get('/job/new')-> with('request')->begin()-> isParameter('module', 'job')-> isParameter('action', 'new')-> end() ;

Ya hemos usado el mtodo click() para simular los clics en los enlaces. El mismo mtodo click() puede utilizarse para enviar un formulario. Un formulario, puede transferir los valores a enviar para cada campo como un segundo argumento del mtodo. Como un verdadero navegador, el objeto browser mezclar los valores por defecto del formulario con los valores enviados. Sin embargo, para pasar los valores del campo, necesitamos saber sus nombres. Si abres el cdigo fuente o usas la Firefox Web Developer Toolbar "Forms > Display Form Details", vers que el nombre del campo company es jobeet_job[company].
Cundo PHP se encuentra con un campo input con un nombre como jobeet_job[company], este lo convierte automticamente a un array de nombre jobeet_job.

Para hacer las cosas un poco ms limpias, vamos a cambiar el formato a job[%s] aadiendo el siguiente cdigo al final del mtodo configure() deJobeetJobForm:
// lib/form/doctrine/JobeetJobForm.class.php $this->widgetSchema->setNameFormat('job[%s]');

Despus de este cambio, el nombre company debera aparecer como job[company] en tu navegador. Ahora es el momento de realmente hacer clic en el botn "Preview your job" y transmitir los valores vlidos al formulario:
// test/functional/frontend/jobActionsTest.php $browser->info('3 - Post a Job page')-> info(' 3.1 - Submit a Job')-> get('/job/new')-> with('request')->begin()-> isParameter('module', 'job')-> isParameter('action', 'new')-> end()-> click('Preview your 'company' => 'url' => 'logo' => labs.gif', 'position' => 'location' => 'description' => our customers.', 'how_to_apply' => 'email' => 'is_public' => )))-> job', array('job' => array( 'Sensio Labs', 'http://www.sensio.com/', sfConfig::get('sf_upload_dir').'/jobs/sensio'Developer', 'Atlanta, USA', 'You will work with symfony to develop websites for 'Send me an email', 'for.a.job@example.com', false,

with('request')->begin()->

isParameter('module', 'job')-> isParameter('action', 'create')-> end() ;

El navegador tambin simula la carga de archivos mediante el paso de la ruta absoluta del archivo a cargar. Despus de enviar el formulario, comprobamos que la accin ejecutada es create.

El Tester de Formularios
El formulario que hemos enviado debera ser vlido. Puedes probarlo usando el tester form:
with('form')->begin()-> hasErrors(false)-> end()->

El tester form tiene varios mtodos para probar el estado del formulario actual, como los errores. Si cometes un error en la prueba, y la prueba no pasa, puedes usar la instruccin with('response')->debug() que hemos visto durante el da 9. Pero tendrs que entrar al HTML generado para ver si hay mensajes de error. Aunque eso no es realmente conveniente. El tester form tambin proporciona un mtodo debug() que muestra el estado del formulario y todos los mensajes de error asociados a l:
with('form')->debug()

Probando la Redireccin
Como el formulario es vlido, el puesto de trabajo debera haber sido creado y el usuario se redirige a la pgina show:
isRedirected()-> followRedirect()-> with('request')->begin()-> isParameter('module', 'job')-> isParameter('action', 'show')-> end()->

El mtodo isRedirected() prueba si la pgina se ha redireccionado y el mtodo followRedirect() sigue la redireccin.


La clase browser no sigue las redirecciones automaticamente como podras imaginar para inferir objetos antes de la redireccin.

El Tester Doctrine

Finalmente, queremos poner a prueba que el puesto de trabajo se ha creado en la base de datos y comprobar que la columna is_activated est enfalse ya que el usuario no lo ha publicado todava. Esto puede hacerse fcilmente mediante el uso de otro tester, el Tester de Propel o Propel tester. Como el tester de Doctrine no est registrado por defecto, vamos a aadirlo ahora al navegador:
$browser->setTester('doctrine', 'sfTesterDoctrine');

El tester Doctrine proporciona el mtodo check() sirve para comprobar que uno o ms objetos en la base de datos coinciden con el criterio pasado como argumento.
with('doctrine')->begin()-> check('JobeetJob', array( 'location' => 'Atlanta, USA', 'is_activated' => false, 'is_public' => false, ))-> end()

El criterio puede ser un array de valores como los anteriores, o un una instancia de Doctrine_Query para bsquedas ms complejas. Puedes probar la existencia de objetos que concuerden con el criterio con un Boolean como tercer argumento (por defecto es true), o el nmero de objetos coincidentes mediante un entero.

Probando los Errores


El formulario de creacin de un puesto de trabajo funciona como se esperaba cuando se envian valores vlidos. Vamos a aadir una prueba para comprobar el comportamiento cuando se envian datos no vlidos:
$browser-> info(' 3.2 - Submit a Job with invalid values')-> get('/job/new')-> click('Preview your 'company' => 'position' => 'location' => 'email' => )))->

job', array('job' => array( 'Sensio Labs', 'Developer', 'Atlanta, USA', 'not.an.email',

with('form')->begin()-> hasErrors(3)-> isError('description', 'required')-> isError('how_to_apply', 'required')-> isError('email', 'invalid')-> end() ;

El mtodo hasErrors() puede poner a prueba el nmero de errores si se pasa un entero. El mtodo isError() prueba el cdigo de error para un determinado campo.
En las pruebas que hemos escrito para el envo de datos no vlidos, no tenemos que probar todo el formulario de nuevo. Slo hemos aadido las pruebas para cosas especficas.

Tambin puedes probar el HTML generado para comprobar que contiene los mensajes de error, pero no es necesario en nuestro caso ya que no hemos personalizado el layout del formulario. Ahora, vamos a probar la barra de administrador de la pgina de vista previa de job. Cuando un puesto de trabajo no se ha activado, se puede editar, eliminar o publicar el puesto de trabajo. Para probar estos tres enlaces, tendremos que crear primero un puesto de trabajo. Pero eso es un montn de copiar y pegar. Como no me gusta, vamos a aadir un mtodo creador de puesto de trabajo en la clase JobeetTestFunctional:
// lib/test/JobeetTestFunctional.class.php class JobeetTestFunctional extends sfTestFunctional { public function createJob($values = array()) { return $this-> get('/job/new')-> click('Preview your job', array('job' => array_merge(array( 'company' => 'Sensio Labs', 'url' => 'http://www.sensio.com/', 'position' => 'Developer', 'location' => 'Atlanta, USA', 'description' => 'You will work with symfony to develop websites for our customers.', 'how_to_apply' => 'Send me an email', 'email' => 'for.a.job@example.com', 'is_public' => false, ), $values)))-> followRedirect() ; } // ... }

El mtodo createJob() crea un puesto de trabajo, sigue la redireccin y regresa al navegador para no romper el fluidez de la navegacin. Puedes tambin pasar un array de valores que se fusionar con algunos valores por defecto.

Forzando al Mtodo HTTP de un Enlace


Probar el enlace "Publish" es ahora ms sencillo:

$browser->info(' 3.3 - On the preview page, you can publish the job')-> createJob(array('position' => 'FOO1'))-> click('Publish', array(), array('method' => 'put', '_with_csrf' => true))-> with('doctrine')->begin()-> check('JobeetJob', array( 'position' => 'FOO1', 'is_activated' => true, ))-> end() ;

Si recuerdas el da 10, el enlace "Publish" se ha configurado para ser llamado con el mtodo HTTP PUT. Como los navegadores no entienden peticiones PUT, el helper link_to() convierte el enlace en un formulario con algun JavaScript. Como el test browser no ejecuta JavaScript, es necesario forzar el mtodo a PUT pasandolo como una tercera opcin del mtodo click(). Por otra parte, la helper link_to() tambin incluye un CSRF token ya que hemos habilitado la proteccin CSRF durante el da 1; la opcin _with_csrf simula este token. Probar el enlace "Delete" es bastante similar:
$browser->info(' 3.4 - On the preview page, you can delete the job')-> createJob(array('position' => 'FOO2'))-> click('Delete', array(), array('method' => 'delete', '_with_csrf' => true))-> with('doctrine')->begin()-> check('JobeetJob', array( 'position' => 'FOO2', ), false)-> end() ;

Pruebas como SafeGuard


Cuando un puesto de trabajo se publica, no se puede editar ms. Incluso si el enlace "Edit" ya no se muestra en la pgina de vista previa, vamos a aadir algunas pruebas de este requisito. En primer lugar, aadir otro argumento al mtodo createJob() para permitir automticamente la publicacin del puesto de trabajo, y crea un mtodo getJobByPosition() que devuelve un puesto de trabajo dado su valor:
// lib/test/JobeetTestFunctional.class.php class JobeetTestFunctional extends sfTestFunctional { public function createJob($values = array(), $publish = false) { $this-> get('/job/new')->

click('Preview your 'company' => 'url' => 'position' => 'location' => 'description' => for our customers.', 'how_to_apply' => 'email' => 'is_public' => ), $values)))-> followRedirect() ;

job', array('job' => array_merge(array( 'Sensio Labs', 'http://www.sensio.com/', 'Developer', 'Atlanta, USA', 'You will work with symfony to develop websites 'Send me an email', 'for.a.job@example.com', false,

if ($publish) { $this-> click('Publish', array(), array('method' => 'put', '_with_csrf' => true))-> followRedirect() ; } return $this; } public function getJobByPosition($position) { $q = Doctrine_Query::create() ->from('JobeetJob j') ->where('j.position = ?', $position); return $q->fetchOne(); } // ... }

Si un puesto de trabajo se publica, la pgina de edicin debe devolver un cdigo de estado 404:
$browser->info(' 3.5 - When a job is published, it cannot be edited anymore')-> createJob(array('position' => 'FOO3'), true)-> get(sprintf('/job/%s/edit', $browser->getJobByPosition('FOO3')>getToken()))-> with('response')->begin()-> isStatusCode(404)-> end() ;

Sin embargo, si ejecutas las pruebas, no tendrs el resultado esperado ya que se te olvid de implementar esta medida de seguridad de ayer. Escribir pruebas es tambin una buena manera de descubrir los errores, ya que necesitas pensar en todos los casos. Arreglar los errores es muy sencillo ya que slo hay que avanzar a una pgina 404, si el puesto esta activado:
// apps/frontend/modules/job/actions/actions.class.php public function executeEdit(sfWebRequest $request) { $job = $this->getRoute()->getObject(); $this->forward404If($job->getIsActivated()); $this->form = new JobeetJobForm($job); }

La solucin es trivial, pero est seguro de que todo lo dems sigue funcionando como se esperaba? Puedes abrir el navegador y empezar a probar todas las combinaciones posibles para acceder a la pgina de edicin. Pero hay una manera ms sencilla: ejecutar tu conjunto de pruebas; si se ha introducido una regresin u error, Symfony te lo dir enseguida.

Regresando al Futuro en una Prueba


Cuando un puesto de trabajo expira en menos de cinco das, o si ya est vencido, el usuario puede ampliar la validacin del puesto de trabajo por 30 das ms a partir de la fecha actual. Probar este requisito en un navegador no es fcil ya que la fecha de vencimiento se establece automticamente cuando se crea el puesto de trabajo a 30 das en el futuro. Por lo tanto, cuando obtienes la pgina del puesto de trabajo, el enlace para extender la validez del puesto de trabajo no est presente. Claro, se puede hackear la fecha de caducidad en la base de datos, o modificar la plantilla para que se muestre siempre el vnculo, pero eso es tedioso y propenso a errores. Como ya has adivinado, escribir algunas pruebas nos ayudarn una vez ms. Como siempre, tenemos que aadir una nueva ruta para el mtodo extend primero:
# apps/frontend/config/routing.yml job: class: sfDoctrineRouteCollection options: model: JobeetJob column: token object_actions: { publish: PUT, extend: PUT } requirements: token: \w+

A continuacin, la actualizacin del cdigo del enlace "Extend" en el partial _admin:

<!-- apps/frontend/modules/job/templates/_admin.php --> <?php if ($job->expiresSoon()): ?> - <?php echo link_to('Extend', 'job_extend', $job, array('method' => 'put')) ?> for another <?php echo sfConfig::get('app_active_days') ?> days <?php endif; ?>

Entonces, crea la accin extend:


<!-- apps/frontend/modules/job/templates/_admin.php --> <?php if ($job->expiresSoon()): ?> - <?php echo link_to('Extend', 'job_extend', $job, array('method' => 'put')) ?> for another <?php echo sfConfig::get('app_active_days') ?> days <?php endif; ?>

Then, create the extend action:


// apps/frontend/modules/job/actions/actions.class.php public function executeExtend(sfWebRequest $request) { $request->checkCSRFProtection(); $job = $this->getRoute()->getObject(); $this->forward404Unless($job->extend()); $this->getUser()->setFlash('notice', sprintf('Your job validity has been extended until %s.', $job->getDateTimeObject('expires_at')>format('m/d/Y'))); $this->redirect($this->generateUrl('job_show_user', $job)); }

Como era de esperar por la accin, el mtodo extend() de JobeetJob devuelve true si el puesto de trabajo se ha extendido o false de lo contrario:
// lib/model/doctrine/JobeetJob.class.php class JobeetJob extends BaseJobeetJob { public function extend() { if (!$this->expiresSoon()) { return false; } $this->setExpiresAt(date('Y-m-d', time() + 86400 * sfConfig::get('app_active_days'))); $this->save(); return true;

} // ... }

Finalmente, aadir un escenario de prueba:


$browser->info(' 3.6 - A job validity cannot be extended before the job expires soon')-> createJob(array('position' => 'FOO4'), true)-> call(sprintf('/job/%s/extend', $browser->getJobByPosition('FOO4')>getToken()), 'put', array('_with_csrf' => true))-> with('response')->begin()-> isStatusCode(404)-> end() ; $browser->info(' 3.7 - A job validity can be extended when the job expires soon')-> createJob(array('position' => 'FOO5'), true) ; $job = $browser->getJobByPosition('FOO5'); $job->setExpiresAt(date('Y-m-d')); $job->save(); $browser-> call(sprintf('/job/%s/extend', $job->getToken()), 'put', array('_with_csrf' => true))-> with('response')->isRedirected() ; $job->refresh(); $browser->test()->is( $job->getDateTimeObject('expires_at')->format('y/m/d'), date('y/m/d', time() + 86400 * sfConfig::get('app_active_days')) );

Este escenario de pruebas presenta un pocas cosas nuevas:


El mtodo call() trae una URL con un mtodo diferente de GET o POST Despus de que el puesto de trabajo ha sido actualizado por la accin, tenemos que volver a cargar el objeto con $job->refresh() Al final, hemos utilizado el objeto incrustado lime directamente para poner a prueba la nueva fecha de expiracin.

Seguridad en Formularios
La Magia de los Formularios Serializados!

Los Formularios Doctrine son muy fciles de usar, ya que automatizan una gran cantidad de trabajo. Por ejemplo, serializar un formulario a la base de datos es tan simple como una llamada a $form->save(). Cmo funciona? Bsicamente, el mtodo save() hace las siguientes pasos:

Comenzar una transaccin (porque Formularios anidados de Doctrine se guardan todos de una sola vez) Procesar los valores enviados (llamando a mtodos updateCOLUMNColumn() si existen) Llamar al mtodo fromArray() del objeto Doctrine para actualizar los valores de las columnas Guardar el objeto en la base de datos Commit/Finalizar la transaccin

Elementos de Seguridad Incorporados El mtodo fromArray() toma un array los valores y actualiza los correspondientes valores de las columnas. Esto representa un problema de seguridad? Qu pasa si alguien trata de enviar un valor para una columna para la que no dispone de autorizacin? Por ejemplo, se puede forzar la columna token? Vamos a escribir una prueba para simular el envo de un puesto de trabajo con un campo token:
// test/functional/frontend/jobActionsTest.php $browser-> get('/job/new')-> click('Preview your job', array('job' => array( 'token' => 'fake_token', )))-> with('form')->begin()-> hasErrors(7)-> hasGlobalError('extra_fields')-> end() ;

Cuando se envia el formulario, debes tener un error global extra_fields. Esto se debe a que por defecto los formularios no permiten campos extra en valores enviados. As es porque todos los campos de formulario deben tener un validador de asociado.
Tambin puedes enviar campos adicionales desde la comodidad de tu navegador utilizando herramientas con el Firefox Web Developer Toolbar.

Puedes saltear esta medida de seguridad mediante el establecimiento de la opcin allow_extra_fields a true:
class MyForm extends sfForm {

public function configure() { // ... $this->validatorSchema->setOption('allow_extra_fields', true); } }

La prueba debe pasar ahora, pero el valor token, se ha excluido de los valores. As pues, todava no puedes pasar por alto esta medida de seguridad. Pero si realmente quieres el valor, establece la opcin filter_extra_fields a false:
$this->validatorSchema->setOption('filter_extra_fields', false); Las pruebas escritas en esta seccin son nicamente para efectos de demostrativos. Puedes ahora eliminarlos del proyecto Jobeet ya que las pruebas no necesitan validar caractersticas de Symfony.

Proteccin XSS y CSRF Durante el da 1, aprendiste la tarea generate:app creando una aplicacin segura por defecto. Primero, se habilit la proteccin contra XSS. Esto significa que todas las variables utilizadas en las plantillas se escaparn por defecto. Si intentas enviar una descripcin del trabajo con algunas etiquetas HTML dentro, te dars cuenta que cuando Symfony muestra la pgina del puesto de trabajo, las etiquetas HTML de la descripcin no se interpretan, pero si se ven como texto plano sin formato. Entonces se habilita la proteccin CSRF. Cuando un token CSRF es configurado, todos los formularios incrustan un campo oculto _csrf_token.
La estrategia de escape y el CSRF secreto se pueden cambiar en cualquier momento editando el archivo de configuracinapps/frontend/config/settings.yml. En cuanto a el archivo databases.yml, los ajustes son configurables por el entorno: all: .settings: # Form security secret (CSRF protection) csrf_secret: Unique$ecret # Output escaping settings escaping_strategy: true escaping_method: ESC_SPECIALCHARS

Tareas de Mantenimiento
Incluso si Symfony es un framework web, viene con una herramienta de lnea de comandos. Ya la has utilizado para crear no solo la estructura de directorio por defecto del proyecto y de la aplicacin, sino tambin para generar varios archivos del modelo. Aadir una nueva Tarea es muy fcil ya que las herramientas utilizadas por la lnea de comando symfony se empaquetan en un framework.

Cuando un usuario crea un job, deber activarlo para ponerlo en lnea. Pero si no, la base de datos crecer con jobs intiles. Vamos a crear una tarea que elimine esos jobs de la base de datos. Esta tarea tendr que ser ejecutada peridicamente en un cron job.
// lib/task/JobeetCleanupTask.class.php class JobeetCleanupTask extends sfBaseTask { protected function configure() { $this->addOptions(array( new sfCommandOption('application', null, sfCommandOption::PARAMETER_REQUIRED, 'The application', 'frontend'), new sfCommandOption('env', null, sfCommandOption::PARAMETER_REQUIRED, 'The environement', 'prod'), new sfCommandOption('days', null, sfCommandOption::PARAMETER_REQUIRED, '', 90), )); $this->namespace = 'jobeet'; $this->name = 'cleanup'; $this->briefDescription = 'Cleanup Jobeet database'; $this->detailedDescription = <<<EOF The [jobeet:cleanup|INFO] task cleans up the Jobeet database: [./symfony jobeet:cleanup --env=prod --days=90|INFO] EOF; } protected function execute($arguments = array(), $options = array()) { $databaseManager = new sfDatabaseManager($this->configuration); $nb = Doctrine_Core::getTable('JobeetJob')>cleanup($options['days']); $this->logSection('doctrine', sprintf('Removed %d stale jobs', $nb)); } }

La configuracin se realiza en el mtodo configure(). Cada tarea debe tener un nombre nico (namespace:name), y puede tener argumentos y opciones.
Revisa las tareas ya incorporadas de Symfony (lib/task/) para ms ejemplos de su uso.

La tarea jobeet:cleanup define dos opciones: --env y --days con unos valores predeterminados razonables. La ejecucin de la tarea es similar a la ejecucin de cualquier otra tarea ya incorporada en Symfony:

$ php symfony jobeet:cleanup --days=10 --env=dev

Como siempre, el cdigo para tener una base de datos limpia ha sido un refactorizado en la clase JobeetJobTable:
// lib/model/doctrine/JobeetJobTable.class.php public function cleanup($days) { $q = $this->createQuery('a') ->delete() ->andWhere('a.is_activated = ?', 0) ->andWhere('a.created_at < ?', date('Y-m-d', time() - 86400 * $days)); return $q->execute(); } Las tareas de Symfony se comportan muy bien con su entorno ya que regresan un valor de acuerdo con el xito de la Tarea. Puedes forzar un valor devolviendo un nmero entero explcitamente al final de la Tarea.

El Generador de Administrador de contenido


Con la cambios que se hizo ayer en Jobeet, la aplicacin frontend esta ahora completamente utilizable por los oferentes y demandantes de empleo. Es hora de hablar un poco acerca de la aplicacin backend. Hoy, gracias a la funcionalidad del Generador de Admin de symfony, vamos a desarrollar una completa interfaz para el backend de Jobeet en slo una hora.

Creacin del Backend


El primer paso es crear la aplicacin backend. Si tu memoria te sirve bien, deberas recordar cmo hacerlo con la tareagenerate:app:
$ php symfony generate:app backend

La aplicacin backend ya est disponible en http://www.jobeet.com.localhost/backend.php/ para el entorno prod, and athttp://www.jobeet.com.localhost/backend_dev.php/ para el entorno dev.
Cuando creaste la aplicacin frontend, el controlador frontal de produccin fue llamado index.php. Como slo puede tener un index.php por directorio, symfony crea un index.php para el primer controlador frontal de produccin y nombra a los otros con el nombre de la aplicacin.

Si intentas volver a cargar los datos con la tarea doctrine:data-load, no va a funcionar ms. Esto se debe a que el mtodo JobeetJob::save()necesita tener acceso al archivo de configuracin app.yml de la aplicacin frontend. Como tenemos ahora dos aplicaciones, symfony usa la primera que encuentra, que es ahora backend. Pero como se ha visto durante el da 8, los ajustes se pueden configurar en distintos niveles. Al mover el contenido del archivoapps/frontend/config/app.yml a config/app.yml, los ajustes sern compartidos entre todas las aplicaciones y el problema ser corregido. Vamos hacer el cambio ahora ya que vamos a utilizar mucho el modelo de clases en el Generador de Admin, y as tendremos las variables definidas enapp.yml en la aplicacin backend.
La tarea doctrine:data-load tambin tiene una opcin --application. As que, si necesitas algunos ajustes especficos de una u otra aplicacin, esta es la forma de hacerlo: $ php symfony doctrine:data-load --application=frontend

Mdulos del Backend


Para la aplicacin frontend, la tarea doctrine:generate-module se ha utilizado para inicializar un mdulo bsico CRUD basado en una clase del modelo. Para el

backend, la tarea doctrine:generate-admin se utilizar, para generar una completa y funcional interfaz para una clase del modelo:
$ php symfony doctrine:generate-admin backend JobeetJob --module=job $ php symfony doctrine:generate-admin backend JobeetCategory -module=category

Estos dos comandos crean un mdulo job y uno category para las clases JobeetJob y JobeetCategory del modelo respectivamente. La opcin (opcional) --module sobreescribe el nombre del mdulo generado por defecto por la tarea (que habra sido de lo contrario jobeet_job para la clase JobeetJob). Detrs de las escenas, la tarea tambin ha creado una ruta personalizada para cada mdulo:
# apps/backend/config/routing.yml jobeet_job: class: sfDoctrineRouteCollection options: model: JobeetJob module: job prefix_path: job column: id with_wildcard_routes: true

No es de extraar que la ruta de la clase utilizada por el Generador de Admin es sfDoctrineRouteCollection, ya que el principal objetivo de una interfaz de administracin es la gestin del ciclo de vida de los objetos del modelo. Definicin de la ruta tambin define algunas opciones que no hemos visto antes:
prefix_path: Define el prefijo de la url para la ruta generada (por ejemplo, la pgina editar ser algo as como /job/1/edit). column: Define la columna de la tabla a usar en la URL por los enlaces que

hace referencia a un objeto.


with_wildcard_routes: Como la interfaz de administrador tendr ms que

el clsico de operaciones CRUD, esta opcin permite definir una mayor coleccin de objetos y acciones sin editar la ruta.
Como siempre, es una buena idea leer la ayuda antes de usar una nueva tarea. $ php symfony help doctrine:generate-admin Te dar de la tarea, todos los argumentos y opciones, as como algunos clsicos Ejemplos de Uso.

Aspecto del Backend


De buenas a primeras, ya puedes utilizar los mdulos generados:
http://www.jobeet.com.localhost/backend_dev.php/job http://www.jobeet.com.localhost/backend_dev.php/category

Los mdulos de administracin tienen muchas ms funciones que los simples mdulos que hemos generado en los das anteriores. Sin escribir una sola lnea de PHP, cada mdulo proporciona estas caractersticas:

La lista de objetos esta paginada La lista es ordenable La lista puede ser filtrada Los Objetos pueden ser creados, editedos, y eliminados Los objetos seleccionados pueden ser eliminados en batch La validation esta habilitada Los Mensajes Flash dan informacin inmediata al usuario ... y mucho mucho ms

El Generador de Admin proporciona todas las funciones que necesitas para crear una interfaz backend en un paquete fcil de configurar. If you have a look at our two generated modules, you will notice there is no activated webdesign whereas the symfony built-in admin generator feature has a basic graphic interface by default. For now, assets from the sfDoctrinePlugin are not located under the web/ folder. We need to publish them under the web/ folder thanks to the plugin:publish-assets task:
$ php symfony plugin:publish-assets

Para hacer la experiencia de los usuarios un poco mejor, necesitamos personalizar el backend por defecto. Tambin vamos a aadir un men simple para que sea fcil de navegar entre los diferentes mdulos. Sustituye el contenido por defecto del layout.php con el siguiente:
// apps/backend/templates/layout.php <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <title>Jobeet Admin Interface</title> <link rel="shortcut icon" href="/favicon.ico" /> <?php use_stylesheet('admin.css') ?> <?php include_javascripts() ?> <?php include_stylesheets() ?> </head> <body> <div id="container"> <div id="header"> <h1> <a href="<?php echo url_for('homepage') ?>"> <img src="/images/logo.jpg" alt="Jobeet Job Board" /> </a> </h1> </div>

<div id="menu"> <ul> <li> <?php echo link_to('Jobs', 'jobeet_job') ?> </li> <li> <?php echo link_to('Categories', 'jobeet_category') ?> </li> </ul> </div> <div id="content"> <?php echo $sf_content ?> </div> <div id="footer"> <img src="/images/jobeet-mini.png" /> powered by <a href="http://www.symfony-project.org/"> <img src="/images/symfony.gif" alt="symfony framework" /></a> </div> </div> </body> </html>

Este layout usa una hoja de estilos admin.css. Este archivo debe estar presente en web/css/ ya que se instal con el resto de las hojas de estilo durante el da 4.

Eventualmente, cambia la pgina de inicio por defecto en symfony en routing.yml:


# apps/backend/config/routing.yml homepage:

url: / param: { module: job, action: index }

El Cache de Symfony
Si eres lo suficientemente curioso, probablemente ya has abierto los archivos generados por la tarea bajo el directorio apps/backend/modules/. Si no, por favor abrelos ahora. Sorpresa! Los directorios templates estn vacos, y los archivos actions.class.php estn bastante vacos y:
// apps/backend/modules/job/actions/actions.class.php require_once dirname(__FILE__).'/../lib/jobGeneratorConfiguration.class.php'; require_once dirname(__FILE__).'/../lib/jobGeneratorHelper.class.php'; class jobActions extends autoJobActions { }

Cmo es posiblemente que funcione? Si hechas un vistazo ms de cerca, te dars cuenta de que la clase jobActions hereda de autoJobActions. La clase autoJobActions es generada automticamente por symfony si no existe. Que se encuentra en el directoriocache/backend/dev/modules/autoJob/, la cual contiene el mdulo "real":
// cache/backend/dev/modules/autoJob/actions/actions.class.php class autoJobActions extends sfActions { public function preExecute() { $this->configuration = new jobGeneratorConfiguration(); if (!$this->getUser()->hasCredential( $this->configuration->getCredentials($this->getActionName()) )) { // ...

La forma en que el Generador de Admin trabaja debera recordarte algun conocido comportamiento. De hecho, es bastante similar a lo que ya hemos aprendido sobre el modelo y las clases de formulario. Basado en el modelo de la definicin de esquema, symfony genera el modelo y las clases de formulario. Para el Generador de Admin, el mdulo generado se puede configurar editando el archivo config/generator.yml que se encuentra en el mdulo:
# apps/backend/modules/job/config/generator.yml generator: class: sfDoctrineGenerator param: model_class: JobeetJob theme: admin

non_verbose_templates: with_show: singular: plural: route_prefix: with_doctrine_route: config: actions: fields: list: filter: form: edit: new:

true false ~ ~ jobeet_job true

~ ~ ~ ~ ~ ~ ~

Cada vez que actualizas el archivo generator.yml, symfony regenera el cach. Como se ver hoy, la personalizacin de los mdulos generados de administracin es fcil, rpida y divertida.
NOTA La generacin automtica de archivos de cache slo se produce en el entorno de desarrollo En produccin, tendrs que borrar la cach manualmente con la tarea cache:clear (o borrar todo lo que encuentres en /cache).

La Configuracin del Backend


Un mdulo de administracin puede ser personalizado con la edicin de la clave config del archivo generator.yml. La configuracin est organizada en siete secciones:
actions: Configuracin por defecto de las acciones se encuentran en la lista

como en los formilarios


fields: Configuracin por defecto para los campos list: Configuracin de la lista filter: Configuracin de los filtros form: Configuracin del fomulario new/edit edit: Configuracin especfica para la pgina edit new: Configuracin especfica para la pgina new

Vamos a comenzar la personalizacin.

Configuracin del Ttulo


Los ttulos de las secciones list, edit, y new del mdulo category se puede personalizar mediante la definicin de una opcin title:
# apps/backend/modules/category/config/generator.yml config: actions: ~ fields: ~

list: title: filter: form: edit: title: new: title:

Category Management ~ ~ Editing Category "%%name%%" New Category

El title para la seccin edit contiene valores dinmicos: todas las cadenas encerradas entre %% se sustituyen por los correspondientes valores de las columnas del objeto.

La configuracin para el mdulo job es muy similar:


# apps/backend/modules/job/config/generator.yml config: actions: ~ fields: ~ list: title: Job Management filter: ~ form: ~ edit: title: Editing Job "%%company%% is looking for a %%position%%" new: title: Job Creation

La Configuracin de los Campos


Las diferentes vistas (list, new, y edit) se componen de campos. Un campo puede ser una columna de la clase del modelo, o una columna virtual como veremos ms adelante. La configuracin por defecto de los campos pueden ser personalizada con la seccin fields :
# apps/backend/modules/job/config/generator.yml config: fields: is_activated: { label: Activated?, help: Whether the user has activated the job, or not }

is_public: { label: Public?, help: Whether the job can also be published on affiliate websites, or not }

La seccin fields sobreecribe la configuracin de los campos para todas las vistas, lo que significa que label para is_activated se modific para las vistas list, edit, y new. La Configuracin del Generador de Admin se basa en el principio de una configuracin en cascada. Por ejemplo, si deseas cambiar un label solo para la vista list, define una opcin fields bajo la seccin list:
# apps/backend/modules/job/config/generator.yml config: list: fields: is_public: { label: "Public? (label for the list)" }

Cualquier configuracin que se establece en la seccin principal fields puede ser sobreecrita por la configuracin de una vista especfica. The overriding rules are the following:
new y edit heredan de form el cual hereda de fields list hereda de fields filter hereda de fields

Para las secciones form (form, edit, y new), las opciones label y help sobreescriben las definidas en las clases form.

Configuracin de la vista List


display

De forma predeterminada, las columnas de la vista List son todas las columnas del modelo, en el orden del archivo de esquema. La opcin displaysobreescribe lo predefinido ordenando las columnas a mostrar:
# apps/backend/modules/category/config/generator.yml config: list: title: Category Management display: [=name, slug]

El signo = antes del nombre de la columna es una convencin para convertir la cadena en un enlace.

Vamos a hacer lo mismo para el mdulo job para que sea ms legible:
# apps/backend/modules/job/config/generator.yml config: list: title: Job Management display: [company, position, location, url, is_activated, email] layout

La lista puede ser visualizada en diferentes layouts. Por defecto, el layout es tabular,lo que significa que cada valor de columna est en su propia columna de la tabla. Pero para el mdulo job, sera mejor utilizar el layout stacked, que es el otro layout de serie:
# apps/backend/modules/job/config/generator.yml config: list: title: Job Management layout: stacked display: [company, position, location, url, is_activated, email] params: | %%is_activated%% <small>%%category_id%%</small> - %%company%% (<em>%%email%%</em>) is looking for a %%=position%% (%%location%%)

En stacked, cada objeto est representado por una nica cadena, que se define por la opcin params.
La opcin display sigue siendo necesaria ya que define las columnas que ordenarn segn el criterio del usuario.

Las Columnas "Virtuales" Con esta configuracin, el segmento %%category_id%% ser sustituida por la clave principal de la categora. Pero sera ms til mostrar el nombre de la categora. Siempre que utilices la notacin %%, la variable no tiene por qu corresponder a

una columna en el actual esquema de base de datos. El Generador de Admin solo necesita encontrar un metodo get asociado en la clase del modelo. Para mostrar el nombre de la categora, podemos definir un mtodo getCategoryName() en la clase JobeetJob y sustituir %%category_id%% por%%category_name%%. Sin embargo, la clase JobeetJob ya tiene un mtodo getJobeetCategory() que devuelve el objeto de la categora relacionada. Y si usas%%jobeet_category%%, este funcionar ya que la clase JobeetCategory tiene un mtodo mgico __toString() que convierte el objeto a una cadena.
# apps/backend/modules/job/config/generator.yml %%is_activated%% <small>%%jobeet_category%%</small> - %%company%% (<em>%%email%%</em>) is looking for a %%=position%% (%%location%%)) sort

Como administrador, probablemente estes ms interesado en ver las ltimos puestos de trabajo envados. Puede configurar la columna de ordenacin por defecto al aadir una opcin sort:
# apps/backend/modules/job/config/generator.yml config: list: sort: [expires_at, desc] max_per_page

De forma predeterminada, la lista es paginada, y cada pgina contiene 20 items. Esto puede cambiarse con la opcin max_per_page:
# apps/backend/modules/job/config/generator.yml config: list: max_per_page: 10

batch_actions

En una lista, una accin se puede ejecutar sobre varios objetos. Estas acciones por lote no son necesarias para el mdulo category, as, vamos a eliminarlas:
# apps/backend/modules/category/config/generator.yml config: list: batch_actions: {}

La opcin batch_actions define la lista de acciones por lote. Un array vaco permite eliminar las caractersticas. De forma predeterminada, cada mdulo tiene una accin por lote delete definida por el framework, pero para el mdulo job, supongamos que necesitamos una manera de extender la validez de determinados puestos de trabajo para otros 30 das:
# apps/backend/modules/job/config/generator.yml config: list: batch_actions: _delete: ~ extend: ~

Todas las acciones que comienzan con un _ son acciones provistas por el framework. Si actualizas tu navegador y seleccionas las acciones extendidas,

symfony arrojarn una excepcin diciendote que crees un mtodo executeBatchExtend():


// apps/backend/modules/job/actions/actions.class.php class jobActions extends autoJobActions { public function executeBatchExtend(sfWebRequest $request) { $ids = $request->getParameter('ids'); $q = Doctrine_Query::create() ->from('JobeetJob j') ->whereIn('j.id', $ids); foreach ($q->execute() as $job) { $job->extend(true); } $this->getUser()->setFlash('notice', 'The selected jobs have been extended successfully.'); $this->redirect('jobeet_job'); } }

Las claves primarias seleccionadas son almacenados en el parmetro ids de la peticin. Para cada puesto de trabajo seleccionado, el mtodoJobeetJob::extend() se llama con un argumento extra para eludir algunos controles realizados en el mtodo. Tenemos que actualizar el mtodoextend() con un argumento extra para pasar la comprobacion de expiracin. Actualiza el mtodo extend() para tomar este nuevo argumento en cuenta:
// lib/model/doctrine/JobeetJob.class.php class JobeetJob extends BaseJobeetJob { public function extend($force = false) { if (!$force && !$this->expiresSoon()) { return false; } $this->setExpiresAt(date('Y-m-d', time() + 86400 * sfConfig::get('app_active_days'))); $this->save(); return true; } // ...

Despus de que todos los puestos de trabajo se han ampliado, el usuario es redirigido al mdulo job.

object_actions

En la lista, hay una columna adicional para las acciones que puede ejecutarse en un nico objeto. Para el mdulo category , vamos a eliminarlos ya que tenemos un enlace con el nombre de la categora para editarlo, y que realmente no necesitamos ser capaces de borrar una directamente de la lista:
# apps/backend/modules/category/config/generator.yml config: list: object_actions: {}

Para el mdulo job, vamos a mantener las acciones existentes y aadir una nueva accin extend similar a la que hemos aadido como batch action:
# apps/backend/modules/job/config/generator.yml config: list: object_actions: extend: ~ _edit: ~ _delete: ~

Como para las batch actions, las acciones _delete y _edit son las definidas por el framework. Tenemos que definir la accin listExtend() para hacer que el enlace extend funcione:
// apps/backend/modules/job/actions/actions.class.php class jobActions extends autoJobActions { public function executeListExtend(sfWebRequest $request) { $job = $this->getRoute()->getObject(); $job->extend(true); $this->getUser()->setFlash('notice', 'The selected jobs have been extended successfully.');

$this->redirect('jobeet_job'); } // ... }

actions

Ya hemos visto la forma de vincular la accin a una lista de objetos o un objeto nico. La opcin actions define las acciones que no tienen objeto, como la creacin de un nuevo objeto. Vamos a eliminar la accin predeterminada new y aadir una nueva accin, que suprime todos los puestos de trabajo que no se han activado por el usuario por ms de 60 das:
# apps/backend/modules/job/config/generator.yml config: list: actions: deleteNeverActivated: { label: Delete never activated jobs }

Hasta ahora, todas las acciones que hemos definido tenian ~, lo que significa que symfony ya configur la accin automticamente. Cada accin puede ser personalizada mediante la definicin de un array de parmetros. La opcin label sobreescribe el label por defecto generado por symfony. Por defecto, la accin ejecutada cuando haces click en el enlace es el nombre de la accin con prefijo list. Crea la accin listDeleteNeverActivated en el mdulo job:
// apps/backend/modules/job/actions/actions.class.php class jobActions extends autoJobActions { public function executeListDeleteNeverActivated(sfWebRequest $request) { $nb = Doctrine::getTable('JobeetJob')->cleanup(60);

if ($nb) { $this->getUser()->setFlash('notice', sprintf('%d never activated jobs have been deleted successfully.', $nb)); } else { $this->getUser()->setFlash('notice', 'No job to delete.'); } $this->redirect('jobeet_job'); } // ... }

Hemos reutilizado el mtodo JobeetJobTable::cleanup() definido el da de ayer. Es otro gran ejemplo de la reutilizacin proporcionada por el patrn MVC.
Tambin puede cambiar la accin a ejecutar pasando un parmetro action: deleteNeverActivated: { label: Delete never activated jobs, action: foo }

table_method

El nmero de consultas a la base de datos para mostrar el listado de los puestos de trabajo es 13, como muestra la barra de depuracin web.

Si haces clic en ese nmero, se ver que la mayora de las peticiones son para recuperar el nombre de la categora para cada trabajo. Para reducir el nmero de consultas, podemos cambiar el mtodo utilizado para obtener los puestos de trabajo utilizando la opcin table_method:
# apps/backend/modules/job/config/generator.yml config: list: table_method: retrieveBackendJobList

Ahora debes crear el mtodo retrieveBackendJobList en JobeetJobTable situado en lib/model/doctrine/JobeetJobTable.class.php.


// lib/model/doctrine/JobeetJobTable.class.php class JobeetJobTable extends Doctrine_Table { public function retrieveBackendJobList(Doctrine_Query $q) { $rootAlias = $q->getRootAlias(); $q->leftJoin($rootAlias . '.JobeetCategory c'); return $q; } // ...

El mtodo retrieveBackendJobList() agrega un join entre las tablas job y category y crea automticamente el objeto categora relacionado con cada puesto de trabajo. The number of requests is now down to three:

Configuracin de las Vistas Form


La Configuracin de la Vista Form se realiza en tres secciones: form, edit, y new. Todas tienen la misma configuracin y la capacidad de la seccinform slo existe como un mensaje para las secciones edit y new.
display

En cuanto a la lista, puedes cambiar el orden de los campos que se muestran con la opcin display. Pero, como el formulario mostrado se define por una clase, no trates de eliminar un campo, ya que podra dar lugar a errores de validacin inesperados. La opcin display para vistas form tambin se puede utilizar para organizar los campos en grupos:
# apps/backend/modules/job/config/generator.yml config: form: display: Content: [category_id, type, company, logo, url, position, location, description, how_to_apply, is_public, email] Admin: [_generated_token, is_activated, expires_atxpires_at]

La configuracin anterior define dos grupos (Content y Admin), each que contienen un subconjunto de los campos del formulario.

El Generador de Admin tiene soporte para la relacin muchos a muchos. En el formilario categora, tienes un input para el nombre, uno para el slug, y un cuadro desplegable para los afiliados relacionados. Como no tiene sentido editar esta relacin en esta pgina, vamos a eliminarla:
// lib/form/doctrine/JobeetCategoryForm.class.php class JobeetCategoryForm extends BaseJobeetCategoryForm { public function configure() { unset($this['created_at'], $this['updated_at'], $this['jobeet_affiliates_list']); } }

Columnas "Virtuales" En las opcines display para el formulario de puestos de trabajo, el campo _generated_token comienza con un guin bajo (_). Esto significa que la visualizacin de este campo ser manejado por un partial personalizado de nombre _generated_token.php: Crea este partial con el siguiente contenido:
// apps/backend/modules/job/templates/_generated_token.php <div class="sf_admin_form_row"> <label>Token</label> <?php echo $form->getObject()->getToken() ?> </div>

En el partial, tienes acceso al actual form ($form) y el objeto relacionado es accesible a travs del mtodo getObject().
Tambin puede delegar la visualizacin a un componente con el prefijo de un tilde (~) al nombre del campo. class

Como el formulario ser utilizado por los administradores, hemos mostrado ms informacin que para el usuario del formulario job. Pero por ahora, algunos de ellos no aparecen en la formulario ya que se han eliminado en la clase JobeetJobForm. Para tener diferentes formularios para el frontend y el backend, tenemos que crear dos clases form. Vamos a crear una clase BackendJobeetJobFormque herede de la clase JobeetJobForm. Como no tienen los mismos campos ocultos, tambin tenemos que refactorizar la clase JobeetJobForm un poco para mover la declaracin unset() en un mtodo que ser sobreescrito en BackendJobeetJobForm:
// lib/form/doctrine/JobeetJobForm.class.php class JobeetJobForm extends BaseJobeetJobForm { public function configure() { $this->removeFields(); $this->validatorSchema['email'] = new sfValidatorAnd(array( $this->validatorSchema['email'], new sfValidatorEmail(), )); // ... } protected function removeFields() {

unset( $this['created_at'], $this['updated_at'], $this['expires_at'], $this['is_activated'], $this['token'] ); } } // lib/form/doctrine/BackendJobeetJobForm.class.php class BackendJobeetJobForm extends JobeetJobForm { public function configure() { parent::configure(); } protected function removeFields() { unset( $this['created_at'], $this['updated_at'], $this['token'] ); } }

La clase predeterminado form utilizado por el Generador de Administracin puede ser sobreescrita con el ajuste de la opcin class:
# apps/backend/modules/job/config/generator.yml config: form: class: BackendJobeetJobForm Como hemos aadido una nueva clase, no te olvides de limpiar el cache.

El formulario edit todava tiene una pequea molestia. El logotipo subido no aparece en ningun lugar y no podrs quitar el actual. El widgetsfWidgetFormInputFileEditable aade capacidades de edicin a un simple widget input file:
// lib/form/doctrine/BackendJobeetJobForm.class.php class BackendJobeetJobForm extends JobeetJobForm { public function configure() { parent::configure(); $this->widgetSchema['logo'] = new sfWidgetFormInputFileEditable(array( 'label' => 'Company logo', 'file_src' => '/uploads/jobs/'.$this->getObject()->getLogo(), 'is_image' => true,

'edit_mode' => !$this->isNew(), 'template' => '<div>%file%<br />%input%<br />%delete% %delete_label%</div>', )); $this->validatorSchema['logo_delete'] = new sfValidatorPass(); } // ... }

El widget sfWidgetFormInputFileEditable tiene varias opciones para modificar sus caractersticas y visualizacin:
file_src: La ruta web del archivo subido is_image: Si es true, el archivo ser mostrado como una imagen edit_mode: Si el formulario est en modo de edicin o no with_delete: Si se desea mostrar una casilla de verificacin para eliminar template: La plantilla a utilizar para mostrar el widget

El aspecto del Generador de Admin puede ser ajustado muy fcilmente ya que las plantillas definen una gran cantidad de atributos class y id . Por ejemplo, el logotipo puede ser personalizado utilizando sf_admin_form_field_logo. Cada campo tambin tiene una clase, dependiendo de el tipo de campo como sf_admin_text o sf_admin_boolean.

La opcin edit_mode utiliza el mtodo sfDoctrineRecord::isNew(). Devuelve true si el objeto del formulario es nuevo, y false de lo contrario. Esto es de gran ayuda cuando es necesario que tengas diferentes widgets o validadores, dependiendo del estado del objeto invocado.

Configuracin de Filtros
La configuracin de los filtros es la misma que la configuracin de las vistas forms. Como cuestin de hecho, los filtros son slo forms. Y como para los forms, las clases se han generado por la tarea doctrine:build --all. Tambin puedes volver a generarlas con la tarea doctrine:build --filters. Estas clases se encuentran en el directorio lib/filter/ y cada uno de clase del modelo tiene asociada una clase de filtros (JobeetJobFormFilterpara JobeetJobForm). Vamos a eliminarlas por completo para el mdulo category:
# apps/backend/modules/category/config/generator.yml config: filter: class: false

Para el mdulo job , vamos a eliminar algunos de ellos:


# apps/backend/modules/job/config/generator.yml filter: display: [category_id, company, position, description, is_activated, is_public, email, expires_at]

Como los filtros son siempre opcional, no hay necesidad de sobreescribir clase filtro para configurar los campos que se mostrarn.

Acciones Personalizadas
Cuando la configuracin no es suficiente, puedes agregar nuevos mtodos a la clase de acciones, como hemos visto con la caracterstica de extend, pero tambin puede sobreescribir la accin de los mtodos generados: Mtodo
executeIndex() executeFilter() executeNew() executeCreate() executeEdit() executeUpdate() executeDelete() executeBatch()

Descripcin La accin de la vista list Actualiza los filtros La accin de la vista new Crea un nuevo Job La accin de la vista edit Actualiza un Job Borra un Job Ejecuta una accin por lote

executeBatchDelete() Ejecuta la accin por lote _delete processForm() getFilters() setFilters() getPager() getPage() setPage() buildCriteria() addSortCriteria() getSort() setSort()

Procesa el formualrio Job Devuelve los filtros actuales Establece los filtros Devuelve el paginador de la lista Obtiene la pgina de la lista Establece la pgina de la lista Construye el Criteria para la lista agrega un Criteria ordenado para la lista Devuelve la columna utilizada para ordenar Establece la columna utilizada para ordenar

Como cada mtodo generado hace solo una cosa, es fcil cambiar un comportamiento sin tener que copiar y pegar cdigo demasiado.

Personalizacin de Plantillas
Hemos visto cmo personalizar las plantillas generadas gracias a los atributos class y id aadidos por el Generador de administrador en el cdigo HTML.

En cuanto a las clases, tambin puedes sobreescribir las plantillas originales. Como las plantillas son simples archivos PHP y no clases PHP , una plantilla puede ser sobreescritapor creando una plantilla del mismo nombre en el mdulo (por ejemplo, en el directorioapps/backend/modules/job/templates/ para el mdulo de administracin job): Plantilla
_assets.php _filters.php _filters_field.php _flashes.php _form.php _form_actions.php _form_field.php _form_fieldset.php _form_footer.php _form_header.php _list.php _list_actions.php _list_batch_actions.php _list_field_boolean.php _list_footer.php _list_header.php _list_td_actions.php

Descripcin Muestra CSS y JS a usar por las plantillas Muestra los filtros Muestra un nico filtro de campo Muestra los mensajes flash Muestra el formulario Muestra las acciones del formulario Muestra un nico campo de formulario Muestra un fieldset de formulario Muestra el pie de pgina del formulario Muestra cabecera del formulario Muestra la lista Muestra las acciones de lista Muestra la lista de acciones por lotes Muestra un nico campo booleano en la lista Muestra el pie de pgina de lista Muestra la cabecera de lista Muestra las acciones de objeto para una fila

_list_td_batch_actions.php Muestra la casilla de verificacin para una fila _list_td_stacked.php _list_td_tabular.php _list_th_stacked.php

Muestra el stacked layout para una fila Muestra un nico campo de lista Muestra un solo nombre de columna para la cabecera

_list_th_tabular.php

Muestra un solo nombre de columna para la

Plantilla

Descripcin cabecera

_pagination.php editSuccess.php indexSuccess.php newSuccess.php

Muestra la paginacin de lista Muestra la vista edit Muestra la vista list Muestra la vista new

Configuracin Final
La configuracin final de la administracin Jobeet es la siguiente:
# apps/backend/modules/job/config/generator.yml generator: class: sfDoctrineGenerator param: model_class: JobeetJob theme: admin non_verbose_templates: true with_show: false singular: ~ plural: ~ route_prefix: jobeet_job with_doctrine_route: true config: actions: ~ fields: is_activated: { label: Activated?, help: Whether the user has activated the job, or not } is_public: { label: Public? } list: title: Job Management layout: stacked display: [company, position, location, url, is_activated, email] params: | %%is_activated%% <small>%%JobeetCategory%%</small> %%company%% (<em>%%email%%</em>) is looking for a %%=position%% (%%location%%) max_per_page: 10 sort: [expires_at, desc] batch_actions: _delete: ~ extend: ~ object_actions:

extend: ~ _edit: ~ _delete: ~ actions: deleteNeverActivated: { label: Delete never activated jobs } table_method: retrieveBackendJobList filter: display: [category_id, company, position, description, is_activated, is_public, email, expires_at] form: class: BackendJobeetJobForm display: Content: [category_id, type, company, logo, url, position, location, description, how_to_apply, is_public, email] Admin: [_generated_token, is_activated, expires_at] edit: title: Editing Job "%%company%% is looking for a %%position%%" new: title: Job Creation # apps/backend/modules/category/config/generator.yml generator: class: sfDoctrineGenerator param: model_class: JobeetCategory theme: admin non_verbose_templates: true with_show: false singular: ~ plural: ~ route_prefix: jobeet_category with_doctrine_route: true config: actions: ~ fields: ~ list: title: Category Management display: [=name, slug] batch_actions: {} object_actions: {} filter: class: false form: actions: _delete: ~ _list: ~ _save: ~ edit: title: Editing Category "%%name%%" new:

title: New Category

Con tan slo estos dos archivos de configuracin, hemos desarrollado una excelente interfaz backend para Jobeet en cuestin de minutos.
Ya sabes que cuando algo es configurable en un archivo YAML, hay tambin la posibilidad de usar cdigo PHP. Para el Generador de administrador, puedes editar apps/backend/modules/job/lib/jobGeneratorConfiguration.class.php. Te da las mismas opciones que YAML pero con un archivo PHP. Para aprender los nombre de los mtodos, echar un vistazo a la clase base generada encache/backend/dev/modules/autoJob/lib/BaseJobGeneratorConfiguration.cla ss.php.

El Usuario
vamos a descubrir cmo Symfony gestiona persistentemente datos entre las peticiones HTTP. Como se puede saber, el protocolo HTTP no tiene memoria, lo que significa que cada peticin es independiente de lo anterior o de la subsiguiente. Los sitios web modernos necesitan una manera de mantener los datos entre las peticiones para mejorar la experiencia del usuario. Una sesin de usuario puede ser identificada mediante una cookie. En Symfony, el desarrollador no tiene que manipular directamente las sesiones, sino que usa el objeto sfUser, que representa al usuario final de la aplicacin.

Mensajes Flashes del Usuario


Ya hemos visto el objeto de usuario en accin con los mensajes flashes. Un flash es un efmero mensaje almacenado en la sesin de usuario que se eliminar automticamente despus de la prxima peticin. Es muy til cuando se necesita mostrar un mensaje al usuario despus de un redireccionamiento. El generador de administracin utiliza flashes para mostrar informacin al usuario cuando se guarda un puesto de trabajo, se borra o extiende su validez.

Un flash se establece utilizando el mtodo setFlash() de sfUser:


// apps/frontend/modules/job/actions/actions.class.php public function executeExtend(sfWebRequest $request) { $request->checkCSRFProtection();

$job = $this->getRoute()->getObject(); $this->forward404Unless($job->extend()); $this->getUser()->setFlash('notice', sprintf('Your job validity has been extended until %s.', $job->getDateTimeObject('expires_at')>format('m/d/Y'))); $this->redirect($this->generateUrl('job_show_user', $job)); }

El primer argumento es el identificador del flash y el segundo es el mensaje a mostrar. Se puede definir cualquier flashes que quieras, pero notice yerror son dos de los ms comunes (que son utilizadas intensivamente por el generador de administracin). Corresponde a los desarrolladores inclur el mensaje flash en las plantillas. Para Jobeet, se muestran en el layout.php:
// apps/frontend/templates/layout.php <?php if ($sf_user->hasFlash('notice')): ?> <div class="flash_notice"><?php echo $sf_user->getFlash('notice') ?></div> <?php endif ?> <?php if ($sf_user->hasFlash('error')): ?> <div class="flash_error"><?php echo $sf_user->getFlash('error') ?></div> <?php endif ?>

En una plantilla, el usuario es accesible a travs de la variable especial $sf_user.


Algunos objetos symfony siempre son accesibles en las plantillas, sin la necesidad de explicitamente pasarlos desde la accin: $sf_request,$sf_user, y $sf_response.

Atributos de Usuario
Lamentablemente, los casos de uso de Jobeet no tienen ningn requisito sobre el almacenamiento de algo en la sesin de usuario. As que vamos a aadir un nuevo requisito: para facilitar la navegacin de los puestos de trabajo, los ltimos tres vistos por el usuario se deben mostrar en un men con enlaces para volver a la pgina de esos puestos de trabajo ms tarde. Cuando un usuario accede a una pgina de puestos de trabajo, el objeto job mostrado necesita ser agregado en el historial del usuario y almacenado en la sesin:
// apps/frontend/modules/job/actions/actions.class.php class jobActions extends sfActions { public function executeShow(sfWebRequest $request) { $this->job = $this->getRoute()->getObject();

// fetch jobs already stored in the job history $jobs = $this->getUser()->getAttribute('job_history', array()); // add the current job at the beginning of the array array_unshift($jobs, $this->job->getId()); // store the new job history back into the session $this->getUser()->setAttribute('job_history', $jobs); } // ... } Podramos haber almacenado los objetos JobeetJob directamente en la sesin. Esto est totalmente desaconsejada ya que las variables de sesin son serializadas (almacenadas) entre las peticiones. Y cuando la sesin se ha cargado, los objetos JobeetJob son de-serializados y pueden quedar "estancos" aun si han sido modificados o borrados en el nterin.

Los mtodos getAttribute(), setAttribute() Dado un identificador, el mtodo sfUser::getAttribute() obtiene los valores de la sesin de usuario. Por el contrario, el mtodo setAttribute()almacena cualquier variable PHP en la sesin, para un determinado identificador. El mtodo getAttribute() tambin tiene un valor predeterminado opcional a devolver si el identificador no est todava definido.
El valor por defecto tomado por el mtodo getAttribute() es un acceso directo para: if (!$value = $this->getAttribute('job_history')) { $value = array(); }

La Clase myUser Lo mejor respeto a la separacin de las capas, es mover el cdigo a la clase myUser. La clase myUser sobreescribe la clase por defecto base symfonysfUser con comportamientos especficos de la aplicacin:
// apps/frontend/modules/job/actions/actions.class.php class jobActions extends sfActions { public function executeShow(sfWebRequest $request) { $this->job = $this->getRoute()->getObject(); $this->getUser()->addJobToHistory($this->job); } // ...

} // apps/frontend/lib/myUser.class.php class myUser extends sfBasicSecurityUser { public function addJobToHistory(JobeetJob $job) { $ids = $this->getAttribute('job_history', array()); if (!in_array($job->getId(), $ids)) { array_unshift($ids, $job->getId()); $this->setAttribute('job_history', array_slice($ids, 0, 3)); } } }

El cdigo tambin ha sido modificado para tener en cuenta todos los requisitos:
!in_array($job->getId(), $ids): Un job no se puede almacenar dos veces

en el historial
array_slice($ids, 0, 3): Slo los tres ltimos puestos de trabajo vistos

por el usuario se muestran En el layout, agrega el cdigo siguiente antes de que la variable $sf_content se muestre:
// apps/frontend/templates/layout.php <div id="job_history"> Recent viewed jobs: <ul> <?php foreach ($sf_user->getJobHistory() as $job): ?> <li> <?php echo link_to($job->getPosition().' - '.$job->getCompany(), 'job_show_user', $job) ?> </li> <?php endforeach ?> </ul> </div> <div class="content"> <?php echo $sf_content ?> </div>

El layout usa un nuevo mtodo getJobHistory() para obtener el historial job:


// apps/frontend/lib/myUser.class.php class myUser extends sfBasicSecurityUser { public function getJobHistory() {

$ids = $this->getAttribute('job_history', array()); if (!empty($ids)) { return Doctrine_Core::getTable('JobeetJob') ->createQuery('a') ->whereIn('a.id', $ids) ->execute() ; } return array(); } // ... }

El sfParameterHolder Para completar el API del historial de job, vamos a aadir un mtodo para resetear el historial:
// apps/frontend/lib/myUser.class.php class myUser extends sfBasicSecurityUser { public function resetJobHistory() { $this->getAttributeHolder()->remove('job_history'); } // ... }

Los atributos del usuario son gestionados por un objeto de la clase sfParameterHolder. Los mtodos getAttribute() y setAttribute() son mtodos proxy para getParameterHolder()->get() y getParameterHolder()>set(). Como el mtodo remove() no tiene mtodo proxy en sfUser, necesitas usar el objeto del contenedor de parmetros directamente.

La clase sfParameterHolder es tambin utilizada por sfRequest para almacenar sus parmetros.

Seguridad de la Aplicacin
Autenticacin Al igual que muchas otras caractersticas, la seguridad es manejada por un archivo YAML, security.yml. Por ejemplo, puedes encontrar la configuracin por defecto para la aplicacin backend en el directorio config/:
# apps/backend/config/security.yml default: is_secure: false

Si cambia el is_secure a true, toda la aplicacin backend necesitar que el usuario deba autenticarse.

En un archivo YAML, un Booleano puede expresarse con la cadena true o false.

Si echas un vistazo a los registros en la barra web de depuracin, se puede observar que el mtodo executeLogin() de la clase defaultActions se llama por cada pgina que intentas acceder.

Cuando un usuario no-autenticado intenta acceder a una accin segura, Symfony remite la peticina la accin login configurada en settings.yml:

all: .actions: login_module: default login_action: login No es posible asegurar a la accin login para evitar una recursin infinita. Como vimos durante el da 4, el mismo archivo de configuracin se pueden definir en varios lugares. Este es tambin el caso de security.yml. Para slo asegurar o desproteger una nica accin o un mdulo, crea un security.yml en the directorio config/ del mdulo: index: is_secure: false

all: is_secure: true

De forma predeterminada, la clase myUser hereda de sfBasicSecurityUser, y no de sfUser. sfBasicSecurityUser proporciona mtodos para gestionar la autenticacin de usuario y autorizacin. Para gestionar la autenticacin de usuario, utiliza los mtodos isAuthenticated() y setAuthenticated():
if (!$this->getUser()->isAuthenticated()) { $this->getUser()->setAuthenticated(true); }

Autorizacin Cuando un usuario est autenticado, el acceso a algunas acciones pueden ser an ms restringida por la definicin de credenciales. Un usuario debe tener las credenciales para acceder a la pgina:
default: is_secure: false credentials: admin

El sistema de credenciales de Symfony es bastante simple y poderoso. Una credencial puede representar todo lo que sea necesario para describir la seguridad al modelo de la aplicacin (como grupos o permisos).
Credenciales Complejas El item credentials de security.yml soporta operadores Booleanos para describir las necesidades complejas en credenciales. Si un usuario debe tener credenciales A y B, hay que envolver las credenciales con corchetes: index: credentials: [A, B] Si un usuario debe tener credenciales A o B, hay que envolver las credenciales con dos pares de corchetes: index: credentials: [[A, B]] Incluso puedes mezclar y combinar entre parntesis para describir cualquier tipo de Expresin booleana con cualquier nmero de credenciales.

Para gestionar las credenciales de usuario, sfBasicSecurityUser da varios mtodos:


// Add one or more credentials $user->addCredential('foo'); $user->addCredentials('foo', 'bar');

// Check if the user has a credential echo $user->hasCredential('foo'); // Check if the user has both credentials echo $user->hasCredential(array('foo', 'bar'));

=>

true

=>

true

// Check if the user has one of the credentials echo $user->hasCredential(array('foo', 'bar'), false); => // Remove a credential $user->removeCredential('foo'); echo $user->hasCredential('foo');

true

=>

false

// Remove all credentials (useful in the logout process) $user->clearCredentials(); echo $user->hasCredential('bar'); =>

false

Por el Backend de Jobeet, no vamos a usar ninguna credencial ya que slo tenemos un perfil: el administrador.

Plugins
Como no nos gusta reinventar la rueda, no vamos a desarrollar la accin de acceso a partir de cero. En lugar de ello, se instalar un plugin symfony. Uno de los grandes puntos fuertes del framework Symfony son los plugins. Como veremos en los prximos das, es muy fcil crear un plugin. Tambin es bastante potente, ya que un plugin puede contener cualquier cosa, desde la configuracin de los mdulos hasta los recursos web. Hoy, vamos a instalar sfDoctrineGuardPlugin para garantizar el acceso al backend
$ php symfony plugin:install sfDoctrineGuardPlugin

La tarea plugin:install instala un plugin por nombre. Todos los plugins son almacenados bajo el directorio plugins/ y cada uno tiene su propio directorio con el nombre del nombre del plugin.
PEAR debe estar instalado para que la tarea plugin:install funcione.

Cuando se instala un plugin con la tarea plugin:install, Symfony instala la ltima versin estable del mismo. Para instalar una versin especfica de un plugin, pasa la opcin --release. La web de los plugins lista todas las versiones disponibles agrupados por la versin de Symfony. Como un plugin es auto-contenido en un directorio, tambin puedes descargar el paquete desde el sitio web de Symfony y descomprimirlo, o alternativamente, haces un enlace svn:externals a su Subversion repository.

Seguridad del Backend


Cada plugin tiene un archivo README que explica cmo configurarlo. Vamos a ver cmo configurar el nuevo plugin. Como el plugin ofrece varias nuevas clases al modelo para la gestin de usuarios, grupos y permisos, necesitas reconstruir tu modelo:
$ php symfony doctrine:build --all --and-load --no-confirmation Recuerde que la tarea doctrine:build --all --and-load elimina todas las tablas existentes antes de volver a crearlas. Para evitar esto, puedes construir los modelos, formularios y filtros y, a continuacin, crear las nuevas tablas ejecutando el SQL generado y almacenado en data/sql/.

Como sfDoctrineGuardPlugin agrega varios mtodos a la clase de usuario, es necesario cambiar la clase base de myUser a sfGuardSecurityUser:
// apps/backend/lib/myUser.class.php class myUser extends sfGuardSecurityUser { } sfDoctrineGuardPlugin proporciona una accin signin en el modulo sfGuardAuth para autenticar a los usuarios:

Edita el archivo settings.yml para cambiar la accin por defecto empleada por la pgina de login:
# apps/backend/config/settings.yml all: .settings: enabled_modules: [default, sfGuardAuth] # ... .actions: login_module: login_action: # ...

sfGuardAuth signin

Como los plugins son compartidos entre todas las aplicaciones de un proyecto, tiene que permitir explcitamente los mdulos que desea utilizar agregando el item enabled_modules.

El ltimo paso es crear un usuario administrador:


$ php symfony guard:create-user fabien SecretPass

$ php symfony guard:promote fabien El sfGuardPlugin proporciona funciones para la gestin de usuarios, grupos y permisos desde la lnea de comandos. Utiliza la tarea list para listar todas las tareas pertenecientes al espacio de nombres guard: $ php symfony list guard

Y cuando el usuario no est autenticado, tenemos que ocultar la barra de men:


// apps/backend/templates/layout.php <?php if ($sf_user->isAuthenticated()): ?> <div id="menu"> <ul> <li><?php echo link_to('Jobs', 'jobeet_job') ?></li> <li><?php echo link_to('Categories', 'jobeet_category') ?></li> </ul> </div> <?php endif ?>

Cuando el usuario es autenticado, tenemos que aadir un enlace de logout en el men:


// apps/backend/templates/layout.php <li><?php echo link_to('Logout', 'sf_guard_signout') ?></li> Para una lista de todas las rutas previstas por sfGuardPlugin, usa la tarea app:routes.

Para pulir el backend an ms, vamos a aadir un nuevo mdulo para la gestin del administrador de usuarios. Afortunadamente, sfGuardPluginproporciona un mdulo de este tipo. Como el mdulo sfGuardAuth, que necesitars habilitarlo en settings.yml:
// apps/backend/config/settings.yml all: .settings: enabled_modules: [default, sfGuardAuth, sfGuardUser]

Aadir un enlace en el men:


// apps/backend/templates/layout.php <li><?php echo link_to('Users', 'sf_guard_user') ?></li>

Terminamos!

Probando al Usuario
El tutorial de hoy no termina ya que no hemos an hablado de las pruebas al usuario. Como el symfony browser simula las cookies, es bastante fcil de probar el comportamiento del usuario mediante el uso del tester sfTesterUser. Vamos a actualizar la pruebas funcionales para el men que hemos aadido hoy. Agrega el cdigo siguiente al final del mdulo de pruebas funcionales job:
// test/functional/frontend/jobActionsTest.php $browser-> info('4 - User job history')-> loadData()-> restart()-> info(' > get('/')-> click('Web Developer', array(), array('position' => 1))-> get('/')-> with('user')->begin()-> isAttribute('job_history', array($browser>getMostRecentProgrammingJob()->getId()))-> end()-> info(' 4.2 - A job is not added twice in the history')-> click('Web Developer', array(), array('position' => 1))-> get('/')-> with('user')->begin()-> isAttribute('job_history', array($browser>getMostRecentProgrammingJob()->getId()))-> end() ; 4.1 - When the user access a job, it is added to its history')-

Para facilitar las pruebas, primero hacemos la recarga datos y reiniciamos el navegador para empezar con una sesin. El mtodo isAttribute() controla un atributo de usuario dado.
El tester sfTesterUser tambin proporciona mtodos isAuthenticated() and hasCredential() para poner a prueba la autenticacin de usuario y autorizaciones.

Los Canales
Si ests buscando un puesto de trabajo, es probable que desees ser informado tan pronto como un nuevo puesto de trabajo se ha publicado. Y no es muy conveniente comprobar el sitio web a cada hora. Vamos hoy a aadir feeds (o canales) de varios puestos de trabajo, para mantener a nuestros usuarios Jobeet actualizados.

Formatos
Symfony tiene soporte nativo para los formatos y tipos MIME. Esto significa que el modelo y el controlador pueden tener diferentes plantillas basadas en el formato solicitado. El formato predeterminado es HTML pero Symfony admite varios formatos de serie como ser txt, js, css, json, xml, rdf, o atom. El formato se puede configurar utilizando el mtodo setRequestFormat() del objeto request:
$request->setRequestFormat('xml');

Pero la mayor parte del tiempo, el formato est includo en la URL. En este caso, Symfony lo establecer por t si la variable especial sf_format se utiliza en la ruta correspondiente. Para la lista de puestos de trabajo (job), la URL es:
http://www.jobeet.com.localhost/frontend_dev.php/job

Esta URL es equivalente a:


http://www.jobeet.com.localhost/frontend_dev.php/job.html

Ambas URL son equivalentes porque las rutas generadas por la clase sfDoctrineRouteCollection tienen la sf_format como extension. Puedes comprobarlo por t mismo ejecutando la tarea app:routes:

Feeds
Feed de los ltimos Puestos de Trabajo Soportar diferentes formatos es tn fcil como la crear diferentes plantillas. Para crear un feed Atom para los ltimos puestos de trabajo, crea una plantilla indexSuccess.atom.php:
<!-- apps/frontend/modules/job/templates/indexSuccess.atom.php --> <?xml version="1.0" encoding="utf-8"?> <feed xmlns="http://www.w3.org/2005/Atom">

<title>Jobeet</title> <subtitle>Latest Jobs</subtitle> <link href="" rel="self"/> <link href=""/> <updated></updated> <author><name>Jobeet</name></author> <id>Unique Id</id> <entry> <title>Job title</title> <link href="" /> <id>Unique id</id> <updated></updated> <summary>Job description</summary> <author><name>Company</name></author> </entry> </feed> Nombres de las Plantillas Como html es el formato ms utilizado para aplicaciones web, ste puede ser omitido del nombre de la plantilla. Ambas plantillas indexSuccess.php yindexSuccess.html.php son equivalentes y Symfony utiliza la primero que encuentre. Por qu las plantillas predeterminadas tienen el sufijo Success? Una accin puede devolver un valor para indicar que plantilla se mostrar. Si la accin no dice o devuelve nada, eso equivalente al siguiente cdigo: return sfView::SUCCESS; // == 'Success' Si deseas cambiar el sufijo, devuelve otra cosa: return sfView::ERROR; // == 'Error' return 'Foo'; Tambin puedes cambiar el nombre de la plantilla utilizando el mtodo setTemplate(): $this->setTemplate('foo');

Por defecto, Symfony cambiar la respuesta Content-Type de acuerdo con el formato, y para todos los formatos que no sean HTML, el layout es deshabilitado. Para un Atom feed, Symfony cambiar el Content-Type a application/atom+xml; charset=utf-8. En el pie de pgina Jobeet, actualiza el enlace para el feed:
<!-- apps/frontend/templates/layout.php --> <li class="feed"> <a href="<?php echo url_for('job', array('sf_format' => 'atom')) ?>">Full feed</a> </li>

El URI interno es el mismo que para la lista job con el sf_format aadido como una variable. Aade una etiqueta <link> en la seccin head del layout:
<!-- apps/frontend/templates/layout.php --> <link rel="alternate" type="application/atom+xml" title="Latest Jobs" href="<?php echo url_for('job', array('sf_format' => 'atom'), true) ?>" />

Para el atributo href del enlace, se utiliza una URL absoluta gracias al segundo argumento del helper url_for(). Vamos a actualizar el header de la plantilla Atom:
<!-- apps/frontend/modules/job/templates/indexSuccess.atom.php --> <title>Jobeet</title> <subtitle>Latest Jobs</subtitle> <link href="<?php echo url_for('job', array('sf_format' => 'atom'), true) ?>" rel="self"/> <link href="<?php echo url_for('homepage', true) ?>"/> <updated><?php echo gmstrftime('%Y-%m-%dT%H:%M:%SZ', Doctrine_Core::getTable('JobeetJob')->getLatestPost()>getDateTimeObject('created_at')->format('U')) ?></updated> <author> <name>Jobeet</name> </author> <id><?php echo sha1(url_for('job', array('sf_format' => 'atom'), true)) ?></id>

Nota la utilizacin de la funcin strtotime() para obtener la fecha created_at como timestamp. Para obtener la fecha del envo, crea el mtodogetLatestPost():
// lib/model/doctrine/JobeetJobTable.class.php class JobeetJobTable extends Doctrine_Table { public function getLatestPost() { $q = Doctrine_Query::create() ->from('JobeetJob j'); $this->addActiveJobsQuery($q); return $q->fetchOne(); } // ... }

Los items del feed se pueden generar con el siguiente cdigo:


<!-- apps/frontend/modules/job/templates/indexSuccess.atom.php --> <?php use_helper('Text') ?> <?php foreach ($categories as $category): ?>

<?php foreach ($category>getActiveJobs(sfConfig::get('app_max_jobs_on_homepage')) as $job): ?> <entry> <title> <?php echo $job->getPosition() ?> (<?php echo $job->getLocation() ?>) </title> <link href="<?php echo url_for('job_show_user', $job, true) ?>" /> <id><?php echo sha1($job->getId()) ?></id> <updated><?php echo gmstrftime('%Y-%m-%dT%H:%M:%SZ', $job>getDateTimeObject('created_at')->format('U')) ?></updated> <summary type="xhtml"> <div xmlns="http://www.w3.org/1999/xhtml"> <?php if ($job->getLogo()): ?> <div> <a href="<?php echo $job->getUrl() ?>"> <img src="http://<?php echo $sf_request>getHost().'/uploads/jobs/'.$job->getLogo() ?>" alt="<?php echo $job->getCompany() ?> logo" /> </a> </div> <?php endif ?> <div> <?php echo simple_format_text($job->getDescription()) ?> </div> <h4>How to apply?</h4> <p><?php echo $job->getHowToApply() ?></p> </div> </summary> <author> <name><?php echo $job->getCompany() ?></name> </author> </entry> <?php endforeach ?> <?php endforeach ?>

El mtodo getHost() del objeto request ($sf_request) devuelve el actual host, que viene muy bien para crear un vnculo absoluto para el logo de la empresa.

Cuando se crea un feed, la depuracin es ms fcil si utiliza herramientas de lnea de comandos como curl o wget, ya que puedes ver el contenido real del feed.

El Feed de los ltimos Puestos de Trabajo de una Categora Uno de los objetivos de Jobeet es ayudar a la gente a encontrar puestos de trabajo especficos. Por lo tanto, tenemos que proporcionar un feed para cada categora. En primer lugar, vamos a actualizar la ruta category para agregar el soporte para diferentes formatos:
// apps/frontend/config/routing.yml category: url: /category/:slug.:sf_format class: sfDoctrineRoute param: { module: category, action: show, sf_format: html } options: { model: JobeetCategory, type: object } requirements: sf_format: (?:html|atom)

Ahora, la ruta category comprender tanto los formatos html como atom. Actualiza los enlaces de los feeds de la categora en las plantillas:
<!-- apps/frontend/modules/job/templates/indexSuccess.php --> <div class="feed"> <a href="<?php echo url_for('category', array('sf_subject' => $category, 'sf_format' => 'atom')) ?>">Feed</a> </div> [php] <!-- apps/frontend/modules/category/templates/showSuccess.php --> <div class="feed">

<a href="<?php echo url_for('category', array('sf_subject' => $category, 'sf_format' => 'atom')) ?>">Feed</a> </div>

El ltimo paso es la creacin de la plantilla showSuccess.atom.php. Pero como este feed tambin lista puestos de trabajo, podemos refactorizar el cdigo que genera los items del feed mediante la creacin de un partial _list.atom.php. Como el formato html, los partial son de un formato especfico:
<!-- apps/frontend/job/templates/_list.atom.php --> <?php use_helper('Text') ?> <?php foreach ($jobs as $job): ?> <entry> <title><?php echo $job->getPosition() ?> (<?php echo $job>getLocation() ?>)</title> <link href="<?php echo url_for('job_show_user', $job, true) ?>" /> <id><?php echo sha1($job->getId()) ?></id> <updated><?php echo gmstrftime('%Y-%m-%dT%H:%M:%SZ', $job>getDateTimeObject('created_at')->format('U')) ?></updated> <summary type="xhtml"> <div xmlns="http://www.w3.org/1999/xhtml"> <?php if ($job->getLogo()): ?> <div> <a href="<?php echo $job->getUrl() ?>"> <img src="http://<?php echo $sf_request>getHost().'/uploads/jobs/'.$job->getLogo() ?>" alt="<?php echo $job->getCompany() ?> logo" /> </a> </div> <?php endif ?> <div> <?php echo simple_format_text($job->getDescription()) ?> </div> <h4>How to apply?</h4> <p><?php echo $job->getHowToApply() ?></p> </div> </summary> <author> <name><?php echo $job->getCompany() ?></name> </author> </entry> <?php endforeach ?>

Puedes utilizar el partial _list.atom.php para simplificar la plantilla del feed de los puestos de trabajo:
<!-- apps/frontend/modules/job/templates/indexSuccess.atom.php --> <?xml version="1.0" encoding="utf-8"?>

<feed xmlns="http://www.w3.org/2005/Atom"> <title>Jobeet</title> <subtitle>Latest Jobs</subtitle> <link href="<?php echo url_for('job', array('sf_format' => 'atom'), true) ?>" rel="self"/> <link href="<?php echo url_for('homepage', true) ?>"/> <updated><?php echo gmstrftime('%Y-%m-%dT%H:%M:%SZ', Doctrine_Core::getTable('JobeetJob')->getLatestPost()>getDateTimeObject('created_at')->format('U')) ?></updated> <author> <name>Jobeet</name> </author> <id><?php echo sha1(url_for('job', array('sf_format' => 'atom'), true)) ?></id> <?php foreach ($categories as $category): ?> <?php include_partial('job/list', array('jobs' => $category>getActiveJobs(sfConfig::get('app_max_jobs_on_homepage')))) ?> <?php endforeach ?> </feed>

Finalmente, crear la plantilla showSuccess.atom.php:


<!-- apps/frontend/modules/category/templates/showSuccess.atom.php --> <?xml version="1.0" encoding="utf-8"?> <feed xmlns="http://www.w3.org/2005/Atom"> <title>Jobeet (<?php echo $category ?>)</title> <subtitle>Latest Jobs</subtitle> <link href="<?php echo url_for('category', array('sf_subject' => $category, 'sf_format' => 'atom'), true) ?>" rel="self" /> <link href="<?php echo url_for('category', array('sf_subject' => $category), true) ?>" /> <updated><?php echo gmstrftime('%Y-%m-%dT%H:%M:%SZ', $category>getLatestPost()->getDateTimeObject('created_at')->format('U')) ?></updated> <author> <name>Jobeet</name> </author> <id><?php echo sha1(url_for('category', array('sf_subject' => $category), true)) ?></id> <?php include_partial('job/list', array('jobs' => $pager>getResults())) ?> </feed>

Para el feed principal, necesitamos la fecha del ltimo puesto de trabajo para una categora:
// lib/model/doctrine/JobeetCategory.class.php class JobeetCategory extends BaseJobeetCategory { public function getLatestPost()

{ return $this->getActiveJobs(1)->getFirst(); } // ... }

Servicios Web
Con agregar feeds a Jobeet, los solicitantes de puestos de trabajo pueden ahora ser informados de los nuevos puestos de trabajo en tiempo real. En el otro lado de la valla, esta cuando se enva un puesto de empleo, y deseas tener la mayor exposicin/publicidad posible. Si tu trabajo es sindicado en una gran cantidad de pequeos sitios web, tendrs una mejor oportunidad de encontrar a la persona adecuada. Ese es el poder de la Larga Cola o long tail. Los afiliados podrn publicar los puestos de trabajos ms recientes en sus sitios web gracias a los servicios web que se desarrollarn hoy.

Los Afiliados
Segn los requisitos del da 2: "Caso de Uso F7: Un afiliado recupera la lista de puestos de trabajos activos" Los Datos Vamos a crear un nuevo archivo de datos para los afiliados:
# data/fixtures/affiliates.yml JobeetAffiliate: sensio_labs: url: http://www.sensio-labs.com/ email: fabien.potencier@example.com is_active: true token: sensio_labs JobeetCategories: [programming] symfony: url: http://www.symfony-project.org/ email: fabien.potencier@example.org is_active: false token: symfony JobeetCategories: [design, programming]

La creacin de registros para la tabla intermedia de una relacin muchos-amuchos es tan simple como definir un array cuya clave sea el nombre de la relacin. El contenido del array es el nombre de los objetos definidos en los archivos de datos. Puede enlazar objetos desde diferentes archivos, pero los nombres deben haber sido definidos antes. En el archivo de datos, los tokens estn hardcodeados para simplificar las pruebas, pero cuando un usuario real solicita una cuenta, el token tendrn que ser generado:
// lib/model/doctrine/JobeetAffiliate.class.php class JobeetAffiliate extends BaseJobeetAffiliate { public function save(Doctrine_Connection $conn = null)

{ if (!$this->getToken()) { $this->setToken(sha1($this->getEmail().rand(11111, 99999))); } return parent::save($conn); } // ... }

Ahora puedes recargar los datos:


$ php symfony doctrine:data-load

El Servicio Web de los Puestos de Trabajo Como siempre, cuando se crea un nuevo recurso, es un buen hbito primero definir la direccin URL:
# apps/frontend/config/routing.yml api_jobs: url: /api/:token/jobs.:sf_format class: sfDoctrineRoute param: { module: api, action: list } options: { model: JobeetJob, type: list, method: getForToken } requirements: sf_format: (?:xml|json|yaml)

Por esta ruta, la variable especial sf_format termina la direccin URL y los valores vlidos son xml, json, o yaml. El mtodo getForToken() es llamado cuando la accin recupera la coleccin de objetos relacionados con la ruta. Como tenemos que comprobar que el afiliado esta activado, tenemos que sobreescribir el comportamiento predeterminado de la ruta:
// lib/model/doctrine/JobeetJobTable.class.php class JobeetJobTable extends Doctrine_Table { public function getForToken(array $parameters) { $affiliate = Doctrine_Core::getTable('JobeetAffiliate') >findOneByToken($parameters['token']); if (!$affiliate || !$affiliate->getIsActive()) { throw new sfError404Exception(sprintf('Affiliate with token "%s" does not exist or is not activated.', $parameters['token'])); } return $affiliate->getActiveJobs(); }

// ... }

Si el token no existe en la base de datos, arrojamos una excepcin sfError404Exception. Esta clase excepcin se convierten automticamente en una respuesta 404. Esta es la forma ms sencilla de generar una pgina 404 de una clase del modelo. El mtodo getForToken() utiliza un nuevo mtodo llamado getActiveJobs() y devuelve la lista de puestos de trabajo activos actualmente:
// lib/model/doctrine/JobeetAffiliate.class.php class JobeetAffiliate extends BaseJobeetAffiliate { public function getActiveJobs() { $q = Doctrine_Query::create() ->select('j.*') ->from('JobeetJob j') ->leftJoin('j.JobeetCategory c') ->leftJoin('c.JobeetAffiliates a') ->where('a.id = ?', $this->getId()); $q = Doctrine_Core::getTable('JobeetJob')->addActiveJobsQuery($q); return $q->execute(); } // ... }

El ltimo paso es crear la accin y plantillas para la api. Inicializa el mdulo con la tarea generate:module:
$ php symfony generate:module frontend api Como no vamos a utilizar la accin predeterminada index, puedes eliminarla de la clase action, y eliminar la plantilla asociada indexSucess.php.

La Accin Todos los formatos comparten la misma accin list:


// apps/frontend/modules/api/actions/actions.class.php public function executeList(sfWebRequest $request) { $this->jobs = array(); foreach ($this->getRoute()->getObjects() as $job) { $this->jobs[$this->generateUrl('job_show_user', $job, true)] = $job>asArray($request->getHost()); }

En lugar de pasar un array de objetos JobeetJob a las plantillas, se pasa un array de cadenas. Como tenemos tres modelos diferentes para la misma accin, la lgica de proceso de los valores ha sido refactorizada en el mtodo JobeetJob::asArray():
// lib/model/doctrine/JobeetJob.class.php class JobeetJob extends BaseJobeetJob { public function asArray($host) { return array( 'category' => $this->getJobeetCategory()->getName(), 'type' => $this->getType(), 'company' => $this->getCompany(), 'logo' => $this->getLogo() ? 'http://'.$host.'/uploads/jobs/'.$this->getLogo() : null, 'url' => $this->getUrl(), 'position' => $this->getPosition(), 'location' => $this->getLocation(), 'description' => $this->getDescription(), 'how_to_apply' => $this->getHowToApply(), 'expires_at' => $this->getCreatedAt(), ); } // ... }

El Formato xml Soportar el formato xml es tan simple como crear una plantilla:
<!-- apps/frontend/modules/api/templates/listSuccess.xml.php --> <?xml version="1.0" encoding="utf-8"?> <jobs> <?php foreach ($jobs as $url => $job): ?> <job url="<?php echo $url ?>"> <?php foreach ($job as $key => $value): ?> <<?php echo $key ?>><?php echo $value ?></<?php echo $key ?>> <?php endforeach ?> </job> <?php endforeach ?> </jobs>

El Formato json Soportar el formato JSON es similar:


<!-- apps/frontend/modules/api/templates/listSuccess.json.php --> [

<?php $nb = count($jobs); $i = 0; foreach ($jobs as $url => $job): ++$i ?> { "url": "<?php echo $url ?>", <?php $nb1 = count($job); $j = 0; foreach ($job as $key => $value): ++$j ?> "<?php echo $key ?>": <?php echo json_encode($value).($nb1 == $j ? '' : ',') ?> <?php endforeach ?> }<?php echo $nb == $i ? '' : ',' ?> <?php endforeach ?> ]

El Formato yaml Para Formatos nativos, Symfony hace algunas configuraciones en el fondo, como cambiar el content type, y desactivar el layout. Como el Formato YAML no esta en la lista de los formatos nativos, la respuesta y su content type se puede cambiar y el layout desactivado en la accin:
class apiActions extends sfActions { public function executeList(sfWebRequest $request) { $this->jobs = array(); foreach ($this->getRoute()->getObjects() as $job) { $this->jobs[$this->generateUrl('job_show_user', $job, true)] = $job->asArray($request->getHost()); } switch ($request->getRequestFormat()) { case 'yaml': $this->setLayout(false); $this->getResponse()->setContentType('text/yaml'); break; } } }

En una accin, el mtodo setLayout() cambia el layout por defecto o se desactiva cuando se establece en false. La plantilla de YAML dice lo siguiente:
<!-- apps/frontend/modules/api/templates/listSuccess.yaml.php --> <?php foreach ($jobs as $url => $job): ?> url: <?php echo $url ?>

<?php foreach ($job as $key => $value): ?> <?php echo $key ?>: <?php echo sfYaml::dump($value) ?> <?php endforeach ?> <?php endforeach ?>

Si intentas llamar a los servicios web con un token no-vlido, tendrs una pgina XML 404 para el formato XML, y una pgina JSON 404 para el formato JSON. Pero para el formato YAML, Symfony no sabe qu mostrar. Cuando creas un formato, una plantilla personalizada de error debe ser creada. La plantilla se utilizar para pginas 404, y todas las dems excepciones. Dado que la excepcin debe ser diferente segn sea un entorno de desarrollo o produccin, dos archivos son necesarios (config/error/exception.yaml.php para depuracin, y config/error/error.yaml.php para produccin):
// config/error/exception.yaml.php <?php echo sfYaml::dump(array( 'error' => array( 'code' => $code, 'message' => $message, 'debug' => array( 'name' => $name, 'message' => $message, 'traces' => $traces, ), )), 4) ?> // config/error/error.yaml.php <?php echo sfYaml::dump(array( 'error' => array( 'code' => $code, 'message' => $message, ))) ?>

Antes de probarlo, debes crear un layout para el formato YAML:


// apps/frontend/templates/layout.yaml.php <?php echo $sf_content ?>

Sobreescribiendo las plantillas por defecto de error 404 y de excepcin es tan simple como crear los archivos en cuestin en el directorioconfig/error/.

Probando los Servicios Web


Para probar el servicio web, copia los datos de los afiliados de data/fixtures/ a test/fixtures/ y sustituye el contenido del archivo autogeneradoapiActionsTest.php con el siguiente contenido:
include(dirname(__FILE__).'/../../bootstrap/functional.php'); $browser = new JobeetTestFunctional(new sfBrowser()); $browser->loadData(); $browser-> info('1 - Web service security')-> info(' 1.1 - A token is needed to access the service')-> get('/api/foo/jobs.xml')-> with('response')->isStatusCode(404)-> info(' 1.2 - An inactive account cannot access the web service')-> get('/api/symfony/jobs.xml')-> with('response')->isStatusCode(404)-> info('2 - The jobs returned are limited to the categories configured for the affiliate')-> get('/api/sensio_labs/jobs.xml')-> with('request')->isFormat('xml')-> with('response')->begin()-> isValid()-> checkElement('job', 32)-> end()-> info('3 - The web service supports the JSON format')-> get('/api/sensio_labs/jobs.json')-> with('request')->isFormat('json')-> with('response')->matches('/"category"\: "Programming"/')-> info('4 - The web service supports the YAML format')-> get('/api/sensio_labs/jobs.yaml')-> with('response')->begin()-> isHeader('content-type', 'text/yaml; charset=utf-8')-> matches('/category\: Programming/')-> end() ;

En esta prueba, te dars cuenta de tres nuevos mtodos:


isValid(): Checks whether or not the XML response is well formed isFormat(): Pone a prueba el formato de un request

contains(): Para formatos no-HTML, comprueba si la respuesta contiene el

fragmento de texto esperado

El Formulario de Afiliacin
Ahora que el servicio web est listo para ser utilizado, vamos a crear el Formulario de Afiliacin. Vamos a describir una vez ms el clsico proceso de agregar una nueva funcionalidad a una aplicacin. Enrutamiento Lo sabes. La ruta es la primera cosa a crear:
# apps/frontend/config/routing.yml affiliate: class: sfDoctrineRouteCollection options: model: JobeetAffiliate actions: [new, create] object_actions: { wait: get

Se trata de una clsica coleccin de rutas Doctrine con una nueva opcin de configuracin: actions. Como no necesitamos todas las siete acciones definidas por defecto para la ruta, la opcin actions instruye a la ruta para slo coincidir con las acciones new y create . La ruta adicional wait se utilizarn para dar al inminente afiliado algunos comentarios acerca de su cuenta. Inicializacin El segundo paso clsico es generar un mdulo:
$ php symfony doctrine:generate-module frontend affiliate JobeetAffiliate --non-verbose-templates

Las Plantillas La tarea doctrine:generate-module genera las clsicas siete acciones y sus correspondientes plantillas. En el directorio templates/, elimina todos los archivos, pero no _form.php ni newSuccess.php. Y para los archivos que mantenemos, sustituye sus contenidos con los siguientes:
<!-- apps/frontend/modules/affiliate/templates/newSuccess.php --> <?php use_stylesheet('job.css') ?> <h1>Become an Affiliate</h1> <?php include_partial('form', array('form' => $form)) ?> <!-- apps/frontend/modules/affiliate/templates/_form.php --> <?php include_stylesheets_for_form($form) ?> <?php include_javascripts_for_form($form) ?> <?php echo form_tag_for($form, 'affiliate') ?>

<table id="job_form"> <tfoot> <tr> <td colspan="2"> <input type="submit" value="Submit" /> </td> </tr> </tfoot> <tbody> <?php echo $form ?> </tbody> </table> </form>

Crea la plantilla waitSuccess.php:


<!-- apps/frontend/modules/affiliate/templates/waitSuccess.php --> <h1>Your affiliate account has been created</h1> <div style="padding: 20px"> Thank you! You will receive an email with your affiliate token as soon as your account will be activated. </div>

Por ltimo, cambiar el enlace en el pie de pgina para que apunte al mdulo affiliate:
// apps/frontend/templates/layout.php <li class="last"> <a href="<?php echo url_for('affiliate_new') ?>">Become an affiliate</a> </li>

Las Acciones Una vez ms, ya que slo se utiliza el formulario de creacin, abre el archivo actions.class.php y elimina todos los mtodos pero dejaexecuteNew(), executeCreate(), y processForm(). Para la accin processForm(), cambiar la URL de redireccionamiento a la accin wait:
// apps/frontend/modules/affiliate/actions/actions.class.php $this->redirect($this->generateUrl('affiliate_wait', $jobeet_affiliate));

La accin wait es simple que no hace falta pasarle nada a la plantilla:


// apps/frontend/modules/affiliate/actions/actions.class.php public function executeWait(sfWebRequest $request) { }

El afiliado no puede elegir su token, ni puede activar su cuenta inmediatamente. Abre el archivo JobeetAffiliateForm para personalizar el formulario:
// lib/form/doctrine/JobeetAffiliateForm.class.php class JobeetAffiliateForm extends BaseJobeetAffiliateForm { public function configure() { $this->useFields(array( 'url', 'email', 'jobeet_categories_list' )); $this->widgetSchema['jobeet_categories_list']->setOption('expanded', true); $this->widgetSchema['jobeet_categories_list']>setLabel('Categories'); $this->validatorSchema['jobeet_categories_list']>setOption('required', true); $this->widgetSchema['url']->setLabel('Your website URL'); $this->widgetSchema['url']->setAttribute('size', 50); $this->widgetSchema['email']->setAttribute('size', 50); $this->validatorSchema['email'] = new sfValidatorEmail(array('required' => true)); } }

The new sfForm::useFields() method allows to specify the white list of fields to keep. All non mentionned fields will be removed from the form. El framework de formularios soporta relaciones muchos-a-muchos como cualquier otra columna. Por defecto, esta relacin es mostrada como un cuadro desplegable gracias al widget sfWidgetFormChoice. Como se ha visto durante el da 10, hemos cambiado lo mostrado mediante el uso de la opcin expanded. Como emails y URLs tienden a ser bastante ms largo que el tamao predeterminado de una etiqueta input, los atributos de HTML por defecto se puede configurar utilizando el mtodo setAttribute().

Las Pruebas El ltimo paso es escribir algunas pruebas funcionales para la nueva funcin. Sustituye las pruebas generadas para el mdulo affiliate con el cdigo siguiente:
// test/functional/frontend/affiliateActionsTest.php include(dirname(__FILE__).'/../../bootstrap/functional.php'); $browser = new JobeetTestFunctional(new sfBrowser()); $browser->loadData(); $browser-> info('1 - An affiliate can create an account')-> get('/affiliate/new')-> click('Submit', array('jobeet_affiliate' => array( 'url' => 'http://www.example.com/', 'email' => 'foo@example.com', 'jobeet_categories_list' => array(Doctrine_Core::getTable('JobeetCategory')>findOneBySlug('programming')->getId()), )))-> with('response')->isRedirected()-> followRedirect()-> with('response')->checkElement('#content h1', 'Your affiliate account has been created')-> info('2 - An affiliate must at least select one category')-> get('/affiliate/new')-> click('Submit', array('jobeet_affiliate' => array( 'url' => 'http://www.example.com/', 'email' => 'foo@example.com', )))-> with('form')->isError('jobeet_categories_list') ;

El Backend para Afiliados


Por el backend, un module affiliate debe ser creado para activar los afiliados por el administrador:
$ php symfony doctrine:generate-admin backend JobeetAffiliate -module=affiliate

Para acceder al nuevo mdulo creado, aade un enlace en el men principal con el nmero de afiliados que deben ser activados:
<!-- apps/backend/templates/layout.php -->

<li> <a href="<?php echo url_for('jobeet_affiliate_affiliate') ?>"> Affiliates - <strong><?php echo Doctrine_Core::getTable('JobeetAffiliate')->countToBeActivated() ?></strong> </a> </li> // lib/model/doctrine/JobeetAffiliateTable.class.php class JobeetAffiliateTable extends Doctrine_Table { public function countToBeActivated() { $q = $this->createQuery('a') ->where('a.is_active = ?', 0); return $q->count(); } // ... }

Como la nica accin necesaria en el backend es para activar o desactivar las cuentas, cambia la seccin config del generador por defecto para simplificar la interfaz un poco y aade un enlace para activar directamente las cuentas en la vista list:
# apps/backend/modules/affiliate/config/generator.yml config: fields: is_active: { label: Active? } list: title: Affiliate Management display: [is_active, url, email, token] sort: [is_active] object_actions: activate: ~ deactivate: ~ batch_actions: activate: ~ deactivate: ~ actions: {} filter: display: [url, email, is_active]

Para que los administradores sean ms productivos, cambiar el filtro para mostrar nicamente los afiliados a ser activados:
// apps/backend/modules/affiliate/lib/affiliateGeneratorConfiguration.class. php

class affiliateGeneratorConfiguration extends BaseAffiliateGeneratorConfiguration { public function getFilterDefaults() { return array('is_active' => '0'); } }

El nico otro cdigo a escribir es para las acciones activate, deactivate :


// apps/backend/modules/affiliate/actions/actions.class.php class affiliateActions extends autoAffiliateActions { public function executeListActivate() { $this->getRoute()->getObject()->activate(); $this->redirect('jobeet_affiliate'); } public function executeListDeactivate() { $this->getRoute()->getObject()->deactivate(); $this->redirect('jobeet_affiliate'); } public function executeBatchActivate(sfWebRequest $request) { $q = Doctrine_Query::create() ->from('JobeetAffiliate a') ->whereIn('a.id', $request->getParameter('ids')); $affiliates = $q->execute(); foreach ($affiliates as $affiliate) { $affiliate->activate(); } $this->redirect('jobeet_affiliate'); } public function executeBatchDeactivate(sfWebRequest $request) { $q = Doctrine_Query::create() ->from('JobeetAffiliate a') ->whereIn('a.id', $request->getParameter('ids')); $affiliates = $q->execute();

foreach ($affiliates as $affiliate) { $affiliate->deactivate(); } $this->redirect('jobeet_affiliate'); } } // lib/model/doctrine/JobeetAffiliate.class.php class JobeetAffiliate extends BaseJobeetAffiliate { public function activate() { $this->setIsActive(true); return $this->save(); } public function deactivate() { $this->setIsActive(false); return $this->save(); } // ... }

El correo
El framework Symfony viene con una de las mejores soluciones PHP de correo: Swift Mailer. Por supuesto, la librera esta completamente integrada con Symfony, con algunas caractersticas interesantes agregadas por encima de las caracterticas predeterminadas.
Symfony 1.3/1.4 usa Swift Mailer versin 4.1.

Envando Emails simples


Vamos a empezar enviando un simple email para notificar al afiliado cuando su cuenta ha sido confirmada y darle el token. Reemplace la accin activate con el siguiente cdigo:
// apps/backend/modules/affiliate/actions/actions.class.php class affiliateActions extends autoAffiliateActions { public function executeListActivate() { $affiliate = $this->getRoute()->getObject(); $affiliate->activate(); // send an email to the affiliate $message = $this->getMailer()->compose( array('jobeet@example.com' => 'Jobeet Bot'), $affiliate->getEmail(), 'Jobeet affiliate token', <<<EOF Your Jobeet affiliate account has been activated. Your token is {$affiliate->getToken()}. The Jobeet Bot. EOF ); $this->getMailer()->send($message); $this->redirect('jobeet_affiliate'); } // ... } Para que el cdigo funcione debidamente, deberas cambiar jobeet@example.com por una direccin de email real.

La gestin del Email en Symfony es centrada alrededor de un objeto mailer, el cual es obtenido desde la accin con un mtodo getMailer().

El mtodo compose() toma cuatro argumentos y devuelve un objeto de mensaje de email:


el remitente (from); destinatario(s) (to); el asunto del mensaje; el cuerpo del mensaje;

El enviar el mensaje es entonces tan simple como llamar al mtodo send() desde la instancia mailer y pasarle el mensaje como un argumento. Como un atajo, puedes solo escribir y enviar un email en un mtodo usando composeAndSend().
El mensaje es una instnacia de la clase Swift_Message. Visita la documentacin oficial Swift Mailer para aprender mas acerca de este objeto, y como hacer cosas mas avanzadas como adjuntar archivos.

Configuracin
Por defecto, el mtodo send() intenta usar el servidor local SMTP para el envio del mensaje al destinatario. Por supuesto, como muchas cosas en Symfony, esto es totalmente configurable. Factorias Durante los das anteriores, hemos hablado acerca de los objetos del ncleo symfony como user, request, response, o routing. Esos objetos son automaticamente creados, configurados, y gestionados por el framework. Ellos son siempre accesibles desde el objeto sfContext, y como muchas cosas del framework, ellas son configurables via un archivo de configuracin: factories.yml. Este archivo es configurable por entorno. Cuando sfContext inicializa las factorias del ncleo, este lee al archivo factories.yml por los nombres de clases (class) y los parmetros (param) a pasar al constructor:
response: class: sfWebResponse param: send_http_headers: false

En cdigo anterior, para crear un response factory, symfony instancia un objeto sfWebResponse y pasa la opcin send_http_headers como un parmetro.
La clase sfContext El objeto sfContext contiene referencias a los objetos del ncleo symfony como el request, response, user, y as. Como sfContext actua como un singleton, puedes usar la sentencia sfContext::getInstance() para obtenerlo desde cualquier lugar y entonces poder acceder a cualquiera de los objetos del ncleo: $mailer = sfContext::getInstance()->getMailer();

Si quieres usar sfContext::getInstance() en una de tus clases, piensalo dos veces ya que este hace un strong coupling. Esto es simpre bastante mejor a pasar el objeto que necesitas como un argumento. Puedes an usar sfContext como un registro y agregar tus propios objetos usando los mtodos set(). Este toma un nombre y un objeto como argumentos y el mtodo the get() puede ser usado luego para obtener un objeto por el nombre: sfContext::getInstance()->set('job', $job); $job = sfContext::getInstance()->get('job');

Delivery Strategy Como otros muchos objetos del ncleo, el mailer es un factory. Por eso, este es configurado en el archivo de configuracin factories.yml. La configuraci por defecto se lee asi:
mailer: class: sfMailer param: logging: %SF_LOGGING_ENABLED% charset: %SF_CHARSET% delivery_strategy: realtime transport: class: Swift_SmtpTransport param: host: localhost port: 25 encryption: ~ username: ~ password: ~

Cuando se crea una nueva aplicacin, el archivo de configuracin factories.yml sobreescribe la configuracin predeterminada con algunos sensibles valores para los entornos env y test:
test: mailer: param: delivery_strategy: none dev: mailer: param: delivery_strategy: none

La configuracin delivery_strategy le dice a Symfony como entregar correos. Por defecto, Symfony viene con cuatro diferentes estrategias:
realtime: Los Mensajes son entregados en tiempo real. single_address: Los Mensajes son entregados a una solo direccin. spool: Los Mensajes son guardados en cola.

none: Los Mensajes son simplemente ignorados.

No importa cual sea la estrategia, los emails son siempre registrados en el log y estan disponibles en el panel "mailer" del web debug toolbar. Mail Transport Los mensajes de correo son siempre enviados por un transport. El transport es configurado en el archivo de configuracin factories.yml, y la configuracin por defecto usa el servidor SMTP del equipo local:
transport: class: Swift_SmtpTransport param: host: localhost port: 25 encryption: ~ username: ~ password: ~

Swift Mailer viene con tres clases transport diferentes:


Swift_SmtpTransport: Usa un servidor SMTP para enviar los mensajes. Swift_SendmailTransport: Usa sendmail para enviar los mensajes. Swift_MailTransport: Usa la funcin nativa de PHP mail() fpara enviar los

mensajes.
La seccin "Transport Types" de la documentacin oficial Swift Mailer describe todo lo que necesitas saber acerca de las clases transport y sus diferentes parmetros.

Probando Emails
Ahora qe vimos como enviar un email con symfony mailer, vamos a escribir algunas pruebas funcionales para asegurarnos que lo hicimos bien. Por defecto, symfony registras un tester mailer (sfMailerTester) para facilitar la prueba. First, change the mailer factory's configuration for the test environment if your web server does not have a local SMTP server. We have to replace the current Swift_SmtpTransport class by Swift_MailTransport:
# apps/backend/config/factories.yml test: # ... mailer: param: delivery_strategy: none transport: class: Swift_MailTransport

Then, add a new test/fixtures/administrators.yml file containing the following YAML definition:
sfGuardUser: admin: email_address: admin@example.com username: admin password: admin first_name: Fabien last_name: Potencier is_super_admin: true

Finally, replace the affiliate functional test file for the backend application with the following code:
// test/functional/backend/affiliateActionsTest.php include(dirname(__FILE__).'/../../bootstrap/functional.php'); $browser = new JobeetTestFunctional(new sfBrowser()); $browser->loadData(); $browser-> info('1 - Authentication')-> get('/affiliate')-> click('Signin', array( 'signin' => array('username' => 'admin', 'password' => 'admin'), array('_with_csrf' => true) ))-> with('response')->isRedirected()-> followRedirect()-> info('2 - When validating an affiliate, an email must be sent with its token')-> click('Activate', array(), array('position' => 1))-> with('mailer')->begin()-> checkHeader('Subject', '/Jobeet affiliate token/')-> checkBody('/Your token is symfony/')-> end() ;

Cada email enviado puede ser probado con la ayuda de los mtodos checkHeader() y checkBody(). El segundo argumento de checkHeader() y el primer argumento de checkBody() puede ser lo siguiente:

una cadena para comprobar que coincide exactamente; una expresin regular para comprobar el valor; una expresin regular negativa (es una que empieza con un !) para comprobar que el valor no coincide.

Por defecto, las comprobaciones son hechas en el primer email enviado. si muchos emails se enviaron, puedes elegir uno para probarlo con el mtodo withMessage(). El mtodo withMessage() toma un destinatario como su primer argumento. Este tambin

toma un segundo argumento para indicar cual email quieres probar si varios han sido enviados al mismo destinatario.

El Motor de Busqueda
La Tecnologa
Antes de saltar de cabeza, en primer lugar, hablemos un poco sobre la historia de Symfony. Abogamos por un montn de buenas prcticas, como pruebas y refactoring, y tambin tratamos de aplicarlas al framework mismo. Por ejemplo, nos gusta el famoso lema "No reinventar la rueda". Como cuestin de hecho, el framework Symfony comenz su vida hace cuatro aos como la unin entre dos existentes softwares Open-Source: Mojavi y Propel. Y cada vez que necesitamos hacer frente a un nuevo problema, buscamos una biblioteca que haga el trabajo mucho antes de hacer la codificacin uno mismo desde cero. Hoy, queremos aadir un motor de bsqueda para Jobeet, y el Zend Framework ofrece una gran biblioteca, llamada Zend Lucene, la cual es un port del bien conocido proyecto Java Lucene. En lugar de crear un nuevo motor de bsqueda para Jobeet, lo cual es una tarea compleja, vamos a utilizar Zend Lucene. En la pgina de la documentacin de Zend Lucene, la biblioteca se describe de la siguiente manera:
... un motor de bsqueda textual de propsito general escrito ntegramente en PHP 5. Como almacena sus ndices en el sistema de archivos y no requiere de un servidor de bases de datos, ste puede aadir capacidades de bsqueda a casi cualquier sitio web PHP. Zend_Search_Lucene soporta las siguientes caractersticas: Bsqueda por Ranking - mostrar al principio los mejores resultados Muchos tipos de consultas poderosas: consultas de tipo textual, booleaneas, wildcard por proximidad, rangos y muchas otras Bsqueda por un campo especfico (e.g., ttulo, autor, contenidos)

Este captulo no es un tutorial sobre la biblioteca Zend Lucene, sino como integrarla en el sitio web Jobeet; o ms en general, la forma de integrar bibliotecas de terceros en un proyecto symfony. Si deseas ms informacin sobre esta tecnologa, por favor visita la Documentacin de Zend Lucene.

Instalacin y Configuracin del Zend Framework Las bibliotecas Zend Lucene son parte del Zend Framework. Como no necesitamos de todo del Zend Framework, slo se necesita instalar algunas partes en el directorio lib/vendor/, junto con el symfony framework. En primer lugar, la descarga Zend Framework y descomprime los archivos de modo que tengas un directorio lib/vendor/Zend/.
Las siguientes explicaciones han sido probadas con la versin 1.8.0 de the Zend Framework.

Puedes limpiar el directorio eliminando todo menos los siguientes archivos y directorios:
Exception.php Loader/ Loader.php Search/

Luego, agrega el cdigo siguiente a la claseProjectConfiguration para proporcionar una manera simple de registrar el Zend autoloader:
// config/ProjectConfiguration.class.php class ProjectConfiguration extends sfProjectConfiguration { static protected $zendLoaded = false; static public function registerZend() { if (self::$zendLoaded) { return; }

set_include_path(sfConfig::get('sf_lib_dir').'/vendor'.PATH_SEPARATOR.get _include_path()); require_once sfConfig::get('sf_lib_dir').'/vendor/Zend/Loader/Autoloader.php'; Zend_Loader_Autoloader::getInstance(); self::$zendLoaded = true; } // ... }

Indexacin
El motor de bsqueda de Jobeet debera ser capaz de devolver todos los puestos que corresponden a palabras clave introducidas por el usuario. Antes de ser capaz de buscar cualquier cosa, un ndice debe ser construdo para los puestos de trabajo; para Jobeet, se almacenarn en el directorio data/. Zend Lucene proporciona dos mtodos para recuperar un ndice en funcin de si ya existe uno o no. Vamos a crear un mtodo helper en la claseJobeetJobTable que devuelve un ndice existente o crea uno nuevo para nosotros:
// lib/model/doctrine/JobeetJobTable.class.php public function getLuceneIndex() { ProjectConfiguration::registerZend();

if (file_exists($index = $this->getLuceneIndexFile())) { return Zend_Search_Lucene::open($index); } else { return Zend_Search_Lucene::create($index); } } public function getLuceneIndexFile() { return sfConfig::get('sf_data_dir').'/job.'.sfConfig::get('sf_environment').'.in dex'; }

El Mtodo save() Cada vez que se crea un puesto de trabajo, es actualizado o borrado, el ndice debe ser actualizado. Edita JobeetJob para actualizar el ndice cada vez que un trabajo es guardado en la base de datos:
public function save(Doctrine_Connection $conn = null) { // ... $ret = parent::save($conn); $this->updateLuceneIndex(); return $ret; }

Y crear el mtodo updateLuceneIndex() que hace el trabajo:


// lib/model/doctrine/JobeetJob.class.php public function updateLuceneIndex() { $index = JobeetJobTable::getLuceneIndex(); // remove an existing entry if ($hit = $index->find('pk:'.$this->getId())) { $index->delete($hit->id); } // don't index expired and non-activated jobs if ($this->isExpired() || !$this->getIsActivated()) { return; }

$doc = new Zend_Search_Lucene_Document(); // store job primary key URL to identify it in the search results $doc->addField(Zend_Search_Lucene_Field::UnIndexed('pk', $this>getId())); // index job fields $doc->addField(Zend_Search_Lucene_Field::UnStored('position', $this>getPosition(), 'utf-8')); $doc->addField(Zend_Search_Lucene_Field::UnStored('company', $this>getCompany(), 'utf-8')); $doc->addField(Zend_Search_Lucene_Field::UnStored('location', $this>getLocation(), 'utf-8')); $doc->addField(Zend_Search_Lucene_Field::UnStored('description', $this>getDescription(), 'utf-8')); // add job to the index $index->addDocument($doc); $index->commit(); }

Como Zend Lucene no es capaz de actualizar una entrada existente, sta se elimina primero si el puesto de trabajo (job) ya existe en el ndice. La Indexacin de los puestos de trabajo en s es muy sencilla: la clave primaria se almacena para futuras referencias cuando hacemos bsquedas de puestos de trabajo y las principales columnas (position, company, location, y description) se indexan pero no se almacena en el ndice ya que vamos a utilizar los objetos reales para mostrar los resultados. Transacciones Doctrine Qu pasa si hay un problema cuando procede la indexacin de un puesto de trabajo (job) o si el puesto de trabajo (job) no se guarda en la base de datos? Ambas herramientas Doctrine y Zend Lucene arrojarn una excepcin. Pero en algunas circunstancias, podramos tener un puesto de trabajo (job) guardado en la base de datos sin la correspondiente indexacin. Para evitar que esto ocurra, podemos envolver los dos actualizaciones en una transaccin y anularlas en caso de haber un error:
// lib/model/doctrine/JobeetJob.class.php public function save(Doctrine_Connection $conn = null) { // ... $conn = $conn ? $conn : JobeetJobTable::getConnection(); $conn->beginTransaction(); try { $ret = parent::save($conn);

$this->updateLuceneIndex(); $conn->commit(); return $ret; } catch (Exception $e) { $conn->rollBack(); throw $e; } } delete()

Tambin tenemos que sobreescribir el mtodo delete() para eliminar la entrada del puesto de trabajo (job) eliminado a partir del ndice:
// lib/model/doctrine/JobeetJob.class.php public function delete(Doctrine_Connection $conn = null) { $index = JobeetJobTable::getLuceneIndex(); if ($hit = $index->find('pk:'.$this->getId())) { $index->delete($hit->id); } return parent::delete($conn); }

Bsqueda
Ahora que tenemos todo en su lugar, puedes recargar los datos para indexarlos:
$ php symfony doctrine:data-load Para usuarios tipo Unix: ya que el ndice se modifica desde la lnea de comandos y tambin desde el web, debe cambiar el ndice de permisos de directorio segn tu configuracin: comprobar que tanto desde la lnea de comandos y desde el servidor web el usuario pueda escribir el ndice del directorio. Puedes tener algunas advertencias acerca de la clase ZipArchive si no tienes la extension zip compilada en tu PHP. Es un fallo conocido de la clase Zend_Loader.

Implementar la bsqueda en el frontend es pan comido. En primer lugar, crea una ruta:
job_search: url: /search param: { module: job, action: search }

Y la accin correspondiente:

// apps/frontend/modules/job/actions/actions.class.php class jobActions extends sfActions { public function executeSearch(sfWebRequest $request) { $this->forwardUnless($query = $request->getParameter('query'), 'job', 'index'); $this->jobs = Doctrine_Core::getTable('JobeetJob') >getForLuceneQuery($query); } // ... } The new forwardUnless() method forwards the user to the index action of the job module if the query request parameter does not exist or is empty. It's just an alias for the following longer statement: if (!$query = $request->getParameter('query')) { $this->forward('job', 'index'); }

La plantilla es tambin muy sencilla:


// apps/frontend/modules/job/templates/searchSuccess.php <?php use_stylesheet('jobs.css') ?> <div id="jobs"> <?php include_partial('job/list', array('jobs' => $jobs)) ?> </div>

La bsqueda en s misma se delega en el mtodo getForLuceneQuery():


// lib/model/doctrine/JobeetJobTable.class.php public function getForLuceneQuery($query) { $hits = self::getLuceneIndex()->find($query); $pks = array(); foreach ($hits as $hit) { $pks[] = $hit->pk; } if (empty($pks)) { return array(); } $q = $this->createQuery('j') ->whereIn('j.id', $pks) ->limit(20); $q = $this->addActiveJobsQuery($q);

return $q->execute(); }

Despus de obtener todos los resultados del ndice Lucene, vamos a filtrar los puestos de trabajo inactivos, y limitar el nmero de resultados a 20. Para que funcione, actualiza el layout:
// apps/frontend/templates/layout.php <h2>Ask for a job</h2> <form action="<?php echo url_for('job_search') ?>" method="get"> <input type="text" name="query" value="<?php echo $sf_request>getParameter('query') ?>" id="search_keywords" /> <input type="submit" value="search" /> <div class="help"> Enter some keywords (city, country, position, ...) </div> </form> Zend Lucene define un lenguaje de consulta poderoso que soporta operaciones como Booleans, wildcards, fuzzy search,y mucho ms. Todo est documentado en el Zend Lucene manual

Las Pruebas Unitarias


Qu tipo de Pruebas unitarias tenemos que crear para probar el motor de bsqueda? Evidentemente, no probaremos la biblioteca Zend Lucene en s, sino su integracin con la clase JobeetJob. Aade las siguientes pruebas al final del archivo JobeetJobTest.php y no te olvides de actualizar el nmero de pruebas al principio del archivo a 7:
// test/unit/model/JobeetJobTest.php $t->comment('->getForLuceneQuery()'); $job = create_job(array('position' => 'foobar', 'is_activated' => false)); $job->save(); $jobs = Doctrine_Core::getTable('JobeetJob')>getForLuceneQuery('position:foobar'); $t->is(count($jobs), 0, '::getForLuceneQuery() does not return non activated jobs'); $job = create_job(array('position' => 'foobar', 'is_activated' => true)); $job->save(); $jobs = Doctrine_Core::getTable('JobeetJob')>getForLuceneQuery('position:foobar'); $t->is(count($jobs), 1, '::getForLuceneQuery() returns jobs matching the criteria'); $t->is($jobs[0]->getId(), $job->getId(), '::getForLuceneQuery() returns jobs matching the criteria'); $job->delete();

$jobs = Doctrine_Core::getTable('JobeetJob')>getForLuceneQuery('position:foobar'); $t->is(count($jobs), 0, '::getForLuceneQuery() does not return deleted jobs');

Probamos que ningun puesto de trabajo inactivo, o borrado aparezca en los resultados de la bsqueda; tambin comprobamos que los puestos se corresponden al criterio dado para aparecer en los resultados.

Las Tareas
Finalmente, tenemos que crear una tarea de limpieza para el ndice de todos los registros obsoletos (cuando expira un puesto, por ejemplo,) y optimizar el ndice de vez en cuando. Como ya tenemos una tarea de limpieza, vamos a actualizarla para aadirle esas caractersticas:
// lib/task/JobeetCleanupTask.class.php protected function execute($arguments = array(), $options = array()) { $databaseManager = new sfDatabaseManager($this->configuration); // cleanup Lucene index $index = JobeetJobTable::getLuceneIndex(); $q = Doctrine_Query::create() ->from('JobeetJob j') ->where('j.expires_at < ?', date('Y-m-d')); $jobs = $q->execute(); foreach ($jobs as $job) { if ($hit = $index->find('pk:'.$job->getId())) { $index->delete($hit->id); } } $index->optimize(); $this->logSection('lucene', 'Cleaned up and optimized the job index'); // Remove stale jobs $nb = Doctrine_Core::getTable('JobeetJob')->cleanup($options['days']); $this->logSection('doctrine', sprintf('Removed %d stale jobs', $nb)); }

La tarea remueve todos los puestos de trabajo vencidos del ndice y, a continuacin, se lo optimiza gracias al mtodo nativo optimize() de Zend Lucene.

para aumentar la capacidad de respuesta del motor de bsqueda, vamos a aprovechar AJAX para convertir el motor de bsqueda en uno ms vivo. Como el formulario debe trabajar con y sin JavaScript activado, la funcin de bsqueda en vivo se llevar a cabo utilizandounobtrusive JavaScript. Usando unobtrusive JavaScript tambin permite una mejor separacin de las capas en el cdigo del cliente entre HTML, CSS, y el comportamiento JavaScript.

Instalacin de jQuery
En vez de reinventar la rueda y la gestin de las muchas diferencias entre los navegadores, vamos a utilizar una biblioteca JavaScript, jQuery. El framework Symfony es agnstica y puede trabajar con cualquier biblioteca JavaScript. Ve a jQuery, descargar la ltima versin, y coloca los archivos .js dentro de web/js/.

Incluyendo jQuery
Como vamos a necesitar jQuery en todas las pginas, actualiza el layout para incluirlo en el <head>. Tenga cuidado de insertar la funcinuse_javascript() antes de llamar al include_javascripts():
<!-- apps/frontend/templates/layout.php --> <?php use_javascript('jquery-1.2.6.min.js') ?> <?php include_javascripts() ?> </head>

Podramos haber incluido jQuery directamente con un tag <script>, pero utilizando el helper use_javascript() nos aseguramos que el mismo JavaScript no se incluir dos veces.
Por razones de rendimiento, podras tambin mover el helper include_javascripts() justo antes de la etiqueta de cierre </body>.

Agregando comportamientos
Implementar un buscador vivo implica que cada vez que el usuario escribe una letra en el cuadro de bsqueda, se hace una llamada al servidor; a continuacin, el servidor devolver la informacin necesaria para actualizar la areas seleccionadas de la pgina sin actualizar toda la pgina. En lugar de aadir el comportamiento con atributos on*() del HTML, el principio fundamental detrs de jQuery es agregar comportamientos al DOM despus de que la pgina est completamente cargada. De esta forma, si desactivas el soporte de JavaScript en tu navegador, ningn comportamiento es registrado, y el formulario sigue funcionando como antes. El primer paso es interceptar cuando un usuario introduce una tecla en el cuadro de bsqueda:
$('#search_keywords').keyup(function(key)

{ if (this.value.length >= 3 || this.value == '') { // do something } }); No aadas el cdigo por ahora, ya que vamos a modificar todo en gran medida. El cdigo JavaScript final se aadir al layout en la siguiente seccin.

Cada vez que el usuario introduce una tecla, jQuery executa la funcin annima que se defini en el cdigo anterior, pero slo si el usuario ha escrito ms de 3 caracteres o si se elimina todo, de la etiqueta input. Hacer una llamada AJAX al servidor es tan sencillo como utilizar el mtodo load() sobre el elemento DOM:
$('#search_keywords').keyup(function(key) { if (this.value.length >= 3 || this.value == '') { $('#jobs').load( $(this).parents('form').attr('action'), { query: this.value + '*' } ); } });

Para gestionar las llamadas AJAX, la misma accin "normal y corriente" es ejecutada. Los cambios necesarios en la accin se harn en la siguiente seccin. Por ltimo, pero no por ello menos importante, si JavaScript est habilitado, y queremos a quitar el botn de bsqueda:
$('.search input[type="submit"]').hide();

Respuesta al Usuario
Cuando haces una llamada AJAX, la pgina no se actualiza inmediatamente. El navegador esperar la respuesta del servidor antes de actualizar la pgina. En el nterin, es necesario proporcionar respuesta visual al usuario para informarle de que algo est ejecutandose. Una convencin es mostrar un icono de cargador durante la llamada AJAX. Actualiza el layout para aadir la imagen del cargador y ocultarla por defecto:
<!-- apps/frontend/templates/layout.php --> <div class="search"> <h2>Ask for a job</h2> <form action="<?php echo url_for('job_search') ?>" method="get"> <input type="text" name="query" value="<?php echo $sf_request>getParameter('query') ?>" id="search_keywords" /> <input type="submit" value="search" /> <img id="loader" src="/images/loader.gif" style="vertical-align: middle; display: none" />

<div class="help"> Enter some keywords (city, country, position, ...) </div> </form> </div> El cargador por defecto esta optimizado para el actual layout de Jobeet. Si deseas crear el tuyo, encontrars una gran cantidad de gratuitos servicios en lnea como http://www.ajaxload.info/.

Ahora que tienes todas las piezas necesarias para que el cdigo HTML funcione, crea un archivo search.js que tenga el siguiente cdigo JavaScript que escribiremos:
// web/js/search.js $(document).ready(function() { $('.search input[type="submit"]').hide(); $('#search_keywords').keyup(function(key) { if (this.value.length >= 3 || this.value == '') { $('#loader').show(); $('#jobs').load( $(this).parents('form').attr('action'), { query: this.value + '*' }, function() { $('#loader').hide(); } ); } }); });

Tambin necesitas actualizar el layout para incluir este nuevo archivo:


<!-- apps/frontend/templates/layout.php --> <?php use_javascript('search.js') ?> JavaScript como una Accin Aunque el JavaScript que hemos escrito para el motor de busqueda es esttico, a veces, necesitas llamar algn cdigo PHP (para usar el helperurl_for() por ejemplo). JavaScript es solo otro formato como HTML, y como vimos algunos das atrs, Symfony hace la adminitracin de formatos muy fcil. Ya que el archivo JavaScript tendr un comportamiento por pgina, puedes hasta tener la misma URL como para la pgina del archivo JavaScript, pero que termina con.js. por ejemplo, si quieres crear un archivo para el motor de busqueda, puedes modificar la ruta job_search como sigue y crear una platillasearchSuccess.js.php: job_search: url: /search.:sf_format param: { module: job, action: search, sf_format: html } requirements:

sf_format: (?:html|js)

AJAX en una Accin


Si JavaScript est habilitado, jQuery interceptar todas las teclas mecanografiadas en el cuadro de bsqueda, y llamar a la accin search. Si no, la misma accin search es tambin llamada cuando el usuario enva el formulario presionando la tecla "enter" o haciendo clic en el botn "search". As, la accin search ahora debe determinar si la llamada se realiza a travs de AJAX o no. Cuando una peticin se hace con una llamada AJAX, el mtodo isXmlHttpRequest() dar true.
El mtodo isXmlHttpRequest() funciona con todas las principales bibliotecas JavaScript como Prototype, Mootools, o jQuery. // apps/frontend/modules/job/actions/actions.class.php public function executeSearch(sfWebRequest $request) { $this->forwardUnless($query = $request->getParameter('query'), 'job', 'index'); $this->jobs = Doctrine_Core::getTable('JobeetJob')>getForLuceneQuery($query); if ($request->isXmlHttpRequest()) { return $this->renderPartial('job/list', array('jobs' => $this>jobs)); } }

Como jQuery no recarga la pgina, sino slo reemplaza el elemto DOM #jobs con el contenido de la respuesta, la pgina no debe ser redecorada por la layout. Como se trata de una necesidad comn, el layout est desactivado por defecto cuando se presenta una peticin AJAX. Adems, en lugar de devolver la plantilla completa, slo tenemos que devolver el contenido del partial job/list. El mtodo renderPartial() usado en la accin devuelve el partial como la respuesta en lugar de la totalidad de la plantilla. Si el usuario elimina todos los caracteres en el cuadro de bsqueda, o si la bsqueda no devuelve ningn resultado, tenemos que mostrar un mensaje en lugar de una pgina en blanco. Vamos a utilizar el mtodo renderText() para mostrar una simple cadena de prueba:
// apps/frontend/modules/job/actions/actions.class.php public function executeSearch(sfWebRequest $request) { $this->forwardUnless($query = $request->getParameter('query'), 'job', 'index');

$this->jobs = Doctrine_Core::getTable('JobeetJob')>getForLuceneQuery($query); if ($request->isXmlHttpRequest()) { if ('*' == $query || !$this->jobs) { return $this->renderText('No results.'); } return $this->renderPartial('job/list', array('jobs' => $this>jobs)); } } Tambin puedes devolver un componente utilizando el mtodo renderComponent().

Probando AJAX
Como el navegador Symfony no puede simular JavaScript, necesitas ayudarlo cuando pruebas llamadas AJAX. Esto principalmente significa que es necesario aadir manualmente la cabecera jQuery y que todas las dems grandes bibliotecas JavaScript envan con la peticin:
// test/functional/frontend/jobActionsTest.php $browser->setHttpHeader('X_REQUESTED_WITH', 'XMLHttpRequest'); $browser-> info('5 - Live search')-> get('/search?query=sens*')-> with('response')->begin()-> checkElement('table tr', 2)-> end() ;

El mtodo setHttpHeader() pone un header HTTP para la siguiente peticin formulada con el navegador.

Internacionalizacion y localizacion
vamos a hablar de internacionalizacin (o i18n) y localizacin (o l10n). Segn Wikipedia:en:
La Internacionalizacin es un proceso a travs del cual se disean productos de software para que puedan adaptarse a diferentes idiomas y regiones sin necesidad de cambios de ingeniera ni cambios en el cdigo. La Localizacin es el proceso de adaptacin de software para una regin o idioma mediante la incorporacin de componentes especficos de localizacin y traduccin de textos.

Como siempre, el framework symfony no ha reinventado la rueda y su soporte de i18n y l10n esta basado en el ICU standard.

Usuario
La internacionalizacin no es posible sin un usuario. Cuando su sitio web est disponible en varios idiomas o para distintas regiones del mundo, el usuario es el responsable de elegir la que mejor se ajuste a l.
Ya hemos hablado de la clase User de symfony durante el da 13.

La Cultura del Usuario Las caractersticas i18n y l10n de symfony se basan en la cultura del usuario. La cultura es la combinacin del lenguaje y el pas del usuario. Por ejemplo, la cultura para un usuario que habla francs es fr y la cultura para un usuario de Francia es fr_FR. Puedes manejar la cultura por el usuario llamando a los mtodos setCulture() y getCulture() del objeto User:
// in an action $this->getUser()->setCulture('fr_BE'); echo $this->getUser()->getCulture(); El lenguaje est codificado en dos minsculas, de acuerdo con la ISO 639-1 standard, y el pas est codificado con dos caracteres en mayscula, de acuerdo con la ISO 3166-1 standard.

La Preferencia de Cultura Por defecto, la cultura del usuario es la configurada en el archivo de configuracin settings.yml:
# apps/frontend/config/settings.yml all: .settings: default_culture: it_IT

Como la cultura es administrada por el objeto User, se almacena en la sesin del usuario. Durante el desarrollo, si cambias la cultura por defecto, tendrs que limpiar tus cookies de sesin para que el nuevo valor tenga efecto en tu navegador.

Cuando un usuario inicia una sesin en el sitio web Jobeet, tambin podemos determinar la mejor cultura, sobre la base de la informacin proporcionada por la cabecera HTTP Accept-Language. El mtodo getLanguages() del objeto de la peticin devuelve un array de los idiomas aceptados para el usuario actual, ordenados por orden de preferencia:
// in an action $languages = $request->getLanguages();

Pero la mayor parte del tiempo, tu sitio web no estar disponible en los 136 principales idiomas. El mtodo getPreferredCulture() devuelve el mejor lenguaje mediante la comparacin de los idiomas preferidos del usuario y los idiomas de tu sitio web:
// in an action $language = $request->getPreferredCulture(array('en', 'fr'));

En la anterior llamada, el lenguaje devuelto ser Ingls o Francs de acuerdo con los idiomas preferidos del usuario, o Ingls (primer idioma en el array) si no coincide ninguno.

La Cultura en la URL
El sitio web Jobeet estar disponible en Ingls y francs. Como una direccin URL slo puede representar a un nico recurso, la cultura debe estar integrada en la URL. Para ello, abre el archivo routing.yml, y agrega la variable especial :sf_culture para todas las rutas, pero no para api_jobs yhomepage. Para simples rutas, agrega /:sf_culture al principio de la url. Para coleccin de rutas, agrega una opcin prefix_path que comience con/:sf_culture.
# apps/frontend/config/routing.yml affiliate: class: sfDoctrineRouteCollection options: model: JobeetAffiliate actions: [new, create] object_actions: { wait: get } prefix_path: /:sf_culture/affiliate category: url: /:sf_culture/category/:slug.:sf_format class: sfDoctrineRoute param: { module: category, action: show, sf_format: html } options: { model: JobeetCategory, type: object } requirements: sf_format: (?:html|atom)

job_search: url: /:sf_culture/search param: { module: job, action: search } job: class: sfDoctrineRouteCollection options: model: JobeetJob column: token object_actions: { publish: put, extend: put } prefix_path: /:sf_culture/job requirements: token: \w+ job_show_user: url: /:sf_culture/job/:company_slug/:location_slug/:id/:position_slug class: sfDoctrineRoute options: model: JobeetJob type: object method_for_query: retrieveActiveJob param: { module: job, action: show } requirements: id: \d+ sf_method: get

Cuando la variable sf_culture se utiliza en una ruta, symfony automticamente usa su valor para cambiar la cultura del usuario. Como necesitamos muchas pginas de inicio como idiomas soportemos (/en/, /fr/, ...), la pgina de inicio predeterminada (/) deben redirijirnos a la pgina apropiada, de acuerdo con la cultura del usuario. Pero si el usuario no tiene todava una cultura, porque l viene a Jobeet por primera vez, la mejor cultura sern elegidos para l. En primer lugar, aade el mtodo isFirstRequest() a myUser. Devuelve true slo para la primer peticin de una sesin de usuario:
// apps/frontend/lib/myUser.class.php public function isFirstRequest($boolean = null) { if (is_null($boolean)) { return $this->getAttribute('first_request', true); } $this->setAttribute('first_request', $boolean); }

Agrega una ruta localized_homepage:


# apps/frontend/config/routing.yml

localized_homepage: url: /:sf_culture/ param: { module: job, action: index } requirements: sf_culture: (?:fr|en)

Cambia la accin index del mdulo job para aplicar la lgica para redirigir al usuario a la "mejor" pgina de inicio la primer peticin de una sesin:
// apps/frontend/modules/job/actions/actions.class.php public function executeIndex(sfWebRequest $request) { if (!$request->getParameter('sf_culture')) { if ($this->getUser()->isFirstRequest()) { $culture = $request->getPreferredCulture(array('en', 'fr')); $this->getUser()->setCulture($culture); $this->getUser()->isFirstRequest(false); } else { $culture = $this->getUser()->getCulture(); } $this->redirect('localized_homepage'); } $this->categories = Doctrine_Core::getTable('JobeetCategory')>getWithJobs(); }

Si la variable sf_culture no est presente en la peticin, esto significa que el usuario tiene que ir a la URL /. Si este es el caso y la sesin es nueva, la cultura preferida es usada como la cultura del usuario. De lo contrario, se utiliza la cultura actual del usuario. El ltimo paso es redirigir al usuario a la URL localized_homepage. Nota que la variable sf_culture no ha sido pasada en la redireccin ya que symfony la agrega automticamente por t. Ahora, si tratas de ir a la URL /it/, symfony devolver un error 404 ya que restringimos la variable sf_culture a en, o fr. Agrega este requisito para todas las rutas que incluyan la cultura:
requirements: sf_culture: (?:fr|en)

Probando la Cultura
Es hora de poner a prueba nuestra aplicacin. Pero antes de aadir ms pruebas, tenemos que arreglar los ya existentes. Como han cambiado todas las direcciones

URL, edita los archivos de todas las prueba funcionales en test/functional/frontend/ y agrega /en al principio de todas las URLs. No olvides de cambiar las URLs en el archivo lib/test/JobeetTestFunctional.class.php. Poner en marcha el conjunto de pruebas para comprobar que has arreglado correctamente las pruebas:
$ php symfony test:functional frontend

El user tester da un mtodo isCulture() que prueba la cultura del usuario actual. Abre el archivo jobActionsTest y aade las siguientes pruebas:
// test/functional/frontend/jobActionsTest.php $browser->setHttpHeader('ACCEPT_LANGUAGE', 'fr_FR,fr,en;q=0.7'); $browser-> info('6 - User culture')-> restart()-> info(' 6.1 - For the first request, symfony guesses the best culture')-> get('/')-> with('response')->isRedirected()-> followRedirect()-> with('user')->isCulture('fr')-> info(' 6.2 - Available cultures are en and fr')-> get('/it/')-> with('response')->isStatusCode(404) ; $browser->setHttpHeader('ACCEPT_LANGUAGE', 'en,fr;q=0.7'); $browser-> info(' 6.3 - The culture guessing is only for the first request')-> get('/')-> with('response')->isRedirected()-> followRedirect()-> with('user')->isCulture('fr') ;

Cambiando de idioma
Para que el usuario pueda cambiar la cultura, un formulario de idioma hay que aadir en el layout. El framework de formularios no proporciona una formulario de fabrica pero como la necesidad es muy comn para los sitios web internacionalizados, el symfony core team mantiene elsfFormExtraPlugin, que contiene los validadores, widgets, y formularios que no pueden ser incluidos con el paquete principal symfony ya que son demasiado especficas o tienen dependencias externas, pero no obstante son muy til. Instala el plugin con la tarea plugin:install:

$ php symfony plugin:install sfFormExtraPlugin

Or via Subversion with the following command:


$ svn co http://svn.symfonyproject.org/plugins/sfFormExtraPlugin/branches/1.3/ plugins/sfFormExtraPlugin

In order for plugin's classes to be loaded, the sfFormExtraPlugin plugin must be activated in the config/ProjectConfiguration.class.php file as shown below:
// config/ProjectConfiguration.class.php public function setup() { $this->enablePlugins(array( 'sfDoctrinePlugin', 'sfDoctrineGuardPlugin', 'sfFormExtraPlugin' )); } El sfFormExtraPlugin tiene widgets que requieran dependencias externas como bibliotecas JavaScript. Encontrars un widget para seleccionar fechas, un para un editor WYSIWYG, y mucho ms. Tma un tiempo para leer la documentacin ya que encontrars un montn de cosas tiles.

El plugin sfFormExtraPlugin da un formulario sfFormLanguage para gestionar la seleccin de idioma. Aadiendo el formulario de idiomas se puede hacer en el layout as:
El cdigo a continuacin no pretende ser aplicado. Es aqu que te mostramos cmo podras tener la tentacin de aplicar algo de forma equivocada. Vamos a mostrarte cmo aplicarlo correctamente utilizando symfony. // apps/frontend/templates/layout.php <div id="footer"> <div class="content"> <!-- footer content --> <?php $form = new sfFormLanguage( $sf_user, array('languages' => array('en', 'fr')) ) ?> <form action="<?php echo url_for('change_language') ?>"> <?php echo $form ?><input type="submit" value="ok" /> </form> </div> </div>

Detectas el problema? As es, la creacin de un objeto form no pertenece a la capa de la Vista. Debe ser creado en una accin. Pero como el cdigo est en el layout, el formulario debe crearse para cada accin, que est lejos de ser prctico.

En tales casos, debes usar un componente. Un componente es como un partial pero con algo de cdigo en l. Consideralo una accin ligera. Incluyendo un componente en una plantilla se puede hacer mediante el uso del helper include_component():
// apps/frontend/templates/layout.php <div id="footer"> <div class="content"> <!-- footer content --> <?php include_component('language', 'language') ?> </div> </div>

El helper toma el mdulo y la accin como argumentos. El tercer argumento se puede utilizar para pasar parmetros a los componentes. Crea un mdulo language para alojar el componente y la accin que realmente cambiar el idioma del usuario:
$ php symfony generate:module frontend language

Los Componentes se definirn en el archivo actions/components.class.php. Crear este archivo ahora:


// apps/frontend/modules/language/actions/components.class.php class languageComponents extends sfComponents { public function executeLanguage(sfWebRequest $request) { $this->form = new sfFormLanguage( $this->getUser(), array('languages' => array('en', 'fr')) ); } }

Como puedes ver, una clase de componentes es muy similar a una clase de acciones. La plantilla para un componente utiliza la misma convencin de nombres como lo hace un partial: un guin bajo (_) seguido por el nombre del componente:
// apps/frontend/modules/language/templates/_language.php <form action="<?php echo url_for('change_language') ?>"> <?php echo $form ?><input type="submit" value="ok" /> </form>

Como el plugin no proporciona la accin que en realidad cambia la cultura del usuario, edita el archivo routing.yml para crear la rutachange_language:
# apps/frontend/config/routing.yml change_language: url: /change_language

param: { module: language, action: changeLanguage }

Y crea la accin correspondiente:


// apps/frontend/modules/language/actions/actions.class.php class languageActions extends sfActions { public function executeChangeLanguage(sfWebRequest $request) { $form = new sfFormLanguage( $this->getUser(), array('languages' => array('en', 'fr')) ); $form->process($request); return $this->redirect('localized_homepage'); } }

El mtodo process() de sfFormLanguage se encarga de cambiar la cultura del usuario, basado en el formulario envado por el usuario.

Internacionalizacin
Idiomas, Caracteres, y Codificacin Diferentes idiomas tienen diferentes conjuntos de caracteres. El Ingls es el idioma ms simple ya que slo usa los caracteres ASCII, el idioma francs es un poco ms complejo, con caracteres acentuados como "", y las lenguas como el ruso, chino o rabe son mucho ms complejos que todos sus caracteres ya que estn fuera del rango ASCII. Esos idiomas se definen con diferentes conjuntos de caracteres. Cuando se trate de datos internacionalizado, es mejor utilizar la norma Unicode. La idea detrs de Unicode es establecer un conjunto universal de caracteres que contiene todos los caracteres de todos los idiomas. El problema con Unicode es que un solo carcter se puede representar con una cantidad de 21 bits. Por lo tanto, para la web, usamos UTF-8, que mapea el cdigo Unicode apuntandolo a una secuencias de longitud variable de octetos. En UTF-8, la mayora de las lenguas tienen sus caracteres codificados con menos de 3 bits.

UTF-8 es el utilizado por defecto en symfony, y se define en el archivo de configuracin settings.yml:


# apps/frontend/config/settings.yml all: .settings: charset: utf-8

Adems, para habilitar la capa de internacionalizacin de symfony, debes establecer i18n en true dentro de settings.yml:
# apps/frontend/config/settings.yml all: .settings: i18n: true

Plantillas Un sitio web internacionalizado significa que la interfaz de usuario est traducida a varios idiomas. En una plantilla, todas las cadenas que dependen del idioma deben ser envueltas con el helper __() (nota que hay dos guiones bajos). El helper __() es parte del grupo de helpers I18N, que contiene helpers que facilitan la gestin i18n en plantillas. Como este grupo de helper no est cargado por defecto, es necesario agregar manualmente en cada plantilla use_helper('I18N') como ya hizo para el grupo de helper Text, o cargalo a nivel global mediante standard_helpers:
# apps/frontend/config/settings.yml all: .settings: standard_helpers: [Partial, Cache, I18N]

Aqu est cmo usa el helper __() para el pie de pgina de Jobeet:
// apps/frontend/templates/layout.php <div id="footer"> <div class="content"> <span class="symfony"> <img src="/images/jobeet-mini.png" /> powered by <a href="http://www.symfony-project.org/"> <img src="/images/symfony.gif" alt="symfony framework" /></a> </span> <ul> <li> <a href=""><?php echo __('About Jobeet') ?></a> </li> <li class="feed"> <?php echo link_to(__('Full feed'), 'job', array('sf_format' => 'atom')) ?> </li> <li>

<a href=""><?php echo __('Jobeet API') ?></a> </li> <li class="last"> <?php echo link_to(__('Become an affiliate'), 'affiliate_new') ?> </li> </ul> <?php include_component('language', 'language') ?> </div> </div> El helper __() puede tomar la cadena para el idioma por defecto o se puede utilizar tambin un identificador nico para cada cadena. Es slo una cuestin de gusto. Para Jobeet, haremos uso de la antigua estrategia para tener plantillas ms legibles.

Cuando symfony muestra una plantilla, cada vez que el helper __() es llamado, symfony busca por una traduccin para la cultura del usuario actual. Si se encuentra una traduccin, se utiliza, si no, el primer argumento se devuelve como un valor fallback. Todas las traducciones se almacenan en un catlogo. El framework i18n proporciona una gran cantidad de estrategias diferentes para almacenar las traducciones. Vamos a utilizar el formato "XLIFF", que es un estndar y el ms flexible. Tambin es el utilizado por el admin generator y dems symfony plugins.
Oros Catlogos son gettext, MySQL, y SQLite. Como siempre, echa una mirada a la i18n API para ms detalles. i18n:extract

En lugar de crear el catlogo de archivos a mano, utiliza la tarea de serie i18n:extract:


$ php symfony i18n:extract frontend fr --auto-save

La tarea i18n:extract encuentra todas las cadenas que deben traducirse en fr en la aplicacin frontend y crea o actualiza el correspondiente catlogo. La opcin -auto-save guarda las nuevas cadenas de en el catlogo. Tambin puedes utilizar la opcin --auto-delete para eliminar automticamente las cadenas que ya no existen. En nuestro caso, rellena el archivo que hemos creado:
<!-- apps/frontend/i18n/fr/messages.xml --> <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE xliff PUBLIC "-//XLIFF//DTD XLIFF//EN" "http://www.oasis-open.org/committees/xliff/documents/xliff.dtd"> <xliff version="1.0"> <file source-language="EN" target-language="fr" datatype="plaintext" original="messages" date="2008-12-14T12:11:22Z" product-name="messages"> <header/> <body> <trans-unit id="1">

<source>About Jobeet</source> <target/> </trans-unit> <trans-unit id="2"> <source>Feed</source> <target/> </trans-unit> <trans-unit id="3"> <source>Jobeet API</source> <target/> </trans-unit> <trans-unit id="4"> <source>Become an affiliate</source> <target/> </trans-unit> </body> </file> </xliff>

Cada traduccin es administrada por una etiqueta trans-unit que tiene un nico atributo id. Ahora puedes editar este archivo y aadir las traducciones de la lengua francesa:
<!-- apps/frontend/i18n/fr/messages.xml --> <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE xliff PUBLIC "-//XLIFF//DTD XLIFF//EN" "http://www.oasis-open.org/committees/xliff/documents/xliff.dtd"> <xliff version="1.0"> <file source-language="EN" target-language="fr" datatype="plaintext" original="messages" date="2008-12-14T12:11:22Z" product-name="messages"> <header/> <body> <trans-unit id="1"> <source>About Jobeet</source> <target>A propos de Jobeet</target> </trans-unit> <trans-unit id="2"> <source>Feed</source> <target>Fil RSS</target> </trans-unit> <trans-unit id="3"> <source>Jobeet API</source> <target>API Jobeet</target> </trans-unit> <trans-unit id="4"> <source>Become an affiliate</source> <target>Devenir un affili</target> </trans-unit> </body> </file>

</xliff> Como XLIFF es un formato estndar, una gran cantidad de herramientas existentes facilitan el proceso de traduccin. Open Language Tools es un proyecto Java OpenSource con un editor integrado XLIFF. Como XLIFF es un formato de archivo, la misma prioridad y la lgica de las normas que existen para otros archivos de configuracin de symfony son tambin aplicables. Los archivos I18n puede existir en un proyecto, una aplicacin o un mdulo, y los ms especficos archivos sobreescriben las traducciones que se encuentran en la ms global.

Traducciones con Argumentos El principio fundamental detrs de la internacionalizacin es traducir frases. Sin embargo, algunas frases incluyen valores dinmicos. En Jobeet, este es el caso en la pgina de inicio para el enlace "and X more...":
<!-- apps/frontend/modules/job/templates/indexSuccess.php --> <div class="more_jobs"> and <?php echo link_to($count, 'category', $category) ?> more... </div>

El nmero de puestos de trabajo es una variable que debe ser utilizada para la traduccin:
<!-- apps/frontend/modules/job/templates/indexSuccess.php --> <div class="more_jobs"> <?php echo __('and %count% more...', array('%count%' => link_to($count, 'category', $category))) ?> </div>

La cadena a traducir es ahora "and %count% more...", y el %count% es la variable que ser sustituido por el nmero real en tiempo de ejecucin, gracias a el valor dado como segundo argumento al helper __(). Aadir la nueva cadena manualmente insertando una etiqueta trans-unit en el archivo messages.xml, o usa la tarea i18n:extract para actualizar automticamente el archivo:
$ php symfony i18n:extract frontend fr --auto-save

Despus de ejecutar la tarea, abre el archivo XLIFF para aadir la traduccin al francs:
<trans-unit id="6"> <source>and %count% more...</source> <target>et %count% autres...</target> </trans-unit>

El nico requisito en la traducin de la cadena es utilizar el contenedor/variable %count% en algn lugar.

Algunas otras cadenas son an ms complejas ya que implican plurales. Segn algunoss nmeros, la frases cambian, pero no necesariamente del mismo modo para todos los idiomas. Algunos idiomas tienen reglas gramaticales muy complejas para los plurales, como el Polaco o el Ruso. En la pgina de categora, el nmero de puestos de trabajo en la categora actual se muestra:
<!-- apps/frontend/modules/category/templates/showSuccess.php --> <strong><?php echo count($pager) ?></strong> jobs in this category

Cuando una oracin tiene diferentes traducciones de acuerdo con un nmero, el helper format_number_choice() debe utilizarse:
<?php echo format_number_choice( '[0]No job in this category|[1]One job in this category|(1,+Inf]%count% jobs in this category', array('%count%' => '<strong>'.count($pager).'</strong>'), count($pager) ) ?>

El helper format_number_choice() tiene tres argumentos:


La cadena a utilizar en funcin del nmero Un array de variables a reemplazar El nmero a usar que determina qu texto usar

La cadena que describe las diferentes traducciones de acuerdo con el nmero tiene un formato de la siguiente manera:

Cada posibilidad est separado por un carcter barra vertical (|) Cada cadena se compone de un rango seguida de la traduccin

El rango puede describirse con cualquier serie de nmeros:


[1,2]: Acepta valores entre 1 y 2, inclusive (1,2): Acepta valores entre 1 y 2, con exclusin de 1 y 2 {1,2,3,4}: Slo los valores definidos en el juego son aceptadas [-Inf,0): Acepta los valores mayores o iguales a menos infinito y

estrictamente inferior a 0
{n: n % 10 > 1 && n % 10 < 5}: Coincide con los nmeros 2, 3, 4, 22, 23,

24 Traducir la cadena es similar a otras cadenas de mensajes:


<trans-unit id="7"> <source>[0]No job in this category|[1]One job in this category|(1,+Inf]%count% jobs in this category</source> <target>[0]Aucune annonce dans cette catgorie|[1]Une annonce dans cette catgorie|(1,+Inf]%count% annonces dans cette catgorie</target> </trans-unit>

Ahora que sabes cmo internacionalizar todo tipo de cadenas, tomate un tiempo para agregar una llamada al __() para todas las plantillas de la aplicacin frontend. No vamos a internacionalizar la aplicacin backend. Formularios Las clases form contienen muchas cadenas que deben ser traducidas, como etiquetas, mensajes de error y mensajes de ayuda. Todas estas cadenas son automticamente internacionalizadas por symfony, por lo que slo tendr que proporcionar las traducciones en los archivos XLIFF.
Lamentablemente, la tarea i18n:extract an no analiza las clases form para cadenas sin traducir.

Objetos Doctrine Por el sitio web Jobeet, no internacionalizaremos todas las tablas porque no tiene sentido pedir a los usuarios que envan puestos que lo hagan junto con las traducciones en todos los idiomas disponibles. Sin embargo, la tabla category definitivamente debe traducirse. El plugin Doctrine da soporte a tablas i18n en forma nativa. Para cada tabla que contiene datos localizados, dos tablas deben crearse: una para las columnas que sean i18n-independent, y la otra para las columnas que deben ser internacionalizadas. Las dos tablas estn vinculadas por una relacin de uno-amuchos. Actualiza el schema.yml como sigue:
# config/doctrine/schema.yml JobeetCategory: actAs: Timestampable: ~ I18n: fields: [name] actAs: Sluggable: { fields: [name], uniqueBy: [lang, name] } columns: name: { type: string(255), notnull: true }

Al encender el comportamiento I18n, un modelo llamado JobeetCategoryTranslation se crear automticamente y los especificos campos se trasladarn a ese modelo. Nota que simplemente volvimos sobre el comportamiento I18n y movimos el comportamiento Sluggable para ser adjuntado al modeloJobeetCategoryTranslation que se crea automticamente. La opcin uniqueBy le dice al comportamiento Sluggable campos que determinan si una slug es nico o no. En este caso, cada slug debe ser nico para cada par lang y name. Y actualiza los archivos de datos para las categoras:

# data/fixtures/categories.yml JobeetCategory: design: Translation: en: name: Design fr: name: design programming: Translation: en: name: Programming fr: name: Programmation manager: Translation: en: name: Manager fr: name: Manager administrator: Translation: en: name: Administrator fr: name: Administrateur

Tambin tenemos que sobreescribir el mtodo findOneBySlug() en JobeetCategoryTable. Desde que Doctrine da algo de buscadores mgicos para todas las columnas en un modelo, simplemente tenemos que crear el mtodo findOneBySlug() para que por defecto anular la magia de la funcionalidad que Doctrine proporciona. Tenemos que hacer algunos cambios a fin de que la categora es recuperada sobre la base del slug Ingls en la tabla JobeetCategoryTranslation.
// lib/model/doctrine/JobeetCategoryTable.cass.php public function findOneBySlug($slug) { $q = $this->createQuery('a') ->leftJoin('a.Translation t') ->andWhere('t.lang = ?', 'en') ->andWhere('t.slug = ?', $slug); return $q->fetchOne(); }

Reconstruye el modelo:
$ php symfony doctrine:build --all --and-load --no-confirmation $ php symfony cc Como doctrine:build --all --and-load remueve todas las tablas y los datos de la base de datos, no olvides de volver a crear un usuario para acceder al Jobeet backend

con la tarea guard:create-user. Si lo prefieres, puedes aadir un archivo de datos para aadirlo automticamente.

Cuando usamos el comportamieto I18n, proxis son creados entre el objeto JobeetCategory y el objeto JobeetCategoryTranslation de modo que todas las antiguas funciones de recuperacin por el nombre de la categora seguir trabajando y podrs recuperar el valor para la cultura actual.
$category = new JobeetCategory(); $category->setName('foo'); // sets the name for the current culture $category->getName(); // gets the name for the current culture $this->getUser()->setCulture('fr'); // from your actions class $category->setName('foo'); // sets the name for French echo $category->getName(); // gets the name for French Para reducir el nmero de solicitudes a la bases de datos, haz el join de JobeetCategoryTranslation en tus queries. Esto te traer el objeto principal y el i18n en una peticin. $categories = Doctrine_Query::create() ->from('JobeetCategory c') ->leftJoin('c.Translation t WITH t.lang = ?', $culture) ->execute(); El WITH anterior agrega una condicin para automticamente agregar un ON al query. LEFT JOIN c.Translation t ON c.id = t.id AND t.lang = ?

Como la ruta category es apunta a la modelo de clase JobeetCategory porque el slug es ahora parte de JobeetCategoryTranslation, la ruta no est disponible para traer el objeto Category automticamente. Para ayudar al routing, vamos a crear un mtodo que se encargar de la recuperacin del objeto: Puesto que ya sobreescribimos el findOneBySlug() vamos a refactorizar un poco ms estos mtodos para que pueden ser compartidos. Vamos a crear un nuevos mtodos findOneBySlugAndCulture() y doSelectForSlug() y cambiar el findOneBySlug() para simplemente usar elfindOneBySlugAndCulture().
// lib/model/doctrine/JobeetCategoryTable.class.php public function doSelectForSlug($parameters) { return $this->findOneBySlugAndCulture($parameters['slug'], $parameters['sf_culture']); } public function findOneBySlugAndCulture($slug, $culture = 'en') { $q = $this->createQuery('a') ->leftJoin('a.Translation t') ->andWhere('t.lang = ?', $culture) ->andWhere('t.slug = ?', $slug);

return $q->fetchOne(); } public function findOneBySlug($slug) { return $this->findOneBySlugAndCulture($slug, 'en'); }

A continuacin, utiliza la opcin method para decirle a la ruta category que use el mtodo doSelectForSlug() para recuperar el objeto:
# apps/frontend/config/routing.yml category: url: /:sf_culture/category/:slug.:sf_format class: sfDoctrineRoute param: { module: category, action: show, sf_format: html } options: { model: JobeetCategory, type: object, method: doSelectForSlug } requirements: sf_format: (?:html|atom)

Necsesitamos recargar los datos para regenerar los slugs correctos para las categoraes:
$ php symfony doctrine:data-load

Ahora la ruta category se encuentra internacionalizado y la URL de una categora incluye las traducciones del slug:
/frontend_dev.php/fr/category/programmation /frontend_dev.php/en/category/programming

Admin Generador Para el backend, queremos que las traducciones de el francs y el Ingls sean editadas en el mismo formulario:

Incluir un formulario i18n se puede hacer mediante el uso del mtodo embedI18N():
// lib/form/JobeetCategoryForm.class.php class JobeetCategoryForm extends BaseJobeetCategoryForm { public function configure() { unset( $this['jobeet_affiliates_list'], $this['created_at'], $this['updated_at'] ); $this->embedI18n(array('en', 'fr')); $this->widgetSchema->setLabel('en', 'English'); $this->widgetSchema->setLabel('fr', 'French'); } }

La interfaz del admin generator soporta internacionalizacin de fabrica. Viene con traducciones a ms de 20 idiomas, y es muy fcil de aadir uno nuevo, o para personalizar una existente. Copie el archivo para el idioma que desea personalizar de symfony (las traducciones admin se encuentran en lib/vendor/symfony/lib/plugins/sfDoctrinePlugin/i18n/) de la aplicacin en el dir i18n. Como el archivo en tu aplicacin se fusionar con el de symfony, mantiene slo las cadenas modificadas en el archivo de la aplicacin. Notars que los traducciones del admin generator se nombran como sf_admin.fr.xml, en lugar de fr/messages.xml. Como cuestin de hecho,messages es el nombre del catlogo por defecto usado por Symfony, y que puede ser modificado para permitir una mejor separacin entre las distintas partes de tu aplicacin. Usar un catlogo que no sea el predeterminado require que lo especifique cuando usas el helper __():
<?php echo __('About Jobeet', array(), 'jobeet') ?>

En el anterior cdigo __(), symfony buscar por la cadena "About Jobeet" en el Catlogo jobeet. Tests Las pruebas es una parte integrante de la migracin de internacionalizacin. En primer lugar, actualiza los archivos de datos para pruebas de las categoras copiando los archivos de datos que teniamos definidos antes en test/fixtures/categories.yml.

Don't forget to update methods in the lib/test/JobeetTestFunctional.class.php file in order to care of our modifications concerning theJobeetCategory's internationalization.
public function getMostRecentProgrammingJob() { $q = Doctrine_Query::create() ->select('j.*') ->from('JobeetJob j') ->leftJoin('j.JobeetCategory c') ->leftJoin('c.Translation t') ->where('t.slug = ?', 'programming'); $q = Doctrine_Core::getTable('JobeetJob')->addActiveJobsQuery($q); return $q->fetchOne(); }

Reconstruir el modelo para el entorno test:


$ php symfony doctrine:build --all --and-load --env=test

Ahora puedes lanzar todas las pruebas para comprobar que estn funcionando bien:
$ php symfony test:all Cuando hemos desarrollado la interfaz de backend para Jobeet, no hemos escrito pruebas funcionales. Pero cada vez que creas un mdulo con el comando de linea symfony symfony tambin generan las pruebas. Ests son seguras para eliminarlas.

Localizacin
Plantillas Soportando diferentes culturas tambin significa soportar a las diferentes manera de formatear fechas y nmeros. En una plantilla, varios helpers estn a tut disposicin para ayudar a tomar en cuenta todas estas diferencias, basado en la actual cultura del usuario: En el grupo de helper Date : Helper
format_date() format_datetime() time_ago_in_words()

Descripcin Formatos de fecha Formatos de fecha Muestra el tiempo transcurrido entre una fecha y ahora en palabras

Helper

Descripcin

distance_of_time_in_words() Muestra el tiempo transcurrido entre dos fechas en

palabras
format_daterange()

Formatos de un rango de fechas

En el grupo de helper Number : Helper


format_number()

Descripcin Formatos un nmero

format_currency() Formatos de moneda

En el grupo de helper I18N : Helper


format_country()

Descripcin Muestra el nombre de un pas

format_language() Muestra el nombre de un idioma

Formularios El framework de formualrios da varios widgets y los validadores para datos localizados:
sfWidgetFormI18nDate sfWidgetFormI18nDateTime sfWidgetFormI18nTime sfWidgetFormI18nChoiceCountry sfWidgetFormI18nChoiceCurrency sfWidgetFormI18nChoiceLanguage sfWidgetFormI18nChoiceTimezone sfValidatorI18nChoiceCountry sfValidatorI18nChoiceLanguage sfValidatorI18nChoiceTimezone

Los Plugins
Un Plugin Symfony Un plugin ofrece una manera de empaquetar y distribuir un conjunto de archivos del proyecto. Al igual que un proyecto, un plugin puede tener clases, helpers, configuraciones, tareas, mdulos, esquemas, e incluso recursos Web (CSS, JavaScript, etc.). Plugins Privados El primer uso de los plugins es facilitar el intercambio de cdigo entre aplicaciones, o incluso entre distintos proyectos. Recuerda que las aplicaciones symfony slo comparten el modelo. Los Plugins dan una manera de compartir ms componentes entre aplicaciones. Si necesitas volver a utilizar el mismo esquema para los diferentes proyectos o los mismos mdulos, colcalos en un plugin. Como un plugin es solo un directorio, puedes moverlo con bastante facilidad mediante la creacin de un repositorio SVN y el uso de svn:externals, o con slo copiar los archivos de un proyecto a otro. Nosotros llamamos a estos "plugins privados" porque su uso est restringido a una sola empresa o a un solo desarrollador. No estn a disposicin del pblico.
Puedes incluso crear un paquete de tus plugins privados, crear tu propio symfony plugin channel, e instalarlos via la tarea plugin:install.

Plugins Pblicos Los Public Plugins estn disponibles para la comunidad para descargar e instalar. Durante este tutorial, tenemos que usar un par de plugins pblicos: sfDoctrineGuardPlugin y sfFormExtraPlugin. Son exactamente los mismos que los plugins privados. La nica diferencia es que cualquiera puede instalar para sus proyectos. Aprenders ms adelante sobre la manera de publicar y alojar uno pblico en el sitio web de plugins de Symfony. Una Forma Diferente de Organizacin del Cdigo Hay una manera ms de pensar en plugins y usarlos. Olvdate de la reutilizacin y el intercambio. Los Plugins se puede utilizar como una manera diferente para organizar el cdigo. En lugar de organizar los archivos por capas: todos los modelos en lib/model/, las plantillas en templates/, ...; los archivos estn juntos por su caracterstica: todos los archivos job juntos (el modelo, mdulos y plantillas), todos los archivos CMS juntos, y as.

Estructura de Archivos de un Plugin


Un plugin es slo una estructura de directorios con los archivos organizados en una estructura previamente definida, segn la naturaleza de los archivos. Hoy,

pasaremos la mayor parte del cdigo que hemos escrito para Jobeet en un sfJobeetPlugin. El layout bsico que se utilizar es el siguiente:
sfJobeetPlugin/ config/ sfJobeetPluginConfiguration.class.php routing.yml doctrine/ schema.yml lib/ Jobeet.class.php helper/ filter/ form/ model/ task/ modules/ job/ actions/ config/ templates/ web/ images

// Plugin initialization // Routing // Database schema // // // // // // Classes Helpers Filter classes Form classes Model classes Tasks

// Modules

// Assets like JS, CSS, and

El Plugin Jobeet
Inicializar un plugin es tan sencillo como crear un nuevo directorio bajo el directorio plugins/. Para Jobeet, vamos a crear un directoriosfJobeetPlugin:
$ mkdir plugins/sfJobeetPlugin

Then, activate the sfJobeetPlugin in config/ProjectConfiguration.class.php file.


public function setup() { $this->enablePlugins(array( 'sfDoctrinePlugin', 'sfDoctrineGuardPlugin', 'sfFormExtraPlugin', 'sfJobeetPlugin' )); } Todos los plugins deben terminar con Plugin. Tambin es una buena costumbre usar el prefijo sf, aunque no es obligatorio.

El Modelo Primero, mueve el archivo config/doctrine/schema.yml a plugins/sfJobeetPlugin/config/:


$ mkdir plugins/sfJobeetPlugin/config/

$ mkdir plugins/sfJobeetPlugin/config/doctrine $ mv config/doctrine/schema.yml plugins/sfJobeetPlugin/config/doctrine/schema.yml Todos los comandos son para ambientes Unix. Si usas Windows, puedes arrastrar y soltar los archivos en el Explorador de Windows. Y si utilizas Subversion, o cualquier otra herramienta para la gestin de tu cdigo, usa las herramientas incorporadas que ofrecen (como svn mv para mover archivos).

Mueve el modelo, formulario, filtros a plugins/sfJobeetPlugin/lib/:


$ $ $ $ mkdir plugins/sfJobeetPlugin/lib/ mv lib/model/ plugins/sfJobeetPlugin/lib/ mv lib/form/ plugins/sfJobeetPlugin/lib/ mv lib/filter/ plugins/sfJobeetPlugin/lib/

$ rm -rf plugins/sfJobeetPlugin/lib/model/doctrine/sfDoctrineGuardPlugin $ rm -rf plugins/sfJobeetPlugin/lib/form/doctrine/sfDoctrineGuardPlugin $ rm -rf plugins/sfJobeetPlugin/lib/filter/doctrine/sfDoctrineGuardPlugin

Remove the plugins/sfJobeetPlugin/lib/form/BaseForm.class.php file.


$ rm plugins/sfJobeetPlugin/lib/form/BaseForm.class.php

Despus de mover los modelos, formularios y filtros las clases deben ser renombradas, hacerlas abstractas y con el prefijo Plugin.
Solo usa el prefijo Plugin en las clases auto-generadas y no en todas las clases. Por ejemplo no uses el prefijo en las clases que haces a mano. Solo las auto-generadas requieren el prefijo.

Aqu est un ejemplo en el que movemos las clases JobeetAffiliate y JobeetAffiliateTable .


$ mv plugins/sfJobeetPlugin/lib/model/doctrine/JobeetAffiliate.class.php plugins/sfJobeetPlugin/lib/model/doctrine/PluginJobeetAffiliate.class.php

Y el cdigo debe ser actualizado:


abstract class PluginJobeetAffiliate extends BaseJobeetAffiliate { public function save(Doctrine_Connection $conn = null) { if (!$this->getToken()) { $this->setToken(sha1($object->getEmail().rand(11111, 99999))); } parent::save($conn); } // ... }

Ahora vamos a pasar la clase JobeetAffiliateTable:


$ mv plugins/sfJobeetPlugin/lib/model/doctrine/JobeetAffiliateTable.class.php plugins/sfJobeetPlugin/lib/model/doctrine/PluginJobeetAffiliateTable.clas s.php

La definicin de la clase debe tener la siguiente apariencia:


abstract class PluginJobeetAffiliateTable extends Doctrine_Table { // ... }

Ahora hacer lo mismo para las clases forms y filter . Cambiar el nombre de ellos para incluir un prefijo con la palabra Plugin. Asegrate de quitar el directorio base en plugins/sfJobeetPlugin/lib/*/doctrine para los directorios form, filter, y model . Ejemplo:
$ rm -rf plugins/sfJobeetPlugin/lib/form/doctrine/base $ rm -rf plugins/sfJobeetPlugin/lib/filter/doctrine/base $ rm -rf plugins/sfJobeetPlugin/lib/model/doctrine/base

Una vez que se ha mudado, cambiado de nombre y se eliminan algunas clases forms, filters y model corre las tarea para la re-construccin de todas las clases.
$ php symfony doctrine:build --all-classes

Ahora te dars cuenta que algunos de los nuevos directorios creados para mantener los modelos creados a partir de el esquema estan incluido en elsfJobeetPlugin en lib/model/doctrine/sfJobeetPlugin. Este directorio contiene los modelos de nivel superior y la base de clases generada por el esquema. Por ejemplo, el modelo JobeetJob tiene ahora esta estructura de clases.
JobeetJob (extiende de PluginJobeetJob) en lib/model/doctrine/sfJobeetPlugin/JobeetJob.class.php: Nivel

superior donde toda la funcionalidad del modlo puede guardarse. Esto es donde puedas agregar y sobreescribir funcionalidades que ya vienen con los modelos del plugin.
PluginJobeetJob (extiende de BaseJobeetJob) en plugins/sfJobeetPlugin/lib/model/doctrine/PluginJobeetJob.class. php: Esta clase contiene todas las funciones especficas del plugin. Puedes

sobreescribir la funcionalidad en esta clasa y la base modificando la claseJobeetJob.


BaseJobeetJob (extiende de sfDoctrineRecord) en lib/model/doctrine/sfJobeetPlugin/base/BaseJobeetJob.class.php:

Clase base que es generada desde el archivo yaml del esquema cada vez que ejecutes doctrine:build --model.
JobeetJobTable (extiende de PluginJobeetJobTable) en lib/model/doctrine/sfJobeetPlugin/JobeetJobTable.class.php: Lo mismo que para la clase JobeetJob excepto que esta e la instancia de Doctrine_Table que obtendrs cuando llames aDoctrine_Core::getTable('JobeetJob'). PluginJobeetJobTable (extiende de Doctrine_Table) en lib/model/doctrine/sfJobeetPlugin/JobeetJobTable.class.php: Esta

clase contiene todas las funciones especficas para la instancia de Doctrine_Table que obtendrs cuando llames aDoctrine_Core::getTable('JobeetJob'). Con esta estructura que has generado tienes la posibilidad de personalizar los modelos de un plugin editando la clase de nivel superior JobeetJob. Puede personalizar el esquema y aadir columnas, aadir relaciones sobreescribiendo los mtodos setTableDefinition() y setUp().
Cuando mueves las clases classes, asegurate de modificar el mtodo configure() al mtodo setup() y que llame a parent::setup(). Debajo hay un ejemplo. abstract class PluginJobeetAffiliateForm extends BaseJobeetAffiliateForm { public function setup() { parent::setup(); } // ... } Tenemos que asegurarnos de que nuestro plugin no tiene clases base para todas los Doctrine forms. Estos archivos son globales para un proyecto y se volvern a generar con la doctrine:build --forms y doctrine:build --filters.

Quitar los archivos del plugin:


$ rm plugins/sfJobeetPlugin/lib/form/doctrine/BaseFormDoctrine.class.php $ rm plugins/sfJobeetPlugin/lib/filter/doctrine/BaseFormFilterDoctrine.class.p hp

Tambin puedes mover el Jobeet.class.php al plugin:


$ mv lib/Jobeet.class.php plugins/sfJobeetPlugin/lib/

Como hemos pasado archivos, borrar la cach:


$ php symfony cc Si usas un acelerador PHP como APC y cosas extraas pasan en este momento, reinicia Apache.

Ahora que todos los archivos de los modelos se han movido al plugin, ejecutar las pruebas para comprobar que todo funciona bien todava:
$ php symfony test:all

Los controladores y las Vistas El siguiente paso lgico es mover los mdulos al plugin:
$ mv apps/frontend/modules plugins/sfJobeetPlugin/

Para evitar colisiones con el nombre del mdulo, siempre es una buena costumbre poner un prefijo a los nombres de los mdulos con el nombre del plugin:
$ mkdir plugins/sfJobeetPlugin/modules/ $ mv apps/frontend/modules/affiliate plugins/sfJobeetPlugin/modules/sfJobeetAffiliate $ mv apps/frontend/modules/api plugins/sfJobeetPlugin/modules/sfJobeetApi $ mv apps/frontend/modules/category plugins/sfJobeetPlugin/modules/sfJobeetCategory $ mv apps/frontend/modules/job plugins/sfJobeetPlugin/modules/sfJobeetJob $ mv apps/frontend/modules/language plugins/sfJobeetPlugin/modules/sfJobeetLanguage

Por cada mdulo, tambin tienes que cambiar el nombre de la clase en todo los archivos actions.class.php y components.class.php (por ejemplo, la clase affiliateActions necesiat ser renombrada a sfJobeetAffiliateActions). Las llamadas include_partial() y include_component() tambin deben ser modificadas en las siguientes plantillas:
sfJobeetAffiliate/templates/_form.php (change affiliate to sfJobeetAf filiate) sfJobeetCategory/templates/showSuccess.atom.php sfJobeetCategory/templates/showSuccess.php sfJobeetJob/templates/indexSuccess.atom.php sfJobeetJob/templates/indexSuccess.php sfJobeetJob/templates/searchSuccess.php sfJobeetJob/templates/showSuccess.php apps/frontend/templates/layout.php

Actualiza las acciones search y delete :


// plugins/sfJobeetPlugin/modules/sfJobeetJob/actions/actions.class.php class sfJobeetJobActions extends sfActions { public function executeSearch(sfWebRequest $request) { $this->forwardUnless($query = $request->getParameter('query'), 'sfJobeetJob', 'index');

$this->jobs = Doctrine_Core::getTable('JobeetJob') >getForLuceneQuery($query); if ($request->isXmlHttpRequest()) { if ('*' == $query || !$this->jobs) { return $this->renderText('No results.'); } return $this->renderPartial('sfJobeetJob/list', array('jobs' => $this->jobs)); } } public function executeDelete(sfWebRequest $request) { $request->checkCSRFProtection(); $jobeet_job = $this->getRoute()->getObject(); $jobeet_job->delete(); $this->redirect('sfJobeetJob/index'); } // ... }

Ahora, modifica el archivo routing.yml para tomar en cuenta estos cambios:


# apps/frontend/config/routing.yml affiliate: class: sfDoctrineRouteCollection options: model: JobeetAffiliate actions: [new, create] object_actions: { wait: GET } prefix_path: /:sf_culture/affiliate module: sfJobeetAffiliate requirements: sf_culture: (?:fr|en) api_jobs: url: /api/:token/jobs.:sf_format class: sfDoctrineRoute param: { module: sfJobeetApi, action: list } options: { model: JobeetJob, type: list, method: getForToken } requirements: sf_format: (?:xml|json|yaml) category:

url: class: param: options: }

/:sf_culture/category/:slug.:sf_format sfDoctrineRoute { module: sfJobeetCategory, action: show, sf_format: html } { model: JobeetCategory, type: object, method: doSelectForSlug

requirements: sf_format: (?:html|atom) sf_culture: (?:fr|en) job_search: url: /:sf_culture/search.:sf_format param: { module: sfJobeetJob, action: search, sf_format: html } requirements: sf_culture: (?:fr|en) job: class: sfDoctrineRouteCollection options: model: JobeetJob column: token object_actions: { publish: PUT, extend: PUT } prefix_path: /:sf_culture/job module: sfJobeetJob requirements: token: \w+ sf_culture: (?:fr|en) job_show_user: url: /:sf_culture/job/:company_slug/:location_slug/:id/:position_slug class: sfDoctrineRoute options: model: JobeetJob type: object method_for_query: retrieveActiveJob param: { module: sfJobeetJob, action: show } requirements: id: \d+ sf_method: GET sf_culture: (?:fr|en) change_language: url: /change_language param: { module: sfJobeetLanguage, action: changeLanguage } localized_homepage: url: /:sf_culture/ param: { module: sfJobeetJob, action: index } requirements: sf_culture: (?:fr|en)

homepage: url: / param: { module: sfJobeetJob, action: index }

Si tratas de navegar por la pgina web Jobeet ahora, tendrs excepciones diciendo que los mdulos no estn habilitados. Como los plugins son compartidos entre todas las aplicaciones en un proyecto, necesitas especficamente habilitar el mdulo que necesitas para una aplicacin determinada en su archivo de configuracin settings.yml:
# apps/frontend/config/settings.yml all: .settings: enabled_modules: - default - sfJobeetAffiliate - sfJobeetApi - sfJobeetCategory - sfJobeetJob - sfJobeetLanguage

El ltimo paso de la migracin es arreglar las pruebas funcionales donde probamos por el nombre del mdulo. Las Tareas Las Tareas puede ser trasladadas al plugin con bastante facilidad:
$ mv lib/task plugins/sfJobeetPlugin/lib/

Los Archivos i18n Un plugin puede tener Archivos XLIFF:


$ mv apps/frontend/i18n plugins/sfJobeetPlugin/

Las Rutas Un plugin tambin puede contener reglas de enrutamiento:


$ mv apps/frontend/config/routing.yml plugins/sfJobeetPlugin/config/

Los Recursos Incluso si es un poco contra-intuitivo, un plugin tambin puede contener Recursos web como imgenes, hojas de estilo, y JavaScripts. Como no queremos distribuir el Jobeet plugin, en realidad no tiene sentido, pero es posible mediante la creacin de un directorioplugins/sfJobeetPlugin/web/. Un recurso del plugin debe ser accesible en el directorio web/ del proyecto para ser visibles desde un navegador. La tarea plugin:publish-assets se encarga de ello creando enlaces simblicos en plataformas Unix y copiando los archivos en plataformas Windows:
$ php symfony plugin:publish-assets

El Usuario Moviendo los mtodos de las clase myUser que tratan con los historiales es un poco ms implicado. Se podra crear una clase JobeetUser y hacer que myUser herede de ella. Pero hay una forma mejor, sobre todo si varios plugins desean agregar nuevos mtodos a la clase. Los objetos del Nuclo de Symfony notifican eventos durante su ciclo de vida para que se puedan escuchar. En nuestro caso, tenemos que escuchar al evento user.method_not_found, que ocurre cuando un mtodo indefinido se llama en el objeto sfUser. Cuando Symfony es inicializado, todos los plugins tambin se inicializan si tienen una clase de configuracin plugin:
// plugins/sfJobeetPlugin/config/sfJobeetPluginConfiguration.class.php class sfJobeetPluginConfiguration extends sfPluginConfiguration { public function initialize() { $this->dispatcher->connect('user.method_not_found', array('JobeetUser', 'methodNotFound')); } }

Las notificaciones de los Eventos son gestionados por (sfEventDispatcher), el objeto event dispatcher. Registrar un listener es tan simple como llamar a connect(). El mtodo connect() conecta un nombre del evento a un PHP ejecutable.
Un PHP callable es una variable PHP que puede ser utilizada por la funcin call_user_func() y devuelve true cuando pasa a la funcinis_callable(). Una cadena representa una funcin , y un array puede representar un mtodo de objeto o un mtodo de clase.

Con el cdigo anterior en el lugar, el objeto myUser llamar al mtodo esttico methodNotFound() de la clase JobeetUser siempre que sea incapaz de encontrar un mtodo. Es entonces cuando el mtodo methodNotFound() se procesa. Remueve todos los mtodos de la clase myUser y crear la clase JobeetUser:
// apps/frontend/lib/myUser.class.php class myUser extends sfBasicSecurityUser { } // plugins/sfJobeetPlugin/lib/JobeetUser.class.php class JobeetUser { static public function methodNotFound(sfEvent $event) {

if (method_exists('JobeetUser', $event['method'])) { $event->setReturnValue(call_user_func_array( array('JobeetUser', $event['method']), array_merge(array($event->getSubject()), $event['arguments']) )); return true; } } static public function isFirstRequest(sfUser $user, $boolean = null) { if (is_null($boolean)) { return $user->getAttribute('first_request', true); } else { $user->setAttribute('first_request', $boolean); } } static public function addJobToHistory(sfUser $user, JobeetJob $job) { $ids = $user->getAttribute('job_history', array()); if (!in_array($job->getId(), $ids)) { array_unshift($ids, $job->getId()); $user->setAttribute('job_history', array_slice($ids, 0, 3)); } } static public function getJobHistory(sfUser $user) { $ids = $user->getAttribute('job_history', array()); if (!empty($ids)) { return Doctrine_Core::getTable('JobeetJob') ->createQuery('a') ->whereIn('a.id', $ids) ->execute(); } return array(); } static public function resetJobHistory(sfUser $user) {

$user->getAttributeHolder()->remove('job_history'); } }

Cuando el dispatcher llama al mtodo methodNotFound(), este pasa un objeto sfEvent. Si existe el mtodo en la clase JobeetUser , este se llama y su valor devuelto es subsecuentemente devuelto al notificador. Si no, Symfony tratar con el prximo listener registrado o lanzar una Rxcepcin. El mtodo getSubject() regresa el notificador del evento, que en este caso es el actual objeto myUser. Como siempre cuando creas nuevas clases, no olvides de limpiar el cache antes de navegar o ejecutar las pruebas:
$ php symfony cc

Arquitectura por defecto vs. Arquitectura de los Plugins Utilizando la arquitectura de plugin te permite organizar el cdigo de una manera diferente:

Uso de Plugins
Al iniciar la aplicacin de una nueva funcionalidad, o si tratas de resolver un problema clsico de la Web, hay probabilidad de que alguien ya ha resuelto el mismo problema y tal vez empaqueto la solucin como un plugin symfony. Para buscar un plugin pblico symfony, ve a la seccin plugin del sitio web Symfony. Como un plugin esta auto-contenido en un directorio, hay varias manera de instalarlo:

Usando la tarea plugin:install (esto solo funciona si el desarrollador ha creado un plugin package y lo ha subido al sitio web de Symfony) Descarga el package/paquete y manualmente descomprimirlo bajo el directorio plugins/ (tambin es necesario que el desarrollador haya subido un package) La creacin de un svn:externals en plugins/ para el plugin (esto solo funciona si el desarrollador ha alojado su plugin en Subversion)

Las dos ltimas formas son fciles, pero le falta cierta flexibilidad. La primera te permite instalar la versin ms reciente de acuerdo con la versin del proyecto Symfony, fcil de actualizar a la ltima versin estable, y administrar fcilmente las dependencias entre plugins.

Contribuyendo con un Plugin


Empaquetar un Plugin Para crear un plugin package, es necesario agregar algunos archivos obligatorios a la estructura de directorios del plugin. En primer lugar, crear un archivo README en la raz de directorios del plugin y explicar cmo instalar el plugin, lo que proporciona, y lo que no. The README file must be formatted with the Markdown format. Este archivo se utilizar en el sitio web Symfony como la principal pieza de la documentacin. Puede probar la conversin de tu README a HTML utilizando el symfony plugin dingus.
Tareas para Crear Plugins Si te encuentras con frecuencia en la creacin de plugins privados y / o pblicos, considera tomar las ventajas de algunas de las tasks en elsfTaskExtraPlugin. Este plugin, mantenida por el equipo de Symfony, incluye una serie de tareas que te ayudan a agilizar el ciclo de vida del plugin: generate:plugin plugin:package

Tambin necesita crear un archivo LICENSE. La eleccin de una licencia no es una tarea fcil, pero la seccin de symfony plugin slo lista plugins que se liberan bajo una licencia similar a la de Symfony (MIT, BSD, LGPL, y PHP). El contenido de LICENSE se mostrar bajo la pestaa licencia de la pgina de tu plugin. El ltimo paso es crear un archivo package.xml en la raz del directorio del plugin. Este package.xml sigue el PEAR package syntax.
La mejor manera de aprender la sintaxis package.xml es ciertamente hacer una copia del usado por un plugin existente.

El archivo package.xml se compone de varias partes, como puedes ver en este ejemplo:
<!-- plugins/sfJobeetPlugin/package.xml --> <?xml version="1.0" encoding="UTF-8"?>

<package packagerversion="1.4.1" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0 http://pear.php.net/dtd/tasks-1.0.xsd http://pear.php.net/dtd/package2.0 http://pear.php.net/dtd/package-2.0.xsd" > <name>sfJobeetPlugin</name> <channel>plugins.symfony-project.org</channel> <summary>A job board plugin.</summary> <description>A job board plugin.</description> <lead> <name>Fabien POTENCIER</name> <user>fabpot</user> <email>fabien.potencier@symfony-project.com</email> <active>yes</active> </lead> <date>2008-12-20</date> <version> <release>1.0.0</release> <api>1.0.0</api> </version> <stability> <release>stable</release> <api>stable</api> </stability> <license uri="http://www.symfony-project.com/license"> MIT license </license> <notes /> <contents> <!-- CONTENT --> </contents> <dependencies> <!-- DEPENDENCIES --> </dependencies> <phprelease> </phprelease> <changelog> <!-- CHANGELOG --> </changelog> </package>

El <contents> contiene los archivos que hay que poner en el package:

<contents> <dir name="/"> <file role="data" name="README" /> <file role="data" name="LICENSE" /> <dir name="config"> <file role="data" name="config.php" /> <file role="data" name="schema.yml" /> </dir> <!-- ... --> </dir> </contents>

El <dependencies> referencias de todas las dependencias pueda tener el plugin: PHP, Symfony, y tambin otros plugins. Esta informacin es utilizada por la tarea plugin:install para instalar el plugin y su mejor versin para el proyecto y tambin para instalar las dependencias necesarias en caso de ser necesario.
<dependencies> <required> <php> <min>5.0.0</min> </php> <pearinstaller> <min>1.4.1</min> </pearinstaller> <package> <name>symfony</name> <channel>pear.symfony-project.com</channel> <min>1.3.0</min> <max>1.5.0</max> <exclude>1.5.0</exclude> </package> </required> </dependencies>

Siempre debes declarar una dependencia a Symfony, como lo hemos hecho aqu. Declarar un mnimo y un mximo de versin permite a la tareaplugin:install saber que versin de Symfony es obligatoria ya que las versiones puede tener algo diferente en sus APIs. Declara una dependencia con otro plugin tambin es posible:
<package> <name>sfFooPlugin</name> <channel>plugins.symfony-project.org</channel> <min>1.0.0</min> <max>1.2.0</max> <exclude>1.2.0</exclude> </package>

El <changelog> es opcional pero nos da informacin til sobre lo que ha cambiado entre versiones. Esta informacin est disponible bajo la pestaa "Changelog" y tambin en el plugin feed.
<changelog> <release> <version> <release>1.0.0</release> <api>1.0.0</api> </version> <stability> <release>stable</release> <api>stable</api> </stability> <license uri="http://www.symfony-project.com/license"> MIT license </license> <date>2008-12-20</date> <license>MIT</license> <notes> * fabien: First release of the plugin </notes> </release> </changelog>

Alojar un Plugin en el Sitio Web Symfony Si desarrollas un Plugin til y quiere compartirlo con la comunidad Symfony, crea una cuenta symfony si no tiene uno ya, entonces crear un nuevo plugin. Te convertirs automticamente en el administrador del plugin y vers una pestaa "admin" en la interfaz. En esta pestaa, usted encontrar todo lo que necesita para gestionar tu plugin y cargar tus packages.

Creacin de un nuevo Entorno


El framework Symfony ha incorporado muchas estratgias sobre la memoria cache. Por ejemplo, los archivos de configuracin YAML se convierten primero en PHP y luego pasan al cache sobre el sistema de ficheros. Tambin hemos visto que los mdulos de administracin generados por el generador se guardan en cache para un mejor rendimiento. Pero hoy, vamos a hablar de otro cache: el cache HTML. Para mejorar el rendimiento de tu sitio web, puedes poner en el cache todas las pginas HTML o slo partes de ellas. De forma predeterminada, la caracterstica del cache de la plantilla de Symfony est habilitado en el fichero de configuracinsettings.yml para el entorno prod, pero no para test ni dev:
prod: .settings: cache: true dev: .settings: cache: false test: .settings: cache: false

Como tenemos que probar la funcin de cache antes de ir a produccin, podemos activar la memoria cache para el entorno dev o crear un nuevo entorno. Recordemos que un entorno se define por su nombre (una cadena), un controlador frontal asociado, y opcionalmente, un conjunto de valores de configuracin. Para jugar con la memoria cache del sistema en Jobeet, vamos a crear un entorno cache, similar al entorno prod, pero con la informacin disponible del log y debug del entorno dev. Crea el controlador frontal asociado con el nuevo entorno cache copiando el controlador frontal dev enweb/frontend_dev.php a web/frontend_cache.php:
// web/frontend_cache.php if (!in_array(@$_SERVER['REMOTE_ADDR'], array('127.0.0.1', '::1'))) { die('You are not allowed to access this file. Check '.basename(__FILE__).' for more information.'); } require_once(dirname(__FILE__).'/../config/ProjectConfiguration.class.php '); $configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'cache', true);

sfContext::createInstance($configuration)->dispatch();

Eso es todo lo que hay para l. El nuevo entorno cache ahora es utilizable. La nica diferencia es el segundo argumento del mtodogetApplicationConfiguration()que es el nombre del entorno, cache. Puede probar el entorno cache en tu navegador, llamando su controlador:
http://www.jobeet.com.localhost/frontend_cache.php/ El controlador frontal tiene un script que comienza con un cdigo que garantiza que el controlador frontal es slo llamado desde una direccin IP local. Esta medida de seguridad es para proteger el controlador frontal de ser llamado en servidores de produccin. Hablaremos sobre esto en ms detalles en el tutorial de maana.

Por ahora, el entorno cache hereda de la configuracin por defecto. Edita el archivo de configuracin settings.yml para agregar la configuracin especfica de entorno cache:
# apps/frontend/config/settings.yml cache: .settings: error_reporting: <?php echo (E_ALL | E_STRICT)."\n" ?> web_debug: true cache: true etag: false

En estos ajustes, la funcin de cache de plantillas de Symfony se ha activado con el item cachey el web debug toolbar se ha activado con elweb_debug. Como la configuracin por defecto cachea todos los ajustes en la memoria cach, necesitas limpiarla antes de poder ver los cambios en tu navegador:
$ php symfony cc

Ahora, si actualizas tu navegador, el web debug toolbar deben estar presentes en la esquina superior derecha de la pgina, ya que es el caso para entornos dev.

Configuracin del Cache


El cach de las plantillas de Symfony se puede configurar con el archivo de configuracin cache.yml. La configuracin por defecto para la aplicacin se encuentra en apps/frontend/config/cache.yml:
default: enabled: false with_layout: false lifetime: 86400

De forma predeterminada, ya que todas las pginas pueden contener informacin dinmica, la memoria cach es desactivada globalmente (enabled: false). No tenemos que cambiar esta configuracin, porque permitiramos pone en cache pgina por pgina.

El item lifetime define del lado del servidor el tiempo de vida de la memoria cache en segundos (86400 segundos equivalen a un da).
Tambin puedes trabajar a la inversa: habilitar el cache globalmente y luego, deshabilitarlo en determinadas pginas que no se deben poner en cache. >Depende de que representa menos trabajo para tu aplicacin.

Pginas en el Cache
Dado que la pgina principal Jobeet ser probablemente la pgina ms visitada del sitio web, en lugar de pedir los datos a la base de datos cada vez que un usuario accede a ella, sta puede estar en el cache. Crea un archivo cache.yml para el mdulo `sfJobeetJob:
# plugins/sfJobeetPlugin/modules/sfJobeetJob/config/cache.yml index: enabled: true with_layout: true El cache.yml tiene las mismas propiedades que cualquier otro archivo de configuracin de Symfony como view.yml. Esto significa, por ejemplo, que puedes habilitar el cache para todas las acciones de un mdulo mediante el uso de la clave especial all.

Si actualizas tu navegador, vers que Symfony ha decorado la pgina con un cuadro que indica que el contenido se ha puesto en el cache:

El cuadro da una valiosa informacin acerca del cache para la depuracin, como la vida til del cache, y la duracin hasta el momento de ella. Si actualizas la pgina de nuevo, el color de la caja cambia de verde a amarillo, lo que indica que la pgina se ha recuperado del cache:

Observa tambin que no hay consultas a la base de datos en el segundo caso, como se muestra en la web debug toolbar.
Incluso si el idioma se puede cambiar por usuario, el cache an funciona ya que el lenguaje esta includo en la URL.

Cuando una pgina es cacheable, y si el cache an no existe, Symfony almacena el objeto response en el cache al final de la peticin. Para todos los dems de las peticiones, Symfony se enviar la respuesta del cache sin llamar al controlador:

Esto tiene un gran impacto en el rendimiento ya que puedes medirlo por t mismo mediante el uso de herramientas como JMeter.
Una peticin entrante con parmetros GET o enviada con mtodo POST, PUT, o DELETE nunca ser puesta en el cache por Symfony, independientemente de la configuracin.

La pgina de creacin de puestos de trabajo tambin puede ser puesta en el cache:


# plugins/sfJobeetPlugin/modules/sfJobeetJob/config/cache.yml new: enabled: true index: enabled:

true

all: with_layout: true

Ya que las dos pginas se pueden guardar en el cache con el layout, hemos creado una sccin all que define la configuracin por defecto para todos las acciones de sfJobeetJob.

Limpiando el Cache
Si deseas borrar el cache, puedes usar la tarea cache:clear:
$ php symfony cc

La tarea cache:clear limpia todo lo que Symfony guarda bajo el directorio principal cache/. Tambin tiene opciones para selectivamente limpiar algunas partes del cache. Para slo limpiar el cache de las plantillas para el entorno cache, usa las opciones --type y --env:
$ php symfony cc --type=template --env=cache

En lugar de limpiar el cache cada vez que hagas un cambio, puendes tambin deshabilitar el cache agregando cualquier cadena de consulta a la URL, o usando el boton "Ignore cache" de la barra de herramientas de depuracin web:

La Accin en el Cache
A veces, no se puede guardar en cache toda la pgina entera, pero la accin en si misma puede ser guardada en el cache. Dicho de otra manera, puedes guardar todo menos el layout. Para la aplicacin Jobeet, no podemos guardar en cache toda la pgina entera debido a la barra "history job". Cambia la configuracin para el cache del module job de acuerdo a:
# plugins/sfJobeetPlugin/modules/sfJobeetJob/config/cache.yml new: enabled: true index: enabled:

true

all: with_layout: false

Al cambiar el with_layout a false, has desactivado el layout en el cache. Limpiar el cache:

$ php symfony cc

Actualiza tu navegador para ver la diferencia:

Incluso si el flujo de la peticin es bastante similar en el diagrama simplificado, guardar cache sin el layout es mucho ms intensivo el uso de recursos.

Partial y Componente en el Cache


Para sitios web muy dinmicos, a veces es incluso imposible guardar en cache toda la plantilla de la accin. Para esos casos, necesitas una manera de configurar el cache a nivel fino. Afortunadamente, los partials y componentes Tambin se puede poner en el cache.

Vamos a guardar en el cache el componente language creando un archivo cache.yml para el mdulo sfJobeetLanguage:
# plugins/sfJobeetPlugin/modules/sfJobeetLanguage/config/cache.yml _language: enabled: true

La configuracin del cache para un partial o un componente es tan simple como aadir un tem con su nombre. Con la opcin with_layout no se tiene en cuenta para este tipo de cache ya que no tiene ningn sentido:

Contextual o no? El mismo componente o partial puede ser usado en diferentes plantillas. El partial job _list.php por ejemplo, se utiliza en los mdulos job y category. Ya que la visualizacin es siempre la misma, el partial no depende del contexto en el que se utiliza y el cache es el mismo para todas las plantillas (el cache es obviamente todava diferente para un conjunto diferente de parmetros). Pero a veces, un partial o un componente tiene una salida diferente, sobre la base de la accin en la que se incluye (piensa en una barra lateral de un blog, por ejemplo, la cul es ligeramente diferente para la pgina principal del blog y la pgina del artculo). En estos casos el partial o componente es contextual, y el cache debe estar configurado en consecuencia mediante el establecimiento de la opcin contextual a true: _sidebar: enabled: true contextual: true

Formularios en el Cache
El almacenamiento de la pgina de creacin de empleo en el cache es problemtica ya que contiene un formulario. Para comprender mejor el problema, ve a la pgina "Post a Job" en tu navegador. Entonces, limpia tu cookie de sesin, y trata de enviar un puesto de trabajo. Debes ver un mensaje de error con una alerta de "CSRF attack":

Por qu? Como se ha configurado un CSRF secreto cuando se cre el frontend, Symfony incorpora un token CSRF en todos sus formularios. Para protegerte contra los ataques CSRF, este token es nico para un determinado usuario, as como para un determinado formulario. La primera vez que la pgina es mostrada, el HTML generado se almacena en el cache con el token del usuario actual. Si otro usuario viene despus, la pgina desde el cache se mostrar con el token CSRF del primer usuario. Cundo se enva el formulario, los tokens no coinciden, y se lanza un mensaje de error. Cmo podemos solucionar el problema, ya que parece legtimo guardar el formulario en el cache? El formulario de creacin job no depende del usuario, y eso no cambia nada para el usuario actual. En tal caso, ninguna proteccin CSRF se necesita, y podemos quitar el token CSRF:
// plugins/sfJobeetPlugin/lib/form/doctrine/PluginJobeetJobForm.class.php abstract PluginJobeetJobForm extends BaseJobeetJobForm { public function configure() { $this->disableLocalCSRFProtection(); } }

Despus de hacer este cambio, limpiar el cache y re-intentar el mismo escenario que el anterior para demostrar que funciona como se espera ahora. La misma configuracin debe aplicarse al formulario language ya que figura en el layout y se almacenar en el cache. Como el valor por defectosfLanguageForm es usado, en lugar de crear una nueva clase, slo para eliminar el token CSRF, vamos a hacerlo de la accin y componente del mdulo sfJobeetLanguage:
// plugins/sfJobeetPlugin/modules/sfJobeetLanguage/actions/components.class. php class sfJobeetLanguageComponents extends sfComponents

{ public function executeLanguage(sfWebRequest $request) { $this->form = new sfFormLanguage($this->getUser(), array('languages' => array('en', 'fr'))); $this->form->disableLocalCSRFProtection(); } } // plugins/sfJobeetPlugin/modules/sfJobeetLanguage/actions/actions.class.php class sfJobeetLanguageActions extends sfActions { public function executeChangeLanguage(sfWebRequest $request) { $form = new sfFormLanguage($this->getUser(), array('languages' => array('en', 'fr'))); $form->disableLocalCSRFProtection(); // ... } }

The disableLocalCSRFProtection() method disables the CSRF token for this form.

Removiendo el Cache
Cada vez que un usuario envia y activa un puesto de trabajo, la pgina principal debe ser refrescada para listar el nuevo puesto de trabajo. Como no necesitamos que el puesto de trabajo aparezca en tiempo real en la pgina de inicio, la mejor estrategia es reducir el tiempo de vida del cache a algo aceptable:
# plugins/sfJobeetPlugin/modules/sfJobeetJob/config/cache.yml index: enabled: true lifetime: 600

En lugar de la configuracin por defecto de un da, el cache para la pgina principal se eliminar automticamente cada diez minutos. Pero si deseas actualizar la pgina web tan pronto como un usuario activa un nuevo puesto de trabajo, modifiqua el mtodo executePublish() del mdulo sfJobeetJob para aadir manualmente la limpieza del cache:
// plugins/sfJobeetPlugin/modules/sfJobeetJob/actions/actions.class.php public function executePublish(sfWebRequest $request) { $request->checkCSRFProtection(); $job = $this->getRoute()->getObject(); $job->publish();

if ($cache = $this->getContext()->getViewCacheManager()) { $cache->remove('sfJobeetJob/index?sf_culture=*'); $cache->remove('sfJobeetCategory/show?id='.$job->getJobeetCategory()>getId()); } $this->getUser()->setFlash('notice', sprintf('Your job is now online for %s days.', sfConfig::get('app_active_days'))); $this->redirect($this->generateUrl('job_show_user', $job)); }

El cache es gestionado por la clase sfViewCacheManager. El mtodo remove() remueve el cache asociado con una URI interna. Para eliminar el cache para todos los posibles parmetros de una variable, utiliza el * como valor. El sf_culture=* que usamos en cdigo anterior significa que Symfony eliminar del cache la pgina principal del Ingls y del Francs. Ya que el administrador del cache es null cuando la memoria cache est desactivada, hemos envuelto la eliminacin del cache en un bloque if.

Probando el Cache
Antes de comenzar, tenemos que cambiar la configuracin para el entorno test para habilitar la capa cache:
# apps/frontend/config/settings.yml test: .settings: error_reporting: <?php echo ((E_ALL | E_STRICT) ^ E_NOTICE)."\n" ?> cache: true web_debug: false etag: false

Vamos a probar la pgina para la creacin de puestos de trabajo:


// test/functional/frontend/jobActionsTest.php $browser-> info(' 7 - Job creation page')-> get('/fr/')-> with('view_cache')->isCached(true, false)-> createJob(array('category_id' => Doctrine_Core::getTable('CategoryTranslation')>findOneBySlug('programming')->getId()), true)-> get('/fr/')-> with('view_cache')->isCached(true, false)->

with('response')->checkElement('.category_programming .more_jobs', '/23/') ;

El tester view_cache se utiliza para probar el cache. El mtodo isCached() toma dos Booleanos:

Si la pgina debe estar en cache o no Si el cache es con layout o no

Incluso con todas las herramientas proporcionadas por el framework de pruebas funcionales, a veces es ms fcil de diagnosticar problemas en el navegador. Es muy fcil lograrlo. Crea un controlador frontal para el entorno test. Los logs guardados en log/frontend_test.log tambin pueden ser muy tiles.

Preparando el Servidor de Produccin


Antes de implementar el proyecto en produccin, hay que asegurarse de que el servidor est configurado correctamente. Se puede volver a leer el da 1, donde se explica cmo configurar el servidor web. En esta seccin, se supone que ya ha instalado el servidor web, servidor de bases de datos, y PHP 5.2.4 o posterior.
Si no dispones de un acceso SSH al servidor web, pasa a la parte en que necesitas tener acceso a la lnea de comandos.

Configuracin del Servidor En primer lugar, necesitas comprobar que PHP est instalado con todas las extensiones necesarias y est correctamente configurado. Como con el da 1, haremos uso del script check_configuration.php provisto con Symfony. Como no vamos a instalar Symfony en el servidor de produccin, descarga el archivo directamente desde la pgina web Symfony:
http://trac.symfonyproject.org/browser/branches/1.4/data/bin/check_configuration.php?format= raw

Copia el archivo en el directorio root Web y ejecutalo desde tu navegador y desde la lnea de comandos:
$ php check_configuration.php

Correge cualquier error fatal que el script encuentre y repite el proceso hasta que todo funcione bien en ambos entornos. PHP Accelerator Para el servidor de produccin, probablemente quieras disfrutar del mejor rendimiento posible. La instalacin de PHP accelerator te dar la mejor mejora.
Desde Wikipedia: Un acelerador PHP funciona guardando en cache el bytecode compilado de los scripts PHP para evitar la sobrecarga de analizar y compilar cdigo fuente en cada peticin.

APC es uno de los ms populares, y es muy fcil de instalar:


$ pecl install APC

Dependiendo de tu sistema operativo, tambin sers capaz de instalarlo con el gestor de paquetes nativo del sistema operativo.
Tmate un tiempo para aprender a configurar APC.

Las Bibliotecas de Symfony


Incluyendo Symfony

Uno de la gran fortalezas de Symfony es un proyecto esta autocontenido. Todos los archivos necesarios para el proyecto funcione se encuentran bajo el directorio raz principal del proyecto. Y puedes mover todo el proyecto a otro directorio sin cambiar nada en el proyecto propiomente dicho ya que Symfony utiliza slo rutas relativas. Esto significa que el directorio del servidor de produccin no tiene que ser el mismo que el de tu equipo de desarrollo. La nica ruta absoluta que posiblemente se puede encontrar esta en el archivo config/ProjectConfiguration.class.php; pero nos ocupamos de l durante el da 1. Comprobamos que en realidad contiene una ruta relativa al autoloader de Symfony:
// config/ProjectConfiguration.class.php require_once dirname(__FILE__).'/../lib/vendor/symfony/lib/autoload/sfCoreAutoload.cla ss.php';

Actualizando Symfony Incluso si todo es autocontenido en un nico directorio, actualizar Symfony a una versin ms reciente es, no obstante, demasiado fcil. Tu deseas actualizar Symfony a la ltima versin menor de vez en cuando, ya que constantemente arreglamos errores y posiblemente, cuestiones de seguridad. La buena noticia es que todas las versiones Symfony se mantienen durante al menos un ao y durante el perodo de mantenimiento, nunca jams aadimos nuevas caractersticas, incluso ni la ms pequea. Por lo tanto, siempre es rpido y seguro actualizar de una versin a otra por menor que sea. Actualizar Symfony es tan sencillo como cambiar el contenido del directorio lib/vendor/symfony/. Si has instalado Symfony con el archivo, elimina los archivos actuales y reemplazalos con las nuevas versiones. Si utilizas Subversion para tu proyecto, tambin puedes enlazar tu proyecto con la ltimas etiquetas de Symfony 1.4:
$ svn propedit svn:externals lib/vendor/ # symfony http://svn.symfony-project.com/tags/RELEASE_1_4_0/

Actualizar Symfony es tan simple como cambiar la etiqueta a la ltima versin de Symfony. Tambin puedes utilizar la rama 1.4 para tener los arreglos en tiempo real:
$ svn propedit svn:externals lib/vendor/ # symfony http://svn.symfony-project.com/branches/1.4/

Ahora, cada vez que hacemos un svn up, tendrs la ltima versin de Symfony 1.4. Cuando se actualiza a una nueva versin, se aconseja siempre que se limpie el cache, especialmente en el entorno de produccin:
$ php symfony cc

Si tambin tienes acceso al FTP del servidor de produccin, se puede simular un symfony cc por la simple eliminacin de todos los archivos y directorios del directorio cache/.

Puedes incluso probar una nueva versin Symfony sin sustituir a la existente. Si lo que deseas es probar una nueva versin, y quieres que se pueda revertir fcilmente, instala Symfony en otro directorio (lib/vendor/symfony_test por ejemplo), cambia la ruta en la claseProjectConfiguration, limpia el cache, y ya est. Restaurar a la versin anterior es tan simple como eliminar el directorio, y restaurar la ruta en ProjectConfiguration.

Afinando la Configuracin
Configuracin de la Base de datos La mayora de las veces, la base de datos de produccin tiene unas credenciales distintas a las locales. Gracias a los entornos de Symfony, es muy sencillo tener una configuracin diferente para la base de datos de produccin:
$ php symfony configure:database "mysql:host=localhost;dbname=prod_dbname" prod_user prod_pass

Tambin puedes editar directamente el archivo de configuracin databases.yml . Recursos ya que Jobeet usa plugins que tienen recursos web, Symfony crea enlaces relativos simblicos en el directorio web/. La tareaplugin:publish-assets los regenera o los crea si instalas plugins sin la tarea plugin:install:
$ php symfony plugin:publish-assets

Personalizar Pginas de Error Antes de entrar a produccin, es mejor personalizar las pginas por defecto Symfony, como la "Page Not Found", o la pgina de excepcin por defecto. Ya hemos configurado la pgina de error para el formato YAML durante el da 15, mediante la creacin de archivos error.yaml.php yexception.yaml.php en el directorio config/error/. El archivo error.yaml.php es usado por Symfony en el entorno prod, mientras queexception.yaml.php es usado en el entorno dev. As que, para personalizar la pgina predeterminada de excepcin para el formato HTML, crea dos archivos:config/error/error.html.php y config/error/exception.html.php. La pgina 404 (page not found) se puede personalizar mediante el cambio de los items error_404_module y error_404_action:
# apps/frontend/config/settings.yml all: .actions: error_404_module: default

error_404_action: error404

Personalizacin de la Estructura de Directorios


Para una mejor estructura y para normalizar el cdigo, Symfony tiene una estructura de directorios por defecto con nombres predefinidos. Pero a veces, no tienes ms opcin que la de cambiar la estructura a causa de algunas limitaciones externas. La configuracin de los nombres de directorios que se puede hacer en la clase config/ProjectConfiguration.class.php. El Directorio Raz Web En algunos servidores de Internet, no puedes cambiar el nombre del directorio raz web. Digamos que en tu sitio, este se llamapublic_html/ en lugar de web/:
// config/ProjectConfiguration.class.php class ProjectConfiguration extends sfProjectConfiguration { public function setup() { $this->setWebDir($this->getRootDir().'/public_html'); } }

El mtodo setWebDir() toma la ruta absoluta del directorio raz web. Si adems desplazas este directorio a otra parte, no olvides de editar el scripts para comprobar que las rutas de acceso al archivo ProjectConfiguration siguen siendo vlidas:
require_once(dirname(__FILE__).'/../config/ProjectConfiguration.class.php ');

Los Directorios Cache y Log El framework Symfony slo escribe en dos directorios: cache/ y log/. Por razones de seguridad, algunos servidores web no establecen permisos de escritura en el directorio principal. Si este es el caso, puede mover estos directorios a otra parte del sistema de archivos :
// config/ProjectConfiguration.class.php class ProjectConfiguration extends sfProjectConfiguration { public function setup() { $this->setCacheDir('/tmp/symfony_cache'); $this->setLogDir('/tmp/symfony_logs'); } }

Como para el mtodo setWebDir() , setCacheDir() y setLogDir() toman una ruta absoluta para los directorios cache/ y log/respectivamente.

Personalizando los Objetos del Ncloe Symfony (factorias)


Durante el da 16, hablamos un poco acerca de las factorias symfony. Poder personalizar las factorias significa que puedes usar una clase personalizada para los objetos del ncleo symfony en lugar de usar la predeterminada. Puedes tambin cambiar el comportamiento predeterminado de esas clases modificando los parmetros enviados a ellos. Demos un vistazo a algunas personalizaciones clsicas que puedes querer hacer. Nombre de la Cookie Para manejar la sesin de usuario, Symfony usa una cookie. Esta cookie tiene el nombre por defecto de symfony, el que puede ser cambiado en factories.yml. Bajo el item all, aade la siguiente configuracin para cambiar el nombre de la cookie a jobeet:
# apps/frontend/config/factories.yml storage: class: sfSessionStorage param: session_name: jobeet

Guardando la Sesin La clase por defecto para guardar la sesin es sfSessionStorage. Esta utiliza el sistema de archivos para almacenar la informacin de la sesin. Si tienes varios servidores web, puedes desear guardar las sesiones en un lugar central, como una tabla de la base de datos:
# apps/frontend/config/factories.yml storage: class: sfPDOSessionStorage param: session_name: jobeet db_table: session database: propel db_id_col: id db_data_col: data db_time_col: time

Tiempo de Vida de las Sesiones De forma predeterminada, el tiempo de la sesin de usuario es 1800 segundos. Esto se puede cambiar editando el item user:
# apps/frontend/config/factories.yml user: class: myUser param: timeout: 1800

Registro de Log Por defecto, no hay log en el entorno prod porque el nombre de clase de logger es sfNoLogger:
# apps/frontend/config/factories.yml prod: logger: class: sfNoLogger param: level: err loggers: ~

Por ejemplo, puedes permitir hacer log sobre el sistema de archivos cambiando el nombre de clase de logger a sfFileLogger:
# apps/frontend/config/factories.yml logger: class: sfFileLogger param: level: err loggers: ~ file: %SF_LOG_DIR%/%SF_APP%_%SF_ENVIRONMENT%.log En el archivo de configuracin factories.yml, las cadenas de tipo %XXX% son reemplazadas con su correspondiente valor del objetosfConfig. Por eso, %SF_APP% en un archivo de configuracin es equivalente a sfConfig::get('sf_app') en el cdigo PHP. Esta notacin se puede utilizar tambin en el archivo de configuracin app.yml. Esto es muy til cuando necesitas para hacer referencia a una ruta en un archivo de configuracin sin hardcodear la ruta (SF_ROOT_DIR, SF_WEB_DIR, ...).

Desplegar
Qu desplegar? Cuando se hace el despliegue (instalacin/implementar) del sitio web Jobeet en el servidor de produccin, tenemos que tener cuidado de no desplegar los archivos innecesarios o sobreescribir archivos subidos por nuestros usuarios, como los logos de la compaa. En un proyecto symfony, hay tres directorios a excluir de la transferencia: cache/, log/, y web/uploads/. Todo lo dems puede ser transferido como esta. Por razones de seguridad, tampoco deseas transferir los controladores frontales "que no sean de produccin", como los scriptsfrontend_dev.php, backend_dev.php yfrontend_cache.php`. Estrategias para hacer el Despliegue del Proyecto En esta seccin, vamos a suponer que tienes un control total sobre el/los servidor/es de produccin. Si slo se puede acceder al servidor con una cuenta de

FTP, la nica solucin posible de despliegue es transferir todos los archivos cada vez que necesite. La forma ms sencilla de implementar tu sitio web es utilizar la tarea nativa project:deploy. Esta usa SSH y rsync para conectar y transferir los archivos de un equipo a otro. Los Servidores para la tarea project:deploy se pueden configurar en el archivo de configuracin config/properties.ini:
# config/properties.ini [production] host=www.jobeet.org port=22 user=jobeet dir=/var/www/jobeet/ type=rsync pass=

Para implementar al recin configurado Servidor de produccin, utiliza l atarea project:deploy:


$ php symfony project:deploy production Antes de ejecutar la tarea project:deploy por primera vez, necesitas conectarte al servidor manualmente para agregar la clave en el archivo de hosts reconocidos.

Si ejecutas este comando, Symfony slo simular la transferencia. Para implementar efectivamente el sitio web, aade la opcin --go:
$ php symfony project:deploy production --go Incluso si puedes proporcionar la clave SSH en el archivo properties.ini, es mejor configurar tu servidor con una clave SSH que permita conexiones sin contrasea.

Por defecto, Symfony no transferir los directorios que hemos hablado en la seccin anterior, ni transferir el scritp del controlador frontal del entorno dev. Eso es porque la tarea project:deploy excluye archivos y directorios se configuran en el archivoconfig/rsync_exclude.txt:
# config/rsync_exclude.txt .svn /web/uploads/* /cache/* /log/* /web/*_dev.php

Para Jobeet, tenemos que aadir el archivo frontend_cache.php:


# config/rsync_exclude.txt .svn /web/uploads/* /cache/* /log/* /web/*_dev.php

/web/frontend_cache.php Tambin puedes crear un archivo config/rsync_include.txt para obligar que algunos archivos o directorios sean transferidos.

Incluso si la tarea project:deploy es muy flexible, es posible que desees personalizarla an ms. Como el despliegue puede ser muy diferente en funcin de tu configuracin y topologa de servidores, no dudes en extender la tarea por defecto. Cada vez que se despliegue un sitio web en produccin, no te olvides de por lo menos limpiar la configuracin de cach en el servidor de produccin:
$ php symfony cc --type=config

Si has cambiado algunas rutas, tambin tendrs que limpiar el cache de las rutas:
$ php symfony cc --type=routing Limpiar el cache selectivamente permite conservar algunas partes del cache, como el cache de las plantillas.

Qu es Symfony?
El framework symfony es un conjunto de relacionados, pero independientes subframeworks, que forman un completo framework MVC (Modelo, Vista, Controlador). Antes de la codificacin inicial, nos tomamos un tiempo para leer la historia y la filosofa de symfony. Entonces, comprobamos del framework sus requisitos previos y usamos el script check_configuration.php para validar tu configuracin. Finalmente, instalamos symfony. Despus de algn tiempo tambin desears actualizar a la ltima versin del framework. El framework tambin proporciona herramientas para facilitar el despliegue.

El Modelo
La parte del Modelo de symfony se puede hacer con la ayuda del ORM Doctrine. Basado en la descripcin de la base de datos, esta genera las clases para los objetos, formularios, y filtros. Doctrine tambin genera las sentencias SQL utilizada para crear las tablas en la base de datos. La configuracin de la base de datos se puede hacer con una tarea o editando un archivo de configuracin. Adems de su configuracin, tambin es posible hacer la inyeccin inicial de datos, gracias a los archivos de datos. Puede incluso hacer que estos archivos sean dinmicos. Los objetos Doctrine tambin puede ser fcilmente internacionalizados.

La Vista
De forma predeterminada, la capa de la Vista de la arquitectura MVC utiliza archivos PHP planos como plantillas. Las plantillas puede utilizar helpers para tareas recurrentes como crear una URL o un enlace. Una plantilla puede ser decorada por unlayout para abstraerse del encabezado y pie de las pginas. Para hacer vistas an ms reutilizables, puedes definir slots, partials, ycomponentes. Para acelerar las cosas, puedes utilizar el sub-framework del cache para guardar en cache una pgina entera, solo la accin, o tan solopartials o componentes. Tambin puedes eliminar el cache manualmente.

El Controlador
La parte del Controlador es gestionado por controladores frontales y acciones. Las tareas se puede utilizar para crear simples mdulos, mdulos CRUD, o aun para generar completos y funcionales mdulos adminpara las clases del modelo. Los mdulos admin te permiten construir una aplicacin completamente funcional sin codificacin alguna.

Para abstraer la implementacin tcnica de un sitio web, symfony utiliza un subframework enrutamiento que genera URLs amigagles. Para hacer la implementacin de servicios web an ms fcil, symfony soporta formatos en forma nativa. Tambin puedes crear tus propios formatos. Una accin puede ser reenviada a otra, o redirigida.

La Configuracin
El framework symfony hace que sea fcil tener diferentes ajustes de configuracin para distintos entornos. Un entorno es un conjunto de ajustes que permite diferentes comportamientos en los servidores de desarrollo o de produccin. Tambin puedes crear nuevos [entornos] #chapter_21_creating_a_new_environment). Los archivos de configuracin de symfony puede definirse en diferentes niveles y la mayora de ellos son conscientes del entorno:
app.yml cache.yml databases.yml factories.yml generator.yml routing.yml schema.yml security.yml settings.yml view.yml

La mayora de los archivos de configuracin usan el formato YAML. En lugar de utilizar la estructura de directorios por defecto y organizar tus archivos de aplicaciones por capas, tambin puedes organizarlos por funcin, y agruparlos en un plugin. Hablando de la estructura de directorios por defecto, tambin puedes personalizarlade acuerdo a tus necesidades.

La Depuracin
Desde el logging hasta la la barra de herramientas de depuracin web, y las significativas excepciones, symfony proporciona un montn de herramientas tiles para ayudar a los desarrolladores a depurar problemas con mayor rapidez.

Los Principales Objetos de Symfony


Los frameworks de symfony proporciona un buen nmero de objetos bsicos que nos abstraen de necesidades recurrentes en los proyectos web: la peticin, la respuesta, el usuario, el logging, las rutas, el mailer, y el administrador del cache .

Estos objetos bsicos son gestionados por el objetos sfContext, y que se configuran a travs de las factorias. El objeto user gestiona la autenticacin, autorizacin, flashes, y atributos para ser guardado en la sesin.

La Seguridad
El framework symfony tiene incorporadas protecciones contra XSS y CSRF. Estos ajustes pueden ser configurados desde la lnea de comandos, o la edicin de un archivo de configuracin. El framework de formularios proporciona caractersticas incorporadas de seguridad.

Los Formularios
Como la gestin de formularios es uno de las ms tediosas tareas para un desarrollador web, symfony proporciona un sub-framework de formularios. El framework de formularios viene con un montn de widgets y validadores. Una de las fortalezas del sub-framework de formularios es que las plantillas son muy fcilmente personalizables. Si usas Doctrine, el framework de formularios tambin facilita la generacin de formularios y filtros basados en tus modelos.

Internacionalizacin y localizacin
Internacionalizacin y localizacin estn soportadas por symfony, gracias al estndar ICU. La cultura del usuario determina el idioma y el pas del usuario. Esto puede ser definido por el usuario mismo, o includo en la URL.

Las Pruebas
La biblioteca lime, usa por las Pruebas Unitarias, proporciona una gran cantidad de mtodos de prueba. Los objetos Doctrine tambin puede ser probados desde una base de datos dedicada y con especficos datos. Las pruebas unitarias se puede ejecutar una a la vez o todas juntas. Las PruebasFfuncionales estn escritas con la clase sfFunctionalTest, la cual usa un simulador de navegador y permite la introspeccin de los objetos del ncleo de symfony a travs de Testers. Los Testers existen para el objeto request, el objeto response, el objeto user, el objeto formulario actual, la capa cache y los Objetos Doctrine. Tambin puedes utilizar herramientas de depuracin para la respuesta y formularios. Ya que las pruebas unitarias, pruebas funcionales se pueden ejecutar una por una o todas juntas. Tambin puedes ejecutar todas las pruebas juntas.

Los Plugins
El framework symfony slo proporciona la base para tus aplicaciones web y se basa en plugins para aadir ms caractersticas. En este tutorial, hemos hablado de sfGuardPlugin, sfFormExtraPlugin, y sfTaskExtraPlugin. Un plugin debe ser activado despus de la instalacin. Plugins are the best way to contribute back to the symfony project.

Las Tareas
El CLI de symfony proporciona una gran cantidad de tareas, y las ms tiles han sido discutidas en este tutorial:
app:routes cache:clear configure:database generate:project generate:app generate:module help i18n:extract list plugin:install plugin:publish-assets project:deploy doctrine:build --all doctrine:build --all --and-load doctrine:build-forms doctrine:build-model doctrine:build-sql doctrine:data-load doctrine:generate-admin doctrine:generate-module doctrine:insert-sql test:all test:coverage test:functional test:unit

Tambin puedes crear tus propias tareass.

Hasta pronto
Aprendiendo con la Prctica El framework symfony, como cualquier pieza de software, tiene una curva de aprendizaje. En el proceso, el primer paso es aprender desde ejemplos prcticos con un libro como este. El segundo paso es practicar. Nada nunca reemplazar a la prctica. Es algo que puede empezar hoy mismo. Piensa en un muy simple proyecto web que de algn valor: una lista de tareas, un simple blog, un conversor de tiempo o moneda, lo que sea... Elije uno y empieza implementandolo con lo que ya sabes hoy. Usa la tarea de ayuda para aprender las diferentes opciones, busca cdigo generado por symfony, usa un editor que tenga auto-completion de PHP comoEclipse, y lee la gua de referencia para buscar toda la configuracin del framework. Disfruta de todo el material que tienes a tu disposicin para aprender mas de symfony. La Comunidad Antes de dejarte, me gustara hablar de una ltima cosa acerca de symfony. El framework tiene un montn de grandes caractersticas y una gran cantidad de documentacin libre. Sin embargo, uno de los ms valiosos recursos que el OpenSource puede tener es su comunidad. Y symfony tiene una de las ms sorprendentes y activa comunidades. En caso de empezar a usar symfony para tus proyectos, considera la posibilidad de unirte a la comunidad symfony:

Suscrbete al user mailing-list Suscrbete al official blog feed Suscrbete al symfony planet feed Ven y chatea en el #symfony IRC canal de freenode

You might also like