Symfony2 - Jobeet - Jour 06 - Aller plus loin avec le Modèle

1 1 1 1 1 1 1 1 1 1 Rating 0.00 (0 Votes)
Submit to DeliciousSubmit to DiggSubmit to FacebookSubmit to Google PlusSubmit to StumbleuponSubmit to TechnoratiSubmit to TwitterSubmit to LinkedIn

L'objet Query de Doctrine

Pour la deuxième story : "Sur la page d'accueil, l'utilisateur voit les dernières offres actives." Mais actuellement, toutes les offres sont affichées, qu'elles soient actives ou non :

  •  src/Erlem/JobeetBundle/Controller/JobController.php
// ...

class JobController extends Controller
{
    public function indexAction()
    {
        $em = $this->getDoctrine()->getManager();

        $entities = $em->getRepository('ErlemJobeetBundle:Job')->findAll();

        return $this->render('ErlemJobeetBundle:Job:index.html.twig', array(
            'entities' => $entities
        ));

 // ...
}

Une offre active est une offre de moins de 30 jours. La méthode $entities = $em->getRepository('ErlemJobeetBundle:Job')->findAll(); va créer une requête à la BDD pour obtenir toutes les offres. Nous ne spécifions aucune condition, ce qui signifie que tous les enregistrements sont extraits de la BDD.

Changeons cela pour sélectionner uniquement les offres actives :

  •  src/Erlem/JobeetBundle/Controller/JobController.php
public function indexAction()
{
    $em = $this->getDoctrine()->getManager();

    $query = $em->createQuery(
        'SELECT j FROM ErlemJobeetBundle: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('ErlemJobeetBundle:Job:index.html.twig', array(
        'entities' => $entities
    ));
}

Débuguer les requêtes générées par Doctrine

Parfois, il est utile de voir les requêtes générées par Doctrine, par exemple, pour débuguer une requête qui ne fonctionne pas comme prévu. Dans l'environnement de dev, grâce à la Web Debug Toolbar de Symfony, toutes les informations dont vous avez besoin sont disponibles dans votre navigateur http://jobeet.local/app_dev.php) :

01-105-symfony2-jour-06-aller-plus-loin-avec-le-modele

02-105-symfony2-jour-06-aller-plus-loin-avec-le-modele

Sérialisation d'objets

Même si le code ci-dessus fonctionne, il est loin d'être parfait, car il ne tient pas compte de certains scénarios du jour 2 (Symfony2 - Jobeet - Jour 02 - Le projet) : "Un utilisateur peut revenir réactiver ou prolonger la validité de l'offre pour une période de 30 jours supplémentaires...".

Mais comme le code ci-dessus ne repose que sur la valeur created_at, et parce que cette colonne stocke la date de création, on ne peut pas satisfaire ce scénario.

Si vous vous souvenez du schéma de BDD que nous avons décrit dans le jour 3 (Symfony2 - Jobeet - Jour 03 - Le Modèle de Données), nous avons aussi défini une colonne expires_at. À l'heure actuelle, si cette valeur n'est pas définie dans le fichier d'installation, il reste toujours vide. Mais quand une offre est créée, cette valeur peut être automatiquement réglée à 30 jours après la date du jour.

Lorsque vous avez besoin de faire quelque chose automatiquement avant qu'un objet Doctrine ne soit sérialisé dans la BDD, vous pouvez ajouter une nouvelle entrée lifecycleCallbacks dans le fichier qui mappe les objets de la BDD, comme nous l'avons fait précédemment pour la colonne created_at:

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

Maintenant, nous devons reconstruire les classes d'entités afin que Doctrine ajoute la nouvelle fonction :

php app/console doctrine:generate:entities ErlemJobeetBundle

Ouvrez le fichier src/Erlem/JobeetBundle/Entity/Job.php et modifiez la nouvelle fonction :

  •  src/Erlem/JobeetBundle/Entity/Job.php
// ...

class Job
{
    // ... 

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

Maintenant, nous allons modifier l'action pour utiliser la colonne expires_at au lieu de created_at pour sélectionner les offres actives :

  • src/Erlem/JobeetBundle/Controller/JobController.php
// ...

    public function indexAction()
    {
        $em = $this->getDoctrine()->getManager();

        $query = $em->createQuery(
            'SELECT j FROM ErlemJobeetBundle:Job j WHERE j.expires_at > :date'
    )->setParameter('date', date('Y-m-d H:i:s', time()));
        $entities = $query->getResult();

        return $this->render('ErlemJobeetBundle:Job:index.html.twig', array(
            'entities' => $entities
        ));
    }

// ...

Aller plus loin avec les fixtures

L'actualisation de la page d'accueil Jobeet dans votre navigateur ne changera rien car les offres dans la BDD ont été affichées il y a seulement quelques jours. Nous allons changer les fixtures pour ajouter une offre qui est déjà expirée :

