Jobeet FR

Le tutoriel pour Symfony2 en français

Vue normale

Dans le chapitre précédent, nous avons créé notre premier formulaire avec Symfony2. Les utilisateurs sont maintenant en mesure de publier une nouvelle offre d'emploi dans Jobeet mais nous avons manqué de temps avant que nous puissions ajouter quelques tests. C'est ce que nous allons faire.


Soumission d'un formulaire

Ouvrez le fichier JobControllerTest.php afin d'ajouter des tests fonctionnels pour la création d'offres et le processus de validation. A la fin du fichier, ajoutez le code suivant pour obtenir la page de création d'offres:

// src/Ens/JobeetBundle/Tests/Controller/JobControllerTest.php
// ...
 
public function testJobForm()
{
  $client = static::createClient();
 
  $crawler = $client->request('GET', '/job/new');
  $this->assertEquals('Ens\JobeetBundle\Controller\JobController::newAction', $client->getRequest()->attributes->get('_controller'));
}

Pour sélectionner des formulaires, nous allons utiliser la méthode SelectButton(). Cette méthode peut sélectionner des balises button et des boutons submit. Une fois que vous avez un Crawler représentant un bouton, appelez la méthode form() pour obtenir une instance de Form pour le formulaire enveloppant le nœud du bouton:

$form = $crawler->selectButton('submit')->form();

Lorsque vous appelez la méthode form(), vous pouvez aussi passer un tableau de valeurs des champs qui sont substituées à celles par défaut:

$form = $crawler->selectButton('submit')->form(array(
  'name' => 'Fabien',
  'my_form[subject]' => 'Symfony Rocks!'
));

Mais pour passer les valeurs des champs, nous avons besoin de connaître leurs noms. Si vous ouvrez le code source ou utilisez la Web Developer Toolbar de Firefox "Forms > Display Form Details", vous verrez que le nom du champ company est ens_jobeetbundle_jobtype[company]. Pour rendre les choses un peu plus propres, nous allons changer le format de job[%s] en remplaçant la méthode getName avec le code suivant à la fin de la classe JobType:

// src/Ens/JobeetBundle/Form/JobType.php
// ...
 
public function getName()
{
  return 'job';
}

Après ce changement, le nom de la société doit être job[company] dans votre navigateur. Il est maintenant temps de sélectionner et transmettre les valeurs valides au formulaire:

// src/Ens/JobeetBundle/Tests/Controller/JobControllerTest.php
// ...
 
public function testJobForm()
{
  $client = static::createClient();
 
  $crawler = $client->request('GET', '/job/new');
  $this->assertEquals('Ens\JobeetBundle\Controller\JobController::newAction', $client->getRequest()->attributes->get('_controller'));
 
  $form = $crawler->selectButton('Preview your job')->form(array(
    'job[company]'      => 'Sensio Labs',
    'job[url]'          => 'http://www.sensio.com/',
    'job[file]'         => __DIR__.'/../../../../../web/bundles/ensjobeet/images/sensio-labs.gif',
    'job[position]'     => 'Developer',
    'job[location]'     => 'Atlanta, USA',
    'job[description]'  => 'You will work with symfony to develop websites for our customers.',
    'job[how_to_apply]' => 'Send me an email',
    'job[email]'        => 'for.a.job@example.com',
    'job[is_public]'    => false,
  ));
 
  $client->submit($form);
  $this->assertEquals('Ens\JobeetBundle\Controller\JobController::createAction', $client->getRequest()->attributes->get('_controller'));
}

Le navigateur simule aussi l'upload de fichiers si vous passez le chemin absolu du fichier à uploader.

Après avoir soumis le formulaire, nous avons vérifié que l'action exécutée est créée.


Test du formulaire

Si le formulaire est valide, l'offre doit être créée et l'utilisateur redirigé vers la page d'aperçu:

$client->followRedirect();
$this->assertEquals('Ens\JobeetBundle\Controller\JobController::previewAction', $client->getRequest()->attributes->get('_controller'));

Test de l'enregistrement dans la BDD

Finalement, nous voulons vérifier que l'offre a été créée dans la BDD et vérifier que la colonne is_activated est définie sur false lorsque l'utilisateur n'a pas encore publié.

$kernel = static::createKernel();
$kernel->boot();
$em = $kernel->getContainer()->get('doctrine.orm.entity_manager');
 
$query = $em->createQuery('SELECT count(j.id) from EnsJobeetBundle:Job j WHERE j.location = :location AND j.is_activated IS NULL AND j.is_public = 0');
$query->setParameter('location', 'Atlanta, USA');
$this->assertTrue(0 < $query->getSingleScalarResult());

Test d'erreurs

Le formulaire de création d'offre fonctionne comme prévu lorsque nous soumettons des valeurs valides. Ajoutons un test pour vérifier le comportement lorsque nous soumettons des données non valides:

$crawler = $client->request('GET', '/job/new');
$form = $crawler->selectButton('Preview your job')->form(array(
  'job[company]'      => 'Sensio Labs',
  'job[position]'     => 'Developer',
  'job[location]'     => 'Atlanta, USA',
  'job[email]'        => 'not.an.email',
));
$crawler = $client->submit($form);
 
// check if we have 3 errors
$this->assertTrue($crawler->filter('.error_list')->count() == 3);
// check if we have error on job_description field
$this->assertTrue($crawler->filter('#job_description')->siblings()->first()->filter('.error_list')->count() == 1);
// check if we have error on job_how_to_apply field
$this->assertTrue($crawler->filter('#job_how_to_apply')->siblings()->first()->filter('.error_list')->count() == 1);
// check if we have error on job_email field
$this->assertTrue($crawler->filter('#job_email')->siblings()->first()->filter('.error_list')->count() == 1);

Maintenant, nous avons besoin de tester la barre d'administration sur la page de prévisualisation d'offre. Lorsqu'une offre n'a pas encore été activée, vous pouvez modifier, supprimer ou publier l'offre. Pour tester ces trois actions, il nous faudra d'abord créer une offre. Mais c'est beaucoup de copier/coller. Nous allons donc ajouter une méthode de création d'offre dans la classe JobControllerTest:

// src/Ens/JobeetBundle/Tests/Controller/JobControllerTest.php
// ...
 
public function createJob($values = array())
{
  $client = static::createClient();
  $crawler = $client->request('GET', '/job/new');
  $form = $crawler->selectButton('Preview your job')->form(array_merge(array(
    'job[company]'      => 'Sensio Labs',
    'job[url]'          => 'http://www.sensio.com/',
    'job[position]'     => 'Developer',
    'job[location]'     => 'Atlanta, USA',
    'job[description]'  => 'You will work with symfony to develop websites for our customers.',
    'job[how_to_apply]' => 'Send me an email',
    'job[email]'        => 'for.a.job@example.com',
   'job[is_public]'    => false,
  ), $values));
 
  $client->submit($form);
  $client->followRedirect();
 
  return $client;
}

La méthode createJob() crée une offre, suit la redirection et retourne au navigateur. Vous pouvez aussi passer un tableau de valeurs qui seront fusionnées avec les valeurs par défaut.

Le test de l'action "Publier" est maintenant plus simple:

// src/Ens/JobeetBundle/Tests/Controller/JobControllerTest.php
// ...
 
public function testPublishJob()
{
  $client = $this->createJob(array('job[position]' => 'FOO1'));
  $crawler = $client->getCrawler();
  $form = $crawler->selectButton('Publish')->form();
  $client->submit($form);
 
  $kernel = static::createKernel();
  $kernel->boot();
  $em = $kernel->getContainer()->get('doctrine.orm.entity_manager');
 
  $query = $em->createQuery('SELECT count(j.id) from EnsJobeetBundle:Job j WHERE j.position = :position AND j.is_activated = 1');
  $query->setParameter('position', 'FOO1');
  $this->assertTrue(0 < $query->getSingleScalarResult());
}

Tester l'action "Supprimer" est assez similaire:

// src/Ens/JobeetBundle/Tests/Controller/JobControllerTest.php
// ...
 
public function testDeleteJob()
{
  $client = $this->createJob(array('job[position]' => 'FOO2'));
  $crawler = $client->getCrawler();
  $form = $crawler->selectButton('Delete')->form();
  $client->submit($form);
 
  $kernel = static::createKernel();
  $kernel->boot();
  $em = $kernel->getContainer()->get('doctrine.orm.entity_manager');
 
  $query = $em->createQuery('SELECT count(j.id) from EnsJobeetBundle:Job j WHERE j.position = :position');
  $query->setParameter('position', 'FOO2');
  $this->assertTrue(0 == $query->getSingleScalarResult());
}

Les tests comme protection

Quand une offre est publiée, vous ne pouvez plus la modifier. Même si le lien "Modifier" ne s'affiche plus sur la page de prévisualisation, ajoutons quelques tests de cette exigence.

Tout d'abord, ajoutez un autre argument à la méthode createJob() pour permettre la publication automatique de l'offre, créez une méthode getJobByPosition() qui retourne une offre suivant la valeur de l'intitulé:

// src/Ens/JobeetBundle/Tests/Controller/JobControllerTest.php
// ...
 
