jobeet el parcial de symfony

273
 Introducción El framework Symfony ha sido un proyecto de Código Abierto por más de cuatro años y se ha convertido en uno de los framework PHP más populares gracias a sus excelentes características y gran documentación. Esto ha sido una gran tradición desde sus inicios. Este Libro describe la creac ión de una aplicación webcon el framework, symfony paso a paso desde las especificaciones hasta la implementación. Este esta dirijido a principaintes quienes desean aprender symfony, entender como funciona, y también sobre las mejores prácticas del desarrollo web. La aplicación a diseñar podría haber sido otro motor de blogs, pero queremos usar Symfony en algo más ú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 secr eto por un días más ya que tenemos mucho para hacer por hoy. De todas formas, ya sabes el nombre de la aplicación: Jobeet. Cada capítulo de este ñibro está destinado a durar entre una a dos horas, y será la ocasión de aprender Symfony mediante la codificación de un s itio web real, de principio a fin. Cada día, serán agregadas nuevas características a la aplicación, y nos valdremos de las ventajas de este desarrollo para introducirte a las nuevas funcionalidades de Symfony así como a las mejores prácticas de desarrollo web con Symfony. Este Libro es diferente Recuerdo los primeros días de PHP4 ¡Ah, la Belle Epoque! PHP era uno de los primeros lenguages dedicados a la web y uno de los más fáciles de aprender. Pero con la rápida evolución de las tecnologías web, los desarrolladores web deben mantenerse al tanto de las últimas herramientas y mejores prácticas de desarrollo. Por supuesto que la mejor manera de aprender es leyendo blogs, tutoriales y libros. Hemos leído un montón de estos, sea que estén escritos para PH P, Python, Java, Ruby o Perl, y muchos s e quedan cortos cuando el autor comienza a mostrar fragmentos de código como ejemplos. Probablemente estés acostumbrado a leer adevertencias de este tipo: "Para una aplicación real, no te ol vides de agregar validación 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"

Upload: victor-angel-ferreira

Post on 20-Jul-2015

274 views

Category:

Documents


1 download

TRANSCRIPT

Page 1: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 1/273

 

IntroducciónEl framework Symfony ha sido un proyecto de Código Abierto por más de cuatroaños y se ha convertido en uno de los framework PHP más populares gracias a susexcelentes características y gran documentación. Esto ha sido una gran tradicióndesde sus inicios.

Este Libro describe la creación de una aplicación webcon el framework, symfonypaso a paso desde las especificaciones hasta la implementación. Este esta dirijidoa principaintes quienes desean aprender symfony, entender como funciona, ytambién sobre las mejores prácticas del desarrollo web.

La aplicación a diseñar podría haber sido otro motor de blogs, pero queremos usarSymfony en algo más útil. El objetivo es demostrar que Symfony puede ser usadopara desarrollar aplicaciones con estilo y poco esfuerzo.

Vamos a mantener el contenido del proyecto en secreto por un días más ya que

tenemos mucho para hacer por hoy. De todas formas, ya sabes el nombre de laaplicación: Jobeet.

Cada capítulo de este ñibro está destinado a durar entre una a dos horas, y será laocasión de aprender Symfony mediante la codificación de un sitio web real, deprincipio a fin. Cada día, serán agregadas nuevas características a la aplicación, ynos valdremos de las ventajas de este desarrollo para introducirte a las nuevasfuncionalidades de Symfony así como a las mejores prácticas de desarrollo webcon Symfony.

Este Libro es diferente

Recuerdo los primeros días de PHP4 ¡Ah, la Belle Epoque! PHP era uno de losprimeros lenguages dedicados a la web y uno de los más fáciles de aprender.

Pero con la rápida evolución de las tecnologías web, los desarrolladores web debenmantenerse al tanto de las últimas herramientas y mejores prácticas de desarrollo.Por supuesto que la mejor manera de aprender es leyendo blogs, tutoriales ylibros. Hemos leído un montón de estos, sea que estén escritos para PHP, Python,Java, Ruby o Perl, y muchos se quedan cortos cuando el autor comienza a mostrarfragmentos de código como ejemplos.

Probablemente estés acostumbrado a leer adevertencias de este tipo:

"Para una aplicación real, no te olvides de agregar validación y un manejo deerrores adecuado."

o

"La Seguridad es dejada como un ejercicio para el lector."

o

"Por supuesto que vas a necesitar escribir tests"

Page 2: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 2/273

 

¿Cómo? Estos temas son asuntos importantes. Son quizás la parte más importantede cualquier trozo de código. Como lector, te dejan solo. Si no nos preocupamospor proveer dicha información, los ejemplos resultan mucho menos útiles, nopudiendo utilizarlos como un buen punto de inicio. Eso no puede ser así, ya que laseguridad, la validación, 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, manejode errores, código para validación, y nos aseguraremos de desarrollar unaaplicación segura. Todo ello porque Symfony es sinónimo de programación, debuenas prácticas y de como desarrollar aplicaciones profesionales para la empresa.Podemos darnos dicho lujo porque Symfony provee todas las herramientasnecesarias para programar esos aspectos de forma fácil y sin la necesidad deescribir mucho código.

Debido a que la validación, el manejo de errores, la seguridad y los tests sonciudadanos 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 unframework para proyectos de la vida real.

Todo el código que leerás en este libro es código que puedes usar en un proyectode la vida real. Te invitamos a que copies y pegues el código en tus proyectos oque simplemente robes trozos completos del mismo.

¿Qué hacemos hoy?

Hoy no vamos a escribir código PHP. Pero incluso sin escribir una sola linea decódigo vas a entender los beneficios de utilizar un framework como Symfonysimplemente al configurar un nuevo proyecto.

El objetivo de este capítulo es configurar un entorno de desarrollo y mostrar unapágina web de la aplicación en el navegador web. Esto incluye la instalación deSymfony, la creación de una aplicación y la configuración del servidor web.

Pre requisitos.

Primero que nada, asegúrate de que cuentas con un entorno de desarrollo webfuncionando: 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 línea de comandos constantemente, es preferible utilizar unsistema 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 quisierasutilizar herramientas como tar, gzip, or grepen Windows puedes instalar Cygwin. Ladocumentación oficial puede ser un poco escasa pero una guía interesante puedeencontrarseaquí. Los aventureros también pueden probar con los Servicios Windowspara Unix. de Microsoft.

Page 3: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 3/273

 

Como este libro se concentra en el framework Symfony, asumimos que ya cuentascon un conocimiento sólido de PHP 5 y de Programación Orientada a Objetos.

Instalación 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\jobeetc:\> cd c:\development\sfprojects\jobeet

Se recomienda que los usuarios Windows configuren sus proyectos en una ruta sinespacios. Intenta no utilizar el directorioDocuments and Settings, incluyendocualquier ruta bajo Mis Documentos.

Crea un directorio para alojar los archivos del framework Symfony:$ mkdir -p lib/vendor

La página de instalación en el sitio web de Symfony listas y compara todas lasversiones disponibles de Symfony.

Ya que este libro fue escrito para Symfony 1.4, ve a la página de instalaciónde Symfony 1.4. 

Bajo la sección "Source Download", vas a encontrar el archivo en formato .tgz oen formato .zip. Descarga el archivo y colocarlo bajo el directorio lib/vendor queacabas 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 quehayas renombrado el directorio a symfony, la ruta debería ser lasiguiente: c:\development\sfprojects\jobeet\lib\vendor\symfony .

Como las configuraciones PHP varían mucho de una distribución a otra, tenemosque comprobar que tu configuración PHP cumple los requisitos mínimos deSymfony. Inicia el script de comprobación de la configuración que viene con

Symfony desde la línea de comandos:$ cd ../..$ php lib/vendor/symfony/data/bin/check_configuration.php

Page 4: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 4/273

 

Si hay un problema, la salida tedará consejos sobre cómosolucionarlo. También debesejecutar el script desde unnavegador ya que la configuración

PHP puede ser diferente. Copia elarchivo en algún lugar bajo eldirectorio raíz del servidor web yaccede al archivo. No te olvides dequitar el archivo del directorio raízweb después:

$ rm web/check_configuration.php

Si el script no muestra ningún error,comprueba que Symfony se ha

instalado correctamente usando la línea de comandos para mostrar la versión(nota la letra V mayúscula):

$ 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 línea de comandos puede hacerpor 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 línea de comandos de Symfony es la mejor amiga del programador. Te brindaun montón de utilidades para aumentar tu productividad en las actividades del díaa día como limpiar el cache, generar código y mucho más.

Configuración del Proyecto

En Symfony, las aplicaciones que comparten el mismo modelo de datos seagrupan en proyectos. Para el proyecto Jobeet, vamos a tener dos aplicaciones

diferentes: un frontend y un backend.

Creación del Proyecto

Desde el directorio jobeet, ejecuta la tarea symfony generate:project pararealmente crear el proyecto symfony:

$ php lib/vendor/symfony/data/bin/symfony generate:project jobeet

Page 5: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 5/273

 

En Windows:

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

La tarea generate:project genera por defecto la estructura de directorios yarchivos necesarios para un proyecto symfony:

Directorio Descripción

apps/ Hospeda todas las aplicaciones del proyecto

cache/ Los archivos en caché

config/ Los archivos de configuración del proyecto

lib/ Las bibliotecas y clases del proyecto

log/ Los archivos de registro

plugins/ Los plugins instaladostest/ Los archivos de pruebas unitarias y funcionales

web/ El directorio raíz web (véase más adelante)

¿Por qué Symfony genera tantos archivos? Uno de los principales beneficios de lautilización de un completo framework es normalizar tus desarrollos. Gracias aestructura predeterminada de archivos y directorios de Symfony, cualquierdesarrollador con algunos conocimientos de Symfony puede asumir el mantenimientode cualquier proyecto symfony. En cuestión de minutos, será capaz de bucear en elcódigo, corregir los errores, y añadir nuevas funciones.

La tarea generate:project también ha creado un atajo symfony en el directorioraíz del proyecto Jobeet para reducir el número de caracteres que tienes queescribir cuando se ejecuta una tarea.

Así, a partir de ahora, en lugar de utilizar la ruta completa al programa deSymfony, vamos a utilizar el atajo symfony.

Creación de una Aplicación

Ahora, crear la aplicación frontend ejecutando la tarea generate:app:

$ php symfony generate:app frontend

Como el archivo symfony es ejecutable, los usuarios de Unix puede reemplazar todaslas apariciones de 'php symfony' por './symfony' de ahora en adelante.

En Windows puedes copiar el archivo 'symfony.bat' a tu proyecto y usar 'symfony' enlugar de 'php symfony':

c:\> copy lib\vendor\symfony\data\bin\symfony.bat .

Una vez más, la tarea generate:app crea por defecto la estructura necesaria dedirectorios para una aplicación bajo el directorioapps/frontend:

Page 6: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 6/273

 

Directorio Descripción

config/ Los archivos de configuración de la aplicación

lib/ Las bibliotecas y clases de la aplicación

modules/ El código de la aplicación (MVC)

templates/ La plantilla global

Todos los comandos symfony debe ser ejecutados en el directorio raíz del proyecto amenos que se diga expresamente otra cosa.

Security

Por defecto, la tarea generate:app ha asegurado nuestro futuro desarrollo de las dosvulnerabilidades más extendidas se encuentran en la web. Así es, Symfonyautomáticamente toma las medidas de seguridad por nosotros.

Para evitar ataques XSS, el escapar la salida ha sido habilitado; y para prevenir ataqueCSRF, un aleatorio y secreto CSRF ha sido generado.

Por supuesto, puedes establecer estas configuraciones gracias a lassiguientes opciones:

  --escaping-strategy: Permite escapar la salida para evitar ataques XSS

  --csrf-secret: Permite tokens de sesión en los formularios para prevenir losataques CSRF

Si no sabes nada acerca de XSS o CSRF, date un tiempo para aprender más de estasvulnerabilidades de seguridad.

La Ruta de Symfony

Puede obtener la versión utilizada de Symfony por su proyecto escribiendo:

$ php symfony -V

La opción -V también muestra la ruta de acceso al directorio de instalación deSymfony, 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 instalación de Symfonypor una relativa:

// config/ProjectConfiguration.class.php require_oncedirname(__FILE__).'/../lib/vendor/symfony/lib/autoload/sfCoreAutoload.class.php';

Page 7: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 7/273

 

De esta manera, puedes mover el directorio del proyecto Jobeet a cualquier lugarde tu máquina u a otra, y éste funcionará.

Autoloading

La clase sfCoreAutoload es un autoloader|Autoload~ de PHP usado para cargar el

nucléo de clases de Symfony a pedido (on demand). Por defecto, Symfony tambiénregistra otros autoloader para automáticamente cargar las clases guardadas en eldirectorio lib/. Esto significa que nunca necesitarás usar un require en tu código.Este es uno de las numerosas cosas que Symfony automatiza por el desarrollador.

Los Entornos

Si revisas el directorio web/, encontrarás dos archivosPHP: index.php y frontend_dev.php. Estos archivos son llamados controladoresfrontales: todas las peticiones a la aplicación se hacen a través de ellos. Pero,¿por qué tenemos dos controladores frontales si hemos definido sólo unaaplicación?

Ambos archivos apuntan a la misma aplicación pero para distintos entornos.Cuando desarrollas una aplicación, excepto si desarrollas directamente en elservidor de producción, necesitas varios entornos:

  El entorno de desarrollo: Este es el ambiente utilizadopor desarrolladores web para añadir nuevas funciones, corregir loserrores, ...

  El entorno de prueba: Este entorno se utiliza para probarautomáticamente la aplicación.

  El entorno staging: Este entorno es utilizado por el cliente para poner aprueba la aplicación e informar errores o características faltantes.

  El entorno de producción: Este es el entorno donde un usuariofinal interactúa.

¿Qué hace que un entorno sea único? En el entorno de desarrollo, la aplicaciónnecesita registrar todos los detalles de una petición para facilitar la depuración,debe mostrar la excepción en el navegador, pero la cache debe ser deshabilitadapara que todos los cambios realizados al código se tengan en cuenta de inmediato.El entorno de desarrollo debe ser optimizado para el desarrollador:

Page 8: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 8/273

 

 

En el entorno de la producción, la aplicación deberá mostrar mensajes de errorpersonalizados en lugar de excepciones, y por supuesto, la capa del cache debeestar activada. El entorno de producción debe ser optimizado para el rendimiento yla experiencia del usuario final.

Un entorno symfony es un conjunto único de ajustes de configuración. El

framework Symfony incluye tres de ellos: dev, test, y prod. En el capítulo 21,aprenderás como crear nuevos ambientes, tales como el staging.

Si abres los archivos de los controladores frontales, verás que la única diferenciaes 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 controladorfrontal. Veremos más adelante cómo cambiar la configuración de un entorno.

Page 9: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 9/273

 

Configuración del Servidor Web: La Forma Fea

En la sección anterior, un directorio se ha creado para alojar el proyecto Jobeet. Silo creaste bajo el directorio "raíz web" de tu servidor web, ya puedes acceder alproyecto en un navegador web.

Por supuesto, como no hay ninguna configuración, es muy rápido para establecer,pero intenta tener acceso al archivoconfig/databases.yml en tu navegador ycomprederás las malas consecuencias de esta actitud perezosa. Si el usuarioconoce que tu sitio web esta desarrollado con Symfony, él tendrá acceso a unmontón de archivos delicados.

Configuración del Servidor Web: La forma segura

Una buena práctica web es poner bajo el directorio raíz web sólo los archivos a losque necesita tener acceso el navegador web: las hojas de estilo, JavaScripts, oimágenes. Te recomendamos almacenar estos archivos en el subdirectorio web deun proyecto symfony.

Si echas un vistazo a este directorio, encontrarás algunos sub-directorios para losrecursos web y los dos archivos de los controladores frontales. Los controladoresfrontales son los únicos archivos PHP que necesitan estar bajo el directorio raízweb. Todos los demás archivos PHP se pueden ocultar del navegador, la cual esuna buena idea en lo que respecta a seguridad.

La configuración del Servidor Web

Ahora es el momento de cambiar tu configuración de Apache para que el nuevoproyecto sea accesible para el mundo.

Busca y abre el archivo de configuración httpd.conf y añade la siguienteconfiguración al final:

# Asegúrate de tener sólo una vez esta línea en su configuraciónNameVirtualHost 127.0.0.1:8080

# Esta es la configuración de JobeetListen 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 AllAllow 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 AllAllow from All

Page 10: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 10/273

 

</Directory></VirtualHost>

El alias /sf te da acceso a las imágenes y los archivos JavaScript necesarios paraadecuadamente mostrar las páginas symfony por defecto y la barra de herramientasde depuración 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 debería ser sustituida por:

c:\development\sfprojects\jobeet\web

En esta configuración, Apache escucha en el puerto 8080 de tu máquina, por lo queel sitio web de Jobeet será accesible en la siguiente URL:

http://localhost:8080/

Puedes cambiar 8080 por cualquier número mayor que 1024, ya que no serequieren 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 deañadir un nuevo puerto cada vez que se inicia un nuevo proyecto. En lugar de añadirun puerto y agregar una declaración Listen, elige un nombre de dominio y añade ladeclaraciónServerName:

# 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 seejecuta un sistema Linux, que tiene que hacerse en el archivo /etc/hosts. Si ejecutaWindows XP, este archivo se encuentra en eldirectorio C:\WINDOWS\system32\drivers\etc\.

Añade la siguiente línea:

127.0.0.1 jobeet.localhost

Tip Nota del Traductor Cuando

se complican con estos pasos, losusuarios de Distribuciones Linux,como Ubuntu, pueden usar unaherramienta gráfica que simplique aúnmás esta configuración. Rapache, es unsoftware para sistemas Gnome quehace la configuración básica ynecesaria, en los

Page 11: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 11/273

 

archivos /etc/hosts y httpd.conf, en un solo paso además de permitir el reinicio deApache con un clic.

Probar la nueva configuración

Reinicia Apache, y comprueba que ahora tienes acceso a la nueva aplicación

abriendo un navegador y escribiendo http://localhost:8080/index.php/,o http://jobeet.localhost/index.php/ dependiendo de la configuración deApache que has elegido en la sección anterior.

Si tienes el móduloApache mod_rewrite instalado, puedesremover la parte /index.php/ de todas lasURL. Esto es posible gracias a las reglas dereescritura configuradas en elarchivo web/.htaccess.

Deberías tratar de acceder a la aplicaciónen el entorno de desarrollo. Escribe lasiguiente URL:

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

La web debug toolbar o barra de herramientas de depuración web deberíamostrarse en la esquina superior derecha, incluidos los iconos, demostrando quetu configuración alias sf/ es correcta.

Subversion

Es una buena práctica utilizar control de versiones de código fuente en eldesarrollo de una aplicación web. Usar el control de versiones de código fuente nospermitirá:

  trabajar con confianza

  volver a una versión anterior si un cambio rompe algo

  permitir a más de una persona para trabajar eficientemente en el proyecto

  tener acceso a todas las versiones sucesivas de la aplicación

En esta sección, vamos a describir cómo utilizar Subversion con Symfony. Siutilizas otra herramienta de control de código fuente, debe ser muy fácil deadaptar según lo descripto para Subversion.

Suponemos que ya tienes acceso a un servidor Subversion.

Si no tienes un servidor Subversion a tu disposición, puedes crear uno gratis en GoogleCode o solo escribir "free subversion repository" en Google para tener muchas másopciones.

Page 12: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 12/273

 

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

$ svnadmin create /path/to/jobeet/repository

En tu máquina, crea la estructura básica de directorios:

$ svn mkdir -m "created default directory structure"

http://svn.example.com/jobeet/trunk http://svn.example.com/jobeet/tagshttp://svn.example.com/jobeet/branches

Y comprueba el directorio vacío trunk/ :

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

Entonces, elimina el contenido de los directorios cache/ y log/ porque noqueremos ponerlos en el repositorio.

