Tutorial Jobeet con Symfony2 Día 6: Más trabajo con el modelo

De WikiSalud
Saltar a: navegación, buscar

Contenido

El objeto de consulta Doctrine

De los requerimientos del segundo día: "En la página principal, los usuarios ven las últimas ofertas de trabajo activas." Pero hasta ahora, todos las ofertas son mostradas, si están activas o no.

src/Ens/JobeetBundle/Controller/JobController.php
// src/Ens/JobeetBundle/Controller/JobController.php
// ...
 
class JobController extends Controller
{
  public function indexAction()
  {
    $em = $this->getDoctrine()->getEntityManager();
 
    $entities = $em->getRepository('EnsJobeetBundle:Job')->findAll();
 
    return $this->render('EnsJobeetBundle:Job:index.html.twig', array(
      'entities' => $entities
    ));
 
  // ...
}

Una oferta de trabajo activa es aquella que fue publicada hace menos de 30 días. El método $entities = $em->getRepository('EnsJobeetBundle:Job')->findAll() obtendrá todos los trabajos. No estamos especificando ninguna condición, lo cual significa que todos los registros son recuperados de la base de datos Cambiemoslo para seleccionar solo ofertas de trabajo activas


src/Ens/JobeetBundle/Controller/JobController.php
public function indexAction()
{
  $em = $this->getDoctrine()->getEntityManager();
 
  $query = $em->createQuery(
    'SELECT j FROM EnsJobeetBundle:Job j WHERE j.created_at > :date'
  )->setParameter('date', date('Y-m-d H:i:s', time() - 86400 * 30));
  $entities = $query->getResult();
 
  return $this->render('EnsJobeetBundle:Job:index.html.twig', array(
    'entities' => $entities
  ));
}

Depurando el SQL generado por Doctrine

Algunas veces, es bastante útil ver el SQL generado por Doctrine, por ejemplo, para depurar a consulta que no trabaja como se espera. En el ambiente de desarrollo, gracias a la barra de herramientas web para depuración, toda la información que necesitas esta disponible desde la comodidad de tu navegador (http://jobeet.local/app_dev.php):

Primera vista05.png

La información sobre el acceso al modelo parece bastante completa

Primera vista06.png

Serialización de objetos

Incluso si el codigo arriba trabaja, esta lejos de ser perfecto y no toma en cuenta algunos requerimientos del día 2: "Un usuario puede regresar y reactiva o extender la validez de una oferta de trabajo por un extra de 30 días..." But as the above code only relies on the created_at value, and because this column stores the creation date, we cannot satisfy the above requirement.

Si recuerdas el esquema de la base de datos descrita durante el día 3, también tenemos definida una columna expires_at. Actualmente, si este valor no esta configurado en el archivo de fixture, permanece siempre vacío: Cuando una oferta de trabajo es creada, puede ser automáticamente configurada a 30 días después de la actual fecha. Cuando necesitas hacer algo automáticamente antes de que un objeto Doctrine sea serializado a la base de datos, puedes añadir estas acciones a las callback del ciclo de vida en el archivo que mapea objetos a la base de datos, como hicimos antes con la columna created_at column. Agregue entonces setExpiresAtValue en prePersist

src/Ens/JobeetBundle/Resources/config/doctrine/Job.orm.yml
# src/Ens/JobeetBundle/Resources/config/doctrine/Job.orm.yml
# ...
 
  lifecycleCallbacks:
    prePersist: [ setCreatedAtValue, setExpiresAtValue ]
    preUpdate: [ setUpdatedAtValue ]

Ahora tenemos que reconstruir las clases de las entidades, así Doctrine añadirá la nueva función:

php app/console doctrine:generate:entities EnsJobeetBundle

Abre el archivo src/Ens/JobeetBundle/Entity/Job.php y edita la nueva función añadida:

src/Ens/JobeetBundle/Entity/Job.php
# src/Ens/JobeetBundle/Entity/Job.php
# ...
 
public function setExpiresAtValue()
{
  if(!$this->getExpiresAt())
  {
    $now = $this->getCreatedAt() ? $this->getCreatedAt()->format('U') : time();
    $this->expires_at = new \DateTime(date('Y-m-d H:i:s', $now + 86400 * 30));
  }
}

Ahora, cambiemos la acción para usar la columna expires_at en lugar de created_at para seleccionar las ofertas de trabajo activas:

src/Ens/JobeetBundle/Controller/JobController.php
// src/Ens/JobeetBundle/Controller/JobController.php
// ...
 
public function indexAction()
{
  $em = $this->getDoctrine()->getEntityManager();
 
  $query = $em->createQuery(
    'SELECT j FROM EnsJobeetBundle:Job j WHERE j.expires_at > :date'
  )->setParameter('date', date('Y-m-d H:i:s', time()));
  $entities = $query->getResult();
 
  return $this->render('EnsJobeetBundle:Job:index.html.twig', array(
    'entities' => $entities
  ));
}

Trabajando sobre Fixtures

Refresca la página Jobeet en tu navegador, no va a cambiar nada ya que las ofertas de trabajo en la base de datos han sido publicadas hace pocos días. Vamos a cambiar las fixtures para añadir un trabajo que ya haya expirado

src/Ens/JobeetBundle/DataFixtures/ORM/LoadJobData.php
// src/Ens/JobeetBundle/DataFixtures/ORM/LoadJobData.php
// ...
 
$job_expired = new Job();
$job_expired->setCategory($em->merge($this->getReference('category-programming')));
$job_expired->setType('full-time');
$job_expired->setCompany('Sensio Labs');
$job_expired->setLogo('sensio-labs.gif');
$job_expired->setUrl('http://www.sensiolabs.com/');
$job_expired->setPosition('Web Developer Expired');
$job_expired->setLocation('Paris, France');
$job_expired->setDescription('Lorem ipsum dolor sit amet, consectetur adipisicing elit.');
$job_expired->setHowToApply('Send your resume to lorem.ipsum [at] dolor.sit');
$job_expired->setIsPublic(true);
$job_expired->setIsActivated(true);
$job_expired->setToken('job_expired');
$job_expired->setEmail('job@example.com');
$job_expired->setCreatedAt(new \DateTime('2013-12-01'));
 
// ...
 
$em->persist($job_expired);

Recarga las fixtures y refresca tu navegador para asegurarse que la oferta de trabajo vieja no se muestra:

php app/console doctrine:fixtures:load

Refactorizando

Aunque el código que hemos escrito trabaja bien, no es del todo correcto aún. ¿Puedes ver el problema?

El código de la consulta Doctrine no pertenece a la acción (Capa controlador), pertenece a la Capa Modelo. En el modelo MVC, el Modelo define todas la lógica de negocios, y el controlador solo llama al Modelo para recuperar datos de el. Como el código retorna una colección de trabajos moveremos el código al Modelo. Para eso necesitaremos crear un respositorio personalizado para la entidad Job y añadiremos la consulta a esa clase

Abre /src/Ens/JobeetBundle/Resources/config/doctrine/Job.orm.yml, y añade repositoryClass como se muestra a continuación:

/src/Ens/JobeetBundle/Resources/config/doctrine/Job.orm.yml
# /src/Ens/JobeetBundle/Resources/config/doctrine/Job.orm.yml
 
Ens\JobeetBundle\Entity\Job:
  type: entity
  repositoryClass: Ens\JobeetBundle\Repository\JobRepository
  # ...

Doctrine puede generar la clase repositorio para ti corriendo el comando generate:entities usado antes:

php app/console doctrine:generate:entities EnsJobeetBundle

A continuación, añade un método nuevo - getActiveJobs() -a la recientemente creada clase repositorio. Este metodo consultará por todos los entidades trabajos activos ordenados por la columna expires_at (Y filtrada por categoría si recibe el parametro $category_id)

src/Ens/JobeetBundle/Repository/JobRepository.php
// src/Ens/JobeetBundle/Repository/JobRepository.php
 
namespace Ens\JobeetBundle\Repository;
use Doctrine\ORM\EntityRepository;
 
class JobRepository extends EntityRepository
{
  public function getActiveJobs($category_id = null)
  {
    $qb = $this->createQueryBuilder('j')
      ->where('j.expires_at > :date')
      ->setParameter('date', date('Y-m-d H:i:s', time()))
      ->orderBy('j.expires_at', 'DESC');
 
    if($category_id)
    {
      $qb->andWhere('j.category = :category_id')
         ->setParameter('category_id', $category_id);
    }
 
    $query = $qb->getQuery();
 
    return $query->getResult();
  }
}

Ahora el código de la acción puede usar este nuevo metodo para conseguir todas las ofertas de trabajo activas

src/Ens/JobeetBundle/Controller/JobController.php
// src/Ens/JobeetBundle/Controller/JobController.php
// ...
 
public function indexAction()
{
  $em = $this->getDoctrine()->getEntityManager();
 
  $entities = $em->getRepository('EnsJobeetBundle:Job')->getActiveJobs();
 
  return $this->render('EnsJobeetBundle:Job:index.html.twig', array(
    'entities' => $entities
  ));
}
 
// ...

Esta refactorización tiene muchos beneficios sobre el código previo:

  • La lógica que obtiene todas las ofertas de trabajo activas está ahora en el Modelo, donde pertenece
  • El código en el controlador es más delgado y mucho más leíble
  • El método getActiveJobs() es reusable (Por ejemplo en otro controlador)
  • Ahora pueden crearse pruebas unitarias para el código del Modelo

Categorías en la página principal

De acuerdo a los requerimientos del día 2, necesitamos tener las ofertas de trabajo ordenadas por categoría. Hasta ahora, no hemos tomado la propiedad categoría en cuenta. De los requerimientos, la página principal debe mostrar ofertas por categoría. Primero, necesitamos obtener todas las categorías con al menos una oferta de trabajo activas

Crea una clase repositorio para la entidad Category como hicimos para la entidad Job

/src/Ens/JobeetBundle/Resources/config/doctrine/Category.orm.yml
# /src/Ens/JobeetBundle/Resources/config/doctrine/Category.orm.yml
 
Ens\JobeetBundle\Entity\Category:
  type: entity
  repositoryClass: Ens\JobeetBundle\Repository\CategoryRepository
  #...

Generamos la clase repositorio

php app/console doctrine:generate:entities EnsJobeetBundle

Abre la clase CategoryRepository y añade un método getWithJobs()

src/Ens/JobeetBundle/Repository/CategoryRepository.php
// src/Ens/JobeetBundle/Repository/CategoryRepository.php
 
namespace Ens\JobeetBundle\Repository;
use Doctrine\ORM\EntityRepository;
 
class CategoryRepository extends EntityRepository
{
  public function getWithJobs()
  {
    $query = $this->getEntityManager()->createQuery(
      'SELECT c FROM EnsJobeetBundle:Category c LEFT JOIN c.jobs j WHERE j.expires_at > :date'
    )->setParameter('date', date('Y-m-d H:i:s', time()));
 
    return $query->getResult();
  }
}

Cambia la acción index de acuerdo a ello:

src/Ens/JobeetBundle/Controller/JobController.php
public function indexAction()
{
  $em = $this->getDoctrine()->getEntityManager();
 
  $categories = $em->getRepository('EnsJobeetBundle:Category')->getWithJobs();
 
  foreach($categories as $category)
  {
    $category->setActiveJobs($em->getRepository('EnsJobeetBundle:Job')->getActiveJobs($category->getId()));
  }
 
  return $this->render('EnsJobeetBundle:Job:index.html.twig', array(
    'categories' => $categories
  ));
}

Para este trabajo tenemos que añadir una nueva propiedad para nuestra clase Category, active_jobs

src/Ens/JobeetBundle/Entity/Category.php
// src/Ens/JobeetBundle/Entity/Category.php
 
namespace Ens\JobeetBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
 
class Category
{
  // ...
     /*
     * @var string
     */
     private $active_jobs;
 
  // ...
 
  public function setActiveJobs($jobs)
  {
    $this->active_jobs = $jobs;
  }
 
  public function getActiveJobs()
  {
    return $this->active_jobs;
  }
}

En la plantilla, necesitamos iterar a tráves de todas las categorías y mostrar todas las ofertas de trabajo activas

src/Ens/JobeetBundle/Resources/views/Job/index.html.twig
<!-- src/Ens/JobeetBundle/Resources/views/Job/index.html.twig -->
<!-- ... -->
 
{% block content %}
  <div id="jobs">
    {% for category in categories %}
      <div>
        <div class="category">
          <div class="feed">
            <a href="">Feed</a>
          </div>
          <h1>{{ category.name }}</h1>
        </div>
        <table class="jobs">
          {% for entity in category.activejobs %}
            <tr class="{{ cycle(['even', 'odd'], loop.index) }}">
              <td class="location">{{ entity.location }}</td>
              <td class="position">
                <a href="{{ path('job_show', { 'id': entity.id, 'company': entity.companyslug, 'location': entity.locationslug, 'position': entity.positionslug }) }}">
                  {{ entity.position }}
                </a>
              </td>
              <td class="company">{{ entity.company }}</td>
            </tr>
          {% endfor %}
        </table>
      </div>
    {% endfor %}
  </div>
{% endblock %}

Limitando los resultados

Aún hay un requerimiento para implementar en la lista de ofertas de trabajo de la página de inicio: Tenemos que limitar la lista a 10 ítems. Es tan simple como añadir el $max_paramater al método JobRepository::getActiveJobs()

src/Ens/JobeetBundle/Repository/JobRepository.php
  public function getActiveJobs($category_id = null, $max = null)
  {
    $qb = $this->createQueryBuilder('j')
      ->where('j.expires_at > :date')
      ->setParameter('date', date('Y-m-d H:i:s', time()))
      ->orderBy('j.expires_at', 'DESC');

    if($max)
    {
      $qb->setMaxResults($max);
    }

    if($category_id)
    {
      $qb->andWhere('j.category = :category_id')
         ->setParameter('category_id', $category_id);
    }

    $query = $qb->getQuery();

    return $query->getResult();
  }

Cambiamos la llamada a getActiveJobs para incluir el $max_

src/Ens/JobeetBundle/Controller/JobController.php
// src/Ens/JobeetBundle/Controller/JobController.php
 
$category->setActiveJobs($em->getRepository('EnsJobeetBundle:Job')->getActiveJobs($category->getId(), 10));

Personalizando configuración

En el método JobController e indexAction, hemos codificado el número de ofertas de trabajo retornadas por categoría. Hubiera sido mejor hacer configurable ese límite de 10. En symfony, podemos definir parametros personalizados para tu aplicación en el archivo.
En el archivo app/config/config.yml, se importa el archivo parameters.yml

app/config/config.yml
# app/config/config.yml
# ...
 
imports:
    - { resource: parameters.yml }
    - { resource: security.yml }
    ...

Por tanto, modificaremos el archivo app/config/parameters.yml, agregando la propiedad a la que después hemos de acceder. Tenga cuidado, este archivo contiene otros parámetros importantes para toda la aplicación:

app/config/parameters.yml
# This file is auto-generated during the composer install
parameters:
    max_jobs_on_homepage: 10
    database_driver: pdo_pgsql
    database_host: 127.0.0.1
    database_port: null
    ...

Ahora esto puede ser accedido desde un controlador:

src/Ens/JobeetBundle/Controller/JobController
// src/Ens/JobeetBundle/Controller/JobController
// ...
 
public function indexAction()
{
  $em = $this->getDoctrine()->getEntityManager();
 
  $categories = $em->getRepository('EnsJobeetBundle:Category')->getWithJobs();
 
  foreach($categories as $category)
  {
    $category->setActiveJobs($em->getRepository('EnsJobeetBundle:Job')->getActiveJobs($category->getId(), $this->container->getParameter('max_jobs_on_homepage')));
  }
 
  return $this->render('EnsJobeetBundle:Job:index.html.twig', array(
    'categories' => $categories
  ));
}

Dinamic Fixtures

Por ahora, no puedes ver diferencia alguna porque tenemos pequeña cantidad de ofertas de trabajo en nuestra base de datos. Necesitamos agregar un manejo de ofertas a la fixture. Así, puedes copiar y pegar un oferta existente 10 o 20 veces a manos... Pero hay una forma mejor. La duplicación es mala, incluso en archivos fixtures

src/Ens/JobeetBundle/DataFixtures/ORM/LoadJobData.php
// src/Ens/JobeetBundle/DataFixtures/ORM/LoadJobData.php
// ...
 
public function load(ObjectManager $em)
{
  // ...
 
  for($i = 100; $i <= 130; $i++)
 {
   $job = new Job();
   $job->setCategory($em->merge($this->getReference('category-programming')));
    $job->setType('full-time');
    $job->setCompany('Company '.$i);
    $job->setPosition('Web Developer');
    $job->setLocation('Paris, France');
    $job->setDescription('Lorem ipsum dolor sit amet, consectetur adipisicing elit.');
    $job->setHowToApply('Send your resume to lorem.ipsum [at] dolor.sit');
    $job->setIsPublic(true);
    $job->setIsActivated(true);
    $job->setToken('job_'.$i);
    $job->setEmail('job@example.com');
 
    $em->persist($job);
  }
 
  $em->flush();
}
 
// ...

Ahora puedes recargar las fixtures con la tarea doctrine:fictures:load y ver si sólo son mostradas 10 ofertas de trabajo en la página principla para la categoría Programming: Acá va una snapshoot sobre lo que se ve en pantalla

Primera vista07.png

Securiza la página Job

Cuando un trabajo expira, incluso si sabes la URL, no debería ser posible acceder más a él. Prueba la URL para la oferta expirada (Reemplaza la id con el id actual en tu base de datos SELECT id, token FROM jobeet_job WHERE expires_at < NOW()):

/app_dev.php/job/sensio-labs/paris-france/ID/web-developer-expired

En lugar de mostrar el trabajo, necesitamos reenviar al usuario al una página de error 404. Para esto crearemos una nueva función en JobRepository:

src/Ens/JobeetBundle/Repository/JobRepository.php
// src/Ens/JobeetBundle/Repository/JobRepository.php
// ...
 
public function getActiveJob($id)
{
  $query = $this->createQueryBuilder('j')
    ->where('j.id = :id')
    ->setParameter('id', $id)
    ->andWhere('j.expires_at > :date')
    ->setParameter('date', date('Y-m-d H:i:s', time()))
    ->setMaxResults(1)
    ->getQuery();
 
  try {
    $job = $query->getSingleResult();
  } catch (\Doctrine\Orm\NoResultException $e) {
    $job = null;
  }
 
  return $job;
}

El método getSingleResult() lanza a exepción Doctrine\ORM\NoResultException si no se regresan retornado y un Doctrine\ORM\NonUniqueResultException si más de un resultado es retornado. Si usas este método, puedes necesitar envolverlo en un bloque try-catch para asegurarse que solo un resultado es retornado

Cambia ahora el showAction de él JobController para usar el nuevo método del repositorio:

src/Ens/JobeetBundle/Controller/JobController.php
// src/Ens/JobeetBundle/Controller/JobController.php
// ...
 
$entity = $em->getRepository('EnsJobeetBundle:Job')->getActiveJob($id);
 
// ...

Ahora, si intentas obtener una oferta de trabajo expirada, seremos reenviados a una página 404 Acá va una página sobre ese mismo error

Herramientas personales
Espacios de nombres

Variantes
Acciones
Navegación
Herramientas