public function createJob($values = array(), $publish = false)
{
  $client = static::createClient();
  $crawler = $client->request('GET', '/job/new');
  $form = $crawler->selectButton('Preview your job')->form(array_merge(array(
    'job[company]'      => 'Sensio Labs',
    'job[url]'          => 'http://www.sensio.com/',
    'job[position]'     => 'Developer',
    'job[location]'     => 'Atlanta, USA',
    'job[description]'  => 'You will work with symfony to develop websites for our customers.',
    'job[how_to_apply]' => 'Send me an email',
    'job[email]'        => 'for.a.job@example.com',
    'job[is_public]'    => false,
  ), $values));
 
  $client->submit($form);
  $client->followRedirect();
 
  if($publish) {
    $crawler = $client->getCrawler();
    $form = $crawler->selectButton('Publish')->form();
    $client->submit($form);
    $client->followRedirect();
  }
 
  return $client;
}
 
public function getJobByPosition($position)
{
  $kernel = static::createKernel();
  $kernel->boot();
  $em = $kernel->getContainer()->get('doctrine.orm.entity_manager');
 
  $query = $em->createQuery('SELECT j from EnsJobeetBundle:Job j WHERE j.position = :position');
  $query->setParameter('position', $position);
  $query->setMaxResults(1);
  return $query->getSingleResult();
}

Si une offre est publiée, la page de modification doit retourner un code d'état 404:

// src/Ens/JobeetBundle/Controller/JobController.php
// ...
 
public function editAction($token)
{
  $em = $this->getDoctrine()->getEntityManager();
 
  $entity = $em->getRepository('EnsJobeetBundle:Job')->findOneByToken($token);
 
  if (!$entity) {
    throw $this->createNotFoundException('Unable to find Job entity.');
  }
 
  if ($entity->getIsActivated()) {
    throw $this->createNotFoundException('Job is activated and cannot be edited.');
  }
 
  // ...
}

Retour vers le futur dans le test

Quand une offre expire dans moins de cinq jours, ou si elle est déjà expirée, l'utilisateur peut étendre la validité de l'offre pour 30 jours à compter de la date actuelle.

Le test de cette exigence dans un navigateur n'est pas facile car la date d'expiration est automatiquement activée lorsque l'offre est créée pour 30 jours dans le futur. Ainsi, lors de l'obtention de la page de l'offre, le lien pour prolonger l'offre n'est pas présent. Bien sûr, vous pouvez adapter la date d'expiration dans la BDD ou modifier le modèle pour afficher en permanence le lien, mais c'est fastidieux et source d'erreurs. Comme vous l'avez déjà deviné, l'écriture de tests va nous aider une fois de plus.

Comme toujours, nous devons ajouter une nouvelle route pour la méthode extend en premier:

# src/Ens/JobeetBundle/Resources/config/routing/job.yml
# ...
 
ens_job_extend:
    pattern:  /{token}/extend
    defaults: { _controller: "EnsJobeetBundle:Job:extend" }
    requirements: { _method: post }

Ensuite, remplacez le lien "Extend" dans le template admin.html.twig avec le formulaire extend:

<!-- src/Ens/JobeetBundle/Resources/views/Job/admin.html.twig -->
<!-- ... -->
 
{% if job.expiresSoon %}
  <form action="{{ path('ens_job_extend', { 'token': job.token }) }}" method="post">
    {{ form_widget(extend_form) }}
    <button type="submit">Extend</button> for another 30 days
  </form>
{% endif %}
 
<!-- ... -->

Ensuite, créez l'action extend et le formulaire extend:

// src/Ens/JobeetBundle/Controller/JobController.php
// ...
 
public function extendAction($token)
{
  $form = $this->createExtendForm($token);
  $request = $this->getRequest();
 
  $form->bindRequest($request);
 
  if ($form->isValid()) {
    $em = $this->getDoctrine()->getEntityManager();
    $entity = $em->getRepository('EnsJobeetBundle:Job')->findOneByToken($token);
 
    if (!$entity) {
      throw $this->createNotFoundException('Unable to find Job entity.');
    }
 
    if (!$entity->extend()) {
      throw $this->createNotFoundException('Unable to find extend the Job.');
    }
 
    $em->persist($entity);
    $em->flush();
 
    $this->get('session')->setFlash('notice', sprintf('Your job validity has been extended until %s.', $entity->getExpiresAt()->format('m/d/Y')));
  }
 
  return $this->redirect($this->generateUrl('ens_job_preview', array(
    'company' => $entity->getCompanySlug(),
    'location' => $entity->getLocationSlug(),
    'token' => $entity->getToken(),
    'position' => $entity->getPositionSlug()
  )));
}
 
private function createExtendForm($token)
{
  return $this->createFormBuilder(array('token' => $token))
    ->add('token', 'hidden')
    ->getForm()
  ;
}

En outre, ajoutez le formulaire extend à l'action preview:

// src/Ens/JobeetBundle/Controller/JobController.php
// ...
 