$ rm -rf cache/* log/*

Ahora, asegúrate 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 losdirectorios cache/ y /log, es necesario especificar una lista de ignorados:

$ svn propedit svn:ignore cache

El editor de texto por defecto configurado para SVN debería ejecutarse. Subversiondebe 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 paragestionar sus repositorio de Subversion. Los usuarios de Linux tienen muchas opcionesy una de ellas es utilizar el excelente cliente Rapidsvn para gestionar sus repositorio deSubversion.

Page 13: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 13/273

 

El Proyecto

o hemos escrito una sola línea de PHP aún, pero ayer, configuramos el entorno,creamos un proyecto symfony vacío, y nos aseguramos de iniciar con una buenaseguridad por defecto. Si seguiste todo, tienes que buscar en la pantalla de

entonces, ya que se muestra la hermosa página por defecto de symfony para lasnuevas aplicaciones.

Pero quieres más. ¿Quieres sabertodos los detalles de symfony para eldesarrollo de aplicaciones? Por lotanto, vamos a reanudar nuestro viajeal nirvana del desarrollo symfony.

Hoy, nos tomaremos el tiempo para

describir los requisitos del proyectoJobeet con algunos básicos mockups(diseños).

El Foco del Proyecto

Todo el mundo está hablando de la crisis hoy en día. El desempleo estáaumentando de nuevo.

Lo sé, los desarrolladores symfony no están realmente interesados y esto esporque quieren aprender symfony en primer lugar. Pero también es bastante difícilencontrar desarrolladores symfony buenos.

¿Dónde puedes encontrar un desarrollador symfony? ¿Dónde puedes anunciar tushabilidades symfony?

Necesitas encontrar una buena Bolsa de Trabajo. ¿Monster dices? Piensa de nuevo.Necesitas una Bolsa especializada. Una donde puedas encontrar a las mejorespersonas, los expertos. Una donde sea fácil, rápido y divertido buscar un puesto detrabajo, u ofrecer uno.

No busques más. Jobeet es el lugar. Jobeet es un software Open-Source paraBolsas de Trabajo que sólo hace una cosa, pero la hace bien. Es fácil de usar,personalizar, ampliar, e incluir en tu sitio web. Soporta múltiples idiomas, y utilizalas últimas tecnologías Web 2.0 para mejorar la experiencia del usuario. También

proporciona feeds y una API para interactuar con él programáticamente.

¿Ya existe? Como usuario, encontrarás un montón de Bolsas de Trabajo comoJobeet en Internet. Pero trata de encontrar una que sea Open-Source (CódigoAbierto), y con especiales características como las que proponemos aquí.

Si realmente estás buscando un empleo con symfony o quieres convertirte en undesarrollador symfony, puedes ir al sitio websymfonians. 

Page 14: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 14/273

 

Los Casos de Uso del Proyecto

Antes de meternos en el código de cabeza, vamos a describir el proyecto un pocomás. Las secciones siguientes describen las características que queremos aplicaren la primera versión / iteración del proyecto con algunos Casos de Uso sencillos.

El sitio web Jobeet tiene cuatro tipo de usuarios:  administrador: Él es el propietario de la página web y tiene poderes

mágicos

  user: Visita la página web para buscar un puesto de trabajo y se postulapara uno

  poster: Visita la página web para envíar/ofrecer un puesto de trabajo

  affiliate: El re-publica algunos trabajos en su página web

El proyecto tiene dos aplicaciones:el frontend (Casos de Uso F1 a F7, que

están más abajo), donde los usuariosinteractúan con el sitio web, yel backend (Casos de Uso B1 a B3), dondelos administradores gestionan el sitio web.

La aplicación backend tiene seguridad yrequiere de credenciales para acceder.

Caso de Uso F1: En la página principal,los usuarios ven los últimos puestos detrabajo activos.

Cuando un usuario entra a la página webJobeet, ve una lista de los puestos detrabajo activos. Los puestos de trabajo se clasifican por categoría y a continuación,por fecha de publicación (los nuevos puestos de trabajo primero). Para cada

puesto de trabajo, sólo la ubicación, laposición, y la empresa se muestran.

Para cada categoría, la lista sólomuestra los primeros 10 puestos detrabajo y un enlace permite listar todoslos puestos de trabajo para unacategoría determinada (Caso de Uso

F2).

En la página principal, el usuario puederefinar la lista de puestos (Caso de UsoF2), o enviar un nuevo puesto detrabajo (Caso de Uso F5 ).

Page 15: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 15/273

 

Caso de Uso F2: Un usuario puede solicitar todos los puestos de trabajo deuna categoría determinada

Cuando un usuario hace clic en el nombre de una categoría o en un enlace "more jobs" (más trabajos) en la página de inicio, verá todos los puestos de trabajo paraesta categoría ordenados por fecha.

Nota del Traductor Trataremos de hacer la traducciones pertinentes de los textos delas plantillas (por ejemplo, más trabajos), para así poder mantener éstas identicas alas originales del inglés.

La lista está paginada, con 20 puestos de trabajo por página.

Caso de Uso F3: Un usuario refina lalista con algunas palabras clave

El usuario puede introducir algunaspalabras clave para refinar su búsqueda.Las palabras clave pueden ser palabras seencuentra en los campos de la ubicación,la posición, la categoría, y de la companía.

Caso de Uso F4: Un usuario hace clicen un puesto de trabajo para verinformación más detallada

El usuario puede seleccionar un trabajo dela lista para ver información más

detallada.

Caso de Uso F5: Un usuario envía unpuesto de trabajo

Un usuario puede envíar un puesto detrabajo. Un puesto de trabajo estáformado por varias partes de información:

  Compañía

 Tipo (full-time, part-time, ofreelance)

  Logo (opcional)

  URL (opcional)

  Posición

  Ubicación

  Categoría (el usuario elige una de

Page 16: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 16/273

 

una lista de posibles categorías)

  Descripción del trabajo (URL y correos electrónicos son enlazados de formaautomática)

  Cómo aplicar (URL y correos electrónicos son enlazados de formaautomática)

  Público (si el trabajo también 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 sólo dos pasos: en primer lugar, el usuario rellena elformulario con toda la información necesaria para describir el trabajo y, acontinuación, se valida la información con una vista previa de la página de empleofinal.

Incluso si el usuario no tiene cuenta, un puesto de trabajo pueden ser modificadodespués, gracias a un URL concreto (protegido por un token dado al usuariocuando crea el puesto de trabajo).

Cada puesto de trabajo está en línea durante 30 días (esto es configurable por eladministrador - ver (Caso de Uso B2). Un usuario puede volver a activar yextender la validez de un puesto de trabajo por 30 días extra pero sólo cuando eltrabajo expira y entonces tiene menos de 5 días 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 información:  Nombre

  Email

  URL del sitio web

La cuenta de afiliado debe ser activada por el administrador (Caso de Uso B3). Unavez activada, el afiliado recibe un token via email para poder usar la API.

Cuando se registra, el afiliado puede también elegir los puestos de trabajo aobtener de un sub-conjunto de las categorías disponibles.

Caso de Uso F7: Un afiliado recupera la lista activa de puestos de trabajoUn afiliado puede recuperar la actual lista de puestos de trabajo llamando a la APIcon su token de afiliado. La lista puede ser devuelta en formato XML, JSON oYAML.

La lista contiene la información pública disponible para un puesto de trabajo.

El afiliado también puede limitar el número de puestos de trabajo a ser devueltos,y refinar su consulta especificando una categoría.

Page 17: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 17/273

 

Caso de Uso B1: Un administrador configura el sitio web

Un administrador puede modificar las categorías disponibles en el sitio web.

También puede hacer algunos ajustes:

  El número máximo de puestos de trabajo que figura en la página de inicio

  Idioma de la página web

  Número de días que un trabajo está en línea

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 laactivación de un afiliado y también puede desactivarlo.

Cuando el administrador activa un nuevo afiliado, el sistema crea un único tokenpara ser utilizado por ese afiliado.

Page 18: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 18/273

 

El Modelo y la base de datos

Aquellos de ustedes locos por abrir el editor de texto y hacer algo de PHP estaránencantados 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 módulo de la aplicación. Perocomo Symfony hace una gran parte del trabajo por nosotros, vamos a tener unmódulo web totalmente funcional sin tener que escribir mucho código PHP.

El Modelo Relacional

Los casos de uso que escribimos ayer describen los principales objetos de nuestroproyecto: puestos de trabajo, afiliados, y las categorías. Aquí está elcorrespondiente diagrama de entidad relación:

Además de las columnas descritas en los casos de uso, también hemos añadido un

campo created_at a algunas tablas. Symfony reconoce estos campos y estableceel valor de la hora actual de sistema cuando un registro es creado. Esto es lomismo para el campoupdated_at: su valor se establece siempre a la hora delsistema en que el registro se actualiza.

El Esquema

Para almacenar los puestos de trabajo, los afiliados, y las categorías, es evidenteque necesitamos una base de datos relacional.

Pero como Symfony es un Framework Orientado a Objetos, nos gustaría manipularlos objetos cada vez que podamos. Por ejemplo, en lugar de escribir sentencias

SQL para recuperar los registros de la base de datos, preferimos más usar losobjetos.

La información de la base de datos relacional debe ser mapeada a un modelo deobjetos. Esto se puede hacer con una herramienta ORM, o Mapeador, yafortunadamente, Symfony tiene incluido dos de ellas: Propel y Doctrine. En estetutorial, usaremos Doctrine.

Page 19: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 19/273

 

El ORM necesita una descripción de las tablas y sus relaciones para crear las clasesrelacionadas. Hay dos maneras de crear este esquema de descripción: ingenieríareversa de una base de datos existente o creándolo a mano.

Como la base de datos no existe todavía, y como queremos mantener la base dedatos Jobeet agnóstica, vamos a crear el archivo de esquema a mano editando el

archivo vacío 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: JobeetCategoryrefClass: JobeetCategoryAffiliatelocal: affiliate_idforeign: category_idforeignAlias: JobeetAffiliates

Page 20: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 20/273

 

JobeetCategoryAffiliate:columns:category_id: { type: integer, primary: true }affiliate_id: { type: integer, primary: true }

relations:JobeetCategory: { onDelete: CASCADE, local: category_id, foreign: id

}JobeetAffiliate: { onDelete: CASCADE, local: affiliate_id, foreign:

id }

Si decidiste crear las tablas escribiendo las sentencias SQL, puedes generar el archivode configuración 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 enel databases.yml. Te mostraremos como configurar la base de datos en un pasoposterior. 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 traducción directa de un diagrama de entidad relación en formatoYAML.

El Formato YAML

De acuerdo con el sitio web oficial YAML, YAML es "es una serialización de datosestándar muy amigable para todos los lenguajes de programación"

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 través de la sangría, la secuencia de elementos sedenotan por un guión, y los pares clave/valor están separados por dos puntos. YAMLtambién tiene una sintaxis abreviada para describir la misma estructura con menoslíneas, donde los arrays explícitamente se muestran con [] y los hashes o arrayasociativos con {}.

Si todavía no están familiarizados con YAML, es hora de empezar con él pues elframework Symfony lo utiliza ampliamente para sus archivos de configuración. Unbuen punto de partida es la documentación del componente YAML de Symfony.

Hay una cosa importante que necesitas recordar cuando estás editando un archivoYAML: la indentación debe hacerse con uno o mas espacios en blanco, pero

nunca con tabulaciones.

El archivo schema.yml contiene la descripción de todas las tablas y sus columnas.Cada columna se describe con la siguiente información:

  type: El tipo de columna(boolean, integer, float, decimal, string, array, object, blob, clob, timestamp, time, date, enum,gzip)

  notnull: Es true si deseas que la columna sea obligadoria

Page 21: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 21/273

 

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

El atributo onDelete define el comportamiento ON DELETE para claves foráneas, yDoctrine da soporte para CASCADE, SETNULL, yRESTRICT. Por ejemplo, cuando un

registro de job es borrado, todos los registros jobeet_category_affiliate relacionados serán automáticamente eliminados de la base de datos

Nota del Traductor Si bien no es probable, es posible que se quiera eliminar unacategoría completa del sistema.

El campo category_id de la entidad JobeetJob, tiene al final añadida la opciónonDelete. La misma está establecida como CASCADE, para determinar que cuando una

categoría sea eliminada, tambien se eliminen los registros job (los puestos detrabajo).

Esto se debe a que los registros job tienen establecido como campo obligatorio a la

categoría asociada (a la que pertenece), por lo que no sería correcto dejar huerfano aun registro, ni establecer éste valor a null.

La Base De Datos

El framework Symfony soporta todas las Base De Datos soportadaspor PDO (MySQL, PostgreSQL, SQLite, Oracle, MSSQL, ...). PDO es la capa deabstracción de base de datos que viene con PHP.

Vamos a usar MySQL para este tutorial:

$ mysqladmin -uroot -p create jobeetEnter password: mYsEcret ## La clave se mostrará como ********

Eres libre de elegir otro motor de base de datos si lo deseas. No será difícil adaptar el

código que vamos a escribir ya que vamos a utilizar el ORM quien será quien escriba elSQL 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 deusuario, y la clave de acceso a la base de datos. Si no tienes ninguna contraseñade la base en el servidor de desarrollo, basta con omitir el tercer argumento.

La tarea configure:database almacena la configuración de la base de datos en el

archivo de configuración config/databases.yml. En lugar de utilizar la tarea, puedeseditar este archivo manualmente.

Pasar la clave de la base de datos por linea de comandos es convenientepero inseguro. Dependiendo de quienes tiene acceso a tu entorno, podría ser mejoreditar el config/databases.yml para cambiar la clave. Desde luego, para mantener la

clave a salvo, el acceso al archivo de configuración debería también ser restringido.

Page 22: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 22/273

 

El ORM

Gracias al archivo de descripción de la base de datos schema.yml, podemos utilizaralgunas de las tareas de Doctrine para generar los comandos SQL necesarios paracrear tablas de la base de datos:

Primero para generar el SQL debes contruir el modelo apartir de tus archivosesquema.

$ 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.sqlCREATE TABLE jobeet_category (id BIGINT AUTO_INCREMENT, name VARCHAR(255)

NOT NULL COMMENT 'test', created_at DATETIME, updated_at DATETIME, slugVARCHAR(255), UNIQUE INDEX sluggable_idx (slug), PRIMARY KEY(id))ENGINE = INNODB;

Para realmente crear las tablas en la base de datos, necesitas ejecutar latarea doctrine:insert-sql:

$ php symfony doctrine:insert-sql

Como con cualquier herramienta de línea de comandos, las tareas symfony puedentener argumentos y opciones. Cada tarea viene con un mensaje de ayuda que sepuede 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 valorespor defecto para cada uno de ellos, y proporciona algunos ejemplos útiles.

El ORM también genera las clases PHP que mapea los registros de la tabla con losobjetos:

$ php symfony doctrine:build --model

La tarea doctrine:build --model genera archivos PHP en eldirectorio lib/model/ que se pueden utilizar para interactuar con la base de datos.Navegando por los archivos generados, probablemente habrás notado queDoctrine genera cuatro clases por tabla. Para la tablajobeet_job:

  JobeetJob: Un objeto de esta clase representa un único registro de latabla jobeet_job. La clase está vacía por defecto.

  BaseJobeetJob: La clase padre de JobeetJob. Cada vez queejecutas doctrine:build --model, esta clase es sobreescrita, por lo quetodas las personalizaciones se deben hacer en la clase JobeetJob.

  JobeetJobTable: La clase define los métodos que mayormente devuelvecolecciones de objetos JobeetJob. La clase está vacía por defecto.

Page 23: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 23/273

 

Los valores de las columnas de un registro se pueden manipular con el modelo deobjetos mediante el uso de algunos métodos get*()y métodos set*():

$job = new JobeetJob();$job->setPosition('Web developer');$job->save();

echo $job->getPosition();

$job->delete();

También puedes definir claves foráneas directamente por la vinculación deobjetos:

$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 hanejecutado en esta sección y algunas más. Por lo tanto, ejecuta esta tarea ahorapara generar formularios y validadores para el modelo de clases de Jobeet:

$ php symfony doctrine:build --all --no-confirmation

Verás los validadores en acción al final del día y los formularios se explicarán engran detalle en el día 10.

Los Datos Iniciales

Los tablas se han creado en la base de datos, pero no hay datos en ellas. Para

cualquier aplicación web, hay tres tipos de datos:  Datos Iniciales: Los datos iniciales son necesarios para que la aplicación

funcione. Por ejemplo, Jobeet necesita algunas categorías iniciales. Si no,nadie será capaz de envíar un puesto de trabajo. También necesitamos unadministrador de usuarios para poder acceder al backend.

  Datos de Prueba: Los datos de prueba son necesarios para que laaplicación sea probada. Como desarrollador, escribes pruebas paraasegurarte que Jobeet se comporta como se describe en los casos de uso, yla mejor manera es escribir pruebas automáticas. Por lo tanto, cada vezque 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 aplicación.

Cada vez que Symfony crea las tablas en la base de datos, todos los datos sepierden. Para rellenar la base de datos con algunos los datos iniciales, podriamoscrear un script PHP, o ejecutar sentencias SQL con algun programa mysql. Perocomo la necesidad es bastante común, hay una mejor manera con Symfony: crear

Page 24: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 24/273

 

archivos YAML en el directorio data/fixtures/ y usar la tareadoctrine:data-load para cargarlos en la base de datos.

Primero, crea los siguientes archivos de datos:

# data/fixtures/categories.ymlJobeetCategory:design:name: Design

programming:name: Programming

manager:name: Manager

administrator:name: Administrator

# data/fixtures/jobs.ymlJobeetJob:job_sensio_labs:JobeetCategory: programmingtype: full-timecompany: Sensio Labslogo: sensio-labs.gifurl: http://www.sensiolabs.com/position: Web Developerlocation: Paris, Francedescription: |

You've already developed websites with symfony and you want to workwith Open-Source technologies. You have a minimum of 3 yearsexperience in web development with PHP or Java and you wish to

participate to development of Web 2.0 sites using the bestframeworks available.how_to_apply: |

Send your resume to fabien.potencier [at] sensio.comis_public: trueis_activated: truetoken: job_sensio_labsemail: [email protected]_at: '2008-10-10'

job_extreme_sensio:JobeetCategory: design

type: part-timecompany: Extreme Sensiologo: extreme-sensio.gifurl: http://www.extreme-sensio.com/position: Web Designerlocation: Paris, Francedescription: |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed doeiusmod tempor incididunt ut labore et dolore magna aliqua. Ut

Page 25: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 25/273

 

enim ad minim veniam, quis nostrud exercitation ullamco laborisnisi ut aliquip ex ea commodo consequat. Duis aute irure dolorin 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.comis_public: trueis_activated: truetoken: job_extreme_sensioemail: [email protected]_at: '2008-10-10'

El archivo de datos de puestos de trabajo hace referencia a dos imágenes. Puedesdescargarlas de(http://www.symfony-project.org/get/jobeet/sensio-

labs.gif, http://www.symfony-project.org/get/jobeet/extreme-sensio.gif ) ycolocarlas 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 etiquetadoscon job_sensio_labs y job_extreme_sensio). Estas etiquetas son de gran utilidadpara vincular objetos relacionados sin tener que definir claves primarias (que amenudo son auto-incrementales y no se pueden establecer). Por ejemplo, lacategoría de job_sensio_labs es programming, que es la etiqueta dada a lacategoría '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 labarra vertical (|) para indicar que la cadena aparecerá en varias lineas.

A través 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 númerico que determine elorden en el cual los archivos serán cargados. Con Doctrine esto no es necesario ya quetodos los archivos serán cargados y guardados en el correcto orden para asegurarque 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 debase de datos. Y como Symfony usa Doctrine para cargar los datos en la base dedatos, todos los comportamientos incorporados (como el seteo automáticoa created_at o updated_at), o los comportamientos personalizados que puedeshaber agregado al modelo de las clases son activados.

La carga de los datos iniciales en la base de datos es tan simple como ejecutar latarea doctrine:data-load:

Page 26: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 26/273

 

$ php symfony doctrine:data-load

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

Ejecuta la tarea doctrine:build --all --and-load para asegurarte que todo esgenerado 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 acción en el Navegador

Hemos utilizado la interfaz de línea de comandos mucho, pero eso no es realmenteemocionante, especialmente para un proyecto web. Ahora tenemos todo lo quenecesitamos para crear páginas Web que interactúan con la base de datos.

Vamos a ver cómo mostrar la lista de puestos de trabajo, cómo editar un trabajo,y cómo eliminar un puesto de trabajo. Como se explicó durante el día 1, un

proyecto symfony se hace de las aplicaciones. Cada aplicación está divididaen módulos. Un módulo es un conjunto de código PHP auto-contenido querepresenta una característica de la aplicación (el módulo API, por ejemplo), o unconjunto de manipulaciones que el usuario puede hacer sobre un objeto delmodelo (un módulo job, por ejemplo).

Symfony es capaz de generar automáticamente un módulo para un determinadomodelo que proporciona las características básicas de manipulación:

$ php symfony doctrine:generate-module --with-show --non-verbose-templates frontend job JobeetJob

La tarea doctrine:generate-module genera módulo job en la

aplicación frontend para el modelo JobeetJob. Como con la mayoría de las tareassymfony, algunos archivos y directorios se han creado para ti bajo eldirectorio apps/frontend/modules/job/:

Directorio Descripción

actions/  Acciones del módulo

templates/  Plantillas del módulo

El archivo actions/actions.class.php define todas las acciones disponibles parael módulo job:

Nombre de la

acción

Descripción

index  Muestra los registros de la tabla

show  Muestra los campos de un registro

Page 27: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 27/273

 

Nombre de la

acción

Descripción

new  Muestra un formulario para crear un nuevo registro

create  Crea un nuevo registroedit  Muestra un formulario para crear editar un registro

existente

update  Actualiza un registro con los valores que envió el usuario

delete  Borra un registro de la tabla

Ahora puedes probar el módulo job en un navegador:

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

Si intentas editar un puesto de trabajo, te darás cuenta que el combo Category idtiene una lista

de todos los nombres de las categorías. El valor de cada opción esta dado por elmétodo __toString().

Doctrine tratará de dar un método base __toString() adivinandolo de unacolumna descriptiva de nombre como ser, title, name,subject, etc. Si quieresalgo distinto entonces necesitas agregar tus propios métodos __toString() comose ve más abajo. El modeloJobeetCategory esta listo para adivinar elmétodo __toString() usando la columna name de la tabla jobeet_category.

Page 28: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 28/273

 

// 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 campoobligatorio en blanco, o tratar de dar una fecha no válida. Así es, Symfony hacreado básicas reglas de validación analizando el esquema de base de datos.

Page 29: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 29/273

 

 

El Controlador y la Vista

hemos explorado cómo Symfony simplifica la gestión de bases de datos por

abstracción entre los diferentes motores de bases de datos, y mediante laconversión de elementos relacionales con útiles clases orientadas a objetos.También 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 módulo básico job creado ayer. Elmódulo job existente tiene todo el código que necesitamos para Jobeet:

  Una página que lista todos los puestos de trabajo

  Una página para crear un nuevo puesto de trabajo

  Una página para actualizar un puesto de trabajo existente

  Una página para eliminar un puesto de trabajoAunque el código está listo para ser utilizado como esta, vamos a refactorizar lasplantillas para adaptarlas lo más cerca a los mockups Jobeet.

La arquitectura MVC

Si estás desarrollando con PHP sitios web sin ningún framework, probablementeuses el paradigma de un archivo PHP por página HTML. Estos archivos PHPprobablemente contengan el mismo tipo de estructura: inicialización yconfiguración global, lógica de negocio relacionada con la página solicitada,busqueda de registros en la base, y finalmente el código HTML que arma la página.

Puedes utilizar un motor de plantillas para separar la lógica del HTML. Tal vezutilizas una capa de abstracción de la base de datos para separar el modelo de lalógica de negocio. Sin embargo, la mayoría de las veces, terminas con un montónde código que es una pesadilla para mantener. Es rápido para construir, pero conel tiempo, es más y más difícil de hacer cambios, especialmente porque nadie,excepto tú entiende cómo se construye y cómo funciona.

Al igual que con todos los problemas, hay soluciones agradables. Para desarrolloweb, la solución más común para la organización de su código de hoy en día esel patrón de diseño MVC. En resumen, el patrón de diseño MVC define unamanera de organizar el código de acuerdo a su naturaleza. Este patrón separa el

código en tres capas:

  La capa Modelo define la lógica de negocio (la base de datos pertenece aesta capa). Ya sabes que Symfony guarda todas las clases y archivosrelacionados con el modelo en el directorio lib/model/.

Page 30: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 30/273

 

  La Vista es con lo que elusuario interactúa (un motor deplantillas es parte de esta capa).En Symfony, la vista esprincipalmente la capa de plantillas

PHP. Estas son guardadas envarios directorios templates/ comoveremos más adelante en el día dehoy.

  El Controlador es la piezade código que llama al Modelo paraobtener algunos datos que le pasaa la Vista para la presentación al

cliente. Cuando instalamos Symfony el primer día, vimos que todas lassolicitudes son gestionadas por un controlador frontal(index.php

yfrontend_dev.php

). Estos controladores frontales delegadanla verdadera labor a las acciones. Como vimos ayer, estas acciones son,lógicamente, agrupadas en módulos.

Hoy, usaremos el mockup definido el día 2 para personalizar la página principal yla página de puestos de trabajos. Vamos hacerlas dinámicas. A lo largo delcamino, vamos a modificar un montón de cosas en diferentes archivos parademostrar la estructura de directorios symfony y la forma de separar el códigoentre las capas.

El diseñoEn primer lugar, si miraste de cerca los mockups, te darás cuenta de que granparte de cada una de las páginas tiene el mismo aspecto. Ya sabes que laduplicación de código esta mal, ya sea si estamos hablando de código HTML o PHP,por lo que necesitamos encontrar una manera de prevenir estos elementos devista común resultantes de la duplicación de código.

Una forma de resolver el problema es definir un encabezado y un pie de página ylo incluyes en cada plantilla:

Pero los archivos de la cabecera y el pie de página no contienen HTML válido. Debehaber una mejor manera. En lugar de reinventar la rueda, vamos a utilizar otropatrón de diseño para resolver este problema: el patrón de diseño decorador. Elpatrón de diseño decorador resuelve el problema al revés: la plantilla es decorada

Page 31: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 31/273

 

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

El layout de una aplicación se llama layout.php y se puede encontrar en eldirectorio apps/frontend/templates/. Este directorio contiene todas las plantillasglobales para una aplicación.

Reemplaza el layout por defecto de Symfony por el siguiente código:

<!-- 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>

Page 32: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 32/273

 

</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 sólo un simple archivo PHP. En la plantilla layout, verásllamadas a funciones PHP y referencias a variables PHP.$sf_content es la variablemás interesante: la define el mismo framework y contiene el código HTMLgenerado por la acción.

Page 33: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 33/273

 

Si navegas elmódulo job (http://www.jobeet.com.localhost/frontend_dev.php/job), verásque todas las acciones ahora son decoradas por el layout.

Las Hojas de Estilo,

Imágenes, y JavaScriptsYa que este tutorial no es acercade diseño web, tenemos yapreparado todo lo necesario queusaremos para Jobeet: descargalos archivo gráficos y copiarlosdentro deldirectorio web/images/; descargalos archivo de hojas de estilo ycopiarlos dentro deldirectorioweb/css/.

En el layout, hemos incluídoun favicon. Puedes descargarlo deJobeet y ponerlo bajo eldirectorio web/.

Por defecto, la tarea generate:project ha creado tres directorios para los recusos deproject: web/images/ para las imágenes,web/css/ para las hojas de estilo,y web/js/ para los Javascripts. Esta es una de las muchas convenciones definidas porSymfony, 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 mencionadoen cualquier lugar del layout por defecto, está sin duda presentes en el códigoHTML generado. Pero no los otros. ¿Cómo es esto posible? El archivo de estilo seha incluido por la llamada a la función include_stylesheets() que encuentra laetiqueta <head>. La función include_stylesheets() es llamada un helper. Unhelper es una función, definida por Symfony, que puede tener parámetros ydevolver código HTML. La mayoría de las veces, los helpers te ahorran tiempo,ellos empaquetan código en snippets (porciones de código) utilizados confrecuencia en las plantillas. El helperinclude_stylesheets() genera unaetiqueta <link> para las hojas de estilo.

Pero, ¿cómo hace el helper para saber que hojas de estilo incluir?

La capa de la Vista se puede configurar editando el archivo deconfiguración view.yml de la aplicación. Aquí está el archivo por defecto generadopor la tarea generate:app:

# apps/frontend/config/view.ymldefault:http_metas:

Page 34: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 34/273

 

content-type: text/html

metas:#title: symfony project#description: symfony project#keywords: symfony, project

#language: en#robots: index, follow

stylesheets: [main.css]

javascripts: []

has_layout: truelayout: layout

El archivo view.yml establece la configuración por defecto para todas lasplantillas de la aplicación. Por ejemplo, para las hojas de estilo define un array

de archivos de estilo para incluir en todas las páginas de la aplicación (la inclusiónse hace por el helperinclude_stylesheets()).

En el archivo de configuración view.yml por defecto, se hace referencia alarchivo main.css, y no al /css/main.css. Como cuestión de hecho, ambasdefiniciones son equivalentes pues el prefijo relativo symfony es /css/.

Si muchos archivos se definen, Symfony los incluye en el mismo orden que ladefinición:

stylesheets: [main.css, jobs.css, job.css]

También puedes cambiar el atributo media y omitir el sufijo .css:

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

Esta configuración 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 configuración view.yml también define el layout por defecto usado por laaplicación. Por defecto, el nombre es layout, y así Symfony decora cada página con elarchivo layout.php. Puedes también deshabilitar el proceso de decoración completocambiando el valor de has_layout a false.

Funciona como está pero el archivo jobs.css es solo necesario en la páginaprincipal y el job.css sólo es necesario para la página job. El archivo view.yml se

Page 35: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 35/273

 

puede personalizar partiendo de una base por-módulo. Cambia elarchivo view.yml de la aplicación sólo para tener el archivo main.css:

# apps/frontend/config/view.ymlstylesheets: [main.css]

Para personalizar la vista del módulo job, crea un nuevo archivo view.yml en eldirectorio apps/frontend/modules/job/config/:

# apps/frontend/modules/job/config/view.ymlindexSuccess:stylesheets: [jobs.css]

showSuccess:stylesheets: [job.css]

Bajo las secciones indexSuccess y showSuccess (ellas son las plantillas asociadas alas acciones index y show, como veremos más adelante), puedes personalizarcualquiera de los items encontrados bajo la sección default del view.yml de la

aplicación. Todos los items se fusionan con la configuración de la aplicación.También puedes definir algunas configuraciones para todas las acciones de unmódulo con la sección especial all.

Principios de configuración en Symfony

Para los muchos archivos de configuración de Symfony, la misma configuración sepuede definir en diferentes niveles:

  La configuración por defecto se encuentra en el framework

  La configuración global para el proyecto (en config/)

  La configuración local de una aplicación (en apps/APP/config/)

  La configuración local limitada a un módulo(en apps/APP/modules/MODULE/config/)

En tiempo de ejecución, la configuración del sistema combina todos los valores de losdiferentes archivos si existen y guarda en la memoria cache el resultado para un mejorrendimiento.

Como regla empírica, cuando algo es configurable a través de un archivo deconfiguración, la misma puede realizarse con código PHP. En lugar de crear unarchivo view.yml para el módulo job por ejemplo, también puedes utilizar elhelper use_stylesheet() a fin de incluir una hoja de estilos a partir de una

plantilla:<?php use_stylesheet('main.css') ?>

También puedes utilizar este helper en el layout a fin de incluir una hoja de estiloglobalmente.

Elegir entre un método u otro es realmente una cuestión de gusto. Elarchivo view.yml proporciona una manera de definir las cosas para todas lasacciones de un módulo, las cuales no son posible en una plantilla, pero la

Page 36: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 36/273

 

configuración es bastante estático. Por otro lado, utilizando elhelper use_stylesheet() es más flexible y además, todo está en el mismo lugar:la definición de estilos y el código HTML. Para Jobeet, vamos a utilizar elhelper use_stylesheet(), osea puedes quitar el view.yml que recién creamos yactualiza 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 configuración de JavaScript se realiza a través de lalinea javascripts del archivo de configuración view.yml y elhelper use_javascript() define los archivos JavaScript a incluir para una plantilla.

La Página Principal de Puesto de Trabajo

Como se observa en el día 3, la página principal de puestos de trabajo (job) esgenerada por la acción index del módulo job. La acciónindex es la parte delControlador de la página y la plantilla asociada, indexSuccess.php, en la parte dela Vista:

apps/frontend/modules/

job/actions/

actions.class.phptemplates/

indexSuccess.php

La Acción

Cada acción está representada por un método de una clase. Para la páginaprincipal de puestos de trabajo, la clase es jobActions (el nombre del móduloseguido de Actions) y el método es executeIndex() (execute seguido por elnombre de la acción). Esto recupera todos los puestos de trabajo de la base dedatos:

// 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();

}

// ... }

Page 37: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 37/273

 

Echemos un vistazo más de cerca al código: el método executeIndex() (elControlador) llama a la Tabla JobeetJob para crear una consulta que recuperetodos los puestos de trabajo. Devuelve una Doctrine_Collection deobjetos JobeetJob que se asignan a la propiedad jobeet_jobs del objeto.

Todas las propiedades del objeto luego se pasan automáticamente a la plantilla (la

Vista). Para pasar los datos del Controlador a la Vista, solo crea una nuevapropiedad:

public function executeFooBar(sfWebRequest $request){$this->foo = 'bar';$this->bar = array('bar', 'baz');

}

Este código hará que las variables $foo y $bar sean accesibles en la plantilla.

La Plantilla

De forma predeterminada, el nombre de plantilla asociado con una acción sededuce por Symfony gracias a un convención (el nombre de la acción seguidapor Success).

La plantilla indexSuccess.php genera una tabla HTML para todos los puestos detrabajo. Aquí esta el códido 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>

Page 38: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 38/273

 

<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 código de plantilla,el foreach itera a través de lalista deobjetos Job ($jobeet_jobs), ypara cada puesto de trabajo,cada valor de la columna esmostrado. Recuerda, el accesoa un valor de una columna estan simple como una llamadaal método de acceso cuyonombre comienza con get y elnombre de la columna enformato CamelCase (porejemplo, elmétodo getCreatedAt() para la columnacreated_at).

Vamos a limpiar esto un poco para mostrar sólo un subconjunto de las columnasdisponibles:

<!-- 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>

Page 39: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 39/273

 

La función url_for() en esta plantilla es un helper symfony que vamos a discutirmañana.

La Plantilla para los Puestos de Trabajo

Ahora vamos a personalizar la plantilla de la página de puestos de trabajo. Abre el

archivo showSuccess.php y reemplaza su contenido con el siguiente código:<!-- 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 acción para mostrar lainformación del puesto de trabajo. Como hemos rebautizado el nombre de variable

Page 40: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 40/273

 

pasada a la plantilla de $jobeet_job a $job, es necesario también realizar estecambio en la acción 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 descripción del trabajo usa elhelper simple_format_text() para formatearlo como HTML,sustituyendo los retornos de carrocon<br /> por ejemplo. Como estehelper pertenece al grupo dehelper Text, que no se carga por

defecto, tenemos que cargarlomanualmente utilizando elhelper use_helper().

Los Slots

Ahora, el título de todas laspáginas se define en laetiqueta <title> del layout:

<title>Jobeet - Your best job board</title>

Sin embargo, para la página de puestos de trabajo, queremos darle más

información ú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:

Añade un slot al layoutpara permitir que el títulosea dinámico:

// 

apps/frontend/templates/layout.php 

<title><?php include_slot('title') ?></title>

Cada slot es definido por un nombre (title) y se pueden visualizar mediante eluso del helper include_slot(). Ahora, al comienzo de laplantilla showSuccess.php, usa el helper slot() para definir el contenido del slotpara la página de puestos de trabajo:

// apps/frontend/modules/job/templates/showSuccess.php <?php slot('title',

Page 41: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 41/273

 

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

Si el título es complejo de generar, el helper slot() también se puede utilizar conun bloque de código:

// 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 páginas, como la página de inicio, sólo necesitamos un títulogenérico. En lugar de repetir el mismo título una y otra vez en las plantillas,podemos definir un título 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, eltítulo predeterminado será utilizado.

Ya hemos visto bastantes helpers comenzando con include_. Estos helpers generan elHTML y en la mayoría de los casos tienen un helper get_ como contrapartida solo paradevolver el contenido:

<?php include_slot('title') ?><?php echo get_slot('title') ?>

<?php include_stylesheets() ?><?php echo get_stylesheets() ?>

La Acción de la Página de Puestos de Trabajo

La página de puestos de trabajo es generada por la acción show, definida en elmétodo executeShow() del módulo job:

class jobActions extends sfActions{

public function executeShow(sfWebRequest $request){$this->job = Doctrine::getTable('JobeetJob')-> find($request-

>getParameter('id'));$this->forward404Unless($this->job);

}

// ... }

Page 42: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 42/273

 

Como en la acción index, la clase JobeetJob es usada para obtener un puesto detrabajo, esta vez es mediante el uso del métodofind(). El parámetro de estemétodo es el identificador único de un puesto de trabajo, su clave principal. Lasiguiente sección explicará el por qué la sentencia $request-

>getParameter('id') devuelve la clave principal del puesto de trabajo.

Si el puesto de trabajo noexiste en la base de datos,queremos llevar al usuario auna página 404, que esexactamente lo que hace elmétodo forward404Unless().Este toma un Booleano comoprimer argumento y, a menosque sea true, detiene el flujode la actual ejecución. Como

los métodos forward detienenla ejecución de la acciónenseguida lanzandoun sfError404Exception, no

necesitas después volver atrás.

En cuanto a las excepciones, la página que aparece al usuario es diferente en elentorno prod del entorno dev:

Antes de implementar el sitio web Jobeet para el servidor de producción, aprenderáscómo personalizar la página 404 por defecto.

La Familia de Métodos "forward"

El forward404Unless es en realidad equivalente a:

$this->forward404If(!$this->job);

que también es equivalente a:

if (!$this->job){$this->forward404();

}

El método forward404() en sí mismo es sólo un atajo para:

$this->forward('default', '404');

El método forward() hace un forward a otra acción de la misma aplicación; en elejemplo anterior, para la acción 404 del módulodefault. El módulo default esincluído con Symfony y da acciones predeterminadas para mostrar páginas 404, deseguridad, y de login.

Page 43: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 43/273

 

La Petición y la Respuesta

Cuando navegas por las páginas /job o /job/show/id/1 en tu navegador, estásiniciando un viaje de ida y vuelta al servidor web. El navegador está enviandouna Petición y el servidor devuelve una Respuesta.

Ya hemos visto que Symfony encapsula la petición en unobject sfWebRequest (mirá el método executeShow()). Y como Symfony es unFramework Orientado a Objetos, la respuesta es también un objeto, de laclase sfWebResponse. Puedes acceder al objeto respuesta en la acción llamandoa $this->getResponse().

Estos objetos proporcionan una gran cantidad de métodos convenientes paraacceder a la información de funciones PHP y variables globales PHP.

¿Por qué Symfony envuelve funcionalidades PHP existentes? En primer lugar, porqueSymfony y sus métodos son más poderosos que su homólogo PHP. Luego, porquecuando se prueba una aplicación, es mucho más fácil para simular un request o un

response con Objetos que tratar de ver alrededor de variables globales o trabajar confunciones PHP como header() la cuales hacen demasiado magia por detrás.

La Petición

La clase sfWebRequest envuelve a los arrays PHPglobales $_SERVER, $_COOKIE, $_GET, $_POST, y $_FILES:

Nombre del método PHP equivalente

getMethod()  $_SERVER['REQUEST_METHOD'] 

getUri()  $_SERVER['REQUEST_URI'] 

getReferer()  $_SERVER['HTTP_REFERER'] 

getHost()  $_SERVER['HTTP_HOST'] 

getLanguages()  $_SERVER['HTTP_ACCEPT_LANGUAGE'] 

getCharsets()  $_SERVER['HTTP_ACCEPT_CHARSET'] 

isXmlHttpRequest()  $_SERVER['X_REQUESTED_WITH'] == 'XMLHttpRequest' 

getHttpHeader()  $_SERVER 

getCookie()  $_COOKIE 

isSecure()  $_SERVER['HTTPS'] 

getFiles()  $_FILES 

getGetParameter()  $_GET 

getPostParameter()  $_POST 

Page 44: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 44/273

 

Nombre del método PHP equivalente

getUrlParameter()  $_SERVER['PATH_INFO'] 

getRemoteAddress()  $_SERVER['REMOTE_ADDR'] 

Ya hemos accedido a parámetros de solicitud usando el método getParameter().Devuelve un valor de la variable global $_GET o $_POST, o de la variable PATH_INFO.

Si deseas asegurarte de que un parámetro de la petición procede de uno particularde estas variables, necesitas utilizar losmétodosgetGetParameter(), getPostParameter(),y getUrlParameter() respectivamente.

Si deseas restringir la acción de un método específico, por ejemplo, cuando que deseasasegurarte de que un formulario es enviado como POST, puede utilizar elmétodo isMethod(): $this->forwardUnless($request->isMethod('POST')); .

La Respuesta

La clase sfWebResponse envuelve a los métodos PHP header() y setrawcookie():

Nombre del método PHP equivalente

setCookie()  setrawcookie() 

setStatusCode()  header() 

setHttpHeader()  header() 

setContentType()  header() 

addVaryHttpHeader()  header() 

addCacheControlHttpHeader()  header() 

Por supuesto, que la clase sfWebResponse también proporciona una manera deconfigurar el contenido de la respuesta (setContent()) y enviar la respuesta alnavegador (send()).

Hoy hemos visto cómo gestionar hojas de estilo y JavaScripts tanto enel view.yml como en las plantillas. Al final, ambas técnicas utilizan el objetoResponse y sus métodos addStylesheet() y addJavascript().

Las clases sfAction, sfRequest, y sfResponse dan muchos otros métodos útiles. Nodudes en navegar por la API documentationpara aprender más acerca de todas lasclases internas de Symfony. TIP Las clases sfAction, sfRequest, y sfResponse danmuchos otros métodos útiles. No dudes en navegar por la documentación API paraaprender más acerca de todas las clases internas de Symfony.

Page 45: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 45/273

 

El Enrutamiento

ahora deberías estar familiarizado con el patrón MVC y deberías sentir más y másnatural esta forma de codificación. Dedica un poco más de tiempo con esto parano tener que volver y mirar hacia atrás. Para practicar un poco el día de ayer,

hemos personalizado la páginas Jobeet y en el proceso, también examinamosvarios conceptos de Symfony, como el layout, los helpers, y los slots.

Hoy nos sumergiremos en el maravilloso mundo del Framework de Enrutamientode Symfony .

Las URLs

Si haces clic en un puesto de trabajo en la página principal Jobeet, la URL separece a esto: /job/show/id/1. Si ya has desarrollado sitios web PHP,probablemente estés más acostumbrados a las URL como /job.php?id=1. ¿CómoSymfony hace para que funcione? ¿Cómo Symfony determina la acción a llamar

basándose 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. Enun contexto web, una URL es el identificador único de un recurso web. Cuandoaccedes a una URL, estás pidiendo al navegador obtener un recurso identificadopor esa URL. Por lo tanto, como la dirección URL es la interfaz entre la página weby el usuario, debe transmitir información significativa sobre algún recurso al quehace referencia. Pero las "tradicionales" URLs realmente no describen al recurso,sino que exponen la estructura interna de la aplicación. Al usuario no le importaque 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 internode tu aplicación es también es bastante malo en lo que medida de seguridad serefiere: ¿Qué pasa si el usuario intenta adivinar la dirección URL de los recursosque no tienen acceso? Así es, el desarrollador debe asegurarlos de la maneraadecuada, pero más te vale ocultar la información sensible.

Las URL son tan importantes en Symfony que tiene todo un framework dedicado asu gestión: el framework de enrutamiento. El enrutamiento gestiona el URIinterno y la URL externa. Cuando una petición llega, el enrutamiento analiza laURL y la convierte en un URI interno.

Ya has visto el URI interno de la página 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

Page 46: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 46/273

 

El URI interno está hecho de varias partes: job es el módulo, show es la acción y lacadena de consulta añade los parámetros a pasar a la acción. El modelo genéricopara los URIs internos es:

MÓDULO/ACCIÓN?clave=valor&clave_1=valor_1&...

Como el enrutamiento de Symfony es un proceso bidireccional, puedes cambiar lasURLs sin cambiar la implementación técnica. Esta es una de las principalesventajas del patrón de diseño sobre controlador frontal.

La Configuración del Enrutamiento

El mapeo entre los URIs internos y las URLs externas esta listo en el archivo deconfiguración routing.yml:

# apps/frontend/config/routing.ymlhomepage:url: /param: { module: default, action: index }

default_index:url: /:moduleparam: { action: index }

default:url: /:module/:action/*

El archivo routing.yml describe las rutas. Una ruta tiene un nombre (homepage),un patrón (/:module/:action/*), y algunos parámetros (bajo la clave param).

Cuando una petición llega, el Enrutamiento trata de hacerla coincidir la URL con un

patrón dado. La primera ruta que coincida gana, por lo tanto el ordenen routing.yml es importante. Echemos un vistazo a algunos ejemplos paracomprender mejor cómo funciona esto.

Cuando solicitas la página de inicio Jobeet, la cual tiene la URL /job, la primeraruta que coincide es con default_index. En un patrón, una palabra con un prefijodos puntos (:) es una variable, por eso el patrón /:module significa: Concidir conun / seguida por cualquier cosa. En nuestro ejemplo, lavariable module será job como su valor. Este valor puede ser obtenidocon $request->getParameter('module'). Esta ruta también define un valor pordefecto para la variable action. Por eso, para todas las URLs que coincidan conesta ruta, la petición también tendrá un parámetro action con index como su

valor.

Si solicitas la página /job/show/id/1, Symfony coincidirá con el últimopatrón: /:module/:action/*. En un patrón, un asterisco (*) coincide con unacolección de pares variable/valor separados por una barra (/):

Page 47: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 47/273

 

Parámetro de petición Valor

módulo job

acción show

id 1Las variables módulo y acción son especiales ya que son utilizados por Symfony para

determinar la acción a ejecutar.

La URL /job/show/id/1 se pueden crear desde una plantilla utilizando la siguientellamada al helper url_for():

url_for('job/show?id='.$job->getId())

También 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 más rápido ya que elenrutamiento no tiene que analizar todas las rutas para encontrar el mejor patróncoincidente, y es menos complicado su implementación (los nombres del módulo yla acción no están presentes en el URI interno).

Personalizaciones del Enrutamiento

Por el momento, cuando la solicitas la URL / en un navegador, tienes por defectola página de felicitaciones de Symfony. Esto se debe a que esta URL coincide conla ruta homepage. Pero tiene sentido cambiarla para que no sea la página de iniciode Jobeet. Para hacer el cambio, modifica la variable module de laruta

homepageajob

:

# 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 laruta 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 fácil!

Cuando se actualiza la configuración del enrutamiento, los cambios sonimmediatamente tomados en cuenta en el entorno de desarrollo. Pero para hacerlosque tambien funcionen en el entorno de producción, necesitas limpiar el cacheejecutando la tareacache:clear.

Page 48: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 48/273

 

Para algo un poco más aplicado, vamos a cambiar el la URL de la página job a algomás significativo:

/job/sensio-labs/paris-france/1/web-developer

Sin saber nada acerca de Jobeet, y sin mirar en la página, se puede entender a

partir de la URL que Sensio Labs está buscando un Web developer para trabajar enParis, France.

Las URLs amigables son importantes porque ellas transmiten información al usuario. Estambién útil cuando copias y pegas la URL en un email o para optimizar tu sitio webpara los motores de búsqueda.

El siguiente patrón coincide con esta URL:

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

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

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

Si actualizas la página de inicio Jobeet, los enlaces a jobs no han cambiado. Estose debe a que para generar una ruta, necesitas pasar todas las variablesrequeridas. Por eso, necesitas cambiar la llamadaa 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 también puede ser expresado como un array:

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

))

Los Requisitos

Durante el primer día de tutoría, hemos hablado de la validación y manejo deerrores por una buena razón. El sistema de enrutamiento tiene incorporada unafunción de validación. Cada variable patrón pueda ser validada por una expresiónregular definida utilizando la linea requirements en la definición de la ruta:

job_show_user:url: /job/:company/:location/:id/:positionparam: { module: job, action: show }requirements:id: \d+

Page 49: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 49/273

 

La anterior linea requirements fuerza a que el id sea un valor numérico. Sino loes, 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 ladefinición de la ruta. Si estás familiarizado con el protocolo HTTP, sabes que éstedefine varios "métodos", comoGET, POST, HEAD, DELETE, y PUT. Los tres primeroscuentan con soporte en todos los navegadores, mientras que los otros dos no.

Para restringir una ruta a sólo la coincidencia con algunos métodos, puedesmodificar la clase de enrutamiento a sfRequestRoute y añadir un requisito para lavariable virtual sf_method:

job_show_user:url: /job/:company/:location/:id/:positionclass: sfRequestRoute

param: { module: job, action: show }requirements:id: \d+sf_method: [get]

Exigir una ruta para solo algunos métodos HTTP no es totalmente equivalente ausar sfWebRequest::isMethod() en tus acciones. Eso es porque el enrutamiento

continuará buscando por una ruta coincidente si el método 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 deescribir, (url_for('job/show?id='.$job->getId().'&company='.$job->getCompany().'&location='.$job->getLocation().'&position='.$job->getPosition())), pero como acabamos de aprender en la sección anterior, laclase route puede ser modificada. Para la ruta job_show_user, es mejorutilizarsfDoctrineRoute ya que la clase está optimizada para las rutas querepresentan objetos Doctrine o colecciones de objetos Doctrine:

job_show_user:url: /job/:company/:location/:id/:positionclass: sfDoctrineRouteoptions: { 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í, laopción model define la clase del módelo Doctrine (JobeetJob) relacionada a la ruta,y la opción type define que esta ruta está vinculada a un objeto (también puedesutilizar list si una ruta representa una colección de objetos).

Page 50: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 50/273

 

La ruta job_show_user es ahora consciente de su relación 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 más argumentos que sólo unobjeto.

Funciona porque todas las variables en la ruta tiene su método correspondiente enla clase JobeetJob (por ejemplo, la variable companyes reemplazada con el valorde getCompany()).

Si hechas una mirada a las URL generadas, no son todavía bastantes amigablescomo 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 sustitución de todos loscaracteres no ASCII por un -. Abre el archivoJobeetJob y añade los siguientesmétodos 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 continuación, crea el archivo lib/Jobeet.class.php y añadir elmétodo 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, '-'));

Page 51: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 51/273

 

 return $text;

}}

En este tutorial, nunca mostramos la sentencia de apertura <?php en los ejemplos decódigo que solo tienen código PHP puro para optimizar el espacio. Deberías obviamenterecordar agregarlos cuando creas un nuevo archivo PHP.

Tenemos definidos tres nuevos métodos "virtuales": getCompanySlug(), getPositionSlug(), y getLocationSlug(). Ellos devuelvensus correspondiente valor de columna después de pasalos por elmétodo slugify(). Ahora, puedes sustituir los nombres de las columas reales porsus virtuales en la ruta job_show_user:

job_show_user:url: /job/:company_slug/:location_slug/:id/:position_slugclass: sfDoctrineRouteoptions: { model: JobeetJob, type: object }param: { module: job, action: show }requirements:id: \d+sf_method: [get]

Tendrás ahora las URLs esperadas:

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

Pero esa es sólo la mitad de la historia. La ruta es capaz de generar una URL sobrela base de un objeto, pero también es capaz de encontrar el objeto en relación conuna determinada URL. Los objetos pueden ser recuperados con elmétodo getObject() de la ruta. Al analizar una petición, el enrutamiento guarda laruta del objeto coincidentes para que la uses en las acciones. Por lo tanto, cambiael método executeShow() para utilizar el objeto route para obtener elobjeto Jobeet:

class jobActions extends sfActions{public function executeShow(sfWebRequest $request){$this->job = $this->getRoute()->getObject();

$this->forward404Unless($this->job);}

// ... }

Si intentas obtener un jobpara un desconocido id,verás una página de error

Page 52: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 52/273

 

404 pero el mensaje de error ha cambiado:

Esto es así porque el error 404 ha sido lanzado de forma automática por elmétodo getRoute(). Así, podemos simplificar el métodoexecuteShow aún más:

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 laopción allow_empty a true.

El objeto relacionado de una ruta es cargado ligeramente. Este es obtenido desde labase de datos solo si llamas al métodogetRoute().

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 también toman una URI interna como unargumento, como el helper link_to() el cual genera una etiqueta <a>:

<?php echo link_to($job->getPosition(), 'job_show_user', $job) ?>

Genera el siguiente código HTML:

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

Para ambos url_for() y link_to() también 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 acción, puedes usar elmétodo generateUrl():

$this->redirect($this->generateUrl('job_show_user', $job));

La Familia de Métodos "redirect"

En el tutorial de ayer, hablamos acerca del método "forward". Estos métodos envían lapetición actual a otra acción sin regresar al navegador.

El método "redirect" redirige al usuario a otra URL. Al igual que con forward, puedesutilizar el método redirect(), o los métodosredirectIf() y redirectUnless().

Page 53: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 53/273

 

La Clase de Colección de Rutas

Para el módulo job, ya tenemos personalizado la ruta de la acción show, pero lasURLs para los otros métodos (index, new, edit,create, update, and delete) estánaun gestionadas por la ruta default:

default:url: /:module/:action/*

La ruta default es una gran manera de comenzar la codificación sin definirdemasiadas rutas. Pero como la ruta actúa como un "catch-all", no puede serconfigurada para necesidades específicas.

Como todas las acciones job están relacionadas con el modelo de laclase JobeetJob, podemos facilmente definir una rutaparticularsfDoctrineRoute para cada uno de ellas como ya lo hemos hecho para laacción show. Sin embargo, como el módulo job define las clásicas siete posiblesacciones para un modelo, también podemos utilizar la

clase sfDoctrineRouteCollection. Abre el archivorouting.yml y modificalo paraque se lea como sigue:

# apps/frontend/config/routing.ymljob:class: sfDoctrineRouteCollectionoptions: { model: JobeetJob }

job_show_user:url: /job/:company_slug/:location_slug/:id/:position_slugclass: sfDoctrineRouteoptions: { model: JobeetJob, type: object }param: { module: job, action: show }

requirements:id: \d+sf_method: [get]

# default ruleshomepage:url: /param: { module: job, action: index }

default_index:url: /:moduleparam: { action: index }

default:url: /:module/:action/*

La ruta job anterior es en realidad un acceso directo que generaráautomáticamente las siguientes siete rutas sfDoctrineRoute:

job:url: /job.:sf_format

Page 54: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 54/273

 

class: sfDoctrineRouteoptions: { model: JobeetJob, type: list }param: { module: job, action: index, sf_format: html }requirements: { sf_method: get }

job_new:

url: /job/new.:sf_formatclass: sfDoctrineRouteoptions: { model: JobeetJob, type: object }param: { module: job, action: new, sf_format: html }requirements: { sf_method: get }

job_create:url: /job.:sf_formatclass: sfDoctrineRouteoptions: { model: JobeetJob, type: object }param: { module: job, action: create, sf_format: html }requirements: { sf_method: post }

job_edit:url: /job/:id/edit.:sf_formatclass: sfDoctrineRouteoptions: { model: JobeetJob, type: object }param: { module: job, action: edit, sf_format: html }requirements: { sf_method: get }

job_update:url: /job/:id.:sf_formatclass: sfDoctrineRouteoptions: { model: JobeetJob, type: object }

param: { module: job, action: update, sf_format: html }requirements: { sf_method: put }

job_delete:url: /job/:id.:sf_formatclass: sfDoctrineRouteoptions: { model: JobeetJob, type: object }param: { module: job, action: delete, sf_format: html }requirements: { sf_method: delete }

job_show:url: /job/:id.:sf_format

class: sfDoctrineRouteoptions: { 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. Elenrutamiento está aún disponible para usarlas porque todas ellas tienen diferentesparámetro en el método HTTP.

Page 55: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 55/273

 

Las rutas job_delete y job_update require métodos HTTP que no son compatiblescon los navegadores (DELETE y PUTrespectivamente). Esto funciona porque lossimula 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 métodoHTTP que desea pasando el parámetro especialsf_method.

Symfony tiene otros parámetros especiales como sf_method, todo lo que inicie con elprefijo sf_. En las rutas generadas antes, se puede ver otro:  sf_format, que seexplicará en los próximos días.

Debugeando la Ruta

Cuando se utiliza una colección de rutas, a veces es útil listar de rutas generadas.La tarea app:routes muestra todas las rutas para una aplicación determinada:

$ php symfony app:routes frontend

También puedes tener una gran cantidad de información de depuración para una

ruta pasando su nombre como un argumento adicional:$ php symfony app:routes frontend job_edit

Las Rutas por defecto

Es una buena práctica definir las rutas para todas tus URLs. Ya que laruta job define todas las rutas necesarias para describir la aplicación Jobeet, sigueadelante y quita o comenta las rutas por defecto del archivo deconfiguración routing.yml:

# apps/frontend/config/routing.yml#default_index:

# url: /:module# param: { action: index }##default:# url: /:module/:action/*

La aplicación Jobeet debe seguir funcionando como antes.

Page 56: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 56/273

 

Mas cerca del Modelo

yer fue un gran día. Aprendiste como crear URLs amigables y como usar elframework Symfony para automatizar un montón de cosas por ti.

Hoy, mejoraremos el sitio web Jobeet afinando el código aquí y allá. En el proceso,

aprenderás más acerca de todas las características que hemos introducido durantelos primeros cinco dias de este tutorial.

El Objeto Query de Doctrine

De los requisitos del día 2:

"Cuando un usuario llega al sitio de Jobeet, verá una lista de los puestos detrabajos activos."

Pero hasta ahora, todos los puestos de trabajo serán mostrados, sea que esténactivos 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 envíado hace menos de 30 días. Elmétodo Doctrine_Query::execute() ejecutará una petición contra la base dedatos. En el código anterior, no hemos especificado ninguna condición lo quesignifica 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 lasdiferencias que hay entre los motores de base de datos y generará las sentencias

Page 57: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 57/273

 

SQL optimizadas para el motor de la base de datos que elejíste el día 3. Peroalgunas veces, es de gran ayuda para ver el SQL generado por el Doctrine; porejemplo, para depurar una consulta que no funciona como esperamos. En elentorno dev, symfony registra esas consultas (junto a otras muchas más) en eldirectorio log/. Hay un archivo log para cada combinacion de aplicación y entorno.

El archivo que estámos buscando es frontend_dev.log:# log/frontend_dev.logDec 04 13:58:33 symfony [info] {sfDoctrineLogger} executeQuery : SELECTj.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 jWHERE j.created_at > ? (2008-11-08 01:13:35)

Puedes ver por tí mismo que Doctrine tiene una claúsula where para lacolumna created_at (WHERE j.created_at > ?).

La cadena ? en la consulta indica que Doctrine genera una sentencia preparada. Elvalor actual de ? ('2008-11-08 01:13:35' en el ejemplo anterior) es pasado durante laejecución de la consulta y escapado apropiadamente por el motor de la base de datos.El uso de sentencias preparadas dramáticamente reduce tu exposición a los ataquesde inyecciones SQL. 

Esto esta bueno, pero es bastante molesto tener que cambiar del navegador , alIDE, y el archivo log cada vez que necesitas probar un cambio. Gracias a la barra

web de depuración de symfony, toda la información que necesitas esta tambiéndisponible dentro de la comodidad de tu navegador:

Serialización de Objetos

Aún si el código anterior funciona, esta lejos de ser perfecto ya que no toma encuenta algunos requisitos del día 2:

"Un usuario puede volver a re-activar y extender la validez de un puesto de

trabajo por 30 días extra..."

Pero ya que el código anterior solo se basa en el valor de created_at, y porqueesta columna almacena el día de creación, no podemos satisfacer el requisitoanterior.

Pero si recuerdas el esquema de la base de datos que describimos durante el día3, también tenemos definido una columna expires_at. Actualmente este valoresta siempre vacío ya que este no se establece en el archivo de datos. Pero

Page 58: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 58/273

 

cuando un puesto de trabajo es creado, puede ser automáticamente establecido a30 días del día actual.

Cuando necesitas hacer algo automáticamente antes que un objeto Doctrine seaguardado en la base de datos, puedes sobreescribir el método save() de la clasedel 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 método isNew() devuelve true cuando el objeto no ha sido serializado aún en labase de datos, y false de lo contratio.

Ahora, vamos a cambiar la acción para usar la columna expires_at en lugarde 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 undía expires_at en el futuro.

Con Datos

Actualizando la página de inicio de Jobeet en tu navegador vemos que nocambiamos ningún puesto de trabajo en la base de datos que habíamos dejadohace unos pocos días atrás. Vamos a cambiar el archivo fixtures para agregar unpuesto de trabajo que ya haya expirado:

# data/fixtures/jobs.ymlJobeetJob:# other jobs

Page 59: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 59/273

 

 expired_job:JobeetCategory: programmingcompany: Sensio Labsposition: Web Developerlocation: Paris, France

description: Lorem ipsum dolor sit amet, consectetur adipisicingelit.

how_to_apply: Send your resume to lorem.ipsum [at] dolor.sitis_public: trueis_activated: truecreated_at: '2005-12-01 00:00:00'token: job_expiredemail: [email protected]

Ten cuidado cuando copies y pegues códifo en un archivo de datos para no romper laindentación. El expired_job debe solo tener dos espacios en blanco después de si.

Recarga los datos y actualiza tu navegador para asegurarte que los viejos puestosde trabajo no se muestran más:

$ php symfony doctrine:data-load

También puedes ejecutar la siguiente consulta para asegurarte que lacolumna expires_at es automáticamente completada por el método save(),basado en el valor de created_at:

SELECT `position`, `created_at`, `expires_at` FROM `jobeet_job`;

Configuración Personalizada

En el método JobeetJob::save(), hemos tenido que hardcodear el número de días

para que los puestos de trabajo expiren. Podría mejorarse haciendo que los 30días sean configurables. El framework Symfony trae incluído un archivo deconfiguración para la configuración específica de una aplicación, elarchivo app.yml. Este archivo de formato YAML puede contener cualquierconfiguración de desees:

# apps/frontend/config/app.ymlall:active_days: 30

En la aplicación, esas configuraciones están disponibles a través de la claseglobal sfConfig:

sfConfig::get('app_active_days')

El parámetro tiene un prefijo app_ porque la clase sfConfig también da acceso a laconfiguración de symfony como veremos más tarde.

Vamos a actualizar el código para tomar esta nueva configuración en cuenta:

public function save(Doctrine_Connection $conn = null){if ($this->isNew() && !$this->getExpiresAt())

Page 60: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 60/273

 

{$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 configuración app.yml e una gran forma de centralizarconfiguraciones globales para tu aplicación.

Refactorizando

Todo el código escrito funciona bien, pero aún no esta del todo bien. ¿Puedes verel problema?

El código Doctrine_Query no pertenece a la acción (capa del Controlador), sino

que pertenece a la capa del Modelo. En el modelo MVC, el modelo define toda lalógicas de negocios, y el Controlador solo invoca al modelo para obtener los datosde éste. Como el código devuelve una colección de puestos de trabajo, vamos amover el código a la clase JobeetJobTable y crear un método 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 código de la acción puede usar este nuevo método para obtener lospuestos de trabajo activos:

public function executeIndex(sfWebRequest $request){$this->jobeet_jobs = Doctrine_Core::getTable('JobeetJob')-

>getActiveJobs();}

Esta refactorización tiene varios beneficios sobre el anterior código:

  La lógica para obtener los puestos de trabajo activos está ahora en elmodelo, donde pertenerce

  El código en el controlador es mucho mas legible

  El método getActiveJobs() es re-usable (por ejemplo en otra acción)

  El código del modelo ahora puede ser probado con pruebas unitarias

Page 61: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 61/273

 

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 método orderBy añade una claúsula ORDER BY al SQL generado(addOrderBy() también existe).

Categorías en la Página de Inicio

De los requisitos del día 2:

"Los puestos de trabajo son ordenados por categoría y entonces por la fecha de

publicación (los nuevos primeros)."Hasta ahora, no teníamos la categoría en cuenta. De los requisitos, la página deinicio debe mostrar los puestos de trabajo por categoría. Primero, necesitamosobtener todas las categorías con al menos un puesto de trabajo activo.

Abre la clase JobeetCategoryTable y agregale el método 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 acción 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 través de todas las categorías y mostrar lospuestos de trabajo activos:

// apps/frontend/modules/job/indexSuccess.php <?php use_stylesheet('jobs.css') ?>

Page 62: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 62/273

 

<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 categoría en la plantilla, usamos echo $category. ¿Tesuena raro? $category es un objeto, ¿Cómo puedeecho mágicamente mostrar elnombre de la categoría? La respuesta fue dada durante el día 3 cuando teníamos quedefinir el método mágico __toString() para todas las clasese del modelo.

Para que funcione, necesitamos agregar el método getActiveJobs() a laclase JobeetCategory que devuelve los puestos de trabajo activos para el objetocategoría:

// 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);}

Page 63: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 63/273

 

El método JobeetCategory::getActiveJobs() usa almétodo Doctrine::getTable('JobeetJob')->getActiveJobs() para obtener lospuestos de trabajo activos para una categoría dada.

Cuando llamamos al Doctrine::getTable('JobeetJob')->getActiveJobs() , loqueremos para restringir la condición aún más para una categoría dada. En lugar

de pasar el objeto categoría, tenemos decidido pasar el objeto Doctrine_Query yaque este es la mejor forma de encapsular una condición genérica.

El método getActiveJobs() necesita combinar este objeto Doctrine_Query con supropio 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

Aún queda un requisito por implementar para la lista de puestos de trabajo de lapágina de inicio:

"Por cada categoría, la lista solo muestra los primeros 10 puestos de trabajo y unenlace que permite listar todos los puestos de una categoría dada."

Es tán simple de agregar al método 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 claúsula LIMIT es ahora hardcodeada dentro del Modelo, pero esmejor que este valor sea configurable. Cambia la plantilla para pasar el númeromáximo de puestos de trabajo establecido en app.yml:

<!-- apps/frontend/modules/job/indexSuccess.php -->

Page 64: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 64/273

 

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

y agrega esta nueva configuración en app.yml:

all:

active_days: 30max_jobs_on_homepage: 10

Datos Dinámicos

A menos que bajes el max_jobs_on_homepage, no verás 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 amano... pero hay una mejor manera. La duplicación esta mal, aún es archivosfixture.

¡Symfony al rescate! Los archivos YAML en symfony pueden tener código PHP queserá evaluado justo antes de ser analizado. Edita el archivo de datos jobs.yml yañade el siguiente código al final:

JobeetJob:

Page 65: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 65/273

 

# Starts at the beginning of the line (no whitespace before) <?php for ($i = 100; $i <= 130; $i++): ?>job_<?php echo $i ?>:JobeetCategory: programmingcompany: Company <?php echo $i."\n" ?>position: Web Developer

location: Paris, Francedescription: Lorem ipsum dolor sit amet, consectetur adipisicing

elit.how_to_apply: |

Send your resume to lorem.ipsum [at] company_<?php echo $i ?>.sitis_public: trueis_activated: truetoken: job_<?php echo $i."\n" ?>email: [email protected]

<?php endfor ?>

Ten cuidado, al analizar YAML no olvides ninguna indentación. Manten en mentelos siguientes simples tips cuando añadas código PHP a un archivo YAML

  La declaración <?php ?> debe siempre empezar la linea o ser incrustada enun valor.

  Si una declaración <?php ?> finaliza una linea, necesitarás explícitamenteagregar una nueva linea ("\n").

Puedes ahora recargar los archivos de datos con la tarea doctrine:data-load yver si solo 10 puestos de trabajo son mostrados en la página de inicio para lacategoría Programming. En la siguiente captura de pantalla, tenemos modificado elnúmero máximo de puestos de trabajo a cinco para hacer una imágen maspequeña:

Page 66: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 66/273

 

Asegurar la Página

Cuando un puesto de trabajo expira, aun sabiendo la URL, no debería ser posibleacceder a él nunca más. 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 información, necesitarás redirigir al usuario a una página404. Perp, ¿Cómo puedo hacer esto cuando la info es cargada automaticamentevía la ruta?

# apps/frontend/config/routing.ymljob_show_user:url: /job/:company_slug/:location_slug/:id/:position_slugclass: sfDoctrineRouteoptions:model: JobeetJob

type: objectmethod_for_query: retrieveActiveJob

param: { module: job, action: show }requirements:id: \d+sf_method: [GET]

El método retrieveActiveJob() recibirá el objeto Doctrine_Query ya listo porparte 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 unpuesto de trabajo expirado,

serás enviadoa una página 404.

Enlazar a la Página de laCategoría

Ahora, vamos a agregar unenlace a la página de la

Page 67: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 67/273

 

categorías en la página de inicio y crear dicha página.

Pero, aguarda un minuto. La hora no terminó aun y ya hemos trabajado mucho.Por eso, ¡estás libre y con suficiente conocimientos para hacer esto por tí mismo.!Vamos hacer el ejecicio. Revisa mañana nuestra implementción.

Page 68: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 68/273

 

Jugando con la pagina de Categorias

La Ruta Category

Primero, necesitamos agregar una ruta para definir una URL amigable para lapágina de la categoría. Agrégalo al inicio del archivo de enrutamiento:

# apps/frontend/config/routing.ymlcategory:url: /category/:slugclass: sfDoctrineRouteparam: { module: category, action: show }options: { model: JobeetCategory, type: object }

Si vas a comenzar la implementación de una nueva funcionalidad, es una buenapráctica primero pensar acerca de la URL y crear la ruta asociada. Y esto es obligatoriosi quitas las reglas de enrutamiento por defecto.

Como slug no es una columna de la tabla category, necesitamos para agregar unmétodo virtual en JobeetCategory para que la ruta funcione:

Un ruta puede usar cualquier columna de su objeto asociado como parámetro.También puede usa cualquier otro valor si hay un método asociado definido en laclase del objeto. Debido a que el parámetro slug no tiene una columnacorrespondiente en la tablacategory, necesitamos agregar un método de accesovirtual en JobeetCategory para que la ruta funcione:

// lib/model/doctrine/JobeetCategory.class.php public function getSlug(){

return Jobeet::slugify($this->getName());}

El Enlace Categoría

Ahora, edita la plantilla indexSuccess.php del módulo job para agregar el enlace ala página de la categoría:

<!-- 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">

Page 69: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 69/273

 

and <?php echo link_to($count, 'category', $category) ?>more...

</div><?php endif; ?>

</div><?php endforeach; ?>

</div>

Solo agregamos el enlace si hay más de 10 puestos de trabajo a mostrar para lacategoría actual. El enlace tiene el número de puestos de trabajo no mostrados.Para que esta plantilla funcione, necesitamos agregar elmétodo 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 método countActiveJobs() emplea un método countActiveJobs() que aún noexiste en JobeetJobTable. Reemplaza el contenido delarchivo JobeetJobTable.php con el siguiente código: [php] //lib/model/doctrine/JobeetJobTable.class.php class JobeetJobTable extendsDoctrine_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()))

Page 70: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 70/273

 

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

return $q;}

}

Como puedes ver por ti mismo, tenemos que refactorizar todo el códigode JobeetJobTable para introducir un nuevo métodocompartido addActiveJobsQuery() para hacer el código más DRY (Don't RepeatYourself). 

La primera vez, un trozo de código es re-usado, copiando el código puede sersuficiente. Pero si encuentras otra función para él necesitas refactorizar para reutilizarla función o método, como hemos hecho aquí.

En el método countActiveJobs(), en vez de usar execute() y recién contar elnúmero de resultados, usamos el método mas rápidocount().

Hemos cambiado un montón de archivos, recién para esta simple funcionalidad.Pero cada vez que debas agregar algún código, tenemos que tratar de ponerlo enla capa correcta de la aplicación y también tratar de hace el código más reusable.En el proceso, tenemos que rectorizar algún código existente. Ese es el típicoworkflow cuando trabajamos en un proyecto symfony.

Page 71: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 71/273

 

 

Creación del Módulo Category

Es hora de crear el módulo category:

$ php symfony generate:module frontend category

Si has creado un módulo, probablemente has utilizado el doctrine:generate-module. Eso está bien, pero como no es necesario el 90% del código generado,usa generate:module para crear un módulo vacío.

¿Por qué no añadir una acción category al módulo job? Podríamos, pero como eltema principal de la página de categoría es una categoría, se siente más natural crearun módulo dedicado category.

Al acceder a la página de categoría, la ruta category tendrá que encontrar lacategoría asociada con la variable slug de la petición. Pero como slug no sealmacena en la base de datos, y porque no podemos deducir el nombre de lacategoría del slug, no hay forma de encontrar la categoría asociada con el slug.

Actualizar la Base de Datos

Tenemos que añadir una columna slug para la tabla category:

Esta columna slug puede ser tomada con cuidado por un comportamiento Doctrinellamado Sluggable. Simplemente necesitamos habilitar el comportamiento sobrenuestro modelo JobeetCategory y este se encargará de todo por tí.

# config/doctrine/schema.ymlJobeetCategory:actAs:Timestampable: ~Sluggable:

fields: [name]columns:name:

type: string(255)notnull: true

Ahora que slug es una columna real, es necesario eliminar elmétodo getSlug() de JobeetCategory.

La configuración de la columna slug es tomada automáticamente 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 labase 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 método executeShow(). Reemplazael contenido del archivo de acciones category con el siguiente código:

Page 72: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 72/273

 

// 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 método generado executeIndex(), también puedes quitar laplatilla automáticamentegeneradaindexSuccess.php (apps/frontend/modules/category/templates/indexSuccess.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 lasplantillas, haciendo uso del helperinclude_partial(), es por eso que su traducciónliteral no es muy amigable, ya que debemos pensar no en parcial sino en porción(snippet de código de la capa Vista)

Page 73: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 73/273

 

Observa que hemos copiado y pegado la etiqueta <table> que crear una lista depuestos de trabajo en la plantilla indexSuccess.php. Eso esta mal. Es tiempo paraaprender un nuevo truco. Cuando necesites volver a utilizar una parte de unaplantilla, lo que necesitas es crear un partial. Un partial es un snippet de códigode plantilla que puede ser compartido entre varias plantillas. Un partial es sólo otra

plantilla que comienza con un guión 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 módulo, una /, y el nombre del partial sin el_). El segundo argumentoes un array de las variables a pasar al partial.

¿Por qué no utilizar el método include() incluído en PHP en lugar delhelper include_partial()? La principal diferencia entre los dos es el soporte de cache

incluído del helper include_partial().

Reemplaza el HTML <table> de ambas plantillas con la llamadaa 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 día 2:

Page 74: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 74/273

 

"La lista es paginada, con 20 puestos de trabajo por página."

Para paginar una lista de un Objetos Doctrine, symfony proporciona una clasededicada a ello: sfDoctrinePager. En la acción categoryen lugar de pasar losobjetos (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 método sfRequest::getParameter() toma un valor por defecto como segundoargumento. En la acción anterior, si el parámetro de la petición page no existe,

entonces getParameter() devolverá 1.

El constructor de sfDoctrinePager tiene una clase del modelo y el número máximode elementos a regresar por página. Añade este último valor a tu archivo deconfiguración:

# apps/frontend/config/app.ymlall:active_days: 30

max_jobs_on_homepage: 10max_jobs_on_category: 20

El método sfDoctrinePager::setQuery() toma un objeto Doctrine_Query parautilizarlo a la hora de seleccionar los elementos de la base de datos.

Agrega el método 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 método getActiveJobsQuery(), podemos refactorizar otrosmétodos de JobeetCategory para usarlos:

// lib/model/doctrine/JobeetCategory.class.php public function getActiveJobs($max = 10)

Page 75: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 75/273

 

{$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="Previouspage" />

</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; ?>

Page 76: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 76/273

 

<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 código se refiere a los enlaces a otras páginas. Aquí está lalista de métodos sfDoctrinePager usados en esta plantilla:

  getResults(): Devuelve un array objetos Doctrine para la página actual

  getNbResults(): Devuelve el número total de resultados

  haveToPaginate(): Devuelve true si hay más de una página

  getLinks(): Devuelve una lista de los enlaces de la página a mostrar

  getPage(): Devuelve el número de la página actual  getPreviousPage(): Devuelve el número de la página anterior

  getNextPage(): Devuelve el número de la siguiente página

  getLastPage(): Devuelve el número de la última página

Page 77: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 77/273

 

 

Page 78: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 78/273

 

Pruebas Unitarias

Durante las últimas dos semanas hemos revisado todas las funciones aprendidasdurante los cinco primeros días del calendario de Jobeet para personalizar y añadirnuevas funciones. En el proceso, también hemos tocado otras funciones más

avanzadas de symfony.

Hoy, vamos a empezar hablando de algo completamentediferente: pruebas automáticas. Como el tema es bastante grande, nos llevarádos días completos para cubrir todo.

Las Pruebas en Symfony

Existen dos tipos de pruebas automáticas en symfony: PruebasUnitarias y Pruebas Funcionales.

Las Pruebas Unitarias verificar que cada método y función está trabajando

correctamente. Cada pruebas deberá ser lo más independiente posible de lasdemás.

Por otro lado, Pruebas Funcionales verifican que la aplicación resultante secomporta correctamente en todo su conjunto.

Todas las pruebas en symfony están ubicadas bajo el directorio test/ delproyecto. 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 cubrirán en el tutorial de hoy, mientras que en el demañana se dedicará a las Pruebas Funcionales .

Pruebas UnitariasEscribir pruebas unitarias es quizás una de las mejores prácticas de desarrolloweb, más difíciles de poner en práctica. Como los desarrolladores web realmenteno las utilizan para poner a prueba su trabajo, muchas preguntas surgen: ¿Tengoque escribir las pruebas antes de la implementación de una función? ¿Qué necesitopara hacer la prueba? ¿Mis pruebas necesitan cubrir todos y cada uno de los casosde uso? ¿Cómo puedo estar seguro de que todo está bien probado? Perofrecuentemente, la primer pregunta es la más básica: ¿Donde empezar?

Incluso si eres un fervoroso partidario de las pruebas, el enfoque de symfony espragmático: siempre es mejor disponer de algunas pruebas que no tener ninguna.¿Ya tienes un montón de código sin ningún tipo de prueba? No hay problema. Noes necesario disponer de un completo conjunto de pruebas para beneficiarse de lasventajas de ellas. Empieza por agregar pruebas cada vez que encuentras un falloen el código. Con el tiempo, el código será mejor, el código aumentará, y serás undesarrollador con mayor confianza en tí mismo. Empezando con un enfoque máspragmático, te sentirás más cómodo con las pruebas con el paso del tiempo. Elsiguiente paso es escribir las pruebas de las nuevas características. En brevetiempo, te convertirás en un adicto a las pruebas.

Page 79: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 79/273

 

El problema con la mayoría de las bibliotecas de pruebas es su empinada curva deaprendizaje. Es por eso que symfony proporciona una muy simple librería parapruebas, lime, para hacer la escritura de pruebas increíblemente fácil.

Aún si este tutorial describe extensamente la librería lime que viene incorporada enSymfony, puedes utilizar cualquier librería para pruebas, como la excelente

librería PHPUnit. 

El Framework de Pruebas lime 

Todas las pruebas unitarias escritas con el framework lime comienzan con elmismo código:

require_once dirname(__FILE__).'/../bootstrap/unit.php';

$t = new lime_test(1);

Primero, el archivo incluído unit.php hace la inicialización de un par de cosas. Acontinuación, un nuevo objeto lime_test se crea y el número de pruebas queplaneamos lanzar se pasa como argumento.

El plan permite a lime mostrar un mensaje de error aún en caso de que sean pocas laspruebas que se ejecutan (por ejemplo, cuando una prueba genera un error fatal dePHP). Las Pruebas implican llamar a un método o a una función con un conjuntopredefinido de argumentos y, a continuación, comparar la salida con los resultadosesperados. Esta comparación determina si una prueba pasa (aprueba) o no.

Para facilitar la comparación, el objeto lime_test proporciona varios métodos:

Método Descripción

ok($test)  Prueba una condición y pasa si es true

is($value1, $value2) Compara dos valores y pasa si son iguales (==)

isnt($value1, $value2)  Compara dos valores y pasa si son distintos

like($string, $regexp)  Prueba una cadena contra una expresión regular

unlike($string, $regexp)  Comprueba que la cadena difiera de la expresión

regular

is_deeply($array1,

$array2) 

Comprueba que dos arrays tienen los mismos

valoresPuedes preguntarte por qué lime define tantos métodos de prueba, si todas laspruebas se pueden escribir solo usando el métodook(). El beneficio de los métodosalternativos estan en los mensajes de error mucho más explícitos en caso de que unaprueba falle y en la mejora de la legibilidad de las pruebas.

El objeto lime_test también ofrece otros convenientes métodos de prueba:

Page 80: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 80/273

 

Método Descripción

fail()  Siempre falla -útil para probar las excepciones

pass()  Siempre pasa -útil para probar las excepciones

skip($msg,

$nb_tests) Cuenta como $nb_tests pruebas -para pruebascondicionales

todo()  Cuenta como una prueba -útil para pruebas aun no

escritas

Por último, el método comment($msg) muestra un comentario o mensaje pero norealiza ninguna prueba.

Ejecutando Pruebas Unitarias

Todas las pruebas unitarias son guardadas en el directorio test/unit/. Porconvención, las pruebas son nombradas con el nombre de la clase que ellasprueban más el sufijo Test. Puedes organizar los archivos en eldirectorio test/unit/ de la forma que deseas, te recomendamos replicar laestructura de directorios del directorio lib/.

Crea un archivo test/unit/JobeetTest.php y copia en él, el siguiente código:

// 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 losresultados de la prueba en colores rojo ni verde. Pero su utilizas Cygwin, puedes forzara Symfony a usar colores pasando la opción --color a la tarea.

Probando slugify 

Vamos a comenzar nuestro viaje al maravilloso mundo de las pruebas unitariasescribiendo las pruebas para el métodoJobeet::slugify().

Page 81: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 81/273

 

Creamos el método slugify() durante el día 5 para limpiar una cadena para quepueda ser seguro incluírla en una URL. La conversión consiste en algunas básicastransformaciones como la de convertir todos los carácteres no-ASCII en un guión(-) o convertir la cadena a minúsculas:

Entrada SalidaSensio Labs sensio-labs

Paris, France paris-france

Reemplaza el contenido del archivo de pruebas con el siguiente código:

// 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, notarás que cada linea soloprueba un sola cosa. Esto es algo que necesitas mantener en mente cuandodesarrollas pruebas unitarias. Prueba una sola cosa a la vez.

Puedes ahora ejecutar el archivo de pruebas. Si todas pruebas pasan, comoesperamos 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, lasalida te dará algunainformación acerca delporque ésta falló; pero sitienes cientos de pruebasen un archivo, puede serdifícil identificarrápidamente cual falló.

Todos los métodos de prueba lime toman una cadena como su último argumentoque sirve como descripción para la prueba. Esto es muy conveniente pues tefuerza a describir que es lo que deseas realmente probar. También te puede servircomo una forma de documentación para el comportamiento esperado del método.Vamos agregar algunos mensajes para el archivo de pruebas slugify:

require_once dirname(__FILE__).'/../bootstrap/unit.php';

Page 82: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 82/273

 

$t = new lime_test(6);

$t->comment('::slugify()');$t->is(Jobeet::slugify('Sensio'), 'sensio', '::slugify() converts allcharacters 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 - atthe beginning of a string');$t->is(Jobeet::slugify('sensio '), 'sensio', '::slugify() removes - atthe end of a string');$t->is(Jobeet::slugify('paris,france'), 'paris-france', '::slugify()replaces non-ASCII characters by a -');

La descripción de la pruebaes también una herramientaimportante cuando tratas demostrar qué vamos aprobar. Puedes ver unpatrón en las cadenas depruebas: ellas sonsentencias que describen como el método se comporta y ellas siempre comienzancon el nombre del método a probar.

Cobertura del Código

Al escribir pruebas, es fácil olvidar una porción del código.Para ayudarte a comprobar que todo el código está bien probado, symfony proporcionala tarea test:coverage. Para esta tarea pasale un archivo o directorio test y unarchivo o directorio lib un directorio como argumentos y te dirá el porcentaje de códigode tu sistema que la prueba cubre:

$ php symfony test:coverage test/unit/JobeetTest.php lib/Jobeet.class.php

Si quieres saber que líneas no están cubiertos por tus pruebas, usa la opción --

detailed:

$ php symfony test:coverage --detailed test/unit/JobeetTest.phplib/Jobeet.class.php

Ten en cuenta que cuando la tarea te indica que el código esta completamenteprobado, simplemente significa que cada línea ha sido ejecutada, no que todos loscasos de uso han sido probados.

Como test:coverage depende de XDebug para recoger esta información, necesitasinstalarlo y habilitarlo.

Agregando Pruebas para las nuevas Características

Page 83: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 83/273

 

El slug de una cadena vacía es una cadena vacía. Puedes probarlo, va a funcionar.Pero una cadena vacía en una URL no es que una gran idea. Vamos a cambiar elmétodo slugify() para que devuelva la cadena "n-a" en caso de una cadenavacía.

Puedes escribir la prueba primero, entonces actualiza el método, o al revés. Es

realmente una cuestión de gusto, pero escribir la prueba primero te da laconfianza de que tu código se ajusta en realidad lo que previste:

$t->is(Jobeet::slugify(''), 'n-a', '::slugify() converts the empty stringto n-a');

Si lanzas las pruebas ahora, debes obtener una barra roja. Si no es así, significaque la característica ya está implementada o tu prueba no está probando lo quedebería estar probando.

Ahora, edita la clase Jobeet y añade la siguiente condición al inicio:

// lib/Jobeet.class.php 

static public function slugify($text){if (empty($text)){return 'n-a';

}

// ... }

La prueba debe pasar ahora según lo esperado, y puedes disfrutar de la barraverde, pero sólo si has recordado actualizar el plan de pruebas. Si no es así,

tendrás un mensaje que te dice planeaste seis pruebas y se ejecutó una extra.Después de haber planificado las pruebas debes contar con ellas hasta la fecha yesto es importante, ya que te mantendrá informado si la secuencia de comandosde 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 extrañoerror: algunos vínculos a los puestos de trabajo apuntan a una Página de error404. Después de algunas investigaciones, vas encontrar que por alguna razón,esos puestos de trabajo no tienen company, position, o location slug. ¿Cómo esposible? Ves a través de los registros en la base de datos y las columnas no están

vacías. Lo piensas por un rato, y bingo, encuentras la causa. Cuando una cadenasólo contiene caracteres no ASCII, el métodoslugify() lo convierte a una cadenavacía. Tan feliz de haber encontrado la causa, abres la clase Jobeet y solucionas elproblema de inmediato. Eso es una mala idea. En primer lugar, vamos a añadiruna prueba:

$t->is(Jobeet::slugify(' - '), 'n-a', '::slugify() converts a string thatonly contains non-ASCII characters to n-a');

Page 84: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 84/273

 

 

Después de comprobar que la prueba no pasa, edita la clase Jobeet y pasa lacadena vacía a comprobar al final del método:

static public function slugify($text){// ... 

if (empty($text)){return 'n-a';

}

return $text;}

La nueva prueba ahora pasa, al igualque todas los demás.

El slugify() tenía 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 tucódigo. También significa que tu código va mejorar con el tiempo, lo que siemprees algo bueno.

Hacia un mejor Método slugify 

Probablemente sabes que Symfony ha sido creado por Franceses, así que vamos aagregar una prueba con una palabra francesa que contiene un "acento":

$t->is(Jobeet::slugify('Développeur Web'), 'developpeur-web',

'::slugify() removes accents');

La prueba debe fallar. En lugar de sustituir é por e, el Método slugify() lo hasustituido por un guión (-). Eso es un problema difícil, llamado Transliteración.Esperemos que, si tiene "iconv" instalado, haga el trabajo para nosotros. Reemplaza elcódigo del método 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);

}

Page 85: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 85/273

 

 // 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 codificación UTF-8, ya que esta es lacodificación por defecto de Symfony, y la utilizada por "iconv" para hacer laTransliteración.

También cambia el archivo de prueba para el funcionamiento de la prueba sólo si"iconv" está disponible:

if (function_exists('iconv')){$t->is(Jobeet::slugify('Développeur Web'), 'developpeur-web',

'::slugify() removes accents');}else{$t->skip('::slugify() removes accents - iconv not installed');

}

Pruebas Unitarias y Doctrine

Configuración de la Base de datos

Probar en forma unitaria una clase Doctrine del modelo es un poco más complejoya que requiere una conexión de base de datos. Ya tienes la que utilizas para eldesarrollo, pero es un buen hábito crear una base de datos especial para laspruebas.

Durante el día 1, se presentaron los entornos como una forma de variar laconfiguración de una aplicación. Por defecto, todas las pruebas de symfony se

ejecutan en el entorno test, así que vamos a configurar una base de datos para elentorno test:

$ php symfony configure:database --name=doctrine --class=sfDoctrineDatabase --env=test"mysql:host=localhost;dbname=jobeet_test" root mYsEcret

Page 86: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 86/273

 

La opción env le dice a la tarea que la configuración de la base de datos es sólopara el entorno test. Cuando usamos esta tarea durante el día 3, no pasó ningunaopción env, por lo que la configuración se aplica a todos los entornos.

Si eres curioso, abre el archivo de configuración config/databases.yml para ver comoSymfony hace que sea fácil de cambiar la configuración en función del entorno.

Ahora que hemos configurado la base de datos, podemos iniciarla usando latarea doctrine:insert-sql:

$ mysqladmin -uroot -pmYsEcret create jobeet_test$ php symfony doctrine:insert-sql --env=test

Principios de Configuración en Symfony

Durante el día 4, vimos los ajustes procedentes de los archivos de configuración puedeser definido a diferentes niveles.

Estos valores también pueden ser dependientes del entorno. Esto es verdad para lamayoría de los archivos de configuración que hemos utilizado hastaahora: databases.yml, app.yml, view.yml, y settings.yml. En todos los archivos, laclave principal es el entorno, la clave all está indicando que los ajustes son para todoslos entornos:

# config/databases.ymldev:doctrine:class: sfDoctrineDatabase

test:doctrine:class: sfDoctrineDatabase

param:dsn: 'mysql:host=localhost;dbname=jobeet_test'

all:doctrine:class: sfDoctrineDatabaseparam:

dsn: 'mysql:host=localhost;dbname=jobeet'username: rootpassword: null

Datos de Prueba

Ahora que ya tenemos una base de datos sólo para pruebas, tenemos que llenarlacon datos de prueba. Durante el día 3 aprendimos a utilizar latarea doctrine:data-load, pero en las pruebas es necesario volver a cargar losdatos cada vez que ejecutamos las pruebas para conocer el estado inicial de labase de datos.

La tarea doctrine:data-load internamente utiliza elmétodo Doctrine_Core::loadData() para cargar los datos:

Page 87: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 87/273

 

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

El objeto sfConfig puede ser utilizado para obtener la ruta completa de un sub-directorio del proyecto. Uso que permite a la estructura de directorio por defecto serpersonalizada.

El método loadData() toma un directorio o un archivo como primer argumento.También puede tomar un array directorios y/o archivos.

Ya hemos creado algunos datos iniciales en el directorio data/fixtures/. Para laspruebas, pondremos los datos en el directoriotest/fixtures/. Estos datos seutilizarán para pruebas unitarias y pruebas funcionales con objetos Doctrine.

Por el momento, copiar los archivos de data/fixtures/ aldirectorio test/fixtures/.

Probando JobeetJob 

Vamos a crear algunas de las pruebas unitarias para la clase del

modelo, JobeetJob.Como todos nuestros objetos Doctrine harán las pruebas unitarias comenzarán conel mismo código, crea un archivo Doctrine.php en el directorio bootstrap/ con elsiguiente código:

// 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 unobjeto de configuración para el entorno test :

$configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'test', true);

  Creamos un gestor de bases de datos e inicializamos la conexión Doctrinecargando el archivo de configuración databases.yml.

new sfDatabaseManager($configuration);

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

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

Doctrine se conecta a la base de datos sólo si tiene algunas sentencias SQL paraejecutar.

Page 88: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 88/273

 

Ahora que todo está en su lugar, podemos empezar a probar la clase JobeetJob.

En primer lugar, tenemos que crear elarchivo 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 elmétodo 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 sólo prueba el método getCompanySlug() y no si el slug es correcto ono, ya que lo estamos probando a éste en otros lugares.

Escribir pruebas para el método save() es ligeramente más 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-08-08', '->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',

Page 89: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 89/273

 

'location' => 'Paris, France','description' => 'Testing is fun','how_to_apply' => 'Send e-Mail','email' => '[email protected]','token' => rand(1111, 9999),'is_activated' => true,

), $defaults));

return $job;}

Cada vez que añadas pruebas, no te olvides de actualizar el número de pruebasprevisto (el plan) en el método constructorlime_test. Para elarchivo JobeetJobTest es necesario cambiar de 1 a 3.

Prueba otras Clases Doctrine

Ahora puedes añadir pruebas para todas las demás clases de Doctrine. Comoahora te acostumbraste al proceso de la escritura de pruebas unitarias, debería serbastante fácil. Comprueba el repositorio para el día de hoy si quieres ver losarchivos de datos que hemos creado, y los pruebas unitarias asociadas (bajo laetiqueta release_day_08).

Set de Pruebas Unitarias

La tarea test:unit también se puede utilizar para poner en marcha todas laspruebas 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 elscript se detuvo/murió antes de llegar al final. Ejecutando un archivo de pruebas enforma individual te dará el mensaje de error exacto.

Page 90: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 90/273

 

Las Pruebas Funcionales

Las Pruebas Funcionales son una gran herramienta para probar tus aplicaciones deprincipio a fin: desde la petición hecha en el navegador hasta la respuesta queenvía el servidor. Ellas prueban todas las capas de una aplicación: Elenrutamiento, el modelo, las acciones, y las plantillas. Ellas son muy similares a lo

que probablemente ya haces manualmente: cada vez que vamos a añadir omodificar una acción, es necesario ir al navegador y comprobar que todo funcionacomo se esperaba, haciendo clic en los enlaces y controlando que los elementos semuestran en la página. En otras palabras, corres un escenario correspondiente alcaso de uso que acabas de implementar.

Como el proceso es manual, es tedioso y propenso a errores. Cada vez quecambias algo en el código, debes pasar a través de todos los escenarios paraasegurarte de que no rompiste algo. Eso es una locura. Las Pruebas Funcionalesen Symfony proporcionar un método sencillo para describir escenarios. Cadaescenario puede ser ejecutado automáticamente una y otra vez simulando la

experiencia de lo que un usuario ha hecho en su navegador. Al igual que pruebasunitarias, ellas te dan la confianza para que el código quede en paz.

El framework de pruebas funcionales no reemplaza las herramientas como "Selenium".Selenium funciona directamente en el navegador para automatizar las pruebas através de muchas plataformas y navegadores y, así, habilitar la prueba del JavaScriptde tu aplicación.

La Clase sfBrowser 

En Symfony, las pruebas funcionales se ejecutan a través de un navegadorespecial, ejecutadas por la clase sfBrowser. Esta actúa como un navegador

adaptado para tu aplicación y directamente conectada a ella, sin la necesidad deun servidor web. Te da acceso a todos los objetos Symfony antes y después decada petición, dándote la oportunidad de inspeccionarlos y hacer los controles quedeseas programaticamente.

sfBrowser brinda métodos de navegación que simula lo que hace el clásiconavegador:

Método Descripción

get()  Obtiene una dirección URL

post()  Envía a una URL

call() Pide una URL (utilizado para los métodos PUT y DELETE)

back()  Se remonta a una página atrás en el historial

Page 91: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 91/273

 

Método Descripción

forward()  Va adelante una página en el historial

reload()  Recarga la página actual

click()  Hace clic en un enlace o un botón

select()  Selecciona una casilla de verificación o de opción

deselect()  Deselecciona una casilla de verificación o de opción

restart()  Reinicia el navegador

He aquí algunos ejemplos de uso de los métodos 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 métodos adicionales para configurar el comportamiento delnavegador:

Método Descripción

setHttpHeader()  Establece una cabecera HTTP

setAuth()  Establece las credenciales de autenticación básica

setCookie()  Establecer una cookie

removeCookie()  Removes a cookie

clearCookie()  Borra todas las cookies

Page 92: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 92/273

 

Método Descripción

followRedirect()  Sigue un redireccionamiento

La Clase sfTestFunctional 

Tenemos un navegador, pero necesitamos una forma de inspeccionar los objetosSymfony para hacer la prueba real. Se puede hacer con lime y algunos métodosde sfBrowser como getResponse() y getRequest() pero Symfony proporciona unamejor manera.

Los métodos de pruebas son proporcionados por otra clase, sfTestFunctional quetoma una instancia de sfBrowser en su constructor. LaclasesfTestFunctional delega las pruebas a objetos tester. Varios testers sonempaquetados con Symfony, y también puedes crear el tuyo propio.

Como vimos ayer, las pruebas funcionales se guardan en eldirectorio test/functional/. Para Jobeet, la pruebas se encuentran en elsubdirectoriotest/functional/frontend/ ya que cada aplicación tiene su propiosubdirectorio. Este directorio ya contiene dos archivos:categoryActionsTest.php,y jobActionsTest.php como todas las tareas que generan un módulo creanautomáticamente 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 quelos métodos de sfBrowser y sfTestFunctional implementan unainterfaz fluída quesiempre devuelve un objeto $this. Te permite encadenar llamadas a métodos parala mejor legibilidad. El snippet anterior es equivalente a:

// test/functional/frontend/categoryActionsTest.php include(dirname(__FILE__).'/../../bootstrap/functional.php');

Page 93: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 93/273

 

 $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 decontexto de prueba comienza con with('TESTER NAME')->begin() y finalizacon end():

$browser->with('request')->begin()->isParameter('module', 'category')->isParameter('action', 'index')->

end();

El código prueba que el parámetro de la petición module es iguala category y action es igual a index.

Cuando sólo necesitas llamar a un solo método en el tester, no es necesario crear unbloque: with('request')->isParameter('module', 'category').

El Tester de la PeticiónEl request tester proporciona métodos tester para probar e inspeccionar elobjeto sfWebRequest:

Método Descripción

isParameter()  Comprueba un valor de un parámetro

isFormat()  Verifica el formato de una petición

isMethod()  Verifica el método

hasCookie()  Chequea si la petición tiene una cookie con el nombre dado

isCookie()  Verifica el valor de una cookie

Page 94: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 94/273

 

El Tester de la Respuesta

También hay una clase response tester que proporciona métodos tester contra elobjeto sfWebResponse:

Método Descripción

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

algunos criterios

checkForm() Checks an sfForm form object

debug()  Prints the response output to ease debug

matches()  Tests a response against a regexp

isHeader()  Verifica el valor de una cabecera

isStatusCode()  Verifica el código de estado de la respuesta

isRedirected()  Verifica si la respuesta actual es un redireccionamiento

Vamos a describir más clases testers en los próximos días (para forms, user, cache,...).

Ejecutando las pruebas funcionales

Así como para las pruebas unitarias, ejecutar las pruebas funcionales se puedehacer directamente desde un archivo de pruebas:

$ php test/functional/frontend/categoryActionsTest.php

O usando la tarea test:functional:

$ php symfony test:functional frontend categoryActions

Page 95: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 95/273

 

Probar los Datos

Asi como para Doctrine en las pruebas unitarias, tenemos que cargar los datos deensayo cada vez que un lanzemos una prueba funcional. Podemos reutilizar elcódigo 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 más fácil que en pruebasunitarias 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 códigoen cada archivo de prueba, pero nos vamos a crear nuestra propia clase funcionalque 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. Yatenemos por escrito todos los escenarios que necesitamos para poner a pruebacomo parte del día 2.

En primer lugar, vamos a probar la página principal Jobeet editandoel jobActionsTest.php. Reemplace el código 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()->

Page 96: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 96/273

 

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

método info() para hacer la salida más legible. Para verificar la exclusión de lospuestos de trabajo expirados en la página de inicio, comprobamos que el selectorCSS .jobs td.position:contains("expired") no coincide con ninguna parte en larespuesta y su contenido HTML (Recuerdo que en los archivos de datos, el únicopuesto vencido que tenemos tiene "expired" en la posición). Cuando el segundoargumento del método checkElement() es un Boolean, el método prueba laexistencia de nodos que coincidan con el selector CSS.

El método checkElement() es capaz de interpretar la mayoría de los selectoresválidows CSS3.

Solo n puestos se listan para una categoría

Agrega el código 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 método checkElement() también puede comprobar que un selector CSS coincide'n' nodos en el documento pasando un entero como su segundo argumento.

Una categoría tiene un enlace a la página de categoría sólo si tienemuchos 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 categoríade design (.category_design .more_jobs no existe), y que existe un enlace "more jobs" para la categoría programming (.category_programming .more_jobs existe).

Page 97: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 97/273

 

Puestos de trabajo están 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 páginaprincipal como esperamos. Esto puede hacerse comprobando que la URL contengala clave primaria esperada. Como la clave principal puede cambiar entreejecuciones, tenemos que obtener el objeto Doctrine a partir de la primera base dedatos.

Incluso si la prueba funciona como debe, tenemos que refactorizar el código unpoco, ya que conseguir el primer trabajo de la categoría programming pueden ser

reutilizados en otras partes de nuestras pruebas. No vamos a mover el código a lacapa del Modelo que que es el código de una prueba específica. En lugar de ello,vamos a mover el código a la clase JobeetTestFunctional que hemos creadoanteriormente. Esta clase actúa como una clase tester funcional para un DominioEspecífico 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();}

Page 98: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 98/273

 

  // ... }

Puedes ahora reemplazar el código 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 página 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 detailedinformation')->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 vínculo de un puesto en la página de inicio, simularemos un clic enel texto "Web Developer". Como hay muchos de ellos en la página, hemos pedidoexplícitamente al navegador que haga clic en el primero (array('position' =>1)).

Cada parámetro de la petición se prueba para asegurarte que la ruta ha hecho sutrabajo correctamente.

Aprender con el Ejemplo

En esta sección, hemos proporcionado todo el código necesario para poner aprueba la páginas de puestos de trabajo y categoría. Lee el códigocuidadosamente, ya que puedes aprender algunos trucos nuevos:

// lib/test/JobeetTestFunctional.class.php class JobeetTestFunctional extends sfTestFunctional{

Page 99: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 99/273

 

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))->

Page 100: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 100/273

 

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 toomany 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 detailedinformation')->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');

Page 101: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 101/273

 

 $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 FuncionalesA veces una prueba funcional falla. Como Symfony simula un navegador sinningún tipo de interfaz gráfica, puede ser difícil de diagnosticar el problema.Afortunadamente, Symfony proporciona el método debug() para mostrar lacabecera dela respuesta y su contenido:

$browser->with('response')->debug();

Page 102: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 102/273

 

El método debug() se puede insertar en cualquier lugar de un bloquetester response y detener el script de ejecución.

Grupo de Pruebas Funcionales

La tarea test:functional también se puede utilizar para poner en marcha todas

las pruebas funcionales para una aplicación:$ php symfony test:functional frontend

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

Grupo de Pruebas

Como puedes esperar, también hay una tarea para poner en marcha todas laspruebas para un proyecto (unitarias y funcionales):

$ php symfony test:all

Page 103: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 103/273

 

Los Formularios

La segunda semana de Jobeet pasó volando con la introducción del framework depruebas de Symfony. Vamos a continuar hoy con el framework de formularios.

El Framework de FormulariosCualquier sitio web tiene formularios; desde el simple formulario de contacto hastalos complejos con decenas de campos. Crear formularios es también una de lasmás complejas y tediosas tareas de un desarrollador web: necesitas crear el HTMLdel formulario, implementar las reglas de validación para cada campo, procesar losvalores para luego guardarlos en la base de datos, mostrar mensajes de error,rellenar los campos en caso de errores, y mucho más ...

Por supuesto, en lugar de reinventar la rueda una y otra vez, Symfony proporcionauna framework para facilitar la administración de un formulario. El framework deformularios esta hecho de tres partes:

  validación: El sub-framework validation ofrece clases para validar lasentradas (entero, cadenas, dirección de correo electrónico, ...)

  widgets: El sub-framework de widgets ofrece clases para la salida de loscampos HTML (input, textarea, select, ...)

  forms: La clases form representan formularios hechos de widgets yvalidadores y dan métodos para ayudar a gestionar el formulario. Cadacampo del formulario tiene su propio validador y su widget.

Formularios

En Symfony un formulario es una clase hecha de campos. Cada campo tiene unnombre, un validador, y un widget. Un simple ContactForm puede definirse con lasiguiente 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 método configure(), usando losmétodos setValidators() y setWidgets().

Page 104: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 104/273

 

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

Los nombres de las clases de los widgets y validadores son muy explícitos: elcampo email se muestra como una etiqueta

HTML <input>(sfWidgetFormInputText) y validado como una dirección de correoelectrónico (sfValidatorEmail). El campo message se muestra como una etiquetaHTML <textarea> (sfWidgetFormTextarea), y debe ser una cadena de no más de255 caracteres (sfValidatorString).

Por defecto, todos los campos son obligatorios, así el valor por defectopara required es true. Por lo tanto, la definición de la validación para emailesequivalente a new sfValidatorEmail(array('required' => true)).

Es posible combinar un formulario en otro usando el método mergeForm(), o incluir unformulario dentro de otro mediante el método embedForm():

$this->mergeForm(new AnotherForm());$this->embedForm('name', new AnotherForm());

Formularios Doctrine

La mayoría de las veces, un formulario tiene que ser serializado (guardado) para labase de datos. Como Symfony ya sabe todo acerca de su modelo de base dedatos, puede generar automáticamente formularios basados sobre estainformación. De hecho, cuando se puso en marcha la tarea doctrine:build --

all durante el día 3, Symfony automáticamente llamó a la tarea doctrine:build -

-forms:

$ php symfony doctrine:build --forms

La tarea doctrine:build --forms genera las clases form en lib/form/. Laorganización de estos archivos generados es similar a la de lib/model/. Cadamodelo de clase tiene una clase form relacionada (porejemplo JobeetJob tiene JobeetJobForm), que está vacío por defecto, ya quehereda de una clase base:

// lib/form/doctrine/JobeetJobForm.class.php class JobeetJobForm extends BaseJobeetJobForm{public function configure(){

}}

Navegando por los archivos generados en el sub-directorio lib/form/doctrine/base/ verás un montón de ejemplos de uso de widgetssymfony incluidos y de validadores.

Personalización del Job Form

Page 105: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 105/273

 

El formulario Job es un ejemplo perfecto para aprender la personalización de unformulario. Vamos a ver cómo personalizarlo, paso a paso.

En primer lugar, cambia el enlace "Post a Job" en el layout para poder comprobarlos 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 dela columnas de la tabla. Sin embargo, para el job form, algunos de ellos no debenser 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 desconexión de un campo significa que tanto el widget como validador seeliminan.

La configuración del formulario a veces tienen que ser más precisa que lo que sepuede inspeccionar desde el esquema de base de datos. Por ejemplo, lacolumna email es un varchar en el esquema, pero necesitamos que esta columna

sea validada como un email. Vamos a cambiar el valor pordefecto 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 solución, ya que lasreglas de validación por defecto inferidas del esquema de la base de datos se

pierden (new sfValidatorString(array('max_length' => 255))). Es casi siempremejor para agregar un nuevo validador a uno existente usar el validadorespecial sfValidatorAnd:

// lib/form/doctrine/JobeetJobForm.class.php public function configure(){// ... 

Page 106: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 106/273

 

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

));}

El validador sfValidatorAnd toma un arreglo o array de validadores que debenpasarse para que el aor sea válido. El truco aquí es referenciar al actual validador($this->validatorSchema['email']), y añadirle uno nuevo.

También puede usar el validador sfValidatorOr para forzar un valor a pasar al menosun validador. Y por supuesto, puedes mezclar y coinicidir losvalidadores sfValidatorAnd y sfValidatorOr para crear complejos validadoresbasados en boleanos.

Incluso si el type de la columna es también un varchar en el esquema, queremosque su valor este restringido a una lista de opciones: full time, part time, ofreelance.

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 continuación, 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á undiferente widget de acuerdo a las opciones de configuración (expanded ymultiple):

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

false) 

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

'expanded' => false) 

Page 107: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 107/273

 

  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 porejemplo), puedes cambiar el valor por defecto en el esquema de base de datos.

Incluso si piensas que nadie pueda enviar un valor no-válido, un hacker fácilmentepuede pasar por alto las opciones del widget usando herramientas como curl ola Firefox Web Developer Toolbar. Vamos a cambiar el validador para restringir alas 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 relacionadoscon 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 automáticamente genera un label (que seutilizarán al mostrar la etiqueta <label>). Esto puede ser cambiado con laopción label.

También puedes cambiar labels en un batch con el método setLabels() del widgetarray:

$this->widgetSchema->setLabels(array('category_id' => 'Category',

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

));

También 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

Page 108: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 108/273

 

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

Como el validador guarda la ruta relativa en la base de datos, cambia la ruta deacceso 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 método generateLogoFilename() existe en el modelo, este será llamado por elvalidador y el resultado sobreescribirá el nombre del archivo generado pordefecto logo. El método tiene un objeto sfValidatedFile como un argumento.

Así como puedes sobreescribir el label generado de cualquier campo, puedestambien definir un mensaje de ayuda. Vamos agregar uno para lacolumna 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',

Page 109: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 109/273

 

'is_public' => 'Public?','how_to_apply' => '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 alsobe 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 editaruno existente. De hecho, ambas plantilla newSuccess.php y editSuccess.php sonbastante 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 aún, es tiempo de hacerlo para ambasplantillas (<?php use_stylesheet('job.css') ?>).

El formulario en sí mismo es mostrado en el partial _form. Reemplaza el contenidodel partial generado _form con el siguiente código:

<!-- 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>

Page 110: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 110/273

 

</form>

Los helpers use_javascripts_for_form() y use_stylesheets_for_form() incluyenel JavaScript y hoja de estilo necesarios para los form widgets.

Incluso si el formulario job no necesita ningún JavaScript o hoja de estilo, se trata de

un buen hábito mantener estos helper llamandolos "por las dudas". Puede salvar tu díasi decides cambiar un widget que necesite de algunos JavaScript o una hoja de estiloespecífica.

El helper form_tag_for() genera una etiqueta <form> para un formulario y rutadado y cambia los métodos HTTP a POST o PUT dependiendo de si el objeto esnuevo o no. También se ocupa del atributo multipart si el formulario tiene algunfile 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 mayoría de las veces, tendrás que personalizar el diseño de tus formularios. Elobjeto form ofrece muchos métodos útiles para esta personalización:

Método Descripción

render() Muestra el formulario (equivalente a la salida de echo

$form)

renderHiddenFields()  Muestra los campos ocultos

hasErrors() Devuelve true si el form tiene algunos errores

hasGlobalErrors() Devuelve true si el form tiene errores globales

getGlobalErrors()  Devuelve un array de errores globales

renderGlobalErrors()  Muestra errores globales

El formulario también se comporta como un array de campos. Puedes acceder alcampo company con $form['company']. El objeto devuelto proporciona métodos paramostrar cada uno de los elementos del campo:

Método Descripción

renderRow()  Muestra el campo fila

Page 111: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 111/273

 

Método Descripción

render()  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 derealmente hacer que funcione con algunas acciones.

El formulario job es gestionado por cinco métodos en el módulo job:

  new: Muestra un formulario en blanco para crear un nuevo puesto detrabajo

  edit: Muestra un formulario para editar uno existente

  create: Crea un nuevo puesto de trabajo con los valores enviados por elusuario

  update: Actualiza uno existente con los valores enviados

  processForm: Invocado por create y update, este procesa el form(validación, rellena el formulario, y serialización a la base de datos)

Todos los formularios tienen el siguieten ciclo de vida:

Page 112: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 112/273

 

 

Como hemos creado una colección de rutas Doctrine 5 días atras para elmódulo job, podemos simplificar el código del formulario y sus métodos degestión:

// 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');

Page 113: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 113/273

 

}

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 página /job/new, una nueva instancia de form es creada ypasada a la plantilla (acción new).

Cuando el usuario envía el formulario (accióncreate

), el formulario es atado(método bind()) con los valores envíados por el usuario y la validación esdesencadenada.

Una vez que el formulario está, es posible de comprobar su validez usando elmétodo isValid(): Si el formulario es válido (regresa true), el job es guardado enla base de datos ($form->save()), y el usuario es redirigido a la página de vistaprevia; si no, la plantilla newSuccess.php es mostrada de nuevo con los valoresenvíados por el usuario y los mensajes de error asociados.

El método setTemplate() cambia la plantilla empleada para una acción dada. Si elformulario envíado no es válido, los métodos create y updateusan la misma plantilla

para las acciones new y edit respectivamente, para mostrar el formulario conmensajes de error.

La modificación de un puesto de trabajo existente es bastante similar. La únicadiferencia entre la acción new y edit es que el objeto job para ser modificado espasado 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 losformularios Doctrine, un simple array de simples formularios).

Page 114: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 114/273

 

Tambien puedes definir los valores por defecto para la creación. Una forma esdeclarar los valores en el esquema de base de datos. Otra es pasar un pre-modificado objeto Job al constructor de form.

Cambia el método executeNew() para definir full-time como el valor por defectopara 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 sustituirán por los delusuario. El usuario envía valores que se utilizarán para rellenar el formulario cuando el

formulario se devuelve en caso de errores de validación.Proteger el formulario Job con un Token

Todo debe funcionar bien por ahora. A partir de ahora, el usuario debe ingresar eltoken para el puesto de trabajo (job). Pero el token del puesto de trabajo debe sergenerado automáticamente cuando un nuevo puesto de trabajo es creado, ya queno queremos proporcionarle al usuario un único token.

Actualiza el método save() de JobeetJob para agregar la lógica que genera lostoken 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'],

Page 115: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 115/273

 

$this['token']);

// ... }

// ... }

Si recuerdas los casos de uso del día 2, un puesto de trabajo o job puede sereditado solo si el usuario conoce el token asociado. En este momento, es bastantefácil de editar o eliminar cualquier puesto de trabajo, simplemente adivinando laURL. Esto se debe a que la edición del URL es como /job/ID/edit, donde ID es laclave principal del puesto de trabajo (job).

Por defecto, una ruta sfDoctrineRouteCollection genera URLs con la claveprimaria, pero puede ser cambiado a una única columna pasando la opción column:

# apps/frontend/config/routing.ymljob:class: sfDoctrineRouteCollectionoptions: { model: JobeetJob, column: token }requirements: { token: \w+ }

Nota que tenemos también que cambiar el parámetro de requirement token paraque coincida con cualquier cadena ya que el requirements por defecto de Symfonyes \d+ para la clave única.

Ahora, todas las rutas, excepto la job_show_user, tienen un token. Por ejemplo, laruta para editar un job es ahora:

http://www.jobeet.com.localhost/job/TOKEN/edit

También tendrás 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>

También hemos cambiado los requisitos para la columna token ya que para Symfony ysu requirements por defecto es \d+ para la clave principal.

La Página de Vista Previa

La página de vista previa es la misma que para la visualización de la página de lospuestos 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 añadir una barra de admin enla parte superior. Al comienzo de la plantilla showSuccess, añadir un partial paramostrar 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)) ?>

Page 116: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 116/273

 

<?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 montón de código, pero la mayor parte del código es fácil de entender.

Para hacer la plantilla más fácil de leer, hemos añadido un montón de métodos deacceso 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;

}

Page 117: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 117/273

 

 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 puestode trabajo:

Activación y Publicación del Puesto de Trabajo

En la sección anterior, hay un enlace para publicar el trabajo. El enlace debe sercambiado para que apunte a una nueva acción publish. En lugar de crear unanueva ruta, podemos configurar la actual ruta job:

# apps/frontend/config/routing.ymljob:class: sfDoctrineRouteCollection

options:model: JobeetJobcolumn: tokenobject_actions: { publish: put }

requirements:token: \w+

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

Page 118: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 118/273

 

<!-- 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 acción 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 onlinefor %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 elmétodo HTTP put. Para simular el método put, el enlace es automáticamenteconvertido a un formulario cuando haces clic en él.

Y debido a que hemos activado la protección CSRF, el helper link_to() tiene untoken CSRF en el enlace y el método checkCSRFProtection() del objetocomprueba la validez del envío.

El método executePublish() usa un nuevo método 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 característica para publicar desde tu navegador.

Pero todavía tenemos algo que arreglar. Los puestos de trabajo inactivos no debenser accesibles, lo que significa que no debe aparecer en la página principal Jobeet,

y no deben ser accesible por sus URL. Como hemos creado unmétodo addActiveJobsCriteria() para restringir unDoctrine_Query a puestos detrabajo activos, podemos editarlo y añadir la nueva exigencia al final:

// lib/model/doctrine/JobeetJobTable.class.php public function addActiveJobsQuery(Doctrine_Query $q = null){// ... 

$q->andWhere($alias . '.is_activated = ?', 1);

Page 119: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 119/273

 

 return $q;

}

Eso es todo. Puedes probar ahora en tu navegador. Todos los puestos de trabajono activos han desaparecido de la página principal, incluso si conoces su URL, ya

no son accesibles. Sin embargo, son accesibles si se conoce el token URL delpuesto de trabajo. En ese caso, el puesto de trabajo se mostrará con una vistaprevia y el admin bar.

Esta es una de las grandes ventajas del modelo MVC y la refactorización quehemos hecho a lo largo del camino. Un solo cambio en un método fue necesariopara añadir el nuevo requisito.

Cuando creamos el método getWithJobs(), hemos olvidado de utilizar elmétodo addActiveJobsQuery(). Por lo tanto, tenemos que editar y añadir el nuevorequisito:

class JobeetCategoryTable extends Doctrine_Table{public function getWithJobs(){// ... 

$q->andWhere('j.is_activated = ?', 1);

return $q->execute();}

Page 120: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 120/273

 

Probando el Formulario

Eso es lo que haremos el día de hoy. A lo largo del camino, también vamos aaprender más sobre el framework de formularios.

Usando el Framework de Formularios sin Symfony

Los componentes del Framework Symfony están bastante desacoplados. Esto significaque la mayoría de ellos se pueden utilizar sin necesidad de utilizar todo el FrameworkMVC. Ese es el caso del Framework de Formularios, el cual no dependen de Symfony.Puedes utilizarlo en cualquier aplicación PHP obteniendo losdirectorios lib/form/, lib/widgets/, ylib/validators/ .

Otro componente reusable es el framework de enrutamiento. Copia eldirectorio lib/routing/ en tu proyecto non-symfony, y beneficiate URLs ricas sincosto alguno.

Los componentes symfony-independentes de la Plataforma Symfony son:

Enviando un Formulario

Vamos a abrir el archivo jobActionsTest para agregar pruebas funcionales para elproceso de creación y validación de un puesto de trabajo.

Al final del archivo, agrega el código siguiente para obtener la página de creacióndel 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();

Page 121: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 121/273

 

Ya hemos usado el método click() para simular los clics en los enlaces. El mismométodo click() puede utilizarse para enviar un formulario. Un formulario, puedetransferir los valores a enviar para cada campo como un segundo argumento delmétodo. Como un verdadero navegador, el objeto browser mezclará los valorespor defecto del formulario con los valores enviados.

Sin embargo, para pasar los valores del campo, necesitamos saber sus nombres.Si abres el código fuente o usas la Firefox Web Developer Toolbar "Forms >Display Form Details", verás que el nombre delcampo company es jobeet_job[company].

Cuándo PHP se encuentra con un campo input con un nombrecomo jobeet_job[company], este lo convierte automáticamente a un array denombre jobeet_job.

Para hacer las cosas un poco más limpias, vamos a cambiar el formatoa job[%s] añadiendo el siguiente código al final del

método configure() deJobeetJobForm:// lib/form/doctrine/JobeetJobForm.class.php $this->widgetSchema->setNameFormat('job[%s]');

Después de este cambio, el nombre company debería aparecercomo job[company] en tu navegador. Ahora es el momento de realmente hacerclic en el botón "Preview your job" y transmitir los valores válidos 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 job', array('job' => array('company' => 'Sensio Labs','url' => 'http://www.sensio.com/','logo' => sfConfig::get('sf_upload_dir').'/jobs/sensio-

labs.gif','position' => 'Developer',

'location' => 'Atlanta, USA','description' => 'You will work with symfony to develop websites forour customers.',

'how_to_apply' => 'Send me an email','email' => '[email protected]','is_public' => false,

)))->

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

Page 122: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 122/273

 

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

end();

El navegador también simula la carga de archivos mediante el paso de la ruta

absoluta del archivo a cargar.Después de enviar el formulario, comprobamos que la acción ejecutada es create.

El Tester de Formularios

El formulario que hemos enviado debería ser válido. Puedes probarlo usandoel tester form:

with('form')->begin()->hasErrors(false)->

end()->

El tester form tiene varios métodos para probar el estado del formulario actual,como los errores.

Si cometes un error en la prueba, y la prueba no pasa, puedes usar lainstrucción with('response')->debug() que hemos visto durante el día 9. Perotendrás que entrar al HTML generado para ver si hay mensajes de error. Aunqueeso no es realmente conveniente. El tester form también proporciona unmétodo debug() que muestra el estado del formulario y todos los mensajes deerror asociados a él:

with('form')->debug()

Probando la Redirección

Como el formulario es válido, el puesto de trabajo debería haber sido creado y elusuario se redirige a la página show:

isRedirected()->followRedirect()->

with('request')->begin()->isParameter('module', 'job')->isParameter('action', 'show')->

end()->

El método isRedirected() prueba si la página se ha redireccionado y el

método followRedirect() sigue la redirección.La clase browser no sigue las redirecciones automaticamente como podrías imaginarpara inferir objetos antes de la redirección.

El Tester Doctrine

Page 123: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 123/273

 

Finalmente, queremos poner a prueba que el puesto de trabajo se ha creado en labase de datos y comprobar que la columna is_activated está enfalse ya que elusuario no lo ha publicado todavía.

Esto puede hacerse fácilmente mediante el uso de otro tester, el Tester dePropel o Propel tester. Como el tester de Doctrine no está registrado por

defecto, vamos a añadirlo ahora al navegador:$browser->setTester('doctrine', 'sfTesterDoctrine');

El tester Doctrine proporciona el método check() sirve para comprobar que uno omás 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 instanciade Doctrine_Query para búsquedas más complejas. Puedes probar la existencia deobjetos que concuerden con el criterio con un Boolean como tercer argumento (pordefecto es true), o el número de objetos coincidentes mediante un entero.

Probando los Errores

El formulario de creación de un puesto de trabajo funciona como se esperabacuando se envian valores válidos. Vamos a añadir una prueba para comprobar elcomportamiento cuando se envian datos no válidos:

$browser->info(' 3.2 - Submit a Job with invalid values')->

get('/job/new')->click('Preview your job', array('job' => array('company' => 'Sensio Labs','position' => 'Developer','location' => 'Atlanta, USA','email' => 'not.an.email',

)))->

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

end();

Page 124: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 124/273

 

El método hasErrors() puede poner a prueba el número de errores si se pasa unentero. El método isError() prueba el código de error para un determinadocampo.

En las pruebas que hemos escrito para el envío de datos no válidos, no tenemos queprobar todo el formulario de nuevo. Sólo hemos añadido las pruebas para cosas

específicas.

También puedes probar el HTML generado para comprobar que contiene losmensajes de error, pero no es necesario en nuestro caso ya que no hemospersonalizado el layout del formulario.

Ahora, vamos a probar la barra de administrador de la página de vista previa de job. Cuando un puesto de trabajo no se ha activado, se puede editar, eliminar opublicar el puesto de trabajo. Para probar estos tres enlaces, tendremos que crearprimero un puesto de trabajo. Pero eso es un montón de copiar y pegar. Como nome gusta, vamos a añadir un método 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' => '[email protected]','is_public' => false,

), $values)))->followRedirect()

;}

// ... 

}

El método createJob() crea un puesto de trabajo, sigue la redirección y regresa alnavegador para no romper el fluidez de la navegación. Puedes también pasar unarray de valores que se fusionará con algunos valores por defecto.

Forzando al Método HTTP de un Enlace

Probar el enlace "Publish" es ahora más sencillo:

Page 125: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 125/273

 

$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 día 10, el enlace "Publish" se ha configurado para ser llamado conel método HTTP PUT. Como los navegadores no entienden peticiones PUT, elhelper link_to() convierte el enlace en un formulario con algun JavaScript. Comoel test browser no ejecuta JavaScript, es necesario forzar el métodoa PUT pasandolo como una tercera opción del método click(). Por otra parte, la

helper link_to() también incluye un CSRF token ya que hemos habilitado laprotección CSRF durante el día 1; la opción _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 más. Incluso si elenlace "Edit" ya no se muestra en la página de vista previa, vamos a añadiralgunas pruebas de este requisito.

En primer lugar, añadir otro argumento al método createJob() para permitirautomáticamente la publicación del puesto de trabajo, y crea un

método 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')->

Page 126: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 126/273

 

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' => '[email protected]','is_public' => false,

), $values)))->followRedirect()

;

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 página de edición debe devolver un códigode estado 404:

$browser->info(' 3.5 - When a job is published, it cannot be editedanymore')->createJob(array('position' => 'FOO3'), true)->get(sprintf('/job/%s/edit', $browser->getJobByPosition('FOO3')-

>getToken()))->

with('response')->begin()->isStatusCode(404)->

end();

Page 127: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 127/273

 

Sin embargo, si ejecutas las pruebas, no tendrás el resultado esperado ya que sete olvidó de implementar esta medida de seguridad de ayer. Escribir pruebas estambién una buena manera de descubrir los errores, ya que necesitas pensar entodos los casos.

Arreglar los errores es muy sencillo ya que sólo hay que avanzar a una página

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 solución es trivial, pero ¿está seguro de que todo lo demás sigue funcionando

como se esperaba? Puedes abrir el navegador y empezar a probar todas lascombinaciones posibles para acceder a la página de edición. Pero hay una maneramás sencilla: ejecutar tu conjunto de pruebas; si se ha introducido una regresión uerror, Symfony te lo dirá enseguida.

Regresando al Futuro en una Prueba

Cuando un puesto de trabajo expira en menos de cinco días, o si ya está vencido,el usuario puede ampliar la validación del puesto de trabajo por 30 días más apartir de la fecha actual.

Probar este requisito en un navegador no es fácil ya que la fecha de vencimiento

se establece automáticamente cuando se crea el puesto de trabajo a 30 días en elfuturo. Por lo tanto, cuando obtienes la página del puesto de trabajo, el enlacepara extender la validez del puesto de trabajo no está presente. Claro, se puedehackear la fecha de caducidad en la base de datos, o modificar la plantilla para quese muestre siempre el vínculo, pero eso es tedioso y propenso a errores. Como yahas adivinado, escribir algunas pruebas nos ayudarán una vez más.

Como siempre, tenemos que añadir una nueva ruta para elmétodo extend primero:

# apps/frontend/config/routing.ymljob:class: sfDoctrineRouteCollectionoptions:model: JobeetJobcolumn: tokenobject_actions: { publish: PUT, extend: PUT }

requirements:token: \w+

A continuación, la actualización del código del enlace "Extend" en el partial _admin:

Page 128: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 128/273

 

<!-- 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 acción 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 hasbeen 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 acción, elmétodo extend() de JobeetJob devuelve true si el puesto de trabajo se haextendido 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;

Page 129: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 129/273

 

}

// ... }

Finalmente, añadir un escenario de prueba:

$browser->info(' 3.6 - A job validity cannot be extended before the jobexpires 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 jobexpires 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 método call() trae una URL con un método diferente de GET o POST 

  Después de que el puesto de trabajo ha sido actualizado por la acción,tenemos que volver a cargar el objeto con $job->refresh() 

  Al final, hemos utilizado el objeto incrustado lime directamente para ponera prueba la nueva fecha de expiración.

Seguridad en Formularios

La Magia de los Formularios Serializados!

Page 130: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 130/273

 

Los Formularios Doctrine son muy fáciles de usar, ya que automatizan una grancantidad de trabajo. Por ejemplo, serializar un formulario a la base de datos es tansimple como una llamada a $form->save().

¿Cómo funciona? Básicamente, el método save() hace las siguientes pasos:

 Comenzar una transacción (porque Formularios anidados de Doctrine seguardan todos de una sola vez)

  Procesar los valores enviados (llamando amétodos updateCOLUMNColumn() si existen)

  Llamar al método fromArray() del objeto Doctrine para actualizar losvalores de las columnas

  Guardar el objeto en la base de datos

  Commit/Finalizar la transacción

Elementos de Seguridad Incorporados

El método fromArray() toma un array los valores y actualiza los correspondientesvalores de las columnas. ¿Esto representa un problema de seguridad? ¿Qué pasa sialguien trata de enviar un valor para una columna para la que no dispone deautorización? Por ejemplo, ¿se puede forzar la columna token?

Vamos a escribir una prueba para simular el envío de un puesto de trabajo con uncampo 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 sedebe a que por defecto los formularios no permiten campos extra en valoresenviados. Así es porque todos los campos de formulario deben tener un validador

de asociado.También puedes enviar campos adicionales desde la comodidad de tu navegadorutilizando herramientas con el Firefox Web Developer Toolbar.

Puedes saltear esta medida de seguridad mediante el establecimiento de laopción allow_extra_fields a true:

class MyForm extends sfForm{

Page 131: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 131/273

 

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, todavía no puedes pasar por alto esta medida de seguridad. Pero sirealmente quieres el valor, establece la opción filter_extra_fields a false:

$this->validatorSchema->setOption('filter_extra_fields', false);

Las pruebas escritas en esta sección son únicamente para efectos de demostrativos.Puedes ahora eliminarlos del proyecto Jobeet ya que las pruebas no necesitan validarcaracterísticas de Symfony.

Protección XSS y CSRF

Durante el día 1, aprendiste la tarea generate:app creando una aplicación segurapor defecto.

Primero, se habilitó la protección contra XSS. Esto significa que todas las variablesutilizadas en las plantillas se escaparán por defecto. Si intentas enviar unadescripción del trabajo con algunas etiquetas HTML dentro, te darás cuenta quecuando Symfony muestra la página del puesto de trabajo, las etiquetas HTML de ladescripción no se interpretan, pero si se ven como texto plano sin formato.

Entonces se habilita la protección 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 momentoeditando el archivo de configuraciónapps/frontend/config/settings.yml. En cuantoa el archivo databases.yml, los ajustes son configurables por el entorno:

all:.settings:# Form security secret (CSRF protection)csrf_secret: Unique$ecret

# Output escaping settingsescaping_strategy: trueescaping_method: ESC_SPECIALCHARS

Tareas de Mantenimiento

Incluso si Symfony es un framework web, viene con una herramienta de línea decomandos. Ya la has utilizado para crear no solo la estructura de directorio pordefecto del proyecto y de la aplicación, sino también para generar varios archivosdel modelo. Añadir una nueva Tarea es muy fácil ya que las herramientasutilizadas por la línea de comando symfony se empaquetan en un framework.

Page 132: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 132/273

 

Cuando un usuario crea un job, deberá activarlo para ponerlo en línea. Pero si no,la base de datos crecerá con jobs inútiles. Vamos a crear una tarea que elimineesos jobs de la base de datos. Esta tarea tendrá que ser ejecutada periódicamenteen 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 = <<<EOFThe [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 configuración se realiza en el método configure(). Cada tarea debe tener unnombre único (namespace:name), y puede tener argumentos y opciones.

Revisa las tareas ya incorporadas de Symfony (lib/task/) para más ejemplos de suuso.

La tarea jobeet:cleanup define dos opciones: --env y --days con unos valorespredeterminados razonables.

La ejecución de la tarea es similar a la ejecución de cualquier otra tarea yaincorporada en Symfony:

Page 133: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 133/273

 

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

Como siempre, el código para tener una base de datos limpia ha sido unrefactorizado 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 unvalor de acuerdo con el éxito de la Tarea. Puedes forzar un valor devolviendo un

número entero explícitamente al final de la Tarea.

Page 134: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 134/273

 

El Generador de Administrador de contenido

Con la cambios que se hizo ayer en Jobeet, la aplicación frontend esta ahoracompletamente utilizable por los oferentes y demandantes de empleo. Es hora dehablar un poco acerca de la aplicación backend.

Hoy, gracias a la funcionalidad del Generador de Admin de symfony, vamos adesarrollar una completa interfaz para el backend de Jobeet en sólo una hora.

Creación del Backend

El primer paso es crear la aplicación backend. Si tu memoria te sirve bien,deberías recordar cómo hacerlo con la tareagenerate:app:

$ php symfony generate:app backend

La aplicación backend ya está disponibleen 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 aplicación frontend, el controlador frontal de producción fuellamado index.php. Como sólo puede tener un index.php por directorio, symfony

crea un index.php para el primer controlador frontal de producción y nombra a losotros con el nombre de la aplicación.

Si intentas volver a cargar los datos con la tarea doctrine:data-load, no va afuncionar más. Esto se debe a que el método JobeetJob::save()necesita teneracceso al archivo de configuración app.yml de la aplicación frontend. Comotenemos ahora dos aplicaciones, symfony usa la primera que encuentra, que es

ahora backend.Pero como se ha visto durante el día 8, los ajustes se pueden configurar endistintos niveles. Al mover el contenido delarchivoapps/frontend/config/app.yml a config/app.yml, los ajustes seráncompartidos entre todas las aplicaciones y el problema será corregido. Vamoshacer el cambio ahora ya que vamos a utilizar mucho el modelo de clases en elGenerador de Admin, y así tendremos las variables definidas enapp.yml en laaplicación backend.

La tarea doctrine:data-load también tiene una opción --application. Así que, sinecesitas algunos ajustes específicos de una u otra aplicación, esta es la forma de

hacerlo:$ php symfony doctrine:data-load --application=frontend

Módulos del Backend

Para la aplicación frontend, la tarea doctrine:generate-module se ha utilizadopara inicializar un módulo básico CRUD basado en una clase del modelo. Para el

Page 135: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 135/273

 

backend, la tarea doctrine:generate-admin se utilizará, para generar unacompleta 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 módulo job y uno category para lasclases JobeetJob y JobeetCategory del modelo respectivamente.

La opción (opcional) --module sobreescribe el nombre del módulo generado pordefecto por la tarea (que habría sido de lo contrario jobeet_job para laclase JobeetJob).

Detrás de las escenas, la tarea también ha creado una ruta personalizada paracada módulo:

# apps/backend/config/routing.ymljobeet_job:

class: sfDoctrineRouteCollectionoptions:model: JobeetJobmodule: jobprefix_path: jobcolumn: idwith_wildcard_routes: true

No es de extrañar que la ruta de la clase utilizada por el Generador de Admines sfDoctrineRouteCollection, ya que el principal objetivo de una interfaz deadministración es la gestión del ciclo de vida de los objetos del modelo.

Definición de la ruta también define algunas opciones que no hemos visto antes:

  prefix_path: Define el prefijo de la url para la ruta generada (por ejemplo,la página editar será algo así como /job/1/edit).

  column: Define la columna de la tabla a usar en la URL por los enlaces quehace referencia a un objeto.

  with_wildcard_routes: Como la interfaz de administrador tendrá más queel clásico de operaciones CRUD, esta opción permite definir una mayorcolección 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 clásicosEjemplos de Uso.

Aspecto del Backend

De buenas a primeras, ya puedes utilizar los módulos generados:

http://www.jobeet.com.localhost/backend_dev.php/jobhttp://www.jobeet.com.localhost/backend_dev.php/category

Page 136: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 136/273

 

Los módulos de administración tienen muchas más funciones que los simplesmódulos que hemos generado en los días anteriores. Sin escribir una sola línea dePHP, cada módulo proporciona estas características:

  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 información inmediata al usuario

  ... y mucho mucho más

El Generador de Admin proporciona todas las funciones que necesitas para crearuna interfaz backend en un paquete fácil de configurar.

If you have a look at our two generated modules, you will notice there is noactivated webdesign whereas the symfony built-in admin generator feature has abasic graphic interface by default. For now, assets from the sfDoctrinePlugin arenot located under the web/ folder. We need to publish them under the web/ folderthanks to the plugin:publish-assets task:

$ php symfony plugin:publish-assets

Para hacer la experiencia de los usuarios un poco mejor, necesitamos personalizarel backend por defecto. También vamos a añadir un menú simple para que seafácil de navegar entre los diferentes módulos.

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>

Page 137: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 137/273

 

 <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 presenteen web/css/ ya que se instaló con el resto de las hojas de estilo durante el día 4.

Eventualmente, cambia la página de inicio por defecto en symfonyen routing.yml:

# apps/backend/config/routing.ymlhomepage:

Page 138: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 138/273

 

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 favorabrelos ahora. ¡Sorpresa! Los directorios templates están vacíos, y losarchivos actions.class.php están bastante vacíos y:

// apps/backend/modules/job/actions/actions.class.php require_oncedirname(__FILE__).'/../lib/jobGeneratorConfiguration.class.php';require_once dirname(__FILE__).'/../lib/jobGeneratorHelper.class.php';

class jobActions extends autoJobActions{}

¿Cómo es posiblemente que funcione? Si hechas un vistazo más de cerca, te daráscuenta de que la clase jobActions hereda de autoJobActions. Laclase autoJobActions es generada automáticamente por symfony si no existe. Quese encuentra en el directoriocache/backend/dev/modules/autoJob/, la cualcontiene el módulo "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 debería recordarte algun conocidocomportamiento. De hecho, es bastante similar a lo que ya hemos aprendido sobreel modelo y las clases de formulario. Basado en el modelo de la definición deesquema, symfony genera el modelo y las clases de formulario. Para el Generador

de Admin, el módulo generado se puede configurar editando elarchivo config/generator.yml que se encuentra en el módulo:

# apps/backend/modules/job/config/generator.ymlgenerator:class: sfDoctrineGeneratorparam:model_class: JobeetJobtheme: admin

Page 139: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 139/273

 

non_verbose_templates: truewith_show: falsesingular: ~plural: ~route_prefix: jobeet_jobwith_doctrine_route: true

config:actions: ~fields: ~list: ~filter: ~form: ~edit: ~new: ~

Cada vez que actualizas el archivo generator.yml, symfony regenera el caché.Como se verá hoy, la personalización de los módulos generados de administración

es fácil, rápida y divertida.NOTA La generación automática de archivos de cache sólo se produce en el entornode desarrollo En producción, tendrás que borrar la caché manualmente con latarea cache:clear (o borrar todo lo que encuentres en /cache).

La Configuración del Backend

Un módulo de administración puede ser personalizado con la edición de laclave config del archivo generator.yml. La configuración está organizada en sietesecciones:

  actions: Configuración por defecto de las acciones se encuentran en la listacomo en los formilarios

  fields: Configuración por defecto para los campos

  list: Configuración de la lista

  filter: Configuración de los filtros

  form: Configuración del fomulario new/edit

  edit: Configuración específica para la página edit

  new: Configuración específica para la página new

Vamos a comenzar la personalización.

Configuración del Título

Los títulos de las secciones list, edit, y new del módulo category se puedepersonalizar mediante la definición de una opción title:

# apps/backend/modules/category/config/generator.ymlconfig:actions: ~fields: ~

Page 140: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 140/273

 

list:title: Category Management

filter: ~form: ~edit:title: Editing Category "%%name%%"

new:title: New Category

El title para la sección edit contiene valores dinámicos: todas las cadenasencerradas entre %% se sustituyen por los correspondientes valores de lascolumnas del objeto.

La configuración para el módulo job es muy similar:

# apps/backend/modules/job/config/generator.ymlconfig:actions: ~fields: ~list:title: Job Management

filter: ~form: ~edit:title: Editing Job "%%company%% is looking for a %%position%%"

new:title: Job Creation

La Configuración de los Campos

Las diferentes vistas (list, new, y edit) se componen de campos. Un campopuede ser una columna de la clase del modelo, o una columna virtual comoveremos más adelante.

La configuración por defecto de los campos pueden ser personalizada con lasección fields :

# apps/backend/modules/job/config/generator.ymlconfig:fields:is_activated: { label: Activated?, help: Whether the user has

activated the job, or not }

Page 141: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 141/273

 

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

La sección fields sobreecribe la configuración de los campos para todas las vistas,lo que significa que label para is_activated se modificó para lasvistas list, edit, y new.

La Configuración del Generador de Admin se basa en el principio de unaconfiguración en cascada. Por ejemplo, si deseas cambiar un label solo para lavista list, define una opción fields bajo la sección list:

# apps/backend/modules/job/config/generator.ymlconfig:

list:fields:

is_public: { label: "Public? (label for the list)" }

Cualquier configuración que se establece en la sección principal fields puede sersobreecrita por la configuración de una vista específica. The overriding rules arethe 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 sobreescribenlas definidas en las clases form.

Configuración de la vista List

display 

De forma predeterminada, las columnas de la vista List son todas las columnas delmodelo, en el orden del archivo de esquema. La opción displaysobreescribe lopredefinido ordenando las columnas a mostrar:

# apps/backend/modules/category/config/generator.yml

config:list:title: Category Managementdisplay: [=name, slug]

El signo = antes del nombre de la columna es una convención para convertir lacadena en un enlace.

Page 142: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 142/273

 

 

Vamos a hacer lo mismo para el módulo job para que sea más legible:

# apps/backend/modules/job/config/generator.ymlconfig:list:title: Job Managementdisplay: [company, position, location, url, is_activated, email]

layout 

La lista puede ser visualizada en diferentes layouts. Por defecto, el layoutes tabular,lo que significa que cada valor de columna está en su propia columnade la tabla. Pero para el módulo job, sería mejor utilizar el layout stacked, que esel otro layout de serie:

# apps/backend/modules/job/config/generator.ymlconfig:list:title: Job Managementlayout: stackeddisplay: [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 definepor la opción params.

La opción display sigue siendo necesaria ya que define las columnas que ordenaránsegún el criterio del usuario.

Las Columnas "Virtuales"

Con esta configuración, el segmento %%category_id%% será sustituida por la claveprincipal de la categoría. Pero sería más útil mostrar el nombre de la categoría.Siempre que utilices la notación %%, la variable no tiene por qué corresponder a

Page 143: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 143/273

 

una columna en el actual esquema de base de datos. El Generador de Admin solonecesita encontrar un metodo get asociado en la clase del modelo.

Para mostrar el nombre de la categoría, podemos definir unmétodo getCategoryName() en la clase JobeetJob ysustituir %%category_id%% por%%category_name%%.

Sin embargo, la clase JobeetJob ya tiene un método getJobeetCategory() quedevuelve el objeto de la categoría relacionada. Y si usas%%jobeet_category%%, estefuncionará ya que la clase JobeetCategory tiene un métodomágico __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 más interesado en ver las últimospuestos de trabajo envíados. Puede configurar la columna de ordenación pordefecto al añadir una opción sort:

# apps/backend/modules/job/config/generator.ymlconfig:list:sort: [expires_at, desc]

 max_per_page De forma predeterminada, la lista es paginada, y cada página contiene 20 items.Esto puede cambiarse con la opción max_per_page:

# apps/backend/modules/job/config/generator.ymlconfig:list:max_per_page: 10

Page 144: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 144/273

 

 

 batch_actions 

En una lista, una acción se puede ejecutar sobre varios objetos. Estas acciones porlote no son necesarias para el módulo category, así, vamos a eliminarlas:

# apps/backend/modules/category/config/generator.ymlconfig:list:batch_actions: {}

La opción batch_actions define la lista de acciones por lote. Un array vacíopermite eliminar las características. De forma predeterminada, cada módulo tieneuna acción por lote delete definida por el framework, pero para el módulo job,supongamos que necesitamos una manera de extender la validez de determinadospuestos de trabajo para otros 30 días:

# apps/backend/modules/job/config/generator.ymlconfig:list:batch_actions:

_delete: ~extend: ~

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

Page 145: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 145/273

 

symfony arrojarán una excepción diciendote que crees unmétodo 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 beenextended successfully.');

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

}

Las claves primarias seleccionadas son almacenados en el parámetro ids de lapetición. Para cada puesto de trabajo seleccionado, elmétodoJobeetJob::extend() se llama con un argumento extra para eludir algunoscontroles realizados en el método. Tenemos que actualizar el métodoextend() con

un argumento extra para pasar la comprobacion de expiración.Actualiza el método 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;}

// ... 

Page 146: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 146/273

 

}

Después de que todos los puestos de trabajo se han ampliado, el usuario esredirigido al módulo job.

object_actions 

En la lista, hay una columna adicional para las acciones que puede ejecutarse enun único objeto. Para el módulo category , vamos a eliminarlos ya que tenemosun enlace con el nombre de la categoría para editarlo, y que realmente nonecesitamos ser capaces de borrar una directamente de la lista:

# apps/backend/modules/category/config/generator.ymlconfig:list:object_actions: {}

Para el módulo job, vamos a mantener las acciones existentes y añadir una nuevaacción extend similar a la que hemos añadido como batch action:

# apps/backend/modules/job/config/generator.ymlconfig:list:object_actions:

extend: ~_edit: ~_delete: ~

Como para las batch actions, las acciones _delete y _edit son las definidas por elframework. Tenemos que definir la acción listExtend() para hacer que elenlace 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 beenextended successfully.');

Page 147: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 147/273

 

 $this->redirect('jobeet_job');

}

// ... }

actions 

Ya hemos visto la forma de vincular la acción a una lista de objetos o un objetoúnico. La opción actions define las acciones que no tienen objeto, como lacreación de un nuevo objeto. Vamos a eliminar la acción predeterminada new yañadir una nueva acción, que suprime todos los puestos de trabajo que no se hanactivado por el usuario por más de 60 días:

# apps/backend/modules/job/config/generator.ymlconfig:list:actions:

deleteNeverActivated: { label: Delete never activated jobs }

Hasta ahora, todas las acciones que hemos definido tenian ~, lo que significa quesymfony ya configuró la acción automáticamente. Cada acción puede serpersonalizada mediante la definición de un array de parámetros. Laopción label sobreescribe el label por defecto generado por symfony.

Por defecto, la acción ejecutada cuando haces click en el enlace es el nombre de la

acción con prefijo list.Crea la acción listDeleteNeverActivated en el módulo job:

// apps/backend/modules/job/actions/actions.class.php class jobActions extends autoJobActions{public function executeListDeleteNeverActivated(sfWebRequest $request){$nb = Doctrine::getTable('JobeetJob')->cleanup(60);

Page 148: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 148/273

 

 if ($nb){

$this->getUser()->setFlash('notice', sprintf('%d never activatedjobs have been deleted successfully.', $nb));

}

else{

$this->getUser()->setFlash('notice', 'No job to delete.');}

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

// ... }

Hemos reutilizado el método JobeetJobTable::cleanup() definido el día de ayer.

Es otro gran ejemplo de la reutilización proporcionada por el patrón MVC.También puede cambiar la acción a ejecutar pasando un parámetro action:

deleteNeverActivated: { label: Delete never activated jobs, action: foo }

table_method  

El número de consultas a la base de datos para mostrar el listado de los puestosde trabajo es 13, como muestra la barra de depuración web.

Page 149: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 149/273

 

Si haces clic en ese número, se verá que la mayoría de las peticiones son pararecuperar el nombre de la categoría para cada trabajo.

Para reducir el número de consultas, podemos cambiar el método utilizado paraobtener los puestos de trabajo utilizando la opción table_method:

# apps/backend/modules/job/config/generator.ymlconfig:list:table_method: retrieveBackendJobList

Ahora debes crear el método retrieveBackendJobList en JobeetJobTable situadoen 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 método retrieveBackendJobList() agrega un join entre lastablas job y category y crea automáticamente el objeto categoría relacionado concada puesto de trabajo.

The number of requests is now down to three:

Configuración de las Vistas Form

La Configuración de la Vista Form se realiza en tres secciones: form, edit, y new.Todas tienen la misma configuración y la capacidad de la secciónform sólo existecomo un mensaje para las secciones edit y new.

display 

Page 150: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 150/273

 

En cuanto a la lista, puedes cambiar el orden de los campos que se muestran conla opción display. Pero, como el formulario mostrado se define por una clase, notrates de eliminar un campo, ya que podría dar lugar a errores de validacióninesperados.

La opción display para vistas form también se puede utilizar para organizar los

campos en grupos:# apps/backend/modules/job/config/generator.ymlconfig: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 configuración anterior define dos grupos (Content y Admin), each que contienenun subconjunto de los campos del formulario.

El Generador de Admin tiene soporte para la relación muchos a muchos. En elformilario categoría, tienes un input para el nombre, uno para el slug, y un cuadrodesplegable para los afiliados relacionados. Como no tiene sentido editar estarelación en esta página, 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']);}

}

Page 151: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 151/273

 

Columnas "Virtuales"

En las opciónes display para el formulario de puestos de trabajo, elcampo _generated_token comienza con un guión bajo (_). Esto significa que lavisualización de este campo será manejado por un partial personalizado denombre _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 esaccesible a través del método getObject().

También puede delegar la visualización a un componente con el prefijo de un tilde (~)

al nombre del campo.

class 

Como el formulario será utilizado por los administradores, hemos mostrado másinformación que para el usuario del formulario job. Pero por ahora, algunos deellos no aparecen en la formulario ya que se han eliminado en laclase JobeetJobForm.

Para tener diferentes formularios para el frontend y el backend, tenemos que creardos clases form. Vamos a crear una clase BackendJobeetJobFormque herede de laclase JobeetJobForm. Como no tienen los mismos campos ocultos, también

tenemos que refactorizar la clase JobeetJobForm un poco para mover ladeclaración unset() en un método que será sobreescritoen 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(){

Page 152: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 152/273

 

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 Administración puedeser sobreescrita con el ajuste de la opción class:

# apps/backend/modules/job/config/generator.ymlconfig:form:class: BackendJobeetJobForm

Como hemos añadido una nueva clase, no te olvides de limpiar el cache.

El formulario edit todavía tiene una pequeña molestia. El logotipo subido noaparece en ningun lugar y no podrás quitar el actual. ElwidgetsfWidgetFormInputFileEditable añade capacidades de edición a un simplewidget input file:

// lib/form/doctrine/BackendJobeetJobForm.class.php class BackendJobeetJobForm extends JobeetJobForm{public function configure(){parent::configure();

$this->widgetSchema['logo'] = newsfWidgetFormInputFileEditable(array(

'label' => 'Company logo','file_src' => '/uploads/jobs/'.$this->getObject()->getLogo(),'is_image' => true,

Page 153: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 153/273

 

'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 modificarsus características y visualización:

  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 edición o no

 with_delete: Si se desea mostrar una casilla de verificación para eliminar

  template: La plantilla a utilizar para mostrar el widget

El aspecto del Generador de Admin puede ser ajustado muy fácilmente ya que lasplantillas definen una gran cantidad de atributos class y id . Por ejemplo, el logotipo

puede ser personalizado utilizando sf_admin_form_field_logo. Cada campo tambiéntiene una clase, dependiendo de el tipo de campocomo sf_admin_text o sf_admin_boolean.

La opción edit_mode utiliza el método sfDoctrineRecord::isNew().

Devuelve true si el objeto del formulario es nuevo, y false de lo contrario. Esto esde gran ayuda cuando es necesario que tengas diferentes widgets o validadores,dependiendo del estado del objeto invocado.

Page 154: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 154/273

 

Configuración de Filtros

La configuración de los filtros es la misma que la configuración de las vistas forms.Como cuestión de hecho, los filtros son sólo forms. Y como para los forms, lasclases se han generado por la tarea doctrine:build --all. También puedesvolver a generarlas con la tarea doctrine:build --filters.

Estas clases se encuentran en el directorio lib/filter/ y cada uno de clase delmodelo tiene asociada una clase de filtros(JobeetJobFormFilterpara JobeetJobForm).

Vamos a eliminarlas por completo para el módulo category:

# apps/backend/modules/category/config/generator.ymlconfig:filter:class: false

Para el módulo job , vamos a eliminar algunos de ellos:

# apps/backend/modules/job/config/generator.ymlfilter:display: [category_id, company, position, description, is_activated,

is_public, email, expires_at]

Como los filtros son siempre opcional, no hay necesidad de sobreescribir clasefiltro para configurar los campos que se mostrarán.

Page 155: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 155/273

 

Acciones Personalizadas

Cuando la configuración no es suficiente, puedes agregar nuevos métodos a laclase de acciones, como hemos visto con la característica de extend, pero tambiénpuede sobreescribir la acción de los métodos generados:

Método Descripción

executeIndex() La acción de la vista list 

executeFilter()  Actualiza los filtros

executeNew() La acción de la vista new 

executeCreate()  Crea un nuevo Job

executeEdit() La acción de la vista edit 

executeUpdate()  Actualiza un Job

executeDelete()  Borra un Job

executeBatch()  Ejecuta una acción por lote

executeBatchDelete() Ejecuta la acción por lote _delete 

processForm()  Procesa el formualrio Job

getFilters()  Devuelve los filtros actuales

setFilters()  Establece los filtros

getPager()  Devuelve el paginador de la lista

getPage()  Obtiene la página de la lista

setPage()  Establece la página de la lista

buildCriteria() Construye el Criteria para la lista

addSortCriteria() agrega un Criteria ordenado para la lista

getSort()  Devuelve la columna utilizada para ordenar

setSort()  Establece la columna utilizada para ordenar

Como cada método generado hace solo una cosa, es fácil cambiar uncomportamiento sin tener que copiar y pegar código demasiado.

Personalización de Plantillas

Hemos visto cómo personalizar las plantillas generadas gracias a losatributos class y id añadidos por el Generador de administrador en el códigoHTML.

Page 156: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 156/273

 

En cuanto a las clases, también puedes sobreescribir las plantillas originales. Comolas plantillas son simples archivos PHP y no clases PHP , una plantilla puede sersobreescritapor creando una plantilla del mismo nombre en el módulo (porejemplo, en el directorioapps/backend/modules/job/templates/ para el módulo deadministración job):

Plantilla Descripción

_assets.php  Muestra CSS y JS a usar por las plantillas

_filters.php  Muestra los filtros

_filters_field.php  Muestra un único filtro de campo

_flashes.php  Muestra los mensajes flash

_form.php  Muestra el formulario

_form_actions.php  Muestra las acciones del formulario_form_field.php  Muestra un único campo de formulario

_form_fieldset.php  Muestra un fieldset de formulario

_form_footer.php  Muestra el pie de página del formulario

_form_header.php  Muestra cabecera del formulario

_list.php  Muestra la lista

_list_actions.php  Muestra las acciones de lista

_list_batch_actions.php  Muestra la lista de acciones por lotes

_list_field_boolean.php  Muestra un único campo booleano en la lista

_list_footer.php  Muestra el pie de página de lista

_list_header.php  Muestra la cabecera de lista

_list_td_actions.php  Muestra las acciones de objeto para una fila

_list_td_batch_actions.php  Muestra la casilla de verificación para una fila

_list_td_stacked.php  Muestra el stacked layout para una fila

_list_td_tabular.php  Muestra un único campo de lista

_list_th_stacked.php  Muestra un solo nombre de columna para la

cabecera

_list_th_tabular.php  Muestra un solo nombre de columna para la

Page 157: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 157/273

 

Plantilla Descripción

cabecera

_pagination.php  Muestra la paginación de lista

editSuccess.php Muestra la vista edit 

indexSuccess.php Muestra la vista list 

newSuccess.php Muestra la vista new 

Configuración Final

La configuración final de la administración Jobeet es la siguiente:

# apps/backend/modules/job/config/generator.ymlgenerator:class: sfDoctrineGeneratorparam:model_class: JobeetJobtheme: adminnon_verbose_templates: truewith_show: falsesingular: ~plural: ~route_prefix: jobeet_jobwith_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 Managementlayout: stackeddisplay: [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: 10sort: [expires_at, desc]batch_actions:

_delete: ~extend: ~

object_actions:

Page 158: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 158/273

 

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: BackendJobeetJobFormdisplay:

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.ymlgenerator:class: sfDoctrineGeneratorparam:model_class: JobeetCategorytheme: adminnon_verbose_templates: truewith_show: falsesingular: ~plural: ~

route_prefix: jobeet_categorywith_doctrine_route: true

config:actions: ~fields: ~list:title: Category Managementdisplay: [=name, slug]batch_actions: {}object_actions: {}

filter:

class: falseform:actions:

_delete: ~_list: ~_save: ~

edit:title: Editing Category "%%name%%"

new:

Page 159: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 159/273

 

title: New Category

Con tan sólo estos dos archivos de configuración, hemos desarrollado unaexcelente interfaz backend para Jobeet en cuestión de minutos.

Ya sabes que cuando algo es configurable en un archivo YAML, hay también la

posibilidad de usar código PHP. Para el Generador de administrador, puedeseditar apps/backend/modules/job/lib/jobGeneratorConfiguration.class.php . Teda las mismas opciones que YAML pero con un archivo PHP. Para aprender los nombrede los métodos, echar un vistazo a la clase base generadaencache/backend/dev/modules/autoJob/lib/BaseJobGeneratorConfiguration.class.php.

Page 160: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 160/273

 

El Usuariovamos a descubrir cómo Symfony gestiona persistentemente datos entre laspeticiones HTTP. Como se puede saber, el protocolo HTTP no tiene memoria, loque significa que cada petición es independiente de lo anterior o de lasubsiguiente. Los sitios web modernos necesitan una manera de mantener losdatos entre las peticiones para mejorar la experiencia del usuario.

Una sesión de usuario puede ser identificada mediante una cookie. En Symfony, eldesarrollador no tiene que manipular directamente las sesiones, sino que usa elobjeto sfUser, que representa al usuario final de la aplicación.

Mensajes Flashes del Usuario

Ya hemos visto el objeto de usuario en acción con los mensajes flashes. Un flashes un efímero mensaje almacenado en la sesión de usuario que se eliminaráautomáticamente después de la próxima petición. Es muy útil cuando se necesitamostrar un mensaje al usuario después de un redireccionamiento. El generador deadministración utiliza flashes para mostrar información al usuario cuando seguarda un puesto de trabajo, se borra o extiende su validez.

Un flash se establece utilizando el método setFlash() de sfUser:

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

Page 161: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 161/273

 

$job = $this->getRoute()->getObject();$this->forward404Unless($job->extend());

$this->getUser()->setFlash('notice', sprintf('Your job validity hasbeen 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 amostrar. Se puede definir cualquier flashes que quieras, pero notice yerror sondos de los más comunes (que son utilizadas intensivamente por el generador deadministración).

Corresponde a los desarrolladores incluír el mensaje flash en las plantillas. ParaJobeet, 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 través de la variable especial $sf_user.

Algunos objetos symfony siempre son accesibles en las plantillas, sin la necesidad deexplicitamente pasarlos desde la acción: $sf_request,$sf_user, y $sf_response.

Atributos de Usuario

Lamentablemente, los casos de uso de Jobeet no tienen ningún requisito sobre elalmacenamiento de algo en la sesión de usuario. Así que vamos a añadir un nuevorequisito: para facilitar la navegación de los puestos de trabajo, los últimos tresvistos por el usuario se deben mostrar en un menú con enlaces para volver a lapágina de esos puestos de trabajo más tarde.

Cuando un usuario accede a una página de puestos de trabajo, el objeto job

mostrado necesita ser agregado en el historial del usuario y almacenado en lasesión:

// apps/frontend/modules/job/actions/actions.class.php class jobActions extends sfActions{public function executeShow(sfWebRequest $request){$this->job = $this->getRoute()->getObject();

Page 162: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 162/273

 

  // 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);

}

// ... }

Podríamos haber almacenado los objetos JobeetJob directamente en la sesión. Estoestá totalmente desaconsejada ya que las variables de sesión son serializadas(almacenadas) entre las peticiones. Y cuando la sesión se ha cargado, losobjetos JobeetJob son de-serializados y pueden quedar "estancos" aun si han sidomodificados o borrados en el ínterin.

Los métodos getAttribute(), setAttribute() 

Dado un identificador, el método sfUser::getAttribute() obtiene los valores de lasesión de usuario. Por el contrario, el método setAttribute()almacena cualquiervariable PHP en la sesión, para un determinado identificador.

El método getAttribute() también tiene un valor predeterminado opcional adevolver si el identificador no está todavía definido.

El valor por defecto tomado por el método getAttribute() es un acceso directo para:

if (!$value = $this->getAttribute('job_history'))

{$value = array();

}

La Clase  myUser 

Lo mejor respeto a la separación de las capas, es mover el código a laclase myUser. La clase myUser sobreescribe la clase por defecto basesymfonysfUser con comportamientos específicos de la aplicación:

// 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);}

// ... 

Page 163: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 163/273

 

}

// 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 código también 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): Sólo los tres últimos puestos de trabajo vistospor el usuario se muestran

En el layout, agrega el código siguiente antes de que la variable $sf_content semuestre:

// 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 método getJobHistory() para obtener el historial job:

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

Page 164: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 164/273

 

$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 añadir un método para resetearel 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 laclase sfParameterHolder. Los métodos getAttribute() y setAttribute() sonmétodos proxy para getParameterHolder()->get() y getParameterHolder()-

>set(). Como el método remove() no tiene método proxy en sfUser, necesitasusar el objeto del contenedor de parámetros directamente.

Page 165: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 165/273

 

La clase sfParameterHolder es también utilizada por sfRequest para almacenar susparámetros.

Seguridad de la Aplicación

Autenticación

Al igual que muchas otras características, la seguridad es manejada por un archivoYAML, security.yml. Por ejemplo, puedes encontrar la configuración por defectopara la aplicación backend en el directorio config/:

# apps/backend/config/security.ymldefault:is_secure: false

Si cambia el is_secure a true, todala aplicación backend necesitará queel usuario deba autenticarse.

En un archivo YAML, un Booleanopuede expresarse con lacadena true o false.

Si echas un vistazo a los registrosen la barra web de depuración, se puede observar que elmétodo executeLogin() de la clase defaultActions se llama por cada página queintentas acceder.

Cuando un usuario no-autenticado intenta acceder a una acción segura, Symfonyremite la peticióna la acción login configurada en settings.yml:

all:.actions:login_module: defaultlogin_action: login

No es posible asegurar a la acción login para evitar una recursión infinita.

Como vimos durante el día 4, el mismo archivo de configuración se pueden definir envarios lugares. Este es también el caso de security.yml. Para sólo asegurar odesproteger una única acción o un módulo, crea un security.yml en thedirectorio config/ del módulo:

index:is_secure: false

Page 166: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 166/273

 

 all:is_secure: true

De forma predeterminada, la clase myUser hereda de sfBasicSecurityUser, y node sfUser. sfBasicSecurityUser proporciona métodos para gestionar la

autenticación de usuario y autorización.Para gestionar la autenticación de usuario, utiliza losmétodos isAuthenticated() y setAuthenticated():

if (!$this->getUser()->isAuthenticated()){$this->getUser()->setAuthenticated(true);

}

Autorización

Cuando un usuario está autenticado, el acceso a algunas acciones pueden ser aún

más restringida por la definición de credenciales. Un usuario debe tener lascredenciales para acceder a la página:

default:is_secure: falsecredentials: admin

El sistema de credenciales de Symfony es bastante simple y poderoso. Unacredencial puede representar todo lo que sea necesario para describir la seguridadal modelo de la aplicación (como grupos o permisos).

Credenciales Complejas

El item credentials de security.yml soporta operadores Booleanos para describir lasnecesidades complejas en credenciales.

Si un usuario debe tener credenciales A y B, hay que envolver las credenciales concorchetes:

index:credentials: [A, B]

Si un usuario debe tener credenciales A o B, hay que envolver las credenciales con dospares de corchetes:

index:credentials: [[A, B]]

Incluso puedes mezclar y combinar entre paréntesis para describir cualquier tipo deExpresión booleana con cualquier número de credenciales.

Para gestionar las credenciales de usuario, sfBasicSecurityUser da variosmétodos:

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

Page 167: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 167/273

 

 // Check if the user has a credential echo $user->hasCredential('foo'); => true

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

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

// Remove a credential $user->removeCredential('foo');echo $user->hasCredential('foo'); => 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 sólotenemos un perfil: el administrador.

Plugins

Como no nos gusta reinventar la rueda, no vamos a desarrollar la acción de accesoa partir de cero. En lugar de ello, se instalará un plugin symfony.

Uno de los grandes puntos fuertes del framework Symfony son los plugins. Comoveremos en los próximos días, es muy fácil crear un plugin. También es bastantepotente, ya que un plugin puede contener cualquier cosa, desde la configuraciónde los módulos hasta los recursos web.

Hoy, vamos a instalar sfDoctrineGuardPlugin para garantizar el acceso albackend

$ php symfony plugin:install sfDoctrineGuardPlugin

La tarea plugin:install instala un plugin por nombre. Todos los plugins sonalmacenados bajo el directorio plugins/ y cada uno tiene su propio directorio conel 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 versión estable del mismo. Para instalar una versión específica de un plugin,

pasa la opción --release.La web de los plugins lista todas las versiones disponibles agrupados por la versiónde Symfony.

Como un plugin es auto-contenido en un directorio, también puedes descargar elpaquete desde el sitio web de Symfony y descomprimirlo, o alternativamente,haces un enlace svn:externals a su Subversion repository. 

Page 168: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 168/273

 

Seguridad del Backend

Cada plugin tiene un archivo README que explica cómo configurarlo.

Vamos a ver cómo configurar el nuevo plugin. Como el plugin ofrece varias nuevasclases al modelo para la gestión 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 tablasexistentes antes de volver a crearlas. Para evitar esto, puedes construir los modelos,formularios y filtros y, a continuación, crear las nuevas tablas ejecutando el SQLgenerado y almacenado en data/sql/.

Como sfDoctrineGuardPlugin agrega varios métodos a la clase de usuario, esnecesario cambiar la clase base de myUser a sfGuardSecurityUser:

// apps/backend/lib/myUser.class.php class myUser extends sfGuardSecurityUser{}

sfDoctrineGuardPlugin proporciona una acción signin en elmodulo sfGuardAuth para autenticar a los usuarios:

Edita el archivo settings.yml para cambiar la acción por defecto empleada por lapágina de login:

# apps/backend/config/settings.ymlall:.settings:enabled_modules: [default, sfGuardAuth]

# ...

.actions:login_module: sfGuardAuthlogin_action: signin

# ...

Como los plugins soncompartidos entre todas lasaplicaciones de un proyecto,

tiene que permitirexplícitamente los módulosque desea utilizar agregandoel item enabled_modules.

El último paso es crear un usuario administrador:

$ php symfony guard:create-user fabien SecretPass

Page 169: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 169/273

 

$ php symfony guard:promote fabien

El sfGuardPlugin proporciona funciones para la gestión de usuarios, grupos ypermisos desde la línea de comandos. Utiliza la tarea list para listar todas las tareaspertenecientes 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 añadir un enlace de logout en elmenú:

// 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 latarea app:routes.

Para pulir el backend aún más, vamos a añadir un nuevo módulo para la gestióndel administrador de usuarios. Afortunadamente, sfGuardPluginproporciona unmódulo de este tipo. Como el módulo sfGuardAuth, que necesitarás habilitarloen settings.yml:

// apps/backend/config/settings.ymlall:.settings:enabled_modules: [default, sfGuardAuth, sfGuardUser]

Añadir un enlace en el menú:

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

Terminamos!

Page 170: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 170/273

 

Probando al Usuario

El tutorial de hoy no termina ya que no hemos aún hablado de las pruebas alusuario. Como el symfony browser simula las cookies, es bastante fácil de probarel comportamiento del usuario mediante el uso del tester sfTesterUser. 

Vamos a actualizar la pruebas funcionales para el menú que hemos añadido hoy.Agrega el código siguiente al final del módulo de pruebas funcionales job:

// test/functional/frontend/jobActionsTest.php $browser->info('4 - User job history')->

loadData()->restart()->

info(' 4.1 - When the user access a job, it is added to its history')->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()

;

Para facilitar las pruebas, primero hacemos la recarga datos y reiniciamos elnavegador para empezar con una sesión.

El método isAttribute() controla un atributo de usuario dado.

El tester sfTesterUser también proporcionamétodos isAuthenticated() and hasCredential() para poner a prueba laautenticación de usuario y autorizaciones.

Page 171: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 171/273

 

Los Canales

Si estás buscando un puesto de trabajo, es probable que desees ser informado tanpronto como un nuevo puesto de trabajo se ha publicado. Y no es muyconveniente comprobar el sitio web a cada hora. Vamos hoy a añadir feeds (ocanales) de varios puestos de trabajo, para mantener a nuestros usuarios Jobeetactualizados.

Formatos

Symfony tiene soporte nativo para los formatos y tipos MIME. Esto significa que elmodelo y el controlador pueden tener diferentes plantillas basadas en el formatosolicitado. El formato predeterminado es HTML pero Symfony admite variosformatos de serie como ser txt, js, css, json, xml, rdf, o atom.

El formato se puede configurar utilizando el método setRequestFormat() delobjeto request:

$request->setRequestFormat('xml');

Pero la mayor parte del tiempo, el formato está incluído en la URL. En este caso,Symfony lo establecerá por tí si la variable especial sf_format se utiliza en la rutacorrespondiente. 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. Puedescomprobarlo por tí mismo ejecutando la tarea app:routes:

Feeds

Feed de los ÚltimosPuestos de Trabajo

Soportar diferentesformatos es tán fácil

como la creardiferentes plantillas.Para crear un feedAtom 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">

Page 172: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 172/273

 

<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 más utilizado para aplicaciones web, éste puede ser omitidodel nombre de la plantilla. Ambasplantillas indexSuccess.php yindexSuccess.html.php son equivalentes y Symfonyutiliza la primero que encuentre.

¿Por qué las plantillas predeterminadas tienen el sufijo Success? Una acción puededevolver un valor para indicar que plantilla se mostrará. Si la acción no dice o devuelvenada, eso equivalente al siguiente código:

return sfView::SUCCESS; // == 'Success' 

Si deseas cambiar el sufijo, devuelve otra cosa:

return sfView::ERROR; // == 'Error' 

return 'Foo';

También puedes cambiar el nombre de la plantilla utilizando elmétodo setTemplate():

$this->setTemplate('foo');

Por defecto, Symfony cambiará la respuesta Content-Type de acuerdo con elformato, 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 página 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>

Page 173: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 173/273

 

El URI interno es el mismo que para la lista job con el sf_format añadido comouna variable.

Añade una etiqueta <link> en la sección 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 segundoargumento 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 utilización de la función strtotime() para obtener lafecha created_at como timestamp. Para obtener la fecha del envío, crea el

métodogetLatestPost():// 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 código:

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

Page 174: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 174/273

 

<?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 método getHost() del objeto request ($sf_request) devuelve el actual host,que viene muy bien para crear un vínculo absoluto para el logo de la empresa.

Page 175: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 175/273

 

 Cuando se crea un feed, la depuración es más fácil si utiliza herramientas de línea decomandos como curl o wget, ya que puedes ver el contenido real del feed.

El Feed de los Últimos Puestos de Trabajo de una Categoría

Uno de los objetivos de Jobeet es ayudar a la gente a encontrar puestos de trabajoespecíficos. Por lo tanto, tenemos que proporcionar un feed para cada categoría.

En primer lugar, vamos a actualizar la ruta category para agregar el soporte paradiferentes formatos:

// apps/frontend/config/routing.ymlcategory:url: /category/:slug.:sf_formatclass: sfDoctrineRouteparam: { 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. Actualizalos enlaces de los feeds de la categoría 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">

Page 176: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 176/273

 

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

El último paso es la creación de la plantilla showSuccess.atom.php. Pero como estefeed también lista puestos de trabajo, podemos refactorizar el código que genera

los items del feed mediante la creación de un partial _list.atom.php. Como elformato html, los partial son de un formato específico:

<!-- 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 lospuestos de trabajo:

<!-- apps/frontend/modules/job/templates/indexSuccess.atom.php --><?xml version="1.0" encoding="utf-8"?>

Page 177: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 177/273

 

<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 unacategoría:

// lib/model/doctrine/JobeetCategory.class.php class JobeetCategory extends BaseJobeetCategory{public function getLatestPost()

Page 178: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 178/273

 

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

}

// ... }

Page 179: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 179/273

 

Servicios Web

Con agregar feeds a Jobeet, los solicitantes de puestos de trabajo pueden ahoraser informados de los nuevos puestos de trabajo en tiempo real.

En el otro lado de la valla, esta cuando se envía un puesto de empleo, y deseastener la mayor exposición/publicidad posible. Si tu trabajo es sindicado en unagran cantidad de pequeños sitios web, tendrás una mejor oportunidad deencontrar a la persona adecuada. Ese es el poder de la Larga Cola o long tail. Losafiliados podrán publicar los puestos de trabajos más recientes en sus sitios webgracias a los servicios web que se desarrollarán hoy.

Los Afiliados

Según los requisitos del día 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.ymlJobeetAffiliate:sensio_labs:url: http://www.sensio-labs.com/email: [email protected]_active: truetoken: sensio_labsJobeetCategories: [programming]

symfony:url: http://www.symfony-project.org/email: [email protected]_active: falsetoken: symfonyJobeetCategories: [design, programming]

La creación de registros para la tabla intermedia de una relación muchos-a-muchos es tan simple como definir un array cuya clave sea el nombre de larelación. El contenido del array es el nombre de los objetos definidos en losarchivos de datos. Puede enlazar objetos desde diferentes archivos, pero losnombres deben haber sido definidos antes.

En el archivo de datos, los tokens están hardcodeados para simplificar las pruebas,pero cuando un usuario real solicita una cuenta, el token tendrán que sergenerado:

// lib/model/doctrine/JobeetAffiliate.class.php class JobeetAffiliate extends BaseJobeetAffiliate{public function save(Doctrine_Connection $conn = null)

Page 180: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 180/273

 

{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 hábito primero

definir la dirección URL:# apps/frontend/config/routing.ymlapi_jobs:url: /api/:token/jobs.:sf_formatclass: sfDoctrineRouteparam: { 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 dirección URL y los valoresválidos son xml, json, o yaml.

El método getForToken() es llamado cuando la acción recupera la colección deobjetos relacionados con la ruta. Como tenemos que comprobar que el afiliadoesta activado, tenemos que sobreescribir el comportamiento predeterminado de laruta:

// 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();}

Page 181: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 181/273

 

 // ... 

}

Si el token no existe en la base de datos, arrojamos unaexcepción sfError404Exception. Esta clase excepción se convierten

automáticamente en una respuesta 404. Esta es la forma más sencilla de generaruna página 404 de una clase del modelo.

El método getForToken() utiliza un nuevo método llamado getActiveJobs() ydevuelve 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 acción y plantillas para la api. Inicializa el módulo con latarea generate:module:

$ php symfony generate:module frontend api

Como no vamos a utilizar la acción predeterminada index, puedes eliminarla de laclase action, y eliminar la plantilla asociada indexSucess.php.

La Acción

Todos los formatos comparten la misma acción 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());}

Page 182: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 182/273

 

}

En lugar de pasar un array de objetos JobeetJob a las plantillas, se pasa un arrayde cadenas. Como tenemos tres modelos diferentes para la misma acción, la lógicade proceso de los valores ha sido refactorizada en elmétodo 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 -->[

Page 183: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 183/273

 

<?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 ysu content type se puede cambiar y el layout desactivado en la acción:

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 acción, el método setLayout() cambia el layout por defecto o se desactivacuando 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 ?>

Page 184: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 184/273

 

 <?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-válido, tendrás una páginaXML 404 para el formato XML, y una página JSON 404 para el formato JSON. Peropara el formato YAML, Symfony no sabe qué mostrar.

Cuando creas un formato, una plantilla personalizada de error debe ser creada. Laplantilla se utilizará para páginas 404, y todas las demás excepciones.

Dado que la excepción debe ser diferente según sea un entorno de desarrollo oproducción, dos archivos son necesarios (config/error/exception.yaml.php paradepuración, y config/error/error.yaml.php para producción):

// 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 ?>

Page 185: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 185/273

 

Sobreescribiendo las plantillas por defecto de error 404 y de excepción es tan simplecomo crear los archivos en cuestión 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 archivoautogeneradoapiActionsTest.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 configuredfor 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 darás cuenta de tres nuevos métodos:

  isValid(): Checks whether or not the XML response is well formed

  isFormat(): Pone a prueba el formato de un request

Page 186: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 186/273

 

  contains(): Para formatos no-HTML, comprueba si la respuesta contiene elfragmento de texto esperado

El Formulario de Afiliación

Ahora que el servicio web está listo para ser utilizado, vamos a crear el Formulario

de Afiliación. Vamos a describir una vez más el clásico proceso de agregar unanueva funcionalidad a una aplicación.

Enrutamiento

Lo sabes. La ruta es la primera cosa a crear:

# apps/frontend/config/routing.ymlaffiliate:class: sfDoctrineRouteCollectionoptions:model: JobeetAffiliateactions: [new, create]object_actions: { wait: get

Se trata de una clásica colección de rutas Doctrine con una nueva opción deconfiguración: actions. Como no necesitamos todas las siete acciones definidaspor defecto para la ruta, la opción actions instruye a la ruta para sólo coincidir conlas acciones new y create . La ruta adicional wait se utilizarán para dar alinminente afiliado algunos comentarios acerca de su cuenta.

Inicialización

El segundo paso clásico es generar un módulo:

$ php symfony doctrine:generate-module frontend affiliate JobeetAffiliate--non-verbose-templates

Las Plantillas

La tarea doctrine:generate-module genera las clásicas siete acciones y suscorrespondientes 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') ?>

Page 187: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 187/273

 

<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 tokenas soon as your account will be activated.

</div>

Por último, cambiar el enlace en el pie de página para que apunte almódulo 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 más, ya que sólo se utiliza el formulario de creación, abre elarchivo actions.class.php y elimina todos los métodos perodejaexecuteNew(), executeCreate(), y processForm().

Para la acción processForm(), cambiar la URL de redireccionamiento a laacción wait:

// apps/frontend/modules/affiliate/actions/actions.class.php 

$this->redirect($this->generateUrl('affiliate_wait', $jobeet_affiliate));La acción 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){}

Page 188: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 188/273

 

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'] = newsfValidatorEmail(array('required' => true));}

}

The new sfForm::useFields() method allows to specify the white list of fields tokeep. All non mentionned fields will be removed from the form.

El framework de formularios soporta relaciones muchos-a-muchos como cualquierotra columna. Por defecto, esta relación es mostrada como un cuadro desplegablegracias al widget sfWidgetFormChoice. Como se ha visto durante el día 10, hemos

cambiado lo mostradomediante el uso de laopción expanded.

Como emails y URLs

tienden a ser bastantemás largo que el tamañopredeterminado de unaetiqueta input, losatributos de HTML pordefecto se puedeconfigurar utilizando elmétodo setAttribute().

Page 189: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 189/273

 

 

Las Pruebas

El último paso es escribir algunas pruebas funcionales para la nueva función.

Sustituye las pruebas generadas para el módulo affiliate con el códigosiguiente:

// 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' => '[email protected]','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' => '[email protected]',

)))->with('form')->isError('jobeet_categories_list')

;

El Backend para Afiliados

Por el backend, un module affiliate debe ser creado para activar los afiliados porel administrador:

$ php symfony doctrine:generate-admin backend JobeetAffiliate --module=affiliate

Para acceder al nuevo módulo creado, añade un enlace en el menú principal con elnúmero de afiliados que deben ser activados:

<!-- apps/backend/templates/layout.php -->

Page 190: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 190/273

 

<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 acción necesaria en el backend es para activar o desactivar lascuentas, cambia la sección config del generador por defecto para simplificar lainterfaz un poco y añade un enlace para activar directamente las cuentas en lavista list:

# apps/backend/modules/affiliate/config/generator.ymlconfig:

fields:is_active: { label: Active? }list:title: Affiliate Managementdisplay: [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 más productivos, cambiar el filtro para mostrarúnicamente los afiliados a ser activados:

// 

apps/backend/modules/affiliate/lib/affiliateGeneratorConfiguration.class.

 php 

Page 191: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 191/273

 

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

}}

El único otro código 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();

Page 192: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 192/273

 

 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();}

// ... }

Page 193: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 193/273

 

El correoEl framework Symfony viene con una de las mejores soluciones PHP decorreo: Swift Mailer. Por supuesto, la librería esta completamente integrada conSymfony, con algunas características interesantes agregadas por encima de las

caracteríticas predeterminadas.Symfony 1.3/1.4 usa Swift Mailer versión 4.1.

Envíando Emails simples

Vamos a empezar enviando un simple email para notificar al afiliado cuando sucuenta ha sido confirmada y darle el token.

Reemplace la acción activate con el siguiente código:

// 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('[email protected]' => '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 código funcione debidamente, deberas cambiar [email protected] poruna dirección de email real.

La gestión del Email en Symfony es centrada alrededor de un objeto mailer, el cuales obtenido desde la acción con un método getMailer().

Page 194: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 194/273

 

El método compose() toma cuatro argumentos y devuelve un objeto de mensaje deemail:

  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 método send() desdela instancia mailer y pasarle el mensaje como un argumento. Como un atajo,puedes solo escribir y enviar un email en un método usando composeAndSend().

El mensaje es una instnacia de la clase Swift_Message. Visita la documentación oficialSwift Mailer para aprender mas acerca de este objeto, y como hacer cosas masavanzadas como adjuntar archivos.

Configuración

Por defecto, el método send() intenta usar el servidor local SMTP para el envio delmensaje al destinatario. Por supuesto, como muchas cosas en Symfony, esto estotalmente configurable.

Factorias

Durante los días anteriores, hemos hablado acerca de los objetos del núcleosymfony como user, request, response, o routing. Esos objetos sonautomaticamente creados, configurados, y gestionados por el framework. Ellos sonsiempre accesibles desde el objeto sfContext, y como muchas cosas delframework, ellas son configurables via un archivo de

configuración: factories.yml. Este archivo es configurable por entorno.Cuando sfContext inicializa las factorias del núcleo, este lee alarchivo factories.yml por los nombres de clases (class) y los parámetros (param)a pasar al constructor:

response:class: sfWebResponseparam:send_http_headers: false

En código anterior, para crear un response factory, symfony instancia unobjeto sfWebResponse y pasa la opción send_http_headers como un parámetro.

La clase sfContext 

El objeto sfContext contiene referencias a los objetos del núcleo symfony como elrequest, response, user, y así. Como sfContext actua como un singleton, puedes usarla sentencia sfContext::getInstance() para obtenerlo desde cualquier lugar yentonces poder acceder a cualquiera de los objetos del núcleo:

$mailer = sfContext::getInstance()->getMailer();

Page 195: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 195/273

 

Si quieres usar sfContext::getInstance() en una de tus clases, piensalo dos vecesya que este hace un strong coupling. Esto es simpre bastante mejor a pasar el objetoque necesitas como un argumento.

Puedes aún usar sfContext como un registro y agregar tus propios objetos usando losmétodos set(). Este toma un nombre y un objeto como argumentos y el método

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 núcleo, el mailer es un factory. Por eso, este esconfigurado en el archivo de configuración factories.yml. La configuració pordefecto se lee asi:

mailer:class: sfMailer

param:logging: %SF_LOGGING_ENABLED%charset: %SF_CHARSET%delivery_strategy: realtimetransport:

class: Swift_SmtpTransportparam:host: localhostport: 25encryption: ~username: ~password: ~

Cuando se crea una nueva aplicación, el archivo deconfiguración factories.yml sobreescribe la configuración predeterminada conalgunos sensibles valores para los entornos env y test:

test:mailer:param:

delivery_strategy: none

dev:mailer:

param:delivery_strategy: none

La configuración delivery_strategy le dice a Symfony como entregar correos. Pordefecto, Symfony viene con cuatro diferentes estrategias:

  realtime: Los Mensajes son entregados en tiempo real.

  single_address: Los Mensajes son entregados a una solo dirección.

  spool: Los Mensajes son guardados en cola.

Page 196: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 196/273

 

  none: Los Mensajes son simplemente ignorados.

No importa cual sea la estrategia, los emails son siempre registrados en el log yestan disponibles en el panel "mailer" del web debug toolbar.

Mail Transport

Los mensajes de correo son siempre enviados por un transport. El transport esconfigurado en el archivo de configuración factories.yml, y la configuración pordefecto usa el servidor SMTP del equipo local:

transport:class: Swift_SmtpTransportparam:host: localhostport: 25encryption: ~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 función nativa de PHP mail() fpara enviar losmensajes.

La sección "Transport Types" de la documentación oficial Swift Mailer describe todo loque necesitas saber acerca de las clases transport y sus diferentes parámetros.

Probando EmailsAhora qe vimos como enviar un email con symfony mailer, vamos a escribiralgunas 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 yourweb server does not have a local SMTP server. We have to replace thecurrent Swift_SmtpTransport class by Swift_MailTransport:

# apps/backend/config/factories.ymltest:

# ...

mailer:param:

delivery_strategy: nonetransport:class: Swift_MailTransport

Page 197: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 197/273

 

Then, add a new test/fixtures/administrators.yml file containing the followingYAML definition:

sfGuardUser:admin:email_address: [email protected]

username: adminpassword: adminfirst_name: Fabienlast_name: Potencieris_super_admin: true

Finally, replace the affiliate functional test file for the backend application withthe 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 itstoken')->

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 losmétodos checkHeader() y checkBody(). El segundo argumento de checkHeader() yel primer argumento de checkBody() puede ser lo siguiente:

  una cadena para comprobar que coincide exactamente;

  una expresión regular para comprobar el valor;  una expresión 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 muchosemails se enviaron, puedes elegir uno para probarlo con el método withMessage(). Elmétodo withMessage() toma un destinatario como su primer argumento. Este también

Page 198: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 198/273

 

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

Page 199: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 199/273

 

El Motor de Busqueda

La Tecnología

Antes de saltar de cabeza, en primer lugar, hablemos un poco sobre la historia deSymfony. Abogamos por un montón de buenas prácticas, como pruebas yrefactoring, y también tratamos de aplicarlas al framework mismo. Por ejemplo,nos gusta el famoso lema "No reinventar la rueda".

Como cuestión de hecho, el framework Symfony comenzó su vida hace cuatroaños como la unión entre dos existentes softwares Open-Source: Mojavi y Propel.Y cada vez que necesitamos hacer frente a un nuevo problema, buscamos unabiblioteca que haga el trabajo mucho antes de hacer la codificación uno mismodesde cero.

Hoy, queremos añadir un motor de búsqueda para Jobeet, y el Zend Frameworkofrece 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 búsquedapara Jobeet, lo cual es una tarea compleja, vamos a utilizar Zend Lucene.

En la página de la documentación de Zend Lucene, la biblioteca se describe de lasiguiente manera:

... un motor de búsqueda textual de propósito general escrito íntegramente en PHP 5.Como almacena sus índices en el sistema de archivos y no requiere de un servidor debases de datos, éste puede añadir capacidades de búsqueda a casi cualquier sitio webPHP. Zend_Search_Lucene soporta las siguientes características:

  Búsqueda 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

  Búsqueda por un campo específico (e.g., título, autor, contenidos)

Este capítulo no es un tutorial sobre la biblioteca Zend Lucene, sino como integrarla enel sitio web Jobeet; o más en general, la forma de integrar bibliotecas de terceros enun proyecto symfony. Si deseas más información sobre esta tecnología, por favor visitala Documentación de Zend Lucene. 

Instalación y Configuración del Zend Framework

Las bibliotecas Zend Lucene son parte del Zend Framework. Como no necesitamos

de todo del Zend Framework, sólo se necesita instalar algunas partes en eldirectorio lib/vendor/, junto con el symfony framework.

En primer lugar, la descarga Zend Framework y descomprime los archivos demodo que tengas un directorio lib/vendor/Zend/.

Las siguientes explicaciones han sido probadas con la versión 1.8.0 de the ZendFramework.

Page 200: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 200/273

 

Puedes limpiar el directorio eliminando todo menos los siguientes archivos ydirectorios:

  Exception.php 

  Loader/ 

  Loader.php   Search/ 

Luego, agrega el código siguiente a la claseProjectConfiguration paraproporcionar 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_oncesfConfig::get('sf_lib_dir').'/vendor/Zend/Loader/Autoloader.php';

Zend_Loader_Autoloader::getInstance();self::$zendLoaded = true;

}

// ... }

Indexación

El motor de búsqueda de Jobeet debería ser capaz de devolver todos los puestosque corresponden a palabras clave introducidas por el usuario. Antes de ser capazde buscar cualquier cosa, un índice debe ser construído para los puestos detrabajo; para Jobeet, se almacenarán en el directorio data/.

Zend Lucene proporciona dos métodos para recuperar un índice en función de si yaexiste uno o no. Vamos a crear un método helper en la claseJobeetJobTable quedevuelve un índice existente o crea uno nuevo para nosotros:

// lib/model/doctrine/JobeetJobTable.class.php public function getLuceneIndex(){ProjectConfiguration::registerZend();

Page 201: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 201/273

 

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').'.index';}

El Método save() 

Cada vez que se crea un puesto de trabajo, es actualizado o borrado, el índicedebe ser actualizado. Edita JobeetJob para actualizar el índice cada vez que untrabajo 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 método 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;

}

Page 202: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 202/273

 

 $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 seelimina primero si el puesto de trabajo (job) ya existe en el índice.

La Indexación de los puestos de trabajo en sí es muy sencilla: la clave primaria sealmacena para futuras referencias cuando hacemos búsquedas de puestos detrabajo y las principales columnas (position, company, location, y description)se indexan pero no se almacena en el índice ya que vamos a utilizar los objetosreales para mostrar los resultados.

Transacciones Doctrine

¿Qué pasa si hay un problema cuando procede la indexación de un puesto detrabajo (job) o si el puesto de trabajo (job) no se guarda en la base de datos?Ambas herramientas Doctrine y Zend Lucene arrojarán una excepción. Pero enalgunas circunstancias, podríamos tener un puesto de trabajo (job) guardado en labase de datos sin la correspondiente indexación. Para evitar que esto ocurra,podemos envolver los dos actualizaciones en una transacción y anularlas en casode 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);

Page 203: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 203/273

 

 $this->updateLuceneIndex();

$conn->commit();

return $ret;

}catch (Exception $e){$conn->rollBack();throw $e;

}}

delete() 

También tenemos que sobreescribir el método delete() para eliminar la entradadel 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);}

Búsqueda

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 línea de comandos ytambién desde el web, debe cambiar el índice de permisos de directorio según tuconfiguración: comprobar que tanto desde la línea de comandos y desde el servidorweb 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 búsqueda en el frontend es pan comido. En primer lugar, crea unaruta:

job_search:url: /searchparam: { module: job, action: search }

Y la acción correspondiente:

Page 204: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 204/273

 

// 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 también 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 búsqueda en sí misma se delega en el método 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);

Page 205: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 205/273

 

 return $q->execute();

}

Después de obtener todos los resultados del índice Lucene, vamos a filtrar lospuestos de trabajo inactivos, y limitar el número 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 comoBooleans, wildcards, fuzzy search,y mucho más. Todo está documentado en el ZendLucene manual

Las Pruebas Unitarias

¿Qué tipo de Pruebas unitarias tenemos que crear para probar el motor debúsqueda? Evidentemente, no probaremos la biblioteca Zend Lucene en sí, sino suintegración con la clase JobeetJob.

Añade las siguientes pruebas al final del archivo JobeetJobTest.php y no te olvidesde actualizar el número 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 nonactivated 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 thecriteria');$t->is($jobs[0]->getId(), $job->getId(), '::getForLuceneQuery() returnsjobs matching the criteria');

$job->delete();

Page 206: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 206/273

 

$jobs = Doctrine_Core::getTable('JobeetJob')->getForLuceneQuery('position:foobar');$t->is(count($jobs), 0, '::getForLuceneQuery() does not return deletedjobs');

Probamos que ningun puesto de trabajo inactivo, o borrado aparezca en los

resultados de la búsqueda; también comprobamos que los puestos secorresponden al criterio dado para aparecer en los resultados.

Las Tareas

Finalmente, tenemos que crear una tarea de limpieza para el índice de todos losregistros obsoletos (cuando expira un puesto, por ejemplo,) y optimizar el índicede vez en cuando. Como ya tenemos una tarea de limpieza, vamos a actualizarlapara añadirle esas características:

// 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, acontinuación, se lo optimiza gracias al método nativo optimize() de Zend Lucene.

Page 207: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 207/273

 

para aumentar la capacidad de respuesta del motor de búsqueda, vamos aaprovechar AJAX para convertir el motor de búsqueda en uno más vivo.

Como el formulario debe trabajar con y sin JavaScript activado, la función debúsqueda en vivo se llevará a cabo utilizandounobtrusive JavaScript. Usandounobtrusive JavaScript también permite una mejor separación de las capas en el

código del cliente entre HTML, CSS, y el comportamiento JavaScript.

Instalación de jQuery

En vez de reinventar la rueda y la gestión de las muchas diferencias entre losnavegadores, vamos a utilizar una biblioteca JavaScript, jQuery. El frameworkSymfony es agnóstica y puede trabajar con cualquier biblioteca JavaScript.

Ve a jQuery, descargar la última versión, y coloca los archivos .js dentrode web/js/.

Incluyendo jQuery

Como vamos a necesitar jQuery en todas las páginas, actualiza el layout paraincluirlo en el <head>. Tenga cuidado de insertar la funciónuse_javascript() antesde llamar al include_javascripts():

<!-- apps/frontend/templates/layout.php -->

<?php use_javascript('jquery-1.2.6.min.js') ?><?php include_javascripts() ?>

</head>

Podríamos haber incluido jQuery directamente con un tag <script>, peroutilizando el helper use_javascript() nos aseguramos que el mismo JavaScript no

se incluirá dos veces.

Por razones de rendimiento, podrías también mover elhelper include_javascripts() justo antes de la etiqueta de cierre </body>.

Agregando comportamientos

Implementar un buscador vivo implica que cada vez que el usuario escribe unaletra en el cuadro de búsqueda, se hace una llamada al servidor; a continuación, elservidor devolverá la información necesaria para actualizar la areas seleccionadasde la página sin actualizar toda la página.

En lugar de añadir el comportamiento con atributos on*() del HTML, el principiofundamental detrás de jQuery es agregar comportamientos al DOM después deque la página está completamente cargada. De esta forma, si desactivas el soportede JavaScript en tu navegador, ningún comportamiento es registrado, y elformulario sigue funcionando como antes.

El primer paso es interceptar cuando un usuario introduce una tecla en el cuadrode búsqueda:

$('#search_keywords').keyup(function(key)

Page 208: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 208/273

 

{if (this.value.length >= 3 || this.value == ''){// do something  

}});

No añadas el código por ahora, ya que vamos a modificar todo en gran medida. Elcódigo JavaScript final se añadirá al layout en la siguiente sección.

Cada vez que el usuario introduce una tecla, jQuery executa la función anónimaque se definió en el código anterior, pero sólo si el usuario ha escrito más de 3caracteres o si se elimina todo, de la etiqueta input.

Hacer una llamada AJAX al servidor es tan sencillo como utilizar elmétodo 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 acción "normal y corriente" esejecutada. Los cambios necesarios en la acción se harán en la siguiente sección.

Por último, pero no por ello menos importante, si JavaScript está habilitado, yqueremos a quitar el botón de búsqueda:

$('.search input[type="submit"]').hide();

Respuesta al Usuario

Cuando haces una llamada AJAX, la página no se actualiza inmediatamente. Elnavegador esperará la respuesta del servidor antes de actualizar la página. En elínterin, es necesario proporcionar respuesta visual al usuario para informarle deque algo está ejecutandose.

Una convención es mostrar un icono de cargador durante la llamada AJAX.Actualiza el layout para añadir 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" />

Page 209: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 209/273

 

<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 deseascrear el tuyo, encontrarás una gran cantidad de gratuitos servicios en línea comohttp://www.ajaxload.info/.

Ahora que tienes todas las piezas necesarias para que el código HTML funcione,crea un archivo search.js que tenga el siguiente código JavaScript queescribiremos:

// 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(); }

);}

});});

También necesitas actualizar el layout para incluir este nuevo archivo:

<!-- apps/frontend/templates/layout.php --><?php use_javascript('search.js') ?>

JavaScript como una Acción

Aunque el JavaScript que hemos escrito para el motor de busqueda es estático, aveces, necesitas llamar algún código PHP (para usar el helperurl_for() por ejemplo).

JavaScript es solo otro formato como HTML, y como vimos algunos días atrás,Symfony hace la adminitración de formatos muy fácil. Ya que el archivo JavaScript

tendrá un comportamiento por página, puedes hasta tener la misma URL como para lapágina del archivo JavaScript, pero que termina con.js. por ejemplo, si quieres crearun archivo para el motor de busqueda, puedes modificar la ruta job_search comosigue y crear una platillasearchSuccess.js.php:

job_search:url: /search.:sf_formatparam: { module: job, action: search, sf_format: html }requirements:

Page 210: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 210/273

 

sf_format: (?:html|js)

AJAX en una Acción

Si JavaScript está habilitado, jQuery interceptará todas las teclas mecanografiadasen el cuadro de búsqueda, y llamará a la acción search. Si no, la misma

acción search es también llamada cuando el usuario envía el formulariopresionando la tecla "enter" o haciendo clic en el botón "search".

Así, la acción search ahora debe determinar si la llamada se realiza a través deAJAX o no. Cuando una petición se hace con una llamada AJAX, elmétodo isXmlHttpRequest() dará true.

El método isXmlHttpRequest() funciona con todas las principales bibliotecasJavaScript 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 página, sino sólo reemplaza el elemto DOM #jobs conel contenido de la respuesta, la página no debe ser redecorada por la layout. Comose trata de una necesidad común, el layout está desactivado por defecto cuando sepresenta una petición AJAX.

Además, en lugar de devolver la plantilla completa, sólo tenemos que devolver elcontenido del partial job/list. El método renderPartial() usado en la accióndevuelve el partial como la respuesta en lugar de la totalidad de la plantilla.

Si el usuario elimina todos los caracteres en el cuadro de búsqueda, o si labúsqueda no devuelve ningún resultado, tenemos que mostrar un mensaje en

lugar de una página en blanco. Vamos a utilizar el método renderText() paramostrar 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');

Page 211: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 211/273

 

$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));}

}

También puedes devolver un componente utilizando el método renderComponent().

Probando AJAX

Como el navegador Symfony no puede simular JavaScript, necesitas ayudarlocuando pruebas llamadas AJAX. Esto principalmente significa que es necesarioañadir manualmente la cabecera jQuery y que todas las demás grandes bibliotecasJavaScript envían con la petición:

// 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 método setHttpHeader() pone un header HTTP para la siguiente peticiónformulada con el navegador.

Page 212: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 212/273

 

Internacionalizacion y localizacion

vamos a hablar de internacionalización (o i18n) y localización (o l10n).Según Wikipedia:en:

La Internacionalización es un proceso a través del cual se diseñan productos de

software para que puedan adaptarse a diferentes idiomas y regiones sin necesidad decambios de ingeniería ni cambios en el código.

La Localización es el proceso de adaptación de software para una región o idiomamediante la incorporación de componentes específicos de localización y traducción detextos.

Como siempre, el framework symfony no ha reinventado la rueda y su soporte dei18n y l10n esta basado en el ICU standard. 

Usuario

La internacionalización no es posible sin un usuario. Cuando su sitio web está

disponible en varios idiomas o para distintas regiones del mundo, el usuario es elresponsable de elegir la que mejor se ajuste a él.

Ya hemos hablado de la clase User de symfony durante el día 13.

La Cultura del Usuario

Las características i18n y l10n de symfony se basan en la cultura del usuario. Lacultura es la combinación del lenguaje y el país del usuario. Por ejemplo, la culturapara un usuario que habla francés es fr y la cultura para un usuario de Franciaes fr_FR.

Puedes manejar la cultura por el usuario llamando a losmétodos setCulture() y getCulture() del objeto User:

// in an action $this->getUser()->setCulture('fr_BE');echo $this->getUser()->getCulture();

El lenguaje está codificado en dos minúsculas, de acuerdo con la ISO 639-1 standard, y el país está codificado con dos caracteres en mayúscula, de acuerdo con la ISO3166-1 standard. 

La Preferencia de Cultura

Por defecto, la cultura del usuario es la configurada en el archivo deconfiguración settings.yml:

# apps/frontend/config/settings.ymlall:.settings:default_culture: it_IT

Page 213: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 213/273

 

Como la cultura es administrada por el objeto User, se almacena en la sesión delusuario. Durante el desarrollo, si cambias la cultura por defecto, tendrás que limpiartus cookies de sesión para que el nuevo valor tenga efecto en tu navegador.

Cuando un usuario inicia una sesión en el sitio web Jobeet, también podemosdeterminar la mejor cultura, sobre la base de la información proporcionada por la

cabecera HTTP Accept-Language.

El método getLanguages() del objeto de la petición devuelve un array de losidiomas 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 136principales idiomas. El método getPreferredCulture() devuelve el mejor lenguajemediante la comparación de los idiomas preferidos del usuario y los idiomas de tusitio web:

// in an action $language = $request->getPreferredCulture(array('en', 'fr'));

En la anterior llamada, el lenguaje devuelto será Inglés o Francés de acuerdo conlos idiomas preferidos del usuario, o Inglés (primer idioma en el array) si nocoincide ninguno.

La Cultura en la URL

El sitio web Jobeet estará disponible en Inglés y francés. Como una dirección URLsólo puede representar a un único recurso, la cultura debe estar integrada en laURL. Para ello, abre el archivo routing.yml, y agrega la variable

especial :sf_culture para todas las rutas, pero no para api_jobs yhomepage. Parasimples rutas, agrega /:sf_culture al principio de la url. Para colección de rutas,agrega una opción prefix_path que comience con/:sf_culture.

# apps/frontend/config/routing.ymlaffiliate:class: sfDoctrineRouteCollectionoptions:model: JobeetAffiliateactions: [new, create]object_actions: { wait: get }prefix_path: /:sf_culture/affiliate

category:url: /:sf_culture/category/:slug.:sf_formatclass: sfDoctrineRouteparam: { module: category, action: show, sf_format: html }options: { model: JobeetCategory, type: object }requirements:sf_format: (?:html|atom)

Page 214: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 214/273

 

job_search:url: /:sf_culture/searchparam: { module: job, action: search }

job:class: sfDoctrineRouteCollection

options:model: JobeetJobcolumn: tokenobject_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_slugclass: sfDoctrineRoute

options:model: JobeetJobtype: objectmethod_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 automáticamenteusa su valor para cambiar la cultura del usuario.

Como necesitamos muchas páginas de inicio como idiomas soportemos(/en/, /fr/, ...), la página de inicio predeterminada (/) deben redirijirnos a lapágina apropiada, de acuerdo con la cultura del usuario. Pero si el usuario no tienetodavía una cultura, porque él viene a Jobeet por primera vez, la mejor culturaserán elegidos para él.

En primer lugar, añade el método isFirstRequest() a myUser. Devuelve true sólopara la primer petición de una sesión 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

Page 215: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 215/273

 

localized_homepage:url: /:sf_culture/param: { module: job, action: index }requirements:sf_culture: (?:fr|en)

Cambia la acción index del módulo job para aplicar la lógica para redirigir alusuario a la "mejor" página de inicio la primer petición de una sesión:

// 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 petición, esto significa que elusuario tiene que ir a la URL /. Si este es el caso y la sesión es nueva, la culturapreferida es usada como la cultura del usuario. De lo contrario, se utiliza la culturaactual del usuario.

El último paso es redirigir al usuario a la URL localized_homepage. Nota que lavariable sf_culture no ha sido pasada en la redirección ya que symfony la agregaautomáticamente por tí.

Ahora, si tratas de ir a la URL /it/, symfony devolverá un error 404 ya querestringimos 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 aplicación. Pero antes de añadir más pruebas,tenemos que arreglar los ya existentes. Como han cambiado todas las direcciones

Page 216: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 216/273

 

URL, edita los archivos de todas las prueba funcionalesen test/functional/frontend/ y agrega /en al principio de todas las URLs. Noolvides de cambiar las URLs en elarchivo lib/test/JobeetTestFunctional.class.php. Poner en marcha el conjuntode pruebas para comprobar que has arreglado correctamente las pruebas:

$ php symfony test:functional frontend

El user tester da un método isCulture() que prueba la cultura del usuario actual.Abre el archivo jobActionsTest y añade 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 bestculture')->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

añadir en el layout. El framework de formularios no proporciona una formulario defabrica pero como la necesidad es muy común para los sitios webinternacionalizados, el symfony core team mantiene elsfFormExtraPlugin, quecontiene los validadores, widgets, y formularios que no pueden ser incluidos con elpaquete principal symfony ya que son demasiado específicas o tienendependencias externas, pero no obstante son muy útil.

Instala el plugin con la tarea plugin:install:

Page 217: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 217/273

 

$ php symfony plugin:install sfFormExtraPlugin

Or via Subversion with the following command:

$ svn co http://svn.symfony-project.org/plugins/sfFormExtraPlugin/branches/1.3/plugins/sfFormExtraPlugin

In order for plugin's classes to be loaded, the sfFormExtraPlugin plugin must beactivated 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 comobibliotecas JavaScript. Encontrarás un widget para seleccionar fechas, un para uneditor WYSIWYG, y mucho más. Tóma un tiempo para leer la documentación ya queencontrarás un montón de cosas útiles.

El plugin sfFormExtraPlugin da un formulario sfFormLanguage para gestionar laselección de idioma. Añadiendo el formulario de idiomas se puede hacer en ellayout así:

El código a continuación no pretende ser aplicado. Es aquí que te mostramos cómopodrías tener la tentación de aplicar algo de forma equivocada. Vamos a mostrarte

cómo 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 creación de un objeto form no pertenece a lacapa de la Vista. Debe ser creado en una acción. Pero como el código está en ellayout, el formulario debe crearse para cada acción, que está lejos de ser práctico.

Page 218: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 218/273

 

En tales casos, debes usar un componente. Un componente es como un partialpero con algo de código en él. Consideralo una acción ligera.

Incluyendo un componente en una plantilla se puede hacer mediante el uso delhelper 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 módulo y la acción como argumentos. El tercer argumento sepuede utilizar para pasar parámetros a los componentes.

Crea un módulo language para alojar el componente y la acción que realmente

cambiará el idioma del usuario:$ php symfony generate:module frontend language

Los Componentes se definirán 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 deacciones.

La plantilla para un componente utiliza la misma convención de nombres como lohace un partial: un guión 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 acción que en realidad cambia la cultura delusuario, edita el archivo routing.yml para crear la rutachange_language:

# apps/frontend/config/routing.ymlchange_language:url: /change_language

Page 219: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 219/273

 

param: { module: language, action: changeLanguage }

Y crea la acción 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 método process() de sfFormLanguage se encarga de cambiar la cultura delusuario, basado en el formulario envíado por el usuario.

Internacionalización

Idiomas, Caracteres, y Codificación

Diferentes idiomas tienen diferentes conjuntos de caracteres. El Inglés es el idiomamás simple ya que sólo usa los caracteres ASCII, el idioma francés es un poco máscomplejo, con caracteres acentuados como "é", y las lenguas como el ruso,chino o árabe son mucho más complejos que todos sus caracteres ya que estánfuera del rango ASCII. Esos idiomas se definen con diferentes conjuntos de

caracteres.Cuando se trate de datos internacionalizado, es mejor utilizar la norma Unicode. Laidea detrás de Unicode es establecer un conjunto universal de caracteres quecontiene todos los caracteres de todos los idiomas. El problema con Unicode esque un solo carácter se puede representar con una cantidad de 21 bits. Por lotanto, para la web, usamos UTF-8, que mapea el código Unicode apuntandolo auna secuencias de longitud variable de octetos. En UTF-8, la mayoría de laslenguas tienen sus caracteres codificados con menos de 3 bits.

Page 220: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 220/273

 

UTF-8 es el utilizado por defecto en symfony, y se define en el archivo deconfiguración settings.yml:

# apps/frontend/config/settings.ymlall:.settings:

charset: utf-8

Además, para habilitar la capa de internacionalización de symfony, debesestablecer i18n en true dentro de settings.yml:

# apps/frontend/config/settings.ymlall:.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 envueltascon el helper __() (nota que hay dos guiones bajos).

El helper __() es parte del grupo de helpers I18N, que contiene helpers quefacilitan la gestión i18n en plantillas. Como este grupo de helper no está cargadopor defecto, es necesario agregar manualmente en cadaplantilla use_helper('I18N') como ya hizo para el grupo de helper Text, o cargaloa nivel global mediante standard_helpers:

# apps/frontend/config/settings.ymlall:

.settings:standard_helpers: [Partial, Cache, I18N]

Aquí está cómo usa el helper __() para el pie de página 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>

Page 221: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 221/273

 

<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 utilizartambién un identificador único para cada cadena. Es sólo una cuestión de gusto. ParaJobeet, haremos uso de la antigua estrategia para tener plantillas más legibles.

Cuando symfony muestra una plantilla, cada vez que el helper __() es llamado,symfony busca por una traducción para la cultura del usuario actual. Si seencuentra una traducción, se utiliza, si no, el primer argumento se devuelve comoun valor fallback.

Todas las traducciones se almacenan en un catálogo. El framework i18nproporciona una gran cantidad de estrategias diferentes para almacenar lastraducciones. Vamos a utilizar el formato "XLIFF", que es un estándar y el másflexible. También es el utilizado por el admin generator y demás symfony plugins.

Oros Catálogos son gettext, MySQL, y SQLite. Como siempre, echa una mirada ala i18n API para más detalles.

i18n:extract 

En lugar de crear el catálogo de archivos a mano, utiliza la tarea deserie i18n:extract:

$ php symfony i18n:extract frontend fr --auto-save

La tarea i18n:extract encuentra todas las cadenas que deben traducirse en fr enla aplicación frontend y crea o actualiza el correspondiente catálogo. La opción --

auto-save guarda las nuevas cadenas de en el catálogo. También puedes utilizar laopción --auto-delete para eliminar automáticamente las cadenas que ya noexisten.

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">

Page 222: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 222/273

 

<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 traducción es administrada por una etiqueta trans-unit que tiene un únicoatributo id. Ahora puedes editar este archivo y añadir las traducciones de lalengua 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>

Page 223: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 223/273

 

</xliff>

Como XLIFF es un formato estándar, una gran cantidad de herramientas existentesfacilitan el proceso de traducción. Open Language Tools es un proyecto Java Open-Source con un editor integrado XLIFF.

Como XLIFF es un formato de archivo, la misma prioridad y la lógica de las normas queexisten para otros archivos de configuración de symfony son también aplicables. Losarchivos I18n puede existir en un proyecto, una aplicación o un módulo, y los másespecíficos archivos sobreescriben las traducciones que se encuentran en la másglobal.

Traducciones con Argumentos

El principio fundamental detrás de la internacionalización es traducir frases. Sinembargo, algunas frases incluyen valores dinámicos. En Jobeet, este es el caso enla página 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 número de puestos de trabajo es una variable que debe ser utilizada para latraducción:

<!-- 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 variableque será sustituido por el número real en tiempo de ejecución, gracias a el valordado como segundo argumento al helper __().

Añadir la nueva cadena manualmente insertando una etiqueta trans-unit en elarchivo messages.xml, o usa la tarea i18n:extract para actualizarautomáticamente el archivo:

$ php symfony i18n:extract frontend fr --auto-save

Después de ejecutar la tarea, abre el archivo XLIFF para añadir la traducción alfrancés:

<trans-unit id="6"><source>and %count% more...</source><target>et %count% autres...</target>

</trans-unit>

El único requisito en la tradución de la cadena es utilizar elcontenedor/variable %count% en algún lugar.

Page 224: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 224/273

 

Algunas otras cadenas son aún más complejas ya que implican plurales. Segúnalgunoss números, la frases cambian, pero no necesariamente del mismo modopara todos los idiomas. Algunos idiomas tienen reglas gramaticales muy complejaspara los plurales, como el Polaco o el Ruso.

En la página de categoría, el número de puestos de trabajo en la categoría actual

se muestra:<!-- apps/frontend/modules/category/templates/showSuccess.php --><strong><?php echo count($pager) ?></strong> jobs in this category

Cuando una oración tiene diferentes traducciones de acuerdo con un número, elhelper 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 función del número

  Un array de variables a reemplazar

  El número a usar que determina qué texto usar

La cadena que describe las diferentes traducciones de acuerdo con el número tieneun formato de la siguiente manera:

  Cada posibilidad está separado por un carácter barra vertical (|)

  Cada cadena se compone de un rango seguida de la traducción

El rango puede describirse con cualquier serie de números:

  [1,2]: Acepta valores entre 1 y 2, inclusive

  (1,2): Acepta valores entre 1 y 2, con exclusión de 1 y 2

  {1,2,3,4}: Sólo los valores definidos en el juego son aceptadas

  [-Inf,0): Acepta los valores mayores o iguales a menos infinito yestrictamente inferior a 0

  {n: n % 10 > 1 && n % 10 < 5}: Coincide con los números 2, 3, 4, 22, 23,

24Traducir 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 catégorie|[1]Une annonce dans

cette catégorie|(1,+Inf]%count% annonces dans cette catégorie</target></trans-unit>

Page 225: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 225/273

 

Ahora que sabes cómo internacionalizar todo tipo de cadenas, tomate un tiempopara agregar una llamada al __() para todas las plantillas de la aplicaciónfrontend. No vamos a internacionalizar la aplicación backend.

Formularios

Las clases form contienen muchas cadenas que deben ser traducidas, comoetiquetas, mensajes de error y mensajes de ayuda. Todas estas cadenas sonautomáticamente internacionalizadas por symfony, por lo que sólo tendrá queproporcionar las traducciones en los archivos XLIFF.

Lamentablemente, la tarea i18n:extract aún no analiza las clases form para cadenassin traducir.

Objetos Doctrine

Por el sitio web Jobeet, no internacionalizaremos todas las tablas porque no tienesentido pedir a los usuarios que envían puestos que lo hagan junto con las

traducciones en todos los idiomas disponibles. Sin embargo, latabla category definitivamente debe traducirse.

El plugin Doctrine da soporte a tablas i18n en forma nativa. Para cada tabla quecontiene datos localizados, dos tablas deben crearse: una para las columnas quesean i18n-independent, y la otra para las columnas que deben serinternacionalizadas. Las dos tablas están vinculadas por una relación de uno-a-muchos.

Actualiza el schema.yml como sigue:

# config/doctrine/schema.ymlJobeetCategory: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 modelollamado JobeetCategoryTranslation se creará automáticamente y losespecificos campos se trasladarán a ese modelo.

Nota que simplemente volvimos sobre el comportamiento I18n y movimos elcomportamiento Sluggable para ser adjuntado almodeloJobeetCategoryTranslation que se crea automáticamente. Laopción uniqueBy le dice al comportamiento Sluggable campos que determinan siuna slug es único o no. En este caso, cada slug debe ser único para cadapar lang y name.

Y actualiza los archivos de datos para las categorías:

Page 226: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 226/273

 

# data/fixtures/categories.ymlJobeetCategory: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

También tenemos que sobreescribir elmétodo findOneBySlug() en JobeetCategoryTable. Desde que Doctrine da algo debuscadores mágicos para todas las columnas en un modelo, simplemente tenemos

que crear el método findOneBySlug() para que por defecto anular la magia de lafuncionalidad que Doctrine proporciona.

Tenemos que hacer algunos cambios a fin de que la categoría es recuperada sobrela base del slug Inglés 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 labase de datos, no olvides de volver a crear un usuario para acceder al Jobeet backend

Page 227: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 227/273

 

con la tarea guard:create-user. Si lo prefieres, puedes añadir un archivo de datospara añadirlo automáticamente.

Cuando usamos el comportamieto I18n, proxis son creados entre elobjeto JobeetCategory y el objeto JobeetCategoryTranslation de modo que todaslas antiguas funciones de recuperación por el nombre de la categoría seguirá

trabajando y podrás 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 número de solicitudes a la bases de datos, haz el joinde

JobeetCategoryTranslationen tus queries. Esto te traerá el objeto principal y el

i18n en una petición.

$categories = Doctrine_Query::create()->from('JobeetCategory c')->leftJoin('c.Translation t WITH t.lang = ?', $culture)->execute();

El WITH anterior agrega una condición para automáticamente 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 porqueel slug es ahora parte de JobeetCategoryTranslation, la ruta no está disponible

para traer el objeto Category automáticamente. Para ayudar al routing, vamos acrear un método que se encargará de la recuperación del objeto:

Puesto que ya sobreescribimos el findOneBySlug() vamos a refactorizar un pocomás estos métodos para que pueden ser compartidos. Vamos a crear un nuevosmétodos findOneBySlugAndCulture() y doSelectForSlug() y cambiarel 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);

Page 228: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 228/273

 

return $q->fetchOne();}

public function findOneBySlug($slug){return $this->findOneBySlugAndCulture($slug, 'en');

}

A continuación, utiliza la opción method para decirle a la ruta category que use elmétodo doSelectForSlug() para recuperar el objeto:

# apps/frontend/config/routing.ymlcategory:url: /:sf_culture/category/:slug.:sf_formatclass: sfDoctrineRouteparam: { 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 lascategoríaes:

$ php symfony doctrine:data-load

Ahora la ruta category se encuentra internacionalizado y la URL de una categoríaincluye las traducciones del slug:

/frontend_dev.php/fr/category/programmation/frontend_dev.php/en/category/programming

Page 229: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 229/273

 

Admin Generador

Para el backend, queremos que las traducciones de el francés y el Inglés seaneditadas en el mismo formulario:

Incluir un formulario i18n se puede hacer mediante el uso delmétodo 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 internacionalización de fabrica. Viene contraducciones a más de 20 idiomas, y es muy fácil de añadir uno nuevo, o parapersonalizar una existente. Copie el archivo para el idioma que desea personalizarde symfony (las traducciones admin se encuentranen lib/vendor/symfony/lib/plugins/sfDoctrinePlugin/i18n/) de la aplicación en

el dir i18n. Como el archivo en tu aplicación se fusionará con el de symfony,mantiene sólo las cadenas modificadas en el archivo de la aplicación.

Notarás que los traducciones del admin generator se nombrancomo sf_admin.fr.xml, en lugar de fr/messages.xml. Como cuestión dehecho,messages es el nombre del catálogo por defecto usado por Symfony, y quepuede ser modificado para permitir una mejor separación entre las distintas partesde tu aplicación. Usar un catálogo que no sea el predeterminado require que loespecifique cuando usas el helper __():

<?php echo __('About Jobeet', array(), 'jobeet') ?>

En el anterior código __(), symfony buscará por la cadena "About Jobeet" en elCatálogo jobeet.

Tests

Las pruebas es una parte integrante de la migración de internacionalización. Enprimer lugar, actualiza los archivos de datos para pruebas de las categoríascopiando los archivos de datos que teniamos definidos antesen test/fixtures/categories.yml.

Page 230: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 230/273

 

Don't forget to update methods inthe lib/test/JobeetTestFunctional.class.php file in order to care of ourmodifications 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 están funcionandobien:

$ php symfony test:all

Cuando hemos desarrollado la interfaz de backend para Jobeet, no hemos escritopruebas funcionales. Pero cada vez que creas un módulo con el comando de lineasymfony symfony también generan las pruebas. Estás son seguras para eliminarlas.

Localización

Plantillas

Soportando diferentes culturas también significa soportar a las diferentes manerade formatear fechas y números. En una plantilla, varios helpers están a tutdisposición para ayudar a tomar en cuenta todas estas diferencias, basado en laactual cultura del usuario:

En el grupo de helper Date :

Helper Descripción

format_date()  Formatos de fecha

format_datetime()  Formatos de fecha

time_ago_in_words()  Muestra el tiempo transcurrido entre una fecha y

ahora en palabras

Page 231: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 231/273

 

Helper Descripción

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 Descripción

format_number()  Formatos un número

format_currency()  Formatos de moneda

En el grupo de helper I18N :

Helper Descripción

format_country()  Muestra el nombre de un país

format_language()  Muestra el nombre de un idioma

Formularios

El framework de formualrios da varios widgets y los validadores para datoslocalizados:

  sfWidgetFormI18nDate 

  sfWidgetFormI18nDateTime 

  sfWidgetFormI18nTime 

  sfWidgetFormI18nChoiceCountry 

  sfWidgetFormI18nChoiceCurrency 

  sfWidgetFormI18nChoiceLanguage 

  sfWidgetFormI18nChoiceTimezone 

  sfValidatorI18nChoiceCountry 

  sfValidatorI18nChoiceLanguage 

  sfValidatorI18nChoiceTimezone 

Page 232: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 232/273

 

Los Plugins

Un Plugin Symfony

Un plugin ofrece una manera de empaquetar y distribuir un conjunto de archivosdel proyecto. Al igual que un proyecto, un plugin puede tener clases, helpers,

configuraciones, tareas, módulos, esquemas, e incluso recursos Web (CSS,JavaScript, etc.).

Plugins Privados

El primer uso de los plugins es facilitar el intercambio de código entre aplicaciones,o incluso entre distintos proyectos. Recuerda que las aplicaciones symfony sólocomparten el modelo. Los Plugins dan una manera de compartir más componentesentre aplicaciones.

Si necesitas volver a utilizar el mismo esquema para los diferentes proyectos o losmismos módulos, colócalos en un plugin. Como un plugin es solo un directorio,

puedes moverlo con bastante facilidad mediante la creación de un repositorio SVNy el uso de svn:externals, o con sólo copiar los archivos de un proyecto a otro.

Nosotros llamamos a estos "plugins privados" porque su uso está restringido a unasola empresa o a un solo desarrollador. No están a disposición del público.

Puedes incluso crear un paquete de tus plugins privados, crear tu propio symfonyplugin channel, e instalarlos via la tarea plugin:install.

Plugins Públicos

Los Public Plugins están disponibles para la comunidad para descargar e instalar.Durante este tutorial, tenemos que usar un par de pluginspúblicos: sfDoctrineGuardPlugin y sfFormExtraPlugin.

Son exactamente los mismos que los plugins privados. La única diferencia es quecualquiera puede instalar para sus proyectos. Aprenderás más adelante sobre lamanera de publicar y alojar uno público en el sitio web de plugins de Symfony.

Una Forma Diferente de Organización del Código

Hay una manera más de pensar en plugins y usarlos. Olvídate de la reutilización yel intercambio. Los Plugins se puede utilizar como una manera diferente paraorganizar el código. En lugar de organizar los archivos por capas: todos losmodelos en lib/model/, las plantillas en templates/, ...; los archivos están juntospor su característica: todos los archivos job juntos (el modelo, módulos yplantillas), todos los archivos CMS juntos, y así.

Estructura de Archivos de un Plugin

Un plugin es sólo una estructura de directorios con los archivos organizados enuna estructura previamente definida, según la naturaleza de los archivos. Hoy,

Page 233: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 233/273

 

pasaremos la mayor parte del código que hemos escrito para Jobeet enun sfJobeetPlugin. El layout básico que se utilizará es el siguiente:

sfJobeetPlugin/config/sfJobeetPluginConfiguration.class.php // Plugin initialization

routing.yml // Routingdoctrine/

schema.yml // Database schemalib/Jobeet.class.php // Classeshelper/ // Helpersfilter/ // Filter classesform/ // Form classesmodel/ // Model classestask/ // Tasks

modules/job/ // Modules

actions/config/templates/

web/ // Assets like JS, CSS, andimages

El Plugin Jobeet

Inicializar un plugin es tan sencillo como crear un nuevo directorio bajo eldirectorio plugins/. Para Jobeet, vamos a crear un directoriosfJobeetPlugin:

$ mkdir plugins/sfJobeetPlugin

Then, activatethe sfJobeetPlugin in config/ProjectConfiguration.class.php file.

public function setup(){$this->enablePlugins(array('sfDoctrinePlugin','sfDoctrineGuardPlugin','sfFormExtraPlugin','sfJobeetPlugin'

));}

Todos los plugins deben terminar con Plugin. También es una buena costumbre usarel prefijo sf, aunque no es obligatorio.

El Modelo

Primero, mueve elarchivo config/doctrine/schema.yml a plugins/sfJobeetPlugin/config/:

$ mkdir plugins/sfJobeetPlugin/config/

Page 234: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 234/273

 

$ mkdir plugins/sfJobeetPlugin/config/doctrine$ mv config/doctrine/schema.ymlplugins/sfJobeetPlugin/config/doctrine/schema.yml

Todos los comandos son para ambientes Unix. Si usas Windows, puedes arrastrar ysoltar los archivos en el Explorador de Windows. Y si utilizas Subversion, o cualquier

otra herramienta para la gestión de tu código, usa las herramientas incorporadas queofrecen (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

Después de mover los modelos, formularios y filtros las clases deben serrenombradas, hacerlas abstractas y con el prefijo Plugin.

Solo usa el prefijo Plugin en las clases auto-generadas y no en todas las clases. Porejemplo no uses el prefijo en las clases que haces a mano. Solo las auto-generadasrequieren el prefijo.

Aquí está un ejemplo en el que movemos lasclases JobeetAffiliate y JobeetAffiliateTable .

$ mv plugins/sfJobeetPlugin/lib/model/doctrine/JobeetAffiliate.class.phpplugins/sfJobeetPlugin/lib/model/doctrine/PluginJobeetAffiliate.class.php

Y el código 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);}

// ... }

Page 235: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 235/273

 

Ahora vamos a pasar la clase JobeetAffiliateTable:

$ mvplugins/sfJobeetPlugin/lib/model/doctrine/JobeetAffiliateTable.class.phpplugins/sfJobeetPlugin/lib/model/doctrine/PluginJobeetAffiliateTable.class.php

La definición 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 ellospara incluir un prefijo con la palabra Plugin.

Asegúrate de quitar eldirectorio base en plugins/sfJobeetPlugin/lib/*/doctrine para losdirectorios 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 clasesforms, filters y model corre las tarea para la re-construcción de todas las clases.