  • src/Erlem/JobeetBundle/DataFixtures/ORM/LoadJobData.php
// ...
    public function load(ObjectManager $em)
    {
        $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[at]example.com');
        $job_expired->setCreatedAt(new \DateTime('2005-12-01'));          
        $job_sensio_labs = new Job();
        $job_sensio_labs->setCategory($em->merge($this->getReference('category-programming')));
        $job_sensio_labs->setType('full-time');
        $job_sensio_labs->setCompany('Sensio Labs');
        $job_sensio_labs->setLogo('sensio-labs.gif');
        $job_sensio_labs->setUrl('http://www.sensiolabs.com/');
        $job_sensio_labs->setPosition('Web Developer');
        $job_sensio_labs->setLocation('Paris, France');
        $job_sensio_labs->setDescription('You\'ve already developed websites with symfony and you want to work with Open-Source technologies. You have a minimum of 3 years experience in web development with PHP or Java and you wish to participate to development of Web 2.0 sites using the best frameworks available.');
        $job_sensio_labs->setHowToApply('Send your resume to fabien.potencier[at]sensio.com');
        $job_sensio_labs->setIsPublic(true);
        $job_sensio_labs->setIsActivated(true);
        $job_sensio_labs->setToken('job_sensio_labs');
        $job_sensio_labs->setEmail('job[at]example.com');
        $job_sensio_labs->setExpiresAt(new \DateTime('+30 days'));
        $job_extreme_sensio = new Job();
        $job_extreme_sensio->setCategory($em->merge($this->getReference('category-design')));
        $job_extreme_sensio->setType('part-time');
        $job_extreme_sensio->setCompany('Extreme Sensio');
        $job_extreme_sensio->setLogo('extreme-sensio.gif');
        $job_extreme_sensio->setUrl('http://www.extreme-sensio.com/');
        $job_extreme_sensio->setPosition('Web Designer');
        $job_extreme_sensio->setLocation('Paris, France');
        $job_extreme_sensio->setDescription('Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in.');
        $job_extreme_sensio->setHowToApply('Send your resume to fabien.potencier[at]sensio.com');
        $job_extreme_sensio->setIsPublic(true);
        $job_extreme_sensio->setIsActivated(true);
        $job_extreme_sensio->setToken('job_extreme_sensio');
        $job_extreme_sensio->setEmail('job[at]example.com');
        $job_extreme_sensio->setExpiresAt(new \DateTime('+30 days'));

        $em->persist($job_sensio_labs);
        $em->persist($job_extreme_sensio);
        $em->persist($job_expired); 
              
        $em->flush();
    }

// ...

Rechargez les fixtures et rafraîchissez votre navigateur pour vous assurer que l'ancienne offre ne s'affiche pas :

php app/console doctrine:fixtures:load

Répondre Y à la question.

Careful, database will be purged. Do you want to continue Y/N ?Y

Refactorisation

Bien que le code que nous avons écrit fonctionne très bien, ce n'est pas encore parfait. Voyez-vous le problème ?

La requête Doctrine n'appartient pas à l'action (la couche Contrôleur), il appartient à la couche Modèle. Dans le modèle MVC, le Modèle définit toute la logique métier, et le Contrôleur appelle le Modèle pour récupérer les données qu'il contient. Comme le code renvoie une collection d'offres, déplaçons le code vers le Modèle. Pour cela, nous devons créer une classe de dépôt personnalisée pour l'entité Job et ajouter la requête à cette classe.

Ouvrez /src/Erlem/JobeetBundle/Resources/config/doctrine/Job.orm.yml et ajoutez ce qui suit :

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

Doctrine peut générer la classe de dépôt pour vous en exécutant la commande generate:entities utilisée précédemment :

php app/console doctrine:generate:entities ErlemJobeetBundle

Ensuite, ajoutez une nouvelle méthode - getActiveJobs() - à la classe de dépôt nouvellement générée. Cette méthode va récupérer toutes les entités Job actives triées par la colonne expires_at (et filtrées par catégorie si elle reçoit le paramètre $category_id).

