jueves, 21 de agosto de 2014
Symfony - Día 6: Más acerca del Modelo
Ayer fue un gran día. Aprendiste como crear URLs amigables y como usar el framework 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 durante los 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 de trabajos activos."
Pero hasta ahora, todos los puestos de trabajo serán mostrados, sea que estén activos o no:
// apps/frontend/modules/job/actions/actions.class.php class jobActions extends sfActions { public function executeIndex(sfWebRequest $request) { $this->jobeet_jobs = Doctrine::getTable('JobeetJob') ->createQuery('a') ->execute(); } // ... }
Un puesto de trabajo activo es uno que fue envíado hace menos de 30 días. El método
Doctrine_Query::execute()
ejecutará una petición contra la base de datos. En el código anterior, no hemos especificado ninguna condición lo que significa que todos los registros son obtenidos de la base de datos.
Cambiemos para que solo seleccione los puestos de trabajo activos:
public function executeIndex(sfWebRequest $request) { $q = Doctrine_Query::create() ->from('JobeetJob j') ->where('j.created_at > ?', date('Y-m-d h:i:s', time() - 86400 * 30)); $this->jobeet_jobs = $q->execute(); }
Depurando por Doctrine el SQL generado
Como no escribiste ninguna sentencia SQL a mano, el Doctrine cuidará de las diferencias que hay entre los motores de base de datos y generará las sentencias SQL optimizadas para el motor de la base de datos que elejíste el día 3. Pero algunas veces, es de gran ayuda para ver el SQL generado por el Doctrine; por ejemplo, para depurar una consulta que no funciona como esperamos. En el entorno
dev
, symfony registra esas consultas (junto a otras muchas más) en el directorio 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.log
Dec 04 13:58:33 symfony [info] {sfDoctrineLogger} executeQuery : SELECT
j.id AS j__id, j.category_id AS j__category_id, j.type AS j__type,
j.company AS j__company, j.logo AS j__logo, j.url AS j__url,
j.position AS j__position, j.location AS j__location,
j.description AS j__description, j.how_to_apply AS j__how_to_apply,
j.token AS j__token, j.is_public AS j__is_public,
j.is_activated AS j__is_activated, j.email AS j__email,
j.expires_at AS j__expires_at, j.created_at AS j__created_at,
j.updated_at AS j__updated_at FROM jobeet_job j
WHERE j.created_at > ? (2008-11-08 01:13:35)
Puedes ver por tí mismo que Doctrine tiene una claúsula where para la columna
created_at
(WHERE j.created_at > ?
).
La cadena
?
en la consulta indica que Doctrine genera una sentencia preparada. El valor actual de ?
('2008-11-08 01:13:35' en el ejemplo anterior) es pasado durante la ejecució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 ataques de inyecciones SQL.
Esto esta bueno, pero es bastante molesto tener que cambiar del navegador , al IDE, y el archivo log cada vez que necesitas probar un cambio. Gracias a la barra web de depuración de symfony, toda la información que necesitas esta también disponible 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 en cuenta 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 porque esta columna almacena el día de creación, no podemos satisfacer el requisito anterior.
Pero si recuerdas el esquema de la base de datos que describimos durante el día 3, también tenemos definido una columna
expires_at
. Actualmente este valor esta siempre vacío ya que este no se establece en el archivo de datos. Pero cuando un puesto de trabajo es creado, puede ser automáticamente establecido a 30 días del día actual.
Cuando necesitas hacer algo automáticamente antes que un objeto Doctrine sea guardado en la base de datos, puedes sobreescribir el método
save()
de la clase del modelo:// lib/model/doctrine/JobeetJob.class.php class JobeetJob extends BaseJobeetJob { public function save(Doctrine_Connection $conn = null) { if ($this->isNew() && !$this->getExpiresAt()) { $now = $this->getCreatedAt() ? $this->getDateTimeObject('created_at')->format('U') : time(); $this->setExpiresAt(date('Y-m-d H:i:s', $now + 86400 * 30)); } return parent::save($conn); } // ... }
El método
isNew()
devuelve true
cuando el objeto no ha sido serializado aún en la base de datos, y false
de lo contratio.
Ahora, vamos a cambiar la acción para usar la columna
expires_at
en lugar de created_at
para seleccionar los puestos de trabajo activos:public function executeIndex(sfWebRequest $request) { $q = Doctrine_Query::create() ->from('JobeetJob j') ->where('j.expires_at > ?', date('Y-m-d h:i:s', time())); $this->jobeet_jobs = $q->execute(); }
Restringimos la consulta para solo seleccionar los puestos de trabajo con un día
expires_at
en el futuro.Con Datos
Actualizando la página de inicio de Jobeet en tu navegador vemos que no cambiamos ningún puesto de trabajo en la base de datos que habíamos dejado hace unos pocos días atrás. Vamos a cambiar el archivo fixtures para agregar un puesto de trabajo que ya haya expirado:
# data/fixtures/jobs.yml JobeetJob: # other jobs expired_job: JobeetCategory: programming company: Sensio Labs position: Web Developer location: Paris, France description: Lorem ipsum dolor sit amet, consectetur adipisicing elit. how_to_apply: Send your resume to lorem.ipsum [at] dolor.sit is_public: true is_activated: true created_at: '2005-12-01 00:00:00' token: job_expired email: job@example.com
Ten cuidado cuando copies y pegues códifo en un archivo de datos para no romper la indentació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 puestos de trabajo no se muestran más:
$ php symfony doctrine:data-load
También puedes ejecutar la siguiente consulta para asegurarte que la columna
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 30 días sean configurables. El framework Symfony trae incluído un archivo de configuración para la configuración específica de una aplicación, el archivo app.yml
. Este archivo de formato YAML puede contener cualquier configuración de desees:# apps/frontend/config/app.yml all: active_days: 30
En la aplicación, esas configuraciones están disponibles a través de la clase global
sfConfig
:sfConfig::get('app_active_days')
El parámetro tiene un prefijo
app_
porque la clase sfConfig
también da acceso a la configuració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()) { $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 centralizar configuraciones globales para tu aplicación.Refactorizando
Todo el código escrito funciona bien, pero aún no esta del todo bien. ¿Puedes ver el 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 la lógicas de negocios, y el Controlador solo invoca al modelo para obtener los datos de éste. Como el código devuelve una colección de puestos de trabajo, vamos a mover 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 los puestos 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 el modelo, 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
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 de inicio debe mostrar los puestos de trabajo por categoría. Primero, necesitamos obtener 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 los puestos de trabajo activos:
// apps/frontend/modules/job/indexSuccess.php <?php use_stylesheet('jobs.css') ?> <div id="jobs"> <?php foreach ($categories as $category): ?> <div class="category_<?php echo Jobeet::slugify($category->getName()) ?>"> <div class="category"> <div class="feed"> <a href="">Feed</a> </div> <h1><?php echo $category ?></h1> </div> <table class="jobs"> <?php foreach ($category->getActiveJobs() as $i => $job): ?> <tr class="<?php echo fmod($i, 2) ? 'even' : 'odd' ?>"> <td class="location"> <?php echo $job->getLocation() ?> </td> <td class="position"> <?php echo link_to($job->getPosition(), 'job_show_user', $job) ?> </td> <td class="company"> <?php echo $job->getCompany() ?> </td> </tr> <?php endforeach; ?> </table> </div> <?php endforeach; ?> </div>
Para mostrar el nomre de la categoría en la plantilla, usamos
echo $category
. ¿Te suena raro? $category
es un objeto, ¿Cómo puede echo
mágicamente mostrar el nombre de la categoría? La respuesta fue dada durante el día 3 cuando teníamos que definir el método mágico __toString()
para todas las clasese del modelo.
Para que funcione, necesitamos agregar el método
getActiveJobs()
a la clase JobeetCategory
que devuelve los puestos de trabajo activos para el objeto categorí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); }
El método
JobeetCategory::getActiveJobs()
usa al métodoDoctrine::getTable('JobeetJob')->getActiveJobs()
para obtener los puestos de trabajo activos para una categoría dada.
Cuando llamamos al
Doctrine::getTable('JobeetJob')->getActiveJobs()
, lo queremos 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
ya que este es la mejor forma de encapsular una condición genérica.
El método
getActiveJobs()
necesita combinar este objeto Doctrine_Query
con su propio consulta. Ya que Doctrine_Query
es un objeto, esto es bastante simple:// lib/model/doctrine/JobeetJobTable.class.php public function getActiveJobs(Doctrine_Query $q = null) { if (is_null($q)) { $q = Doctrine_Query::create() ->from('JobeetJob j'); } $q->andWhere('j.expires_at > ?', date('Y-m-d h:i:s', time())) ->addOrderBy('j.expires_at DESC'); return $q->execute(); }
Limitar los Resultados
Aún queda un requisito por implementar para la lista de puestos de trabajo de la página de inicio:
"Por cada categoría, la lista solo muestra los primeros 10 puestos de trabajo y un enlace 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 es mejor que este valor sea configurable. Cambia la plantilla para pasar el número máximo de puestos de trabajo establecido en app.yml
:<!-- apps/frontend/modules/job/indexSuccess.php --> <?php foreach ($category->getActiveJobs(sfConfig::get('app_max_jobs_on_homepage')) as $i => $job): ?>
y agrega esta nueva configuración en
app.yml
:all: active_days: 30 max_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 a mano... pero hay una mejor manera. La duplicación esta mal, aún es archivos fixture.
¡Symfony al rescate! Los archivos YAML en symfony pueden tener código PHP que será evaluado justo antes de ser analizado. Edita el archivo de datos
jobs.yml
y añade el siguiente código al final:JobeetJob: # Starts at the beginning of the line (no whitespace before) <?php for ($i = 100; $i <= 130; $i++): ?> job_<?php echo $i ?>: JobeetCategory: programming company: Company <?php echo $i."\n" ?> position: Web Developer location: Paris, France description: Lorem ipsum dolor sit amet, consectetur adipisicing elit. how_to_apply: | Send your resume to lorem.ipsum [at] company_<?php echo $i ?>.sit is_public: true is_activated: true token: job_<?php echo $i."\n" ?> email: job@example.com <?php endfor ?>
Ten cuidado, al analizar YAML no olvides ninguna indentación. Manten en mente los 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 en un valor. - Si una declaración
<?php ?>
finaliza una linea, necesitarás explícitamente agregar una nueva linea ("\n").
Puedes ahora recargar los archivos de datos con la tarea
doctrine:data-load
y ver si solo 10
puestos de trabajo son mostrados en la página de inicio para la categoría Programming
. En la siguiente captura de pantalla, tenemos modificado el número máximo de puestos de trabajo a cinco para hacer una imágen mas pequeña:Asegurar la Página
Cuando un puesto de trabajo expira, aun sabiendo la URL, no debería ser posible acceder a él nunca más. Prueba con la URL para el puesto de trabajo expirado (reemplaza el
id
con el actualid
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ágina 404. Perp, ¿Cómo puedo hacer esto cuando la info es cargada automaticamente vía la ruta?
# apps/frontend/config/routing.yml job_show_user: url: /job/:company_slug/:location_slug/:id/:position_slug class: sfDoctrineRoute options: model: JobeetJob type: object method_for_query: retrieveActiveJob param: { module: job, action: show } requirements: id: \d+ sf_method: [GET]
El método
retrieveActiveJob()
recibirá el objeto Doctrine_Query
ya listo por parte de la ruta:// lib/model/doctrine/JobeetJobTable.class.php class JobeetJobTable extends Doctrine_Table { public function retrieveActiveJob(Doctrine_Query $q) { $q->andWhere('a.expires_at > ?', date('Y-m-d h:i:s', time())); return $q->fetchOne(); } // ... }
Ahora, si tratas de ontener un puesto de trabajo expirado, serás enviadoa una página 404.
Enlazar a la Página de la Categoría
Ahora, vamos a agregar un enlace a la página de la 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.
Referencia: http://symfony.com/legacy/doc/jobeet/1_4/es/06?orm=Doctrine
Suscribirse a:
Enviar comentarios
(
Atom
)
Sígueme en las Redes Sociales
Donaciones
Datos personales
Entradas populares
-
En este apartado vamos a explicar como ejercutar archivos PHP a través del terminal de Ubuntu. Lo primero que tendríamos que hacer es inst...
-
En este blog voy a comentar un tema que se utilizan en casi todas las páginas web que existen, y es el tema de la paginación. La paginaci...
-
Este post trata de la integración de la librería PHPExcel en Codeigniter, aunque se podría aplicar a cualquier librería, como por ejemplo mP...
-
Ejemplo para añadir o sumar un número determinado de hora/s, minuto/s, segundo/s a una fecha en php. Con la función strtotime se puede ...
-
Este tema es uno de los temas primordiales sobre el framework Codeigniter, ya que en alguna ocación nos hemos visto obligados a recoger dato...
© Espacio Daycry - Espacio de programación 2013 . Powered by Bootstrap , Blogger templates and RWD Testing Tool
No hay comentarios :
Publicar un comentario