$ php symfony doctrine:build --all-classes

Ahora te darás cuenta que algunos de los nuevos directorios creados paramantener los modelos creados a partir de el esquema estan incluido enelsfJobeetPlugin en lib/model/doctrine/sfJobeetPlugin.

Este directorio contiene los modelos de nivel superior y la base de clases generadapor el esquema. Por ejemplo, el modelo JobeetJob tiene ahora esta estructura declases.

  JobeetJob (extiende de PluginJobeetJob)en lib/model/doctrine/sfJobeetPlugin/JobeetJob.class.php: Nivelsuperior donde toda la funcionalidad del modélo puede guardarse. Esto esdonde puedas agregar y sobreescribir funcionalidades que ya vienen conlos modelos del plugin.

  PluginJobeetJob (extiende de BaseJobeetJob)en plugins/sfJobeetPlugin/lib/model/doctrine/PluginJobeetJob.class.

php: Esta clase contiene todas las funciones específicas del plugin. Puedessobreescribir la funcionalidad en esta clasa y la base modificando laclaseJobeetJob.

  BaseJobeetJob (extiende de sfDoctrineRecord)en lib/model/doctrine/sfJobeetPlugin/base/BaseJobeetJob.class.php :

Page 236: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 236/273

 

Clase base que es generada desde el archivo yaml del esquema cada vezque ejecutes doctrine:build --model.

  JobeetJobTable (extiende de PluginJobeetJobTable)en lib/model/doctrine/sfJobeetPlugin/JobeetJobTable.class.php : Lomismo que para la clase JobeetJob excepto que esta e la instancia