  • src/Erlem/JobeetBundle/Repository/JobRepository.php
namespace Erlem\JobeetBundle\Repository;

use Doctrine\ORM\EntityRepository;

/**
 * JobRepository
 *
 * This class was generated by the Doctrine ORM. Add your own custom
 * repository methods below.
 */
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();
    }
}

Maintenant, le code de l'action peut utiliser cette nouvelle méthode pour récupérer les offres actives.

  • src/Erlem/JobeetBundle/Controller/JobController.php
// ...

    public function indexAction()
    {
        $em = $this->getDoctrine()->getManager();

        $entities = $em->getRepository('ErlemJobeetBundle:Job')->getActiveJobs();

        return $this->render('ErlemJobeetBundle:Job:index.html.twig', array(
            'entities' => $entities
        ));
    }

// ...

Cette refactorisation a plusieurs avantages par rapport au code précédent:

  • La logique pour obtenir les offres actives est maintenant dans le Modèle, où elle appartient
  • Le code du Contrôleur est plus mince et beaucoup plus lisible
  • La méthode getActiveJobs() est ré-utilisable (par exemple dans une autre action)
  • Le code du modèle est désormais testable unitairement

Les catégories sur la page d'accueil

Conformément aux scénarios du deuxième jour (Symfony2 - Jobeet - Jour 02 - Le projet), nous avons besoin d'avoir des offres classées par catégories. Jusqu'à présent, nous n'avons pas pris la notion de catégorie d'offre en compte. Selon les scénarios, la page d'accueil doit afficher les offres par catégorie. Tout d'abord, nous avons besoin de toutes les catégories avec au moins une offre.

Créez une classe de dépôt pour l'entité Category comme nous l'avons fait pour Job :

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

Générez la classe de dépôt :

php app/console doctrine:generate:entities ErlemJobeetBundle

Ouvrez la classe CategoryRepository et ajoutez une méthode getWithJobs() :

  •  src/Erlem/JobeetBundle/Repository/CategoryRepository.php
namespace Erlem\JobeetBundle\Repository;

use Doctrine\ORM\EntityRepository;

/**
 * CategoryRepository
 *
 * This class was generated by the Doctrine ORM. Add your own custom
 * repository methods below.
 */
class CategoryRepository extends EntityRepository
{
    public function getWithJobs()
    {
        $query = $this->getEntityManager()->createQuery(
            'SELECT c FROM ErlemJobeetBundle: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();
    }   
}

Modifiez l'action index en conséquence :

  •  src/Erlem/JobeetBundle/Controller/JobController.php
// ...

    public function indexAction()
    {
        $em = $this->getDoctrine()->getManager();

        $categories = $em->getRepository('ErlemJobeetBundle:Category')->getWithJobs();

        foreach($categories as $category) {
            $category->setActiveJobs($em->getRepository('ErlemJobeetBundle:Job')->getActiveJobs($category->getId()));
        }

        return $this->render('ErlemJobeetBundle:Job:index.html.twig', array(
            'categories' => $categories
        ));
    }

// ...

Pour que cela fonctionne, nous devons ajouter une nouvelle propriété à notre classe Category, les active_jobs :

  • src/Erlem/JobeetBundle/Entity/Category.php
class Category
{
    // ...

    private $active_jobs;

    // ...

    public function setActiveJobs($jobs)
    {
        $this->active_jobs = $jobs;
    }

    public function getActiveJobs()
    {
        return $this->active_jobs;
    }
}

Dans le template, nous avons besoin de parcourir toutes les catégories et afficher les offres actives :

  • src/Erlem/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('erlem_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 %}

Limiter les résultats

Il reste encore une condition à implémenter pour la liste des offres dans la page d'accueil: il faut limiter la liste des offres à 10 articles. Il est assez simple d'ajouter le paramètre $max à la méthode JobRepository::getActiveJobs() :

  •  src/Erlem/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();
    }

Changez l'appel à getActiveJobs afin d'inclure le paramètre $max :

  • src/Erlem/JobeetBundle/Controller/JobController.php
// ...

    public function indexAction()
    {
        $em = $this->getDoctrine()->getManager();

        $categories = $em->getRepository('ErlemJobeetBundle:Category')->getWithJobs();

        foreach($categories as $category)
        {
            $category->setActiveJobs($em->getRepository('ErlemJobeetBundle:Job')->getActiveJobs($category->getId(), 10));
        }

        return $this->render('ErlemJobeetBundle:Job:index.html.twig', array(
            'categories' => $categories
        ));
    }

// ...

Configuration personnalisée

Dans la méthode indexAction de JobController, nous avons fixé le nombre d'offres maximum retournées pour une catégorie. Il aurait été préférable de laisser la limite configurable. Dans Symfony, vous pouvez définir des paramètres personnalisés pour votre application dans le fichier app/config/config.yml, sous l'entrée parameters :