public function previewAction($token)
{
  $em = $this->getDoctrine()->getEntityManager();
 
  $entity = $em->getRepository('EnsJobeetBundle:Job')->findOneByToken($token);
 
  if (!$entity) {
    throw $this->createNotFoundException('Unable to find Job entity.');
  }
 
  $deleteForm = $this->createDeleteForm($entity->getId());
  $publishForm = $this->createPublishForm($entity->getToken());
  $extendForm = $this->createExtendForm($entity->getToken());
 
  return $this->render('EnsJobeetBundle:Job:show.html.twig', array(
    'entity'      => $entity,
    'delete_form' => $deleteForm->createView(),
    'publish_form' => $publishForm->createView(),
    'extend_form' => $extendForm->createView(),
  ));
}

Comme prévu par l'action, la méthode extend() de Job retourne true si l'offre a été prolongée ou false sinon:

// src/Ens/Jobeetbundle/Entity/Job.php
// ...
 
public function extend()
{
  if (!$this->expiresSoon())
  {
    return false;
  }
 
  $this->expires_at = new \DateTime(date('Y-m-d H:i:s', time() + 86400 * 30));
 
  return true;
}

Enfin, ajoutez un scénario de test:

// src/Ens/JobeetBundle/Tests/Controller/JobControllerTest.php
// ...
 
public function testExtendJob()
{
  // A job validity cannot be extended before the job expires soon
  $client = $this->createJob(array('job[position]' => 'FOO4'), true);
  $crawler = $client->getCrawler();
  $this->assertTrue($crawler->filter('input[type=submit]:contains("Extend")')->count() == 0);
 
  // A job validity can be extended when the job expires soon
 
  // Create a new FOO5 job
  $client = $this->createJob(array('job[position]' => 'FOO5'), true);
  // Get the job and change the expire date to today
  $kernel = static::createKernel();
  $kernel->boot();
  $em = $kernel->getContainer()->get('doctrine.orm.entity_manager');
  $job = $em->getRepository('EnsJobeetBundle:Job')->findOneByPosition('FOO5');
  $job->setExpiresAt(new \DateTime());
  $em->flush();
  // Go to the preview page and extend the job
  $crawler = $client->request('GET', sprintf('/job/%s/%s/%s/%s', $job->getCompanySlug(), $job->getLocationSlug(), $job->getToken(), $job->getPositionSlug()));
  $crawler = $client->getCrawler();
  $form = $crawler->selectButton('Extend')->form();
  $client->submit($form);
  // Reload the job from db
  $job = $this->getJobByPosition('FOO5');
  // Check the expiration date
  $this->assertTrue($job->getExpiresAt()->format('y/m/d') == date('y/m/d', time() + 86400 * 30));
}

Tâches de maintenance

Même si Symfony est un framework web, il est livré avec un outil de ligne de commande. Vous l'avez déjà utilisé pour créer la structure de répertoire par défaut du paquet de l'application et générer des fichiers divers pour le modèle. Ajouter une nouvelle commande est assez facile.

Lorsqu'un utilisateur crée une offre, il faut l'activer pour la mettre en ligne. Sinon, la BDD grandira avec de vieilles offres. Nous allons créer une commande qui supprime de la BDD les vieilles offres. Cette commande devra être exécutée régulièrement dans une tâche cron.

// src/Ens/JobeetBundle/Command/JobeetCleanupCommand.php
 
namespace Ens\JobeetBundle\Command;
 
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Ens\JobeetBundle\Entity\Job;
 
class JobeetCleanupCommand extends ContainerAwareCommand {
 
  protected function configure()
  {
    $this
      ->setName('ens:jobeet:cleanup')
      ->setDescription('Cleanup Jobeet database')
      ->addArgument('days', InputArgument::OPTIONAL, 'The email', 90)
    ;
  }
 
  protected function execute(InputInterface $input, OutputInterface $output)
  {
    $days = $input->getArgument('days');
 
    $em = $this->getContainer()->get('doctrine')->getEntityManager();
    $nb = $em->getRepository('EnsJobeetBundle:Job')->cleanup($days);
 
    $output->writeln(sprintf('Removed %d stale jobs', $nb));
  }
}

Vous devrez ajouter la méthode cleanup à la classe JobRepository:

// src/Ens/JobeetBundle/Repository/JobRepository.php
// ...
 
public function cleanup($days)
{
  $query = $this->createQueryBuilder('j')
    ->delete()
    ->where('j.is_activated IS NULL')
    ->andWhere('j.created_at < :created_at')     ->setParameter('created_at',  date('Y-m-d', time() - 86400 * $days))
    ->getQuery();
 
  return $query->execute();
}

Pour lancer la commande, exécutez la commande suivante à partir du dossier de projet:

php app/console ens:jobeet:cleanup

ou

php app/console ens:jobeet:cleanup 10

pour supprimer de vieilles offres de plus de 10 jours.


Chapitre précédent Chapitre suivant


Une question ? Une réaction ?

comments powered by Disqus