de Doctrine_Table que obtendrás cuando llamesaDoctrine_Core::getTable('JobeetJob').

  PluginJobeetJobTable (extiende de Doctrine_Table)en lib/model/doctrine/sfJobeetPlugin/JobeetJobTable.class.php : Estaclase contiene todas las funciones específicas para la instanciade Doctrine_Table que obtendrás cuando llamesaDoctrine_Core::getTable('JobeetJob').

Con esta estructura que has generado tienes la posibilidad de personalizar losmodelos de un plugin editando la clase de nivel superior JobeetJob. Puedepersonalizar el esquema y añadir columnas, añadir relaciones sobreescribiendo losmétodos setTableDefinition() y setUp().

Cuando mueves las clases classes, asegurate de modificar el método configure() almétodo 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 losDoctrine forms. Estos archivos son globales para un proyecto y se volverán a generarcon la doctrine:build --forms y doctrine:build --filters.

Quitar los archivos del plugin:

$ rm plugins/sfJobeetPlugin/lib/form/doctrine/BaseFormDoctrine.class.php$ rmplugins/sfJobeetPlugin/lib/filter/doctrine/BaseFormFilterDoctrine.class.php

También 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 extrañas pasan en este momento, reiniciaApache.

Page 237: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 237/273

 

Ahora que todos los archivos de los modelos se han movido al plugin, ejecutar laspruebas para comprobar que todo funciona bien todavía:

$ php symfony test:all

Los controladores y las Vistas

El siguiente paso lógico es mover los módulos al plugin:

$ mv apps/frontend/modules plugins/sfJobeetPlugin/

Para evitar colisiones con el nombre del módulo, siempre es una buena costumbreponer un prefijo a los nombres de los módulos con el nombre del plugin:

$ mkdir plugins/sfJobeetPlugin/modules/$ mv apps/frontend/modules/affiliateplugins/sfJobeetPlugin/modules/sfJobeetAffiliate$ mv apps/frontend/modules/api plugins/sfJobeetPlugin/modules/sfJobeetApi$ mv apps/frontend/modules/categoryplugins/sfJobeetPlugin/modules/sfJobeetCategory

$ mv apps/frontend/modules/job plugins/sfJobeetPlugin/modules/sfJobeetJob$ mv apps/frontend/modules/languageplugins/sfJobeetPlugin/modules/sfJobeetLanguage

Por cada módulo, también tienes que cambiar el nombre de la clase en todo losarchivos actions.class.php y components.class.php (por ejemplo, laclase affiliateActions necesiat ser renombrada a sfJobeetAffiliateActions).

Las llamadas include_partial() y include_component() también deben sermodificadas 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');

Page 238: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 238/273

 

$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: sfDoctrineRouteCollectionoptions:model: JobeetAffiliateactions: [new, create]object_actions: { wait: GET }prefix_path: /:sf_culture/affiliatemodule: sfJobeetAffiliate