  • app/config/config.yml
# ...

parameters:
    max_jobs_on_homepage: 10

Cela est maintenant accessible dans un contrôleur :

  • src/Erlem/JobeetBundle/Controller/JobController.php
// ...

    public function indexAction()
    {
        $em = $this->getDoctrine()->getManager();

        $categories = $em->getRepository('ErlemJobeetBundle:Category')->getWithJobs();

        foreach($categories as $category) {
            $category->setActiveJobs($em->getRepository('ErlemJobeetBundle:Job')->getActiveJobs($category->getId(), $this->container->getParameter('max_jobs_on_homepage')));
        }

        return $this->render('ErlemJobeetBundle:Job:index.html.twig', array(
            'categories' => $categories
        ));
    }

// ...

Fixtures dynamiques

Pour l'instant, vous ne verrez aucune différence parce que nous n'avons qu'une très petite quantité d'offres dans notre BDD. Nous avons besoin d'ajouter un tas d'offres à la fixture. Ainsi, vous pouvez copier et coller des offres existantes, dix ou vingt fois à la main... mais il y a une meilleure façon. La duplication est une mauvais solution, même pour les fichiers fixture :

  •  src/Erlem/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[at]example.com');

        $em->persist($job);
    }

    // ... 
    $em->flush();
}

// ...

Vous pouvez maintenant recharger les fixtures avec la commande doctrine:fixtures:load et vérifiez que seulement 10 offres sont affichées sur la page d'accueil pour la catégorie Programming :

php app/console doctrine:fixtures:load

Allez sur la page : http://jobeet.local/app_dev.php/

03-105-symfony2-jour-06-aller-plus-loin-avec-le-modele

Sécuriser la page d'offres

Quand une offre arrive à expiration, même si vous connaissez l'URL, il ne doit pas être possible d'y accéder. Essayez l'URL d'un emploi expiré (remplacez l'ID avec l'id réel dans votre BDD - SELECT id, token FROM jobeet_job WHERE expires_at < NOW()) :

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

Au lieu d'afficher l'offre, nous devons rediriger l'utilisateur vers une page d'erreur 404. Pour cela, nous allons créer une nouvelle fonction dans JobRepository :

  • src/Erlem/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;
    }

La méthode getSingleResult() renvoie une exception Doctrine\ORM\NoResultException si aucun résultat n'est retourné et une exception Doctrine\ORM\NonUniqueResultException si plus d'un résultat est retourné. Si vous utilisez cette méthode, vous pouvez avoir besoin de l'envelopper dans un bloc try-catch et de s'assurer qu'un seul résultat est retourné.

Maintenant, modifiez showAction de JobController afin d'utiliser la méthode du nouveau dépôt :

  • src/Erlem/JobeetBundle/Controller/JobController.php
// ...

$entity = $em->getRepository('ErlemJobeetBundle:Job')->getActiveJob($id);

// ...

Maintenant, si vous essayez d'obtenir une offre expirée, vous serez redirigé vers une page 404.

04-105-symfony2-jobeet-jour-06-aller-plus-loin-avec-le-modele

C'est tout pour aujourd'hui ! Demain, nous jouerons avec la page catégorie.

Code source

Le code source est sur GitHub. Vous pouvez le télécharger en exécutant les commandes ci-dessous (prérequis Symfony2 - Jobeet - Jour 01 - Démarrage du projet) :

su
cd /var/www/
git clone https://github.com/erlem/jobeet.git
cd jobeet/
git checkout tags/v0.6.0
 
composer update
 
php app/console doctrine:database:create
php app/console doctrine:schema:update --force
php app/console doctrine:fixtures:load
 
php app/console assets:install web

chmod 777 -R app/cache/
chmod 777 -R app/logs/

Vous pouvez maintenant tester l'application dans un navigateur: http://jobeet.local/ ou dans l'environnement de développement, http://jobeet.local/app_dev.php.


Symfony2 - Jour 05 - Le Routage
[Sommaire] Symfony2 - Jour 07 - Jouons avec la page Catégorie >


Pour ce tutoriel, je me suis inspiré du tutoriel Symfony2 Jobeet du site IntelligentBee

Submit to DeliciousSubmit to DiggSubmit to FacebookSubmit to Google PlusSubmit to StumbleuponSubmit to TechnoratiSubmit to TwitterSubmit to LinkedIn