requirements:sf_culture: (?:fr|en)

api_jobs:url: /api/:token/jobs.:sf_formatclass: sfDoctrineRouteparam: { module: sfJobeetApi, action: list }options: { model: JobeetJob, type: list, method: getForToken }requirements:sf_format: (?:xml|json|yaml)

category:

Page 239: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 239/273

 

url: /:sf_culture/category/:slug.:sf_formatclass: sfDoctrineRouteparam: { module: sfJobeetCategory, action: show, sf_format: html }options: { model: JobeetCategory, type: object, method: doSelectForSlug

}requirements:

sf_format: (?:html|atom)sf_culture: (?:fr|en)

job_search:url: /:sf_culture/search.:sf_formatparam: { module: sfJobeetJob, action: search, sf_format: html }requirements:sf_culture: (?:fr|en)

job:class: sfDoctrineRouteCollectionoptions:

model: JobeetJobcolumn: tokenobject_actions: { publish: PUT, extend: PUT }prefix_path: /:sf_culture/jobmodule: sfJobeetJob

requirements:token: \w+sf_culture: (?:fr|en)

job_show_user:url:

/:sf_culture/job/:company_slug/:location_slug/:id/:position_slug

class: sfDoctrineRouteoptions:model: JobeetJobtype: objectmethod_for_query: retrieveActiveJob

param: { module: sfJobeetJob, action: show }requirements:id: \d+sf_method: GETsf_culture: (?:fr|en)

change_language:

url: /change_languageparam: { module: sfJobeetLanguage, action: changeLanguage }

localized_homepage:url: /:sf_culture/param: { module: sfJobeetJob, action: index }requirements:sf_culture: (?:fr|en)

Page 240: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 240/273

 

homepage:url: /param: { module: sfJobeetJob, action: index }

Si tratas de navegar por la página web Jobeet ahora, tendrás excepciones diciendoque los módulos no están habilitados. Como los plugins son compartidos entre

todas las aplicaciones en un proyecto, necesitas específicamente habilitar elmódulo que necesitas para una aplicación determinada en su archivo deconfiguración settings.yml:

# apps/frontend/config/settings.ymlall:.settings:enabled_modules:

- default- sfJobeetAffiliate- sfJobeetApi- sfJobeetCategory

- sfJobeetJob- sfJobeetLanguage

El último paso de la migración es arreglar las pruebas funcionales donde probamospor el nombre del módulo.

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 también 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 también puede contener Recursos

web como imágenes, hojas de estilo, y JavaScripts. Como no queremos distribuirel Jobeet plugin, en realidad no tiene sentido, pero es posible mediante la creaciónde un directorioplugins/sfJobeetPlugin/web/.

Un recurso del plugin debe ser accesible en el directorio web/ del proyecto para servisibles desde un navegador. La tarea plugin:publish-assets se encarga de ellocreando enlaces simbólicos en plataformas Unix y copiando los archivos enplataformas Windows:

$ php symfony plugin:publish-assets

Page 241: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 241/273

 

El Usuario

Moviendo los métodos de las clase myUser que tratan con los historiales es un pocomás implicado. Se podría crear una clase JobeetUser y hacer que myUser heredede ella. Pero hay una forma mejor, sobre todo si varios plugins desean agregarnuevos métodos a la clase.

Los objetos del Nucléo de Symfony notifican eventos durante su ciclo de vida paraque se puedan escuchar. En nuestro caso, tenemos que escuchar alevento user.method_not_found, que ocurre cuando un método indefinido se llamaen el objeto sfUser.

Cuando Symfony es inicializado, todos los plugins también se inicializan si tienenuna clase de configuración 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), elobjeto event dispatcher. Registrar un listener es tan simple como llamara connect(). El método connect() conecta un nombre del evento a un PHPejecutable.

Un PHP callable es una variable PHP que puede ser utilizada por la

función call_user_func() y devuelve true cuando pasa a la funciónis_callable().Una cadena representa una función , y un array puede representar un método deobjeto o un método de clase.

Con el código anterior en el lugar, el objeto myUser llamará al métodoestático methodNotFound() de la clase JobeetUser siempre que sea incapaz deencontrar un método. Es entonces cuando el método methodNotFound() seprocesa.

Remueve todos los métodos 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){

Page 242: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 242/273

 

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){

Page 243: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 243/273

 

$user->getAttributeHolder()->remove('job_history');}

}

Cuando el dispatcher llama al método methodNotFound(), este pasa unobjeto sfEvent. 

Si existe el método en la clase JobeetUser , este se llama y su valor devuelto essubsecuentemente devuelto al notificador. Si no, Symfony tratará con el próximolistener registrado o lanzará una Rxcepción.

El método getSubject() regresa el notificador del evento, que en este caso es elactual objeto myUser.

Como siempre cuando creas nuevas clases, no olvides de limpiar el cache antes denavegar o ejecutar las pruebas:

$ php symfony cc

Arquitectura por defecto vs. Arquitectura de los PluginsUtilizando la arquitectura de plugin te permite organizar el código de una maneradiferente:

Uso de Plugins

Al iniciar la aplicación de una nueva funcionalidad, o si tratas de resolver unproblema clásico de la Web, hay probabilidad de que alguien ya ha resuelto elmismo problema y tal vez empaqueto la solución como un plugin symfony. Parabuscar un plugin público symfony, ve a la secciónplugin del sitio web Symfony.

Como un plugin esta auto-contenido en un directorio, hay varias manera deinstalarlo:

Page 244: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 244/273

 

  Usando la tarea plugin:install (esto solo funciona si el desarrollador hacreado un plugin package y lo ha subido al sitio web de Symfony)

  Descarga el package/paquete y manualmente descomprimirlo bajo eldirectorio plugins/ (también es necesario que el desarrollador haya subidoun package)

  La creación de un svn:externals en plugins/ para el plugin (esto solofunciona si el desarrollador ha alojado su plugin en Subversion)

Las dos últimas formas son fáciles, pero le falta cierta flexibilidad. La primera tepermite instalar la versión más reciente de acuerdo con la versión del proyectoSymfony, fácil de actualizar a la última versión estable, y administrar fácilmentelas dependencias entre plugins.

Contribuyendo con un Plugin

Empaquetar un Plugin

Para crear un plugin package, es necesario agregar algunos archivos obligatorios ala estructura de directorios del plugin. En primer lugar, crear un archivo README enla raíz de directorios del plugin y explicar cómo instalar el plugin, lo queproporciona, y lo que no. The README file must be formatted with the Markdownformat. Este archivo se utilizará en el sitio web Symfony como la principal pieza dela documentación. Puede probar la conversión de tu README a HTML utilizandoel symfony plugin dingus. 

Tareas para Crear Plugins

Si te encuentras con frecuencia en la creación de plugins privados y / o públicos,considera tomar las ventajas de algunas de las tasks en elsfTaskExtraPlugin. Esteplugin, mantenida por el equipo de Symfony, incluye una serie de tareas que teayudan a agilizar el ciclo de vida del plugin:

  generate:plugin 

  plugin:package 

También necesita crear un archivo LICENSE. La elección de una licencia no es unatarea fácil, pero la sección de symfony plugin sólo lista plugins que se liberan bajouna licencia similar a la de Symfony (MIT, BSD, LGPL, y PHP). El contenidode LICENSE se mostrará bajo la pestaña licencia de la página de tu plugin.

El último paso es crear un archivo package.xml en la raíz 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 copiadel usado por un plugin existente. 

El archivo package.xml se compone de varias partes, como puedes ver en esteejemplo:

<!-- plugins/sfJobeetPlugin/package.xml -->  <?xml version="1.0" encoding="UTF-8"?>

Page 245: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 245/273

 

<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.0http://pear.php.net/dtd/tasks-1.0.xsd http://pear.php.net/dtd/package-

2.0http://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>[email protected]</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:

Page 246: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 246/273

 

<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 también otros plugins. Esta información es utilizada por latarea plugin:install para instalar el plugin y su mejor versión para el proyecto ytambién 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 mínimo y un máximo de versión permite a latareaplugin:install saber que versión de Symfony es obligatoria ya que lasversiones puede tener algo diferente en sus APIs.

Declara una dependencia con otro plugin también 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>

Page 247: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 247/273

 

El <changelog> es opcional pero nos da información útil sobre lo que ha cambiadoentre versiones. Esta información está disponible bajo la pestaña "Changelog" ytambién 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, creauna cuenta symfony si no tiene uno ya, entonces crear un nuevo plugin. 

Te convertirás automáticamente en el administrador del plugin y verás unapestaña "admin" en la interfaz. En esta pestaña, usted encontrará todo lo quenecesita para gestionar tu plugin y cargar tus packages.

Page 248: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 248/273

 

Creación de un nuevo Entorno

El framework Symfony ha incorporado muchas estratégias sobre la memoriacache. Por ejemplo, los archivos de configuración YAML se convierten primero enPHP y luego pasan al cache sobre el sistema de ficheros. También hemos visto quelos módulos de administración 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 elrendimiento de tu sitio web, puedes poner en el cache todas las páginas HTML osólo partes de ellas.

De forma predeterminada, la característica del cache de la plantilla de Symfonyestá habilitado en el fichero de configuraciónsettings.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 función de cache antes de ir a producción, podemosactivar la memoria cache para el entorno dev o crear un nuevo entorno.Recordemos que un entorno se define por su nombre (una cadena), un controladorfrontal asociado, y opcionalmente, un conjunto de valores de configuración.

Para jugar con la memoria cache del sistema en Jobeet, vamos a crear unentorno cache, similar al entorno prod, pero con la información disponible del log ydebug del entorno dev. Crea el controlador frontal asociado con el nuevoentorno cache copiando el controladorfrontal 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);

Page 249: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 249/273

 

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 delmétodogetApplicationConfiguration()que es el nombre del entorno, cache.

Puede probar el entornocache

en tu navegador, llamando su controlador:

http://www.jobeet.com.localhost/frontend_cache.php/

El controlador frontal tiene un script que comienza con un código que garantiza que elcontrolador frontal es sólo llamado desde una dirección IP local. Esta medida deseguridad es para proteger el controlador frontal de ser llamado en servidores deproducción. Hablaremos sobre esto en más detalles en el tutorial de mañana.

Por ahora, el entorno cache hereda de la configuración por defecto. Edita elarchivo de configuración settings.yml para agregar la configuración específica deentorno cache:

# apps/frontend/config/settings.yml

cache:.settings:error_reporting: <?php echo (E_ALL | E_STRICT)."\n" ?>web_debug: truecache: trueetag: false

En estos ajustes, la función de cache de plantillas de Symfony se ha activado conel item cachey el web debug toolbar se ha activado con elweb_debug.

Como la configuración 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 enla esquina superior derecha de la página, ya que es el caso para entornos dev.

Configuración del Cache

El caché de las plantillas de Symfony se puede configurar con el archivo deconfiguración cache.yml. La configuración por defecto para la aplicación seencuentra en apps/frontend/config/cache.yml:

default:enabled: falsewith_layout: falselifetime: 86400

De forma predeterminada, ya que todas las páginas pueden contener informacióndinámica, la memoria caché es desactivada globalmente (enabled: false). Notenemos que cambiar esta configuración, porque permitiríamos pone en cachepágina por página.

Page 250: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 250/273

 

El item lifetime define del lado del servidor el tiempo de vida de la memoriacache en segundos (86400 segundos equivalen a un día).

También puedes trabajar a la inversa: habilitar el cache globalmente y luego,deshabilitarlo en determinadas páginas que no se deben poner en cache. >Depende deque representa menos trabajo para tu aplicación.

Páginas en el Cache

Dado que la página principal Jobeet será probablemente la página más visitada delsitio web, en lugar de pedir los datos a la base de datos cada vez que un usuarioaccede a ella, ésta puede estar en el cache.

Crea un archivo cache.yml para el módulo `sfJobeetJob:

# plugins/sfJobeetPlugin/modules/sfJobeetJob/config/cache.ymlindex:enabled: truewith_layout: true

El cache.yml tiene las mismas propiedades que cualquier otro archivo de configuraciónde Symfony como view.yml. Esto significa, por ejemplo, que puedes habilitar el cachepara todas las acciones de un módulo mediante el uso de la clave especial all.

Si actualizas tu navegador, verás que Symfony ha decorado la página con uncuadro que indica que el contenido se ha puesto en el cache:

El cuadro da una valiosa información acerca del cache para la depuración, como lavida útil del cache, y la duración hasta el momento de ella.

Si actualizas la página de nuevo, el color de la caja cambia de verde a amarillo, loque indica que la página se ha recuperado del cache:

Page 251: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 251/273

 

 

Observa también 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 aún funciona ya que el

lenguaje esta incluído en la URL.

Cuando una página es cacheable, y si el cache aún no existe, Symfony almacena elobjeto response en el cache al final de la petición. Para todos los demás de laspeticiones, 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í mismomediante el uso de herramientas como JMeter. 

Una petición entrante con parámetros GET o enviada con método POST, PUT,o DELETE nunca será puesta en el cache por Symfony, independientemente de laconfiguración.

La página de creación de puestos de trabajo también puede ser puesta en elcache:

# plugins/sfJobeetPlugin/modules/sfJobeetJob/config/cache.ymlnew:enabled: true

index:enabled: true

Page 252: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 252/273

 

 all:with_layout: true

Ya que las dos páginas se pueden guardar en el cache con el layout, hemos creadouna scción all que define la configuración 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 directorioprincipal cache/. También tiene opciones para selectivamente limpiar algunaspartes del cache. Para sólo limpiar el cache de las plantillas para el entorno cache,usa las opciones --type y --env:

$ php symfony cc --type=template --env=cacheEn lugar de limpiar el cache cada vez que hagas un cambio, puendes tambiéndeshabilitar el cache agregando cualquier cadena de consulta a la URL, o usando elboton "Ignore cache" de la barra de herramientas de depuración web:

La Acción en el Cache

A veces, no se puede guardar en cache toda la página entera, pero la acción en simisma puede ser guardada en el cache. Dicho de otra manera, puedes guardartodo menos el layout.

Para la aplicación Jobeet, no podemos guardar en cache toda la página enteradebido a la barra "history job".

Cambia la configuración para el cache del module job de acuerdo a:

# plugins/sfJobeetPlugin/modules/sfJobeetJob/config/cache.ymlnew: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:

Page 253: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 253/273

 

$ php symfony cc

Actualiza tu navegador para ver la diferencia:

Incluso si el flujo de la petición es bastante similar en el diagrama simplificado,guardar cache sin el layout es mucho más intensivo el uso de recursos.

Partial y Componente en el Cache

Para sitios web muy dinámicos, a veces es incluso imposible guardar en cache todala plantilla de la acción. Para esos casos, necesitas una manera de configurar elcache a nivel fino. Afortunadamente, los partials y componentes También se puedeponer en el cache.

Page 254: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 254/273

 

 

Vamos a guardar en el cache el componente language creando unarchivo cache.yml para el módulo sfJobeetLanguage:

# plugins/sfJobeetPlugin/modules/sfJobeetLanguage/config/cache.yml_language:enabled: true

La configuración del cache para un partial o un componente es tan simple comoañadir un ítem con su nombre. Con la opción with_layout no se tiene en cuentapara este tipo de cache ya que no tiene ningún sentido:

Page 255: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 255/273

 

 

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 módulos job y category. Ya que lavisualización es siempre la misma, el partial no depende del contexto en el que seutiliza y el cache es el mismo para todas las plantillas (el cache es obviamente todavíadiferente para un conjunto diferente de parámetros).

Pero a veces, un partial o un componente tiene una salida diferente, sobre la base dela acción en la que se incluye (piensa en una barra lateral de un blog, por ejemplo, lacuál es ligeramente diferente para la página principal del blog y la página del artículo).En estos casos el partial o componente es contextual, y el cache debe estarconfigurado en consecuencia mediante el establecimiento de laopción contextual a true:

_sidebar:enabled: truecontextual: true

Formularios en el Cache

El almacenamiento de la página de creación de empleo en el cache es problemáticaya que contiene un formulario. Para comprender mejor el problema, ve a la página"Post a Job" en tu navegador. Entonces, limpia tu cookie de sesión, y trata deenviar un puesto de trabajo. Debes ver un mensaje de error con una alerta de

"CSRF attack":

Page 256: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 256/273

 

 

¿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 página es mostrada, el HTML generado se almacena en elcache con el token del usuario actual. Si otro usuario viene después, la páginadesde el cache se mostrará con el token CSRF del primer usuario. Cuándo se envíael formulario, los tokens no coinciden, y se lanza un mensaje de error.

¿Cómo podemos solucionar el problema, ya que parece legítimo guardar elformulario en el cache? El formulario de creación job no depende del usuario, y esono cambia nada para el usuario actual. En tal caso, ninguna protección CSRF senecesita, y podemos quitar el token CSRF:

// plugins/sfJobeetPlugin/lib/form/doctrine/PluginJobeetJobForm.class.php abstract PluginJobeetJobForm extends BaseJobeetJobForm{public function configure(){$this->disableLocalCSRFProtection();

}}

Después de hacer este cambio, limpiar el cache y re-intentar el mismo escenarioque el anterior para demostrar que funciona como se espera ahora.

La misma configuración debe aplicarse al formulario language ya que figura en ellayout y se almacenará en el cache. Como el valor por defectosfLanguageForm esusado, en lugar de crear una nueva clase, sólo para eliminar el token CSRF, vamosa hacerlo de la acción y componente del módulo sfJobeetLanguage:

// 

 plugins/sfJobeetPlugin/modules/sfJobeetLanguage/actions/components.class.

 php class sfJobeetLanguageComponents extends sfComponents

Page 257: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 257/273

 

{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 página principaldebe ser refrescada para listar el nuevo puesto de trabajo.

Como no necesitamos que el puesto de trabajo aparezca en tiempo real en la

página de inicio, la mejor estrategia es reducir el tiempo de vida del cache a algoaceptable:

# plugins/sfJobeetPlugin/modules/sfJobeetJob/config/cache.ymlindex:enabled: truelifetime: 600

En lugar de la configuración por defecto de un día, el cache para la página principalse eliminará automáticamente cada diez minutos.

Pero si deseas actualizar la página web tan pronto como un usuario activa unnuevo puesto de trabajo, modifiqua el método executePublish() delmódulo sfJobeetJob para añadir 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();

Page 258: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 258/273

 

 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 onlinefor %s days.', sfConfig::get('app_active_days')));

$this->redirect($this->generateUrl('job_show_user', $job));}

El cache es gestionado por la clase sfViewCacheManager. Elmétodo remove() remueve el cache asociado con una URI interna. Para eliminar elcache para todos los posibles parámetros de una variable, utiliza el * como valor.El sf_culture=* que usamos en código anterior significa que Symfony eliminará

del cache la página principal del Inglés y del Francés.

Ya que el administrador del cache es null cuando la memoria cache estádesactivada, hemos envuelto la eliminación del cache en un bloque if.

Probando el Cache

Antes de comenzar, tenemos que cambiar la configuración para elentorno test para habilitar la capa cache:

# apps/frontend/config/settings.ymltest:.settings:

error_reporting: <?php echo ((E_ALL | E_STRICT) ^ E_NOTICE)."\n" ?>cache: trueweb_debug: falseetag: false

Vamos a probar la página para la creación 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)->

Page 259: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 259/273

 

with('response')->checkElement('.category_programming .more_jobs','/23/');

El tester view_cache se utiliza para probar el cache. El método isCached() tomados Booleanos:

  Si la página debe estar en cache o no

  Si el cache es con layout o no

Incluso con todas las herramientas proporcionadas por el framework de pruebasfuncionales, a veces es más fácil de diagnosticar problemas en el navegador. Es muyfácil lograrlo. Crea un controlador frontal para el entorno test. Los logs guardadosen log/frontend_test.log también pueden ser muy útiles.

Page 260: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 260/273

 

Preparando el Servidor de Producción

Antes de implementar el proyecto en producción, hay que asegurarse de que elservidor está configurado correctamente. Se puede volver a leer el día 1, donde seexplica cómo configurar el servidor web.

En esta sección, se supone que ya ha instalado el servidor web, servidor de basesde datos, y PHP 5.2.4 o posterior.

Si no dispones de un acceso SSH al servidor web, pasa a la parte en que necesitastener acceso a la línea de comandos.

Configuración del Servidor

En primer lugar, necesitas comprobar que PHP está instalado con todas lasextensiones necesarias y está correctamente configurado. Como con el día 1,haremos uso del script check_configuration.php provisto con Symfony. Como novamos a instalar Symfony en el servidor de producción, descarga el archivo

directamente desde la página web Symfony:http://trac.symfony-project.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 desdela línea de comandos:

$ php check_configuration.php

Correge cualquier error fatal que el script encuentre y repite el proceso hasta quetodo funcione bien en ambos entornos.

PHP AcceleratorPara el servidor de producción, probablemente quieras disfrutar del mejorrendimiento posible. La instalación de PHP accelerator te dará la mejor mejora.

Desde Wikipedia: Un acelerador PHP funciona guardando en cache el bytecodecompilado de los scripts PHP para evitar la sobrecarga de analizar y compilar códigofuente en cada petición.

APC es uno de los más populares, y es muy fácil de instalar:

$ pecl install APC

Dependiendo de tu sistema operativo, también serás capaz de instalarlo con elgestor de paquetes nativo del sistema operativo.

Tómate un tiempo para aprender a configurar APC. 

Las Bibliotecas de Symfony

Incluyendo Symfony

Page 261: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 261/273

 

Uno de la gran fortalezas de Symfony es un proyecto esta autocontenido. Todoslos archivos necesarios para el proyecto funcione se encuentran bajo el directorioraíz principal del proyecto. Y puedes mover todo el proyecto a otro directorio sincambiar nada en el proyecto propiomente dicho ya que Symfony utiliza sólo rutasrelativas. Esto significa que el directorio del servidor de producción no tiene que

ser el mismo que el de tu equipo de desarrollo.La única ruta absoluta que posiblemente se puede encontrar esta en elarchivo config/ProjectConfiguration.class.php; pero nos ocupamos de éldurante el día 1. Comprobamos que en realidad contiene una ruta relativa alautoloader de Symfony:

// config/ProjectConfiguration.class.php require_oncedirname(__FILE__).'/../lib/vendor/symfony/lib/autoload/sfCoreAutoload.class.php';

Actualizando Symfony

Incluso si todo es autocontenido en un único directorio, actualizar Symfony a unaversión más reciente es, no obstante, demasiado fácil.

Tu deseas actualizar Symfony a la última versión menor de vez en cuando, ya queconstantemente arreglamos errores y posiblemente, cuestiones de seguridad. Labuena noticia es que todas las versiones Symfony se mantienen durante al menosun año y durante el período de mantenimiento, nunca jamás añadimos nuevascaracterísticas, incluso ni la más pequeña. Por lo tanto, siempre es rápido y seguroactualizar de una versión 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, eliminalos archivos actuales y reemplazalos con las nuevas versiones.

Si utilizas Subversion para tu proyecto, también 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 versión deSymfony.

También 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, tendrás la última versión de Symfony1.4.

Cuando se actualiza a una nueva versión, se aconseja siempre que se limpie elcache, especialmente en el entorno de producción:

$ php symfony cc

Page 262: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 262/273

 

Si también tienes acceso al FTP del servidor de producción, se puede simularun symfony cc por la simple eliminación de todos los archivos y directorios deldirectorio cache/.

Puedes incluso probar una nueva versión Symfony sin sustituir a la existente. Si loque deseas es probar una nueva versión, y quieres que se pueda revertir

fácilmente, instala Symfony en otro directorio (lib/vendor/symfony_test porejemplo), cambia la ruta en la claseProjectConfiguration, limpia el cache, y yaestá. Restaurar a la versión anterior es tan simple como eliminar el directorio, yrestaurar la ruta en ProjectConfiguration.

Afinando la Configuración

Configuración de la Base de datos

La mayoría de las veces, la base de datos de producción tiene unas credencialesdistintas a las locales. Gracias a los entornos de Symfony, es muy sencillo tener

una configuración diferente para la base de datos de producción:$ php symfony configure:database"mysql:host=localhost;dbname=prod_dbname" prod_user prod_pass

También puedes editar directamente el archivo de configuración databases.yml .

Recursos

ya que Jobeet usa plugins que tienen recursos web, Symfony crea enlacesrelativos simbólicos en el directorio web/. La tareaplugin:publish-assets losregenera o los crea si instalas plugins sin la tarea plugin:install:

$ php symfony plugin:publish-assets

Personalizar Páginas de Error

Antes de entrar a producción, es mejor personalizar las páginas por defectoSymfony, como la "Page Not Found", o la página de excepción por defecto.

Ya hemos configurado la página de error para el formato YAML durante el día 15,mediante la creación de archivos error.yaml.php yexception.yaml.php en eldirectorio config/error/. El archivo error.yaml.php es usado por Symfony en elentorno prod, mientras queexception.yaml.php es usado en el entorno dev.

Así que, para personalizar la página predeterminada de excepción para el formato

HTML, crea dosarchivos:config/error/error.html.php y config/error/exception.html.php.

La página 404 (page not found) se puede personalizar mediante el cambio de lositems error_404_module y error_404_action:

# apps/frontend/config/settings.ymlall:.actions:error_404_module: default

Page 263: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 263/273

 

error_404_action: error404

Personalización de la Estructura de Directorios

Para una mejor estructura y para normalizar el código, Symfony tiene unaestructura de directorios por defecto con nombres predefinidos. Pero a veces, no

tienes más opción que la de cambiar la estructura a causa de algunas limitacionesexternas.

La configuración de los nombres de directorios que se puede hacer en laclase config/ProjectConfiguration.class.php.

El Directorio Raíz Web

En algunos servidores de Internet, no puedes cambiar el nombre del directorio raízweb. 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 método setWebDir() toma la ruta absoluta del directorio raíz web. Si ademásdesplazas este directorio a otra parte, no olvides de editar el scripts paracomprobar que las rutas de acceso al archivo ProjectConfiguration siguen siendoválidas:

require_once(dirname(__FILE__).'/../config/ProjectConfiguration.class.php');

Los Directorios Cache y Log

El framework Symfony sólo escribe en dos directorios: cache/ y log/. Por razonesde seguridad, algunos servidores web no establecen permisos de escritura en eldirectorio principal. Si este es el caso, puede mover estos directorios a otra partedel 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 método setWebDir() , setCacheDir() y setLogDir() toman una rutaabsoluta para los directorios cache/ y log/respectivamente.

Page 264: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 264/273

 

Personalizando los Objetos del Núcloe Symfony (factorias)

Durante el día 16, hablamos un poco acerca de las factorias symfony. Poderpersonalizar las factorias significa que puedes usar una clase personalizada paralos objetos del núcleo symfony en lugar de usar la predeterminada. Puedestambién cambiar el comportamiento predeterminado de esas clases modificando

los parámetros enviados a ellos.

Demos un vistazo a algunas personalizaciones clásicas que puedes querer hacer.

Nombre de la Cookie

Para manejar la sesión de usuario, Symfony usa una cookie. Esta cookie tiene elnombre por defecto de symfony, el que puede ser cambiado en factories.yml.Bajo el item all, añade la siguiente configuración para cambiar el nombre de lacookie a jobeet:

# apps/frontend/config/factories.ymlstorage:class: sfSessionStorageparam:session_name: jobeet

Guardando la Sesión

La clase por defecto para guardar la sesión es sfSessionStorage. Esta utiliza elsistema de archivos para almacenar la información de la sesión. Si tienes variosservidores web, puedes desear guardar las sesiones en un lugar central, como unatabla de la base de datos:

# apps/frontend/config/factories.yml

storage:class: sfPDOSessionStorageparam:session_name: jobeetdb_table: sessiondatabase: propeldb_id_col: iddb_data_col: datadb_time_col: time

Tiempo de Vida de las Sesiones

De forma predeterminada, el tiempo de la sesión de usuario es 1800 segundos.Esto se puede cambiar editando el item user:

# apps/frontend/config/factories.ymluser:class: myUserparam:timeout: 1800

Page 265: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 265/273

 

Registro de Log

Por defecto, no hay log en el entorno prod porque el nombre de clase de loggeres sfNoLogger:

# apps/frontend/config/factories.ymlprod:logger:class: sfNoLoggerparam:

level: errloggers: ~

Por ejemplo, puedes permitir hacer log sobre el sistema de archivos cambiando elnombre de clase de logger a sfFileLogger:

# apps/frontend/config/factories.ymllogger:class: sfFileLogger

param:level: errloggers: ~file: %SF_LOG_DIR%/%SF_APP%_%SF_ENVIRONMENT%.log

En el archivo de configuración factories.yml, las cadenas de tipo %XXX% sonreemplazadas con su correspondiente valor del objetosfConfig. Por eso, %SF_APP% enun archivo de configuración es equivalente a sfConfig::get('sf_app') en el códigoPHP. Esta notación se puede utilizar también en el archivo de configuración app.yml.Esto es muy útil cuando necesitas para hacer referencia a una ruta en un archivo deconfiguración sin hardcodear la ruta (SF_ROOT_DIR, SF_WEB_DIR, ...).

Desplegar¿Qué desplegar?

Cuando se hace el despliegue (instalación/implementar) del sitio web Jobeet en elservidor de producción, tenemos que tener cuidado de no desplegar los archivosinnecesarios o sobreescribir archivos subidos por nuestros usuarios, como los logosde la compañía.

En un proyecto symfony, hay tres directorios a excluir de latransferencia: cache/, log/, y web/uploads/. Todo lo demás puede ser transferidocomo esta.

Por razones de seguridad, tampoco deseas transferir los controladores frontales"que no sean de producción", como los scriptsfrontend_dev.php, backend_dev.phpyfrontend_cache.php`.

Estrategias para hacer el Despliegue del Proyecto

En esta sección, vamos a suponer que tienes un control total sobre el/losservidor/es de producción. Si sólo se puede acceder al servidor con una cuenta de

Page 266: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 266/273

 

FTP, la única solución posible de despliegue es transferir todos los archivos cadavez que necesite.

La forma más sencilla de implementar tu sitio web es utilizar la tareanativa project:deploy. Esta usa SSH y rsync para conectar y transferir losarchivos de un equipo a otro.

Los Servidores para la tarea project:deploy se pueden configurar en el archivo deconfiguración config/properties.ini:

# config/properties.ini[production]host=www.jobeet.orgport=22user=jobeetdir=/var/www/jobeet/type=rsyncpass=

Para implementar al recién configurado Servidor de producción, utiliza latarea project:deploy:

$ php symfony project:deploy production

Antes de ejecutar la tarea project:deploy por primera vez, necesitas conectarte alservidor manualmente para agregar la clave en el archivo de hosts reconocidos.

Si ejecutas este comando, Symfony sólo simulará la transferencia. Paraimplementar efectivamente el sitio web, añade la opción --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 contraseña.

Por defecto, Symfony no transferirá los directorios que hemos hablado en lasección anterior, ni transferirá el scritp del controlador frontal del entorno dev. Esoes porque la tarea project:deploy excluye archivos y directorios se configuran enel archivoconfig/rsync_exclude.txt:

# config/rsync_exclude.txt.svn/web/uploads/*/cache/*/log/*

/web/*_dev.phpPara Jobeet, tenemos que añadir el archivo frontend_cache.php:

# config/rsync_exclude.txt.svn/web/uploads/*/cache/*/log/*/web/*_dev.php

Page 267: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 267/273

 

/web/frontend_cache.php

También puedes crear un archivo config/rsync_include.txt para obligar quealgunos archivos o directorios sean transferidos.

Incluso si la tarea project:deploy es muy flexible, es posible que desees

personalizarla aún más. Como el despliegue puede ser muy diferente en función detu configuración y topología de servidores, no dudes en extender la tarea pordefecto.

Cada vez que se despliegue un sitio web en producción, no te olvides de por lomenos limpiar la configuración de caché en el servidor de producción:

$ php symfony cc --type=config

Si has cambiado algunas rutas, también tendrás 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.

Page 268: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 268/273

 

¿Qué es Symfony?

El framework symfony es un conjunto de relacionados, pero independientes sub-frameworks, que forman un completo framework MVC (Modelo, Vista,Controlador). Antes de la codificación inicial, nos tomamos un tiempo para leer lahistoria y la filosofía de symfony. Entonces, comprobamos del framework

sus requisitos previos y usamos el script check_configuration.php para validar tuconfiguración.

Finalmente, instalamos symfony. Después de algún tiempo tambiéndesearás actualizar a la última versión del framework.

El framework también 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 descripción de la base de datos, esta genera las clases para

los objetos, formularios, y filtros. Doctrine también genera las sentenciasSQL utilizada para crear las tablas en la base de datos. La configuración de la basede datos se puede hacer con una tarea o editando un archivo de configuración. Además de su configuración, también es posible hacer la inyección inicial de datos,gracias a los archivos de datos. Puede incluso hacer que estos archivossean dinámicos. 

Los objetos Doctrine también puede ser fácilmente 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 oun enlace. Una plantilla puede ser decorada por unlayout para abstraerse delencabezado y pie de las páginas. Para hacer vistas aún más reutilizables, puedesdefinir slots, partials, ycomponentes. 

Para acelerar las cosas, puedes utilizar el sub-framework del cache para guardaren cache una página entera, solo la acción, o tan solopartials o componentes. También 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 módulos, módulos CRUD, o aunpara generar completos y funcionales módulos adminpara las clases del modelo.

Los módulos admin te permiten construir una aplicación completamente funcionalsin codificación alguna.

Page 269: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 269/273

 

Para abstraer la implementación técnica de un sitio web, symfony utiliza un sub-framework enrutamiento que genera URLs amigagles. Para hacer laimplementación de servicios web aún más fácil, symfony soporta formatos enforma nativa. También puedes crear tus propios formatos. 

Una acción puede ser reenviada a otra, o redirigida. 

La Configuración

El framework symfony hace que sea fácil tener diferentes ajustes de configuraciónpara distintos entornos. Un entorno es un conjunto de ajustes que permitediferentes comportamientos en los servidores de desarrollo o de producción.También puedes crear nuevos [entornos]#chapter_21_creating_a_new_environment).

Los archivos de configuración de symfony puede definirse en diferentes niveles y lamayoría 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 mayoría de los archivos de configuración usan el formato YAML. 

En lugar de utilizar la estructura de directorios por defecto y organizar tus archivosde aplicaciones por capas, también puedes organizarlos por función, y agruparlosen un plugin. Hablando de la estructura de directorios por defecto, tambiénpuedes personalizarlade acuerdo a tus necesidades.

La Depuración

Desde el logging hasta la la barra de herramientas de depuración web, y

las significativas excepciones, symfony proporciona un montón 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 número de objetos básicos quenos abstraen de necesidades recurrentes en los proyectos web: la petición, la respuesta, el usuario, el logging, las rutas, el mailer, y el administrador delcache . 

Page 270: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 270/273

 

Estos objetos básicos son gestionados por el objetos sfContext, y que seconfiguran a través de las factorias. 

El objeto user gestiona la autenticación, autorización, flashes, y atributos para serguardado en la sesión.

La SeguridadEl framework symfony tiene incorporadas protecciones contra XSS y CSRF. Estosajustes pueden ser configurados desde la línea de comandos, o la edición deun archivo de configuración. 

El framework de formularios proporciona características incorporadas deseguridad. 

Los Formularios

Como la gestión de formularios es uno de las más tediosas tareas para un

desarrollador web, symfony proporciona un sub-framework de formularios. Elframework de formularios viene con un montón de widgets y validadores. Una delas fortalezas del sub-framework de formularios es que las plantillas son muyfácilmente personalizables. 

Si usas Doctrine, el framework de formularios también facilita la generación deformularios y filtros basados en tus modelos.

Internacionalización y localización

Internacionalización y localización están soportadas por symfony, gracias alestándar ICU. La cultura del usuario determina el idioma y el país del usuario. Esto

puede ser definido por el usuario mismo, o incluído en la URL. Las Pruebas

La biblioteca lime, usa por las Pruebas Unitarias, proporciona una gran cantidadde métodos de prueba. Los objetos Doctrine también puede ser probados desdeuna base de datos dedicada y con específicos datos. 

Las pruebas unitarias se puede ejecutar una a la vez o todas juntas. 

Las PruebasFfuncionales están escritas con la clase sfFunctionalTest, la cualusa un simulador de navegador y permite la introspección de los objetos delnúcleo de symfony a través de Testers. Los Testers existen para el objeto request, 

el objeto response, el objeto user, el objeto formulario actual, la capa cache ylos Objetos Doctrine. 

También puedes utilizar herramientas de depuración parala respuesta y formularios. 

Ya que las pruebas unitarias, pruebas funcionales se pueden ejecutar una poruna o todas juntas. 

También puedes ejecutar todas las pruebas juntas. 

Page 271: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 271/273

 

Los Plugins

El framework symfony sólo proporciona la base para tus aplicaciones web y sebasa en plugins para añadir más características. En este tutorial, hemos habladode sfGuardPlugin, sfFormExtraPlugin, y sfTaskExtraPlugin. 

Un plugin debe ser activado después de la instalación.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 más útiles hansido 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 

También puedes crear tus propias tareass. 

Page 272: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 272/273

 

Hasta pronto

Aprendiendo con la Práctica

El framework symfony, como cualquier pieza de software, tiene una curva deaprendizaje. En el proceso, el primer paso es aprender desde ejemplos prácticos

con un libro como este. El segundo paso es practicar. Nada nunca reemplazará ala práctica.

Es algo que puede empezar hoy mismo. Piensa en un muy simple proyecto webque de algún valor: una lista de tareas, un simple blog, un conversor de tiempo omoneda, lo que sea... Elije uno y empieza implementandolo con lo que ya sabeshoy. Usa la tarea de ayuda para aprender las diferentes opciones, busca códigogenerado por symfony, usa un editor que tenga auto-completion de PHPcomoEclipse, y lee la guía de referencia para buscar toda la configuración delframework.

Disfruta de todo el material que tienes a tu disposición para aprender mas de

symfony.

La Comunidad

Antes de dejarte, me gustaría hablar de una última cosa acerca de symfony. Elframework tiene un montón de grandes características y una gran cantidad dedocumentación libre. Sin embargo, uno de los más valiosos recursos que el Open-Source puede tener es su comunidad. Y symfony tiene una de las mássorprendentes y activa comunidades. En caso de empezar a usar symfony para tusproyectos, considera la posibilidad de unirte a la comunidad symfony:

  Suscríbete al user mailing-list

  Suscríbete al official blog feed

  Suscríbete al symfony planet feed

  Ven y chatea en el #symfony IRC canal de freenode

Page 273: Jobeet El Parcial de Symfony

5/17/2018 Jobeet El Parcial de Symfony - slidepdf.com

http://slidepdf.com/reader/full/jobeet-el-parcial-de-symfony 273/273