Overblog
Suivre ce blog Administration + Créer mon blog

Symfony2 Coding Sessions-Cameroon symfony Developers

Publié le par Penda Jose


Salut chers dévelopeurs la première séance de #coddingsession de la communauté symfony cameroun aura effectivement lieu le 23 janvier 2016 à la faculté de genie industriel de l'université de Douala cameroun heure 8h00
Details du programme provisoire: http://www.jobstagestudiant.org/sfcamer/Codingsessions.pdf
------------------------------------------------------------------------------------
Greetings developers,symfony cameroon community organises the first edition of #coddingsession,on january 23,2016 at the Faculty of industrial engineering of the university of Douala Cameroon,from 8 AM

Get here the default program: http://www.jobstagestudiant.org/sfcamer/Codingsessions.pdf

Voir les commentaires

Joomla 3.x Installation Bug Solved

Publié le par Penda Jose

PejdLabs
PejdLabs

Download your joomla 3.x package https://www.joomla.org/download.htm.
Then unzip to your joomla folder in your wamp server

Joomla 3.x Installation Bug Solved

Get Acces to your data base sysyem,with wamp ,know as php my admin

Joomla 3.x Installation Bug Solved

Create your database

Joomla 3.x Installation Bug Solved

Go to your root folder and rename the installation directory

Joomla 3.x Installation Bug Solved

Go in the renammed installation directory -> Sql-> Mysql

Joomla 3.x Installation Bug Solved

Open the joomla.sql file and replace #__   by the prefix of your choice exemple: xyz15_

If you are using notepad++ you can get the shortcut by pressing ctrl key + H

Joomla 3.x Installation Bug Solved

For sample data installation,

Open the sample_testing.sql and replace #__   by the prefix of your choice exemple: xyz15_

If you are using notepad++ you can get the shortcut by pressing ctrl key + H

Joomla 3.x Installation Bug Solved

copy the download configuration file and put to the root of your unzip joomla package.

you can download fils here http://www.jobstagesetudiant.org/JoomlaBugg.zip

open the configuration file and modify the data base name and the prefix name

 

NB: the data base name must be the same as the one you created using php my admin,and the prefix you use to replace must be the same

Joomla 3.x Installation Bug Solved

All done copy the sql.sql content and insert to your database.finally login using

username: admin

password:admin

 

hope you enjoyed

contact me with ingpendajose@hotmail.com

Voir les commentaires

Pigo de la bio et l'agro technologie prend ses debuts au cameroun

Publié le par Penda Jose

pigo
pigo

Apprenons a faire de l'elelvage a partir de nos windows phone,profitons d'une assistance technique et beneficions des conseils que offre pigo afin de maximiser la production.version beta encore en cours,mes sinceres remerciements aux Microsoft Student Partners Cameroon et aussi a Club Microsoft - FGI de m'avoir poussé à fond dans mon aventure et aussi merci à tous ceux qui m'encouragent je vous aime

Voir les commentaires

Surf gratuit Nexttel Cameroun Octobre 2014

Publié le par Penda Jose

Surf gratuit Nexttel Cameroun Octobre 2014

~~ L'operateur de téléphone mobile Viettel Cameroon S.A, filiale camerounaise du groupe vietnamien Viettel, s’appellera Nexttel. Le surf gratuit de Nexttel Cameroun est déjà disponible; Pour ce faire, NB: Si vous avez un modem ou une clé internet déja débloquer (capable de prendre à la fois MTN et Orange), introduisez votre carte sim Nexttel Cameroun et creer un profil de connexion comme suit: Nom d'utilisateur: nexttel mot de passe: nexttel Nom du Point d'Accès (APN): nexttelcm • l'application pd-proxy téléchargeable ici www.pdproxy.com/download.htm • Configuration du proxy du navigateur; cochez Pas de proxy I - création du compte PD-PROXY Tout d'abord, tu te rends sur le site de pd-proxy www.pdproxy.com/signup.htm , tu crée ton compte Puis tu te rends dans ta boite email pour l'activé sinon ton compte ne sera pas activé.et par Conséquence la suite du tutoriel voué a l'échec !! II - Installation & config de application PD-PROXY Tu ouvre ton dossier pd-proxy, tu installe le fichier de commande MS DOS nommé : " Install TAP Drivers " et après ceci tu te connecte a ton modem. , tu rentre dans ton dossier pd-proxy, tu lance application "PD-Proxy"; une interface s'affiche Avec les champs suivant dont tu rempli comme suite: 1 - tu sélection soit le serveur demo1 ou 2 uniquement car les autres c'est pour les comptes Premium 2 - tu laisse le protocole à UDP 3-cliquer sur le troisième onglet puis sur 'clic here to show advanced setting' allez dans 'proto proxy' et mettez au niveau du port UDP le numéro 9201 4 - username tu mets celui dont ta inscrit lors de la création de ton compte Pd-poxy 5 - password également celui enregistré lors de la création du compte pd-proxy 6 - tu click sur " connect " et tu patiente,SI ça dit< server is full>insiste PUISQUE LE RESEAU EST UN PEU Saturé maintenant jusqu'a lorsque tu verras ceci : Initializing Engine... PD-Proxy VPN, Inc <sales@pdproxy.¬com> Tunnel Engine v2.1.9 Build Date 04/19/2012 Checking internet connection... Connecting to UDP Server... Scanning open ports... Connecting to defined UDP Port: 9201 Open port 9201 found... Authenticating.¬.. You are using a trial account! You only have limited access! Enabling interface [Connexion au réseau local 10] Setting IP of interface to 172.22.3.7 Waiting for interface to come up... Updating routing table........ Succesfully connected to server! For help & support visit http://forum.pdproxy.com/ III - COMMENT DEPASSER LA LIMITE DE 100 MEGA PAR JOUR AVEC PD-PROXY Lorsque vous êtes déjà à 100 MB de connexion avec pd-proxy, la connexion s’arrête, et le message « bandwith limit exceeded, please buy a premium account to continue browsing » s’affche, même si vous changer votre compte pd-proxy, ça ne va rien changé. Comment Pd-Proxy fait pour détecté notre ordinateur ? - Pd-proxy lit l’adresse du volume ID de notre disque dur ou le système est installé, et c’est grâce à ça qu’il nous empêche de dépassé la limite de 100MB. Voici comment fait. - Avoir JUSTE 2 compte pd-proxy - Dès que tu dépasse la limite de 100 MEGA, fait ce-ci Il se trouve un fichier dans cette publication que vous allez télécharger ici http://download.sysinternals.com/files/VolumeId.zip « VolumeID.exe », - extraire et Copier le dans le dossier qui s'affiche lorsque vous ouvrez votre invite de commande, dans mon cas c'est C:Documents and SettingsAdministrateur - Et ouvre l'invite de commande »demarrer »executer et tape CMD - Puis tappez volumeid.exe c: xxxx-xxxx (où x est un caractere hexadecimal)Exemple : volumeid.exe c: 1a9f-2cd3 et tapper sur entrée et après avoir fait cela, vous aller voir un message qui dit que Volume ID for drive c updated to xxxx-xxxx. - Redémarrer votre ordinateur et utiliser un autre compte pd-proxy et le surf continue. Surf gratuit Nexttel Cameroun Internet gratuit Nexttel Cameroun Surf gratuit Nexttel Cameroun

Voir les commentaires

Sécurité

Publié le par Penda Jose

Sécurité

Sécurité

// Commentaires ↓

Sécurisation de l'application

La sécurité est un processus en deux étapes dont le but est d'empêcher un utilisateur d'accéder à une ressource dont il ne devrait pas avoir accès. Dans la première étape du processus, l'authentification, le système de sécurité identifie l'utilisateur en l'obligeant à soumettre une sorte d'identification. Une fois que le système sait qui vous êtes, la prochaine étape, appelée l'autorisation, est de déterminer si vous devriez avoir accès à une ressource donnée (il vérifie que vous avez les privilèges pour effectuer une certaine action).

Le composant de sécurité peut être configuré via la configuration de votre application à l'aide du fichier security.yml à partir du dossier app/config. Pour sécuriser notre application ajoutez ce qui suit à votre fichier security.yml:

 
  1. # app/config/security.yml
  2. security:
  3. firewalls:
  4. secured_area:
  5. pattern: ^/
  6. anonymous: ~
  7. form_login:
  8. login_path: /login
  9. check_path: /login_check
  10. access_control:
  11. - { path: ^/admin, roles: ROLE_ADMIN }
  12. providers:
  13. in_memory:
  14. users:
  15. admin: { password: adminpass, roles: 'ROLE_ADMIN' }
  16. encoders:
  17. Symfony\Component\Security\Core\User\User: plaintext

Cette configuration protégera la section /admin du site (toutes les URL qui commencent par /admin) et permettra l'accès uniquement aux utilisateurs avec ROLE_ADMIN (voir la section access_control). Dans cet exemple, l'utilisateur admin est défini dans le fichier de configuration (la section providers) et le mot de passe n'est pas chiffré (encoders).

Pour authentifier les utilisateurs, un formulaire de connexion classique sera utilisé, mais nous devons le mettre en œuvre. D'abord, créez deux routes: l'une qui affiche le formulaire de connexion (par exemple /login) et une qui va gérer la soumission du formulaire de connexion (par exemple /login_check):

 
  1. # src/Ens/JobeetBundle/Resources/config/routing.yml
  2. login:
  3. pattern: /login
  4. defaults: { _controller: EnsJobeetBundle:Default:login }
  5. login_check:
  6. pattern: /login_check
  7. # ...

Nous n'aurons pas besoin de mettre en œuvre un contrôleur pour l'URL /login_check puisque le pare-feu va automatiquement intercepter et transformer n'importe quel formulaire soumis à cette URL. Il est facultatif, mais utile, pour créer une route de sorte qu'il peut être utilisé pour générer l'URL de soumission du formulaire dans le template de connexion ci-dessous.

Ensuite, nous allons créer l'action qui permettra d'afficher le formulaire de connexion:

 
  1. // src/Ens/JobeetBundle/Controller/DefaultController.php
  2. namespace Ens\JobeetBundle\Controller;
  3. use Symfony\Bundle\FrameworkBundle\Controller\Controller;
  4. use Symfony\Component\Security\Core\SecurityContext;
  5. class DefaultController extends Controller
  6. {
  7. // ...
  8. public function loginAction()
  9. {
  10. $request = $this->getRequest();
  11. $session = $request->getSession();
  12. // get the login error if there is one
  13. if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
  14. $error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
  15. } else {
  16. $error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
  17. $session->remove(SecurityContext::AUTHENTICATION_ERROR);
  18. }
  19. return $this->render('EnsJobeetBundle:Default:login.html.twig', array(
  20. // last username entered by the user
  21. 'last_username' => $session->get(SecurityContext::LAST_USERNAME),
  22. 'error' => $error,
  23. ));
  24. }
  25. }

Lorsque l'utilisateur soumet le formulaire, le système de sécurité gère automatiquement la soumission du formulaire pour vous. Si l'utilisateur a soumis un nom d'utilisateur ou mot de passe invalide, cette action lit l'erreur de soumission du formulaire à partir du système de sécurité de sorte qu'il peut être affiché à l'utilisateur. Votre seule tâche est d'afficher le formulaire de connexion et les erreurs de connexion qui peuvent se produire, mais le système de sécurité lui-même prend soin de vérifier le nom d'utilisateur et mot de passe soumis et l'authentification de l'utilisateur.

Enfin, nous allons créer le template correspondant:

 
  1. <!-- src/Ens/JobeetBundle/Resources/views/Default/login.html.twig -->
  2. {% if error %}
  3. <div>{{ error.message }}</div>
  4. {% endif %}
  5. <form action="{{ path('login_check') }}" method="post">
  6. <label for="username">Username:</label>
  7. <input type="text" id="username" name="_username" value="{{ last_username }}" />
  8. <label for="password">Password:</label>
  9. <input type="password" id="password" name="_password" />
  10. <button type="submit">login</button>
  11. </form>

Maintenant, si vous essayez d'accéder à l'URL http://jobeet.local/app_dev.php/admin/dashboard, le formulaire de connexion apparaîtra et vous devrez entrer le nom d'utilisateur et mot de passe défini dans security.yml (admin/adminpass) pour aller dans l'administration de Jobeet.

Les fournisseurs d'utilisateurs

Lors de l'authentification, l'utilisateur soumet un ensemble d'informations d'identification (généralement un nom d'utilisateur et mot de passe). La tâche du système d'authentification est de faire correspondre ces informations à certains groupes d'utilisateurs. Alors d'où vient cette liste d'utilisateurs?

Dans Symfony2, les utilisateurs peuvent venir de n'importe où - un fichier de configuration, une table de la BDD, un Web Service, ou toute autre chose dont vous pouvez rêver. Tout ce qui fournit un ou plusieurs utilisateurs dans le système d'authentification est connu comme un "fournisseur d'utilisateur". Symfony2 est livré en standard avec les deux fournisseurs d'utilisateurs les plus courants: celui qui charge les utilisateurs à partir d'un fichier de configuration et celui qui charge les utilisateurs d'une table de la BDD.

Ci-dessus, nous avons utilisé le premier cas: les utilisateurs spécifiés dans un fichier de configuration.

 
  1. providers:
  2. in_memory:
  3. users:
  4. admin: { password: adminpass, roles: 'ROLE_ADMIN' }

Mais vous voudrez généralement stocker les utilisateurs dans une table de la BDD. Pour ce faire, nous allons ajouter une nouvelle table user dans notre BDD.

D'abord nous allons créer l'ORM pour cette nouvelle table:

 
  1. # src/Ens/JobeetBundle/Resources/config/doctrine/User.orm.yml
  2. Ens\JobeetBundle\Entity\User:
  3. type: entity
  4. table: user
  5. id:
  6. id:
  7. type: integer
  8. generator: { strategy: AUTO }
  9. fields:
  10. username:
  11. type: string
  12. length: 255
  13. password:
  14. type: string
  15. length: 255

Maintenant, exécutez la commande doctrine:generate:entities pour créer la nouvelle entité User:

php app/console doctrine:generate:entities EnsJobeetBundle

Et mettez à jour la BDD:

php app/console doctrine:schema:update --force

La seule exigence pour votre nouvelle classe User est qu'elle implémente l'interface UserInterface. Cela signifie que votre concept d'un "utilisateur" peut être n'importe quoi, tant qu'il implémente cette interface. Ouvrez le fichier User.php et modifiez-le comme suit:

 
  1. // src Ens/JobeetBundle/Entity/User.php
  2. namespace Ens\JobeetBundle\Entity;
  3. use Symfony\Component\Security\Core\User\UserInterface;
  4. use Doctrine\ORM\Mapping as ORM;
  5. class User implements UserInterface
  6. {
  7. private $id;
  8. private $username;
  9. private $password;
  10. public function getId()
  11. {
  12. return $this->id;
  13. }
  14. public function setUsername($username)
  15. {
  16. $this->username = $username;
  17. }
  18. public function getUsername()
  19. {
  20. return $this->username;
  21. }
  22. public function setPassword($password)
  23. {
  24. $this->password = $password;
  25. }
  26. public function getPassword()
  27. {
  28. return $this->password;
  29. }
  30. public function getRoles()
  31. {
  32. return array('ROLE_ADMIN');
  33. }
  34. public function getSalt()
  35. {
  36. return null;
  37. }
  38. public function eraseCredentials()
  39. {
  40. }
  41. public function equals(UserInterface $user)
  42. {
  43. return $user->getUsername() == $this->getUsername();
  44. }
  45. }

Pour l'entité générée, nous avons ajouté les méthodes requises par la classe UserInterface: GetRoles, getSalt, eraseCredentials et equals.

Ensuite, configurez un fournisseur d'entité utilisateur, et faites-le pointer vers votre classe User:

 
  1. # app/config/security.yml
  2. # ...
  3. providers:
  4. main:
  5. entity: { class: Ens\JobeetBundle\Entity\User, property: username }
  6. encoders:
  7. Ens\JobeetBundle\Entity\User: sha512

Nous avons également changé le chiffrage pour notre nouvelle classe User afin d'utiliser l'algorithme sha512 pour hasher les mots de passe.

Maintenant, tout est mis en place, mais nous avons besoin de créer notre premier utilisateur. Pour ce faire, nous allons créer une nouvelle commande de Symfony:

 
  1. // src/Ens/JobeetBundle/Command/JobeetUsersCommand.php
  2. namespace Ens\JobeetBundle\Command;
  3. use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
  4. use Symfony\Component\Console\Input\InputArgument;
  5. use Symfony\Component\Console\Input\InputInterface;
  6. use Symfony\Component\Console\Input\InputOption;
  7. use Symfony\Component\Console\Output\OutputInterface;
  8. use Ens\JobeetBundle\Entity\User;
  9. class JobeetUsersCommand extends ContainerAwareCommand
  10. {
  11. protected function configure()
  12. {
  13. $this
  14. ->setName('ens:jobeet:users')
  15. ->setDescription('Add Jobeet users')
  16. ->addArgument('username', InputArgument::REQUIRED, 'The username')
  17. ->addArgument('password', InputArgument::REQUIRED, 'The password')
  18. ;
  19. }
  20. protected function execute(InputInterface $input, OutputInterface $output)
  21. {
  22. $username = $input->getArgument('username');
  23. $password = $input->getArgument('password');
  24. $em = $this->getContainer()->get('doctrine')->getEntityManager();
  25. $user = new User();
  26. $user->setUsername($username);
  27. // encode the password
  28. $factory = $this->getContainer()->get('security.encoder_factory');
  29. $encoder = $factory->getEncoder($user);
  30. $encodedPassword = $encoder->encodePassword($password, $user->getSalt());
  31. $user->setPassword($encodedPassword);
  32. $em->persist($user);
  33. $em->flush();
  34. $output->writeln(sprintf('Added %s user with password %s', $username, $password));
  35. }
  36. }

Pour ajouter votre premier utilisateur exécutez:

php app/console ens:jobeet:users admin admin

Cela va créer l'utilisateur admin avec le mot de passe admin. Vous pouvez l'utiliser pour vous connecter à l'admininistration.

Déconnexion

La déconnexion est gérée automatiquement par le pare-feu. Tout ce que vous avez à faire est d'activer le paramètre logout:

 
  1. # app/config/security.yml
  2. security:
  3. firewalls:
  4. secured_area:
  5. # ...
  6. logout:
  7. path: /logout
  8. target: /
  9. # ...

Une fois que cela est configuré dans votre pare-feu, l'envoi d'un utilisateur vers /logout (ou le paramètre path que vous avez configuré), déconnectera l'utilisateur en cours. L'utilisateur sera alors envoyé à la page d'accueil (la valeur définie par le paramètre target).

Vous n'aurez pas besoin de mettre en œuvre un contrôleur pour l'URL /logout puisque le pare-feu s'occupe de tout. Vous pouvez, cependant, vouloir créer une route de sorte que vous pouvez l'utiliser pour générer l'URL:

 
  1. # src/Ens/JobeetBundle/Resources/config/routing.yml
  2. # ...
  3. logout:
  4. pattern: /logout
  5. # ...

Tout ce qui reste à faire est d'ajouter le lien de déconnexion à notre administration. Pour ce faire, nous allons remplacer le user_block.html.twig de SonataAdminBundle. Créez le fichier user_block.html.twig dans le répertoire app/Resources/SonataAdminBundle/views/Core/:

 
  1. <!-- app/Resources/SonataAdminBundle/views/Core/user_block.html.twig -->
  2. {% block user_block %}<a href="{{ path('logout') }}">Logout</a>{% endblock %}

Maintenant, si vous essayez d'entrer dans l'administration, il vous sera demandé un nom d'utilisateur et un mot de passe, puis, le lien de déconnexion sera affiché dans le coin supérieur droit.

La session utilisateur

Symfony2 fournit un objet de session que vous pouvez utiliser pour stocker des informations sur l'utilisateur entre les requêtes. Par défaut, Symfony2 stocke les attributs dans un cookie par l'intermédiaire des sessions PHP natives.

Vous pouvez stocker et récupérer des informations à partir de la session facilement depuis le contrôleur:

 
  1. $session = $this->getRequest()->getSession();
  2. // store an attribute for reuse during a later user request
  3. $session->set('foo', 'bar');
  4. // in another controller for another request
  5. $foo = $session->get('foo');

Malheureusement, les scénarios de Jobeet n'ont aucune exigence qui comprend le stockage d'informations dans la session utilisateur. Nous allons donc ajouter une nouvelle exigence: pour faciliter la navigation dans l'offre, les trois dernières offres vues par l'utilisateur doivent être affichées dans le menu avec des liens pour revenir à la page des offres par la suite.

Lorsqu'un utilisateur accède à la page d'une offre, l'objet de l'offre affichée doit être ajouté dans l'historique de l'utilisateur et stocké dans la session:

 
  1. // src/Ens/JobeetBundle/Controller/JobController.php
  2. // ...
  3. public function showAction($id)
  4. {
  5. $em = $this->getDoctrine()->getEntityManager();
  6. $entity = $em->getRepository('EnsJobeetBundle:Job')->getActiveJob($id);
  7. if (!$entity) {
  8. throw $this->createNotFoundException('Unable to find Job entity.');
  9. }
  10. $session = $this->getRequest()->getSession();
  11. // fetch jobs already stored in the job history
  12. $jobs = $session->get('job_history', array());
  13. // store the job as an array so we can put it in the session and avoid entity serialize errors
  14. $job = array('id' => $entity->getId(), 'position' =>$entity->getPosition(), 'company' => $entity->getCompany(), 'companyslug' => $entity->getCompanySlug(), 'locationslug' => $entity->getLocationSlug(), 'positionslug' => $entity->getPositionSlug());
  15. if (!in_array($job, $jobs)) {
  16. // add the current job at the beginning of the array
  17. array_unshift($jobs, $job);
  18. // store the new job history back into the session
  19. $session->set('job_history', array_slice($jobs, 0, 3));
  20. }
  21. $deleteForm = $this->createDeleteForm($id);
  22. return $this->render('EnsJobeetBundle:Job:show.html.twig', array(
  23. 'entity' => $entity,
  24. 'delete_form' => $deleteForm->createView(),
  25. ));
  26. }

Dans le layout, ajoutez le code suivant avant la div #content:

 
  1. <!-- src/End/JobeetBundle/Resources/views/layout.html.twig -->
  2. <!-- ... -->
  3. <div id="job_history">
  4. Recent viewed jobs:
  5. <ul>
  6. {% for job in app.session.get('job_history') %}
  7. <li>
  8. <a href="{{ path('ens_job_show', { 'id': job.id, 'company': job.companyslug, 'location': job.locationslug, 'position': job.positionslug }) }}">{{ job.position }} - {{ job.company }}</a>
  9. </li>
  10. {% endfor %}
  11. </ul>
  12. </div>
  13. <div id="content">
  14. <!-- ... -->

Les messages flash

Les messages flash sont des petits messages que vous pouvez stocker sur la session de l'utilisateur pendant exactement une requête supplémentaire. Cette fonction est utile lors du traitement d'un formulaire: vous souhaitez rediriger et afficher un message spécial sur la requête suivante. Nous avons déjà utilisé des messages flash dans notre projet lorsque nous publions une offre:

 
  1. // src/Ens/JobeetBundle/Controller/JobController.php
  2. // ...
  3. public function publishAction($token)
  4. {
  5. // ...
  6. $this->get('session')->setFlash('notice', 'Your job is now online for 30 days.');
  7. // ...
  8. }

Le premier argument de la fonction setFlash est l'identifiant du flash et le second est le message à afficher. Vous pouvez définir les flashes que vous voulez, mais notice et error sont les deux plus communs.

Pour afficher les messages flash à l'utilisateur, vous devez les inclure dans le template. Nous l'avons fait dans le template layout.html.twig:

 
  1. <!-- src/Ens/JobeetBundle/Resources/views/layout.html.twig -->
  2. <!-- ... -->
  3. {% if app.session.hasFlash('notice') %}
  4. <div>
  5. {{ app.session.flash('notice') }}
  6. </div>
  7. {% endif %}
  8. <!-- ... -->

Voir les commentaires

Flux de données

Publié le par Penda Jose

Flux de données

Flux de données

// Commentaires ↓

Si vous êtes à la recherche d'un emploi, vous aurez probablement besoin d'être informé dès qu'un nouveau poste est disponible. Parce qu'il n'est pas très pratique de consulter le site web toutes les deux heures, nous allons ajouter plusieurs flux d'offres pour que nos utilisateurs Jobeet se tiennent à jour.

Les formats de template

Les templates sont un moyen générique pour rendre le contenu dans n'importe quel format. Bien que dans la plupart des cas vous utilisez des templates pour afficher le contenu HTML, un template peut tout aussi bien générer JavaScript, CSS, XML ou tout autre format.

Par exemple, la même "ressource" est souvent affichée dans plusieurs formats. Pour afficher une page au format XML, il suffit d'inclure le format dans le nom du template:

  • - Nom du template XML: AcmeArticleBundle:Article:index.xml.twig
  • - Nom de fichier du template XML: index.xml.twig

En réalité, ce n'est rien de plus qu'une convention de nommage et le template n'est actuellement pas affiché différemment en fonction de son format.

Dans de nombreux cas, vous voudrez peut-être permettre à un seul contrôleur de rendre plusieurs formats différents en fonction du "format demandé". Pour cette raison, un motif commun est de faire ce qui suit:

 
  1. public function indexAction()
  2. {
  3. $format = $this->getRequest()->getRequestFormat();
  4. return $this->render('AcmeBlogBundle:Blog:index.'.$format.'.twig');
  5. }

La méthode getRequestFormat de l'objet Request est par défaut au format html, mais peut renvoyer n'importe quel autre format basé sur le format demandé par l'utilisateur. Le format de la demande est le plus souvent géré par le routage, où une route peut être configurée de sorte que /contact définit le format html alors que /contact.xml définit le format xml.

Pour créer des liens qui incluent le paramètre de format, incluez une clé _format dans le paramètre:

 
  1. <a href="{{ path('article_show', {'id': 123, '_format': 'pdf'}) }}">
  2. PDF Version
  3. </a>

RSS

Supporter différents formats est aussi facile que créer différents templates. Pour créer un flux Atom des dernières offres, créez un template index.atom.twig:

 
  1. <!-- src/Ens/JobeetBundle/Resources/views/Job/index.atom.twig -->
  2. <?xml version="1.0" encoding="utf-8"?>
  3. <feed xmlns="http://www.w3.org/2005/Atom">
  4. <title>Jobeet</title>
  5. <subtitle>Latest Jobs</subtitle>
  6. <link href="" rel="self"/>
  7. <link href=""/>
  8. <updated></updated>
  9. <author><name>Jobeet</name></author>
  10. <id>Unique Id</id>
  11. <entry>
  12. <title>Job title</title>
  13. <link href="" />
  14. <id>Unique id</id>
  15. <updated></updated>
  16. <summary>Job description</summary>
  17. <author><name>Company</name></author>
  18. </entry>
  19. </feed>

Dans le pied de page de Jobeet, mettez à jour le lien vers le flux:

 
  1. <!-- src/Ens/JobeetBundle/Resources/views/layout.html.twig -->
  2. <!-- ... -->
  3. <li><a href="{{ path('ens_job', {'_format': 'atom'}) }}">Full feed</a></li>
  4. <!-- ... -->

Ajoutez une balise <link> dans la section <head> du layout pour permettre la découverte automatique par le navigateur de notre flux:

 
  1. <!-- src/Ens/JobeetBundle/Resources/views/layout.html.twig -->
  2. <!-- ... -->
  3. <link rel="alternate" type="application/atom+xml" title="Latest Jobs" href="{{ url('ens_job', {'_format': 'atom'}) }}" />
  4. <!-- ... -->

Dans JobController, modifiez indexAction pour afficher le modèle en fonction de _format:

 
  1. // src/Ens/JobeetBundle/Controller/JobController.php
  2. // ...
  3. $format = $this->getRequest()->getRequestFormat();
  4. return $this->render('EnsJobeetBundle:Job:index.'.$format.'.twig', array(
  5. 'categories' => $categories
  6. ));
  7. // ...

Remplacez l'en-tête du template Atom avec le code suivant:

 
  1. <!-- src/Ens/JobeetBundle/Resources/views/Job/index.atom.twig -->
  2. <!-- ... -->
  3. <title>Jobeet</title>
  4. <subtitle>Latest Jobs</subtitle>
  5. <link href="{{ url('ens_job', {'_format': 'atom'}) }}" rel="self"/>
  6. <link href="{{ url('EnsJobeetBundle_homepage') }}"/>
  7. <updated>{{ lastUpdated }}</updated>
  8. <author><name>Jobeet</name></author>
  9. <id>{{ feedId }}</id>
  10. <!-- ... -->

Dans JobController, nous devons envoyer les paramètres LastUpdated et feedId au template:

 
  1. // src/Ens/JobeetBundle/Controller/JobController.php
  2. // ...
  3. return $this->render('EnsJobeetBundle:Job:index.'.$format.'.twig', array(
  4. 'categories' => $categories,
  5. 'lastUpdated' => $em->getRepository('EnsJobeetBundle:Job')->getLatestPost()->getCreatedAt()->format(DATE_ATOM),
  6. 'feedId' => sha1($this->get('router')->generate('ens_job', array('_format'=> 'atom'), true)),
  7. ));
  8. // ...

Pour obtenir la date du dernier article, nous devons créer la méthode getLatestPost() dans JobRepository:

 
  1. // src/Ens/JobeetBundle/Repository/JobRepository.php
  2. // ...
  3. public function getLatestPost()
  4. {
  5. $query = $this->createQueryBuilder('j')
  6. ->where('j.expires_at > :date')
  7. ->setParameter('date', date('Y-m-d H:i:s', time()))
  8. ->andWhere('j.is_activated = :activated')
  9. ->setParameter('activated', 1)
  10. ->orderBy('j.expires_at', 'DESC')
  11. ->setMaxResults(1)
  12. ->getQuery();
  13. try {
  14. $job = $query->getSingleResult();
  15. } catch (\Doctrine\Orm\NoResultException $e) {
  16. $job = null;
  17. }
  18. return $job;
  19. }
  20. // ...

Les entrées du flux peuvent être générées avec le code suivant:

 
  1. <!-- src/Ens/JobeetBundle/Resources/views/Job/index.atom.twig -->
  2. <!-- ... -->
  3. {% for category in categories %}
  4. {% for entity in category.activejobs %}
  5. <entry>
  6. <title>{{ entity.position }} ({{ entity.location }})</title>
  7. <link href="{{ url('ens_job_show', { 'id': entity.id, 'company': entity.companyslug, 'location': entity.locationslug, 'position': entity.positionslug }) }}" />
  8. <id>{{ entity.id }}</id>
  9. <updated>{{ entity.createdAt.format(constant('DATE_ATOM')) }}</updated>
  10. <summary type="xhtml">
  11. <div xmlns="http://www.w3.org/1999/xhtml">
  12. {% if entity.logo %}
  13. <div>
  14. <a href="{{ entity.url }}">
  15. <img src="http://{{ app.request.host }}/uploads/jobs/{{ entity.logo }}" alt="{{ entity.company }} logo" />
  16. </a>
  17. </div>
  18. {% endif %}
  19. <div>
  20. {{ entity.description|nl2br }}
  21. </div>
  22. <h4>How to apply?</h4>
  23. <p>{{ entity.howtoapply }}</p>
  24. </div>
  25. </summary>
  26. <author><name>{{ entity.company }}</name></author>
  27. </entry>
  28. {% endfor %}
  29. {% endfor %}
  30. <!-- ... -->

L'un des objectifs de Jobeet est d'aider les gens à trouver des offres plus ciblées. Nous devons donc fournir un flux pour chaque catégorie.

Tout d'abord, nous allons mettre à jour les liens vers les flux de la catégorie dans les templates:

 
  1. <!-- src/Ens/JobeetBundle/Resources/views/Job/index.html.twig -->
  2. <div class="feed">
  3. <a href="{{ path('EnsJobeetBundle_category', { 'slug': category.slug, '_format': 'atom' }) }}">Feed</a>
  4. </div>
  5. <!-- src/Ens/JobeetBundle/Resources/views/Category/show.html.twig -->
  6. <div class="feed">
  7. <a href="{{ path('EnsJobeetBundle_category', { 'slug': category.slug, '_format': 'atom' }) }}">Feed</a>
  8. </div>

Mettez à jour showAction de CategoryController pour afficher le template correspondant:

 
  1. // src/Ens/JobeetBundle/Controller/CategoryController.php
  2. // ...
  3. $format = $this->getRequest()->getRequestFormat();
  4. return $this->render('EnsJobeetBundle:Category:show.'.$format.'.twig', array(
  5. 'category' => $category,
  6. 'last_page' => $last_page,
  7. 'previous_page' => $previous_page,
  8. 'current_page' => $page,
  9. 'next_page' => $next_page,
  10. 'total_jobs' => $total_jobs,
  11. 'feedId' => sha1($this->get('router')->generate('EnsJobeetBundle_category', array('slug' => $category->getSlug(), '_format' => 'atom'), true)),
  12. ));
  13. // ...

Finalement, créez le template show.atom.twig:

 
  1. <!-- src/Ens/JobeetBundle/Resources/views/Category/show.atom.twig -->
  2. <?xml version="1.0" encoding="utf-8"?>
  3. <feed xmlns="http://www.w3.org/2005/Atom">
  4. <title>Jobeet ({{ category.name }})</title>
  5. <subtitle>Latest Jobs</subtitle>
  6. <link href="{{ url('EnsJobeetBundle_category', { 'slug': category.slug, '_format': 'atom' }) }}" rel="self" />
  7. <link href="{{ url('EnsJobeetBundle_category', { 'slug': category.slug }) }}" />
  8. <updated>{{ category.activejobs[0].createdAt.format(constant('DATE_ATOM')) }}</updated>
  9. <author><name>Jobeet</name></author>
  10. <id>{{ feedId }}</id>
  11. {% for entity in category.activejobs %}
  12. <entry>
  13. <title>{{ entity.position }} ({{ entity.location }})</title>
  14. <link href="{{ url('ens_job_show', { 'id': entity.id, 'company': entity.companyslug, 'location': entity.locationslug, 'position': entity.positionslug }) }}" />
  15. <id>{{ entity.id }}</id>
  16. <updated>{{ entity.createdAt.format(constant('DATE_ATOM')) }}</updated>
  17. <summary type="xhtml">
  18. <div xmlns="http://www.w3.org/1999/xhtml">
  19. {% if entity.logo %}
  20. <div>
  21. <a href="{{ entity.url }}">
  22. <img src="http://{{ app.request.host }}/uploads/jobs/{{ entity.logo }}" alt="{{ entity.company }} logo" />
  23. </a>
  24. </div>
  25. {% endif %}
  26. <div>
  27. {{ entity.description|nl2br }}
  28. </div>
  29. <h4>How to apply?</h4>
  30. <p>{{ entity.howtoapply }}</p>
  31. </div>
  32. </summary>
  33. <author><name>{{ entity.company }}</name></author>
  34. </entry>
  35. {% endfor %}
  36. </feed>

Voir les commentaires

Testez vos formulaires

Publié le par Penda Jose

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:

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

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:

 
  1. $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:

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

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:

 
  1. // src/Ens/JobeetBundle/Form/JobType.php
  2. // ...
  3. public function getName()
  4. {
  5. return 'job';
  6. }

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:

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

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:

 
  1. $client->followRedirect();
  2. $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é.

 
  1. $kernel = static::createKernel();
  2. $kernel->boot();
  3. $em = $kernel->getContainer()->get('doctrine.orm.entity_manager');
  4. $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');
  5. $query->setParameter('location', 'Atlanta, USA');
  6. $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:

 
  1. $crawler = $client->request('GET', '/job/new');
  2. $form = $crawler->selectButton('Preview your job')->form(array(
  3. 'job[company]' => 'Sensio Labs',
  4. 'job[position]' => 'Developer',
  5. 'job[location]' => 'Atlanta, USA',
  6. 'job[email]' => 'not.an.email',
  7. ));
  8. $crawler = $client->submit($form);
  9. // check if we have 3 errors
  10. $this->assertTrue($crawler->filter('.error_list')->count() == 3);
  11. // check if we have error on job_description field
  12. $this->assertTrue($crawler->filter('#job_description')->siblings()->first()->filter('.error_list')->count() == 1);
  13. // check if we have error on job_how_to_apply field
  14. $this->assertTrue($crawler->filter('#job_how_to_apply')->siblings()->first()->filter('.error_list')->count() == 1);
  15. // check if we have error on job_email field
  16. $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:

 
  1. // src/Ens/JobeetBundle/Tests/Controller/JobControllerTest.php
  2. // ...
  3. public function createJob($values = array())
  4. {
  5. $client = static::createClient();
  6. $crawler = $client->request('GET', '/job/new');
  7. $form = $crawler->selectButton('Preview your job')->form(array_merge(array(
  8. 'job[company]' => 'Sensio Labs',
  9. 'job[url]' => 'http://www.sensio.com/',
  10. 'job[position]' => 'Developer',
  11. 'job[location]' => 'Atlanta, USA',
  12. 'job[description]' => 'You will work with symfony to develop websites for our customers.',
  13. 'job[how_to_apply]' => 'Send me an email',
  14. 'job[email]' => 'for.a.job@example.com',
  15. 'job[is_public]' => false,
  16. ), $values));
  17. $client->submit($form);
  18. $client->followRedirect();
  19. return $client;
  20. }

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:

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

Tester l'action "Supprimer" est assez similaire:

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

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é:

 
  1. // src/Ens/JobeetBundle/Tests/Controller/JobControllerTest.php
  2. // ...
  3. public function createJob($values = array(), $publish = false)
  4. {
  5. $client = static::createClient();
  6. $crawler = $client->request('GET', '/job/new');
  7. $form = $crawler->selectButton('Preview your job')->form(array_merge(array(
  8. 'job[company]' => 'Sensio Labs',
  9. 'job[url]' => 'http://www.sensio.com/',
  10. 'job[position]' => 'Developer',
  11. 'job[location]' => 'Atlanta, USA',
  12. 'job[description]' => 'You will work with symfony to develop websites for our customers.',
  13. 'job[how_to_apply]' => 'Send me an email',
  14. 'job[email]' => 'for.a.job@example.com',
  15. 'job[is_public]' => false,
  16. ), $values));
  17. $client->submit($form);
  18. $client->followRedirect();
  19. if($publish) {
  20. $crawler = $client->getCrawler();
  21. $form = $crawler->selectButton('Publish')->form();
  22. $client->submit($form);
  23. $client->followRedirect();
  24. }
  25. return $client;
  26. }
  27. public function getJobByPosition($position)
  28. {
  29. $kernel = static::createKernel();
  30. $kernel->boot();
  31. $em = $kernel->getContainer()->get('doctrine.orm.entity_manager');
  32. $query = $em->createQuery('SELECT j from EnsJobeetBundle:Job j WHERE j.position = :position');
  33. $query->setParameter('position', $position);
  34. $query->setMaxResults(1);
  35. return $query->getSingleResult();
  36. }

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

 
  1. // src/Ens/JobeetBundle/Controller/JobController.php
  2. // ...
  3. public function editAction($token)
  4. {
  5. $em = $this->getDoctrine()->getEntityManager();
  6. $entity = $em->getRepository('EnsJobeetBundle:Job')->findOneByToken($token);
  7. if (!$entity) {
  8. throw $this->createNotFoundException('Unable to find Job entity.');
  9. }
  10. if ($entity->getIsActivated()) {
  11. throw $this->createNotFoundException('Job is activated and cannot be edited.');
  12. }
  13. // ...
  14. }

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:

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

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

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

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

 
  1. // src/Ens/JobeetBundle/Controller/JobController.php
  2. // ...
  3. public function extendAction($token)
  4. {
  5. $form = $this->createExtendForm($token);
  6. $request = $this->getRequest();
  7. $form->bindRequest($request);
  8. if ($form->isValid()) {
  9. $em = $this->getDoctrine()->getEntityManager();
  10. $entity = $em->getRepository('EnsJobeetBundle:Job')->findOneByToken($token);
  11. if (!$entity) {
  12. throw $this->createNotFoundException('Unable to find Job entity.');
  13. }
  14. if (!$entity->extend()) {
  15. throw $this->createNotFoundException('Unable to find extend the Job.');
  16. }
  17. $em->persist($entity);
  18. $em->flush();
  19. $this->get('session')->setFlash('notice', sprintf('Your job validity has been extended until %s.', $entity->getExpiresAt()->format('m/d/Y')));
  20. }
  21. return $this->redirect($this->generateUrl('ens_job_preview', array(
  22. 'company' => $entity->getCompanySlug(),
  23. 'location' => $entity->getLocationSlug(),
  24. 'token' => $entity->getToken(),
  25. 'position' => $entity->getPositionSlug()
  26. )));
  27. }
  28. private function createExtendForm($token)
  29. {
  30. return $this->createFormBuilder(array('token' => $token))
  31. ->add('token', 'hidden')
  32. ->getForm()
  33. ;
  34. }

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

 
  1. // src/Ens/JobeetBundle/Controller/JobController.php
  2. // ...
  3. public function previewAction($token)
  4. {
  5. $em = $this->getDoctrine()->getEntityManager();
  6. $entity = $em->getRepository('EnsJobeetBundle:Job')->findOneByToken($token);
  7. if (!$entity) {
  8. throw $this->createNotFoundException('Unable to find Job entity.');
  9. }
  10. $deleteForm = $this->createDeleteForm($entity->getId());
  11. $publishForm = $this->createPublishForm($entity->getToken());
  12. $extendForm = $this->createExtendForm($entity->getToken());
  13. return $this->render('EnsJobeetBundle:Job:show.html.twig', array(
  14. 'entity' => $entity,
  15. 'delete_form' => $deleteForm->createView(),
  16. 'publish_form' => $publishForm->createView(),
  17. 'extend_form' => $extendForm->createView(),
  18. ));
  19. }

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

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

Enfin, ajoutez un scénario de test:

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

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.

 
  1. // src/Ens/JobeetBundle/Command/JobeetCleanupCommand.php
  2. namespace Ens\JobeetBundle\Command;
  3. use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
  4. use Symfony\Component\Console\Input\InputArgument;
  5. use Symfony\Component\Console\Input\InputInterface;
  6. use Symfony\Component\Console\Input\InputOption;
  7. use Symfony\Component\Console\Output\OutputInterface;
  8. use Ens\JobeetBundle\Entity\Job;
  9. class JobeetCleanupCommand extends ContainerAwareCommand {
  10. protected function configure()
  11. {
  12. $this
  13. ->setName('ens:jobeet:cleanup')
  14. ->setDescription('Cleanup Jobeet database')
  15. ->addArgument('days', InputArgument::OPTIONAL, 'The email', 90)
  16. ;
  17. }
  18. protected function execute(InputInterface $input, OutputInterface $output)
  19. {
  20. $days = $input->getArgument('days');
  21. $em = $this->getContainer()->get('doctrine')->getEntityManager();
  22. $nb = $em->getRepository('EnsJobeetBundle:Job')->cleanup($days);
  23. $output->writeln(sprintf('Removed %d stale jobs', $nb));
  24. }
  25. }

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

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

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.

Voir les commentaires

Les formulaires

Publié le par Penda Jose

Les formulaires

Tout site web a des formulaires, du simple formulaire de contact à celui avec beaucoup de champs. L'écriture de formulaire est également l'une des tâches les plus complexes et fastidieuses pour un développeur web: vous devez écrire le formulaire HTML, les règles de validation pour chaque champ, traiter les valeurs pour les stocker dans une BDD, afficher des messages d'erreur, repeupler les champs en cas d'erreurs, et bien plus encore...

Dans le troisième chapitre de ce tutoriel, nous avons utilisé la commande doctrine:generate:crud pour générer un simple contrôleur CRUD pour l'entité Job. Cela a également engendré un formulaire d'offre que vous pouvez trouver dans le fichier /src/Ens/JobeetBundle/Form/JobType.php.

Personnaliser le formulaire d'offre

Le formulaire d'offre est un parfait exemple pour apprendre la personnalisation des formulaires. Voyons comment le personnaliser, étape par étape.

Tout d'abord, changer le "Post a Job" dans la mise en page pour être en mesure de vérifier les modifications directement dans votre navigateur:

 
  1. <!-- src/Ens/JobeetBundle/Resources/views/layout.html.twig -->
  2. <a href="{{ path('ens_job_new') }}">Post a Job</a>

Puis, modifiez les paramètres de la route ens_job_show dans CreateAction de JobController pour correspondre à la nouvelle route que nous avons créé dans le cinquième chapitre de ce tutoriel:

 
  1. // src/Ens/JobeetBundle/Controller/JobController.php
  2. // ...
  3. public function createAction()
  4. {
  5. $entity = new Job();
  6. $request = $this->getRequest();
  7. $form = $this->createForm(new JobType(), $entity);
  8. $form->bindRequest($request);
  9. if ($form->isValid()) {
  10. $em = $this->getDoctrine()->getEntityManager();
  11. $em->persist($entity);
  12. $em->flush();
  13. return $this->redirect($this->generateUrl('ens_job_show', array(
  14. 'company' => $entity->getCompanySlug(),
  15. 'location' => $entity->getLocationSlug(),
  16. 'id' => $entity->getId(),
  17. 'position' => $entity->getPositionSlug()
  18. )));
  19. }
  20. return $this->render('EnsJobeetBundle:Job:new.html.twig', array(
  21. 'entity' => $entity,
  22. 'form' => $form->createView()
  23. ));
  24. }

Par défaut, le formulaire généré par Doctrine affiche des champs pour toutes les colonnes de la table. Mais pour le formulaire d'offre, certains d'entre eux ne doivent pas être modifiables par l'utilisateur final. Modifiez le formulaire Job comme ci-dessous:

 
  1. // src/Ens/JobeetBundle/Form/JobType.php
  2. namespace Ens\JobeetBundle\Form;
  3. use Symfony\Component\Form\AbstractType;
  4. use Symfony\Component\Form\FormBuilder;
  5. class JobType extends AbstractType
  6. {
  7. public function buildForm(FormBuilder $builder, array $options)
  8. {
  9. $builder->add('category');
  10. $builder->add('type');
  11. $builder->add('company');
  12. $builder->add('logo');
  13. $builder->add('url');
  14. $builder->add('position');
  15. $builder->add('location');
  16. $builder->add('description');
  17. $builder->add('how_to_apply');
  18. $builder->add('token');
  19. $builder->add('is_public');
  20. $builder->add('email');
  21. }
  22. public function getName()
  23. {
  24. return 'ens_jobeetbundle_jobtype';
  25. }
  26. }

La configuration du formulaire doit parfois être plus précise que l'introspection à partir du schéma de la BDD. Par exemple, la colonne email est un varchar dans le schéma, mais nous avons besoin de cette colonne pour être validé comme un email. Dans Symfony2, la validation est appliquée à l'objet sous-jacent (par exemple Job). En d'autres termes, la question n'est pas de savoir si le "formulaire" est valide, mais si oui ou non l'objet Job est valide après que le formulaire ait appliqué les données qui lui sont soumises. Pour ce faire, créez un nouveau fichier validation.yml dans le répertoire Resources/config de notre paquet:

 
  1. # src/Ens/JobeetBundle/Resources/config/validation.yml
  2. Ens\JobeetBundle\Entity\Job:
  3. properties:
  4. email:
  5. - NotBlank: ~
  6. - Email: ~

Même si la colonne type est également un varchar dans le schéma, nous voulons que sa valeur soit limitée à une liste de choix: temps plein, temps partiel, ou freelance.

 
  1. // src/Ens/JobeetBundle/Form/JobType.php
  2. // ...
  3. use Ens\JobeetBundle\Entity\Job;
  4. class JobType extends AbstractType
  5. {
  6. public function buildForm(FormBuilder $builder, array $options)
  7. {
  8. // ...
  9. $builder->add('type', 'choice', array('choices' => Job::getTypes(), 'expanded' => true));
  10. // ...
  11. }
  12. // ...
  13. }

Pour que cela fonctionne, ajoutez les méthodes suivantes en respectant l'entité Job:

 
  1. // src/Ens/JobeetBundle/Entity/Job.php
  2. // ...
  3. public static function getTypes()
  4. {
  5. return array('full-time' => 'Full time', 'part-time' => 'Part time', 'freelance' => 'Freelance');
  6. }
  7. public static function getTypeValues()
  8. {
  9. return array_keys(self::getTypes());
  10. }
  11. // ...

La méthode GetTypes est utilisée dans le formulaire pour obtenir les types possibles pour une offre d'emploi et getTypeValues sera utilisée dans la validation pour obtenir les valeurs valides pour le champ Type.

 
  1. # src/Ens/JobeetBundle/Resources/config/validation.yml
  2. Ens\JobeetBundle\Entity\Job:
  3. properties:
  4. type:
  5. - NotBlank: ~
  6. - Choice: { callback: getTypeValues }
  7. email:
  8. - NotBlank: ~
  9. - Email: ~

Pour chaque champ, Symfony génère automatiquement un label (qui sera utilisé dans la balise <label> rendue). Ceci peut être changé avec l'option label:

 
  1. $builder->add('logo', null, array('label' => 'Company logo'));
  2. $builder->add('how_to_apply', null, array('label' => 'How to apply?'));
  3. $builder->add('is_public', null, array('label' => 'Public?'));

Vous devez également ajouter des contraintes de validation pour le reste des champs:

 
  1. # src/Ens/JobeetBundle/Resources/config/validation.yml
  2. Ens\JobeetBundle\Entity\Job:
  3. properties:
  4. category:
  5. - NotBlank: ~
  6. type:
  7. - NotBlank: ~
  8. - Choice: {callback: getTypeValues}
  9. company:
  10. - NotBlank: ~
  11. position:
  12. - NotBlank: ~
  13. location:
  14. - NotBlank: ~
  15. description:
  16. - NotBlank: ~
  17. how_to_apply:
  18. - NotBlank: ~
  19. token:
  20. - NotBlank: ~
  21. email:
  22. - NotBlank: ~
  23. - Email: ~

Gestion des uploads de fichiers dans Symfony2

Pour gérer le fichier uploadé dans le formulaire, nous allons utiliser un champ file "virtuel". Pour cela, nous allons ajouter une nouvelle propriété file à l'entité Job:

 
  1. // src/Ens/JobeetBundle/Entity/Job.php
  2. // ...
  3. public $file;

Maintenant nous avons besoin de remplacer le logo avec le widget file et le modifier en un input de type file:

 
  1. // src/Ens/JobeetBundle/Form/JobType.php
  2. // ...
  3. $builder->add('file', 'file', array('label' => 'Company logo', 'required' => false));
  4. // ...

Pour vous assurer que le fichier uploadé est une image valide, nous allons utiliser la contrainte de validation Image:

 
  1. # src/Ens/JobeetBundle/Resources/config/validation.yml
  2. Ens\JobeetBundle\Entity\Job:
  3. properties:
  4. # ...
  5. file:
  6. - Image: ~

Lorsque le formulaire est soumis, le champ file sera une instance de UploadedFile. Il peut être utilisé pour déplacer le fichier vers un emplacement permanent. Après cela, nous allons définir la propriété logo au nom du fichier uploadé.

 
  1. // src/Ens/JobeedBundle/Controller/JobController.php
  2. // ...
  3. public function createAction()
  4. {
  5. // ...
  6. if ($form->isValid()) {
  7. $em = $this->getDoctrine()->getEntityManager();
  8. $entity->file->move(__DIR__.'/../../../../web/uploads/jobs', $entity->file->getClientOriginalName());
  9. $entity->setLogo($entity->file->getClientOriginalName());
  10. $em->persist($entity);
  11. $em->flush();
  12. return $this->redirect($this->generateUrl('ens_job_show', array(
  13. 'company' => $entity->getCompanySlug(),
  14. 'location' => $entity->getLocationSlug(),
  15. 'id' => $entity->getId(),
  16. 'position' => $entity->getPositionSlug()
  17. )));
  18. }
  19. // ...
  20. }

Vous devez créer le répertoire logo (web/uploads/jobs/) et vérifier qu'il est accessible en écriture par le serveur web.

Même si cette implémentation fonctionne, la meilleure façon est de gérer l'upload de fichiers à l'aide de l'entité Doctrine Job.

Tout d'abord, ajoutez la ligne suivante à l'entité Job:

 
  1. protected function getUploadDir()
  2. {
  3. return 'uploads/jobs';
  4. }
  5. protected function getUploadRootDir()
  6. {
  7. return __DIR__.'/../../../../web/'.$this->getUploadDir();
  8. }
  9. public function getWebPath()
  10. {
  11. return null === $this->logo ? null : $this->getUploadDir().'/'.$this->logo;
  12. }
  13. public function getAbsolutePath()
  14. {
  15. return null === $this->logo ? null : $this->getUploadRootDir().'/'.$this->logo;
  16. }

La propriété logo enregistre le chemin relatif au fichier et est conservé dans la BDD. getAbsolutePath() est une méthode pratique qui renvoie le chemin absolu vers le fichier alors que getWebPath() est une méthode qui renvoie le chemin web, qui peut être utilisé dans un template pour créer un lien vers le fichier uploadé.

Nous ferons la mise en œuvre de telle sorte que l'opération de la BDD et le déplacement du fichier soient atomiques: si il y a un problème persistant de l'entité ou si le fichier ne peut pas être enregistré, rien ne se passera. Pour ce faire, nous avons besoin de déplacer le fichier de telle sort que Doctrine persiste l'objet dans la BDD. Ceci peut être accompli par hooking dans le lifecycleCallbacks de l'entité Job. Comme nous l'avons fait dans le troisième chapitre du tutoriel Jobeet, nous allons modifier le fichier Job.orm.yml et y ajouter les lifecycleCallbacks preUpload, upload et removeUpload:

 
  1. # src/Ens/JobeetBundle/Resources/config/doctrine/Job.orm.yml
  2. Ens\JobeetBundle\Entity\Job:
  3. # ...
  4. lifecycleCallbacks:
  5. prePersist: [ preUpload, setCreatedAtValue, setExpiresAtValue ]
  6. preUpdate: [ preUpload, setUpdatedAtValue ]
  7. postPersist: [ upload ]
  8. postUpdate: [ upload ]
  9. postRemove: [ removeUpload ]

Maintenant, exécutez la commande Doctrine generate:entities pour ajouter ces nouvelles méthodes à l'entité Job:

php app/console doctrine:generate:entities EnsJobeetBundle

Modifiez l'entité Job et modifiez les méthodes ajoutées à ce qui suit:

 
  1. // src/Ens/JobeetBundle/Entity/Job.php
  2. // ...
  3. /**
  4. * @ORM\prePersist
  5. */
  6. public function preUpload()
  7. {
  8. if (null !== $this->file) {
  9. // do whatever you want to generate a unique name
  10. $this->logo = uniqid().'.'.$this->file->guessExtension();
  11. }
  12. }
  13. /**
  14. * @ORM\postPersist
  15. */
  16. public function upload()
  17. {
  18. if (null === $this->file) {
  19. return;
  20. }
  21. // if there is an error when moving the file, an exception will
  22. // be automatically thrown by move(). This will properly prevent
  23. // the entity from being persisted to the database on error
  24. $this->file->move($this->getUploadRootDir(), $this->logo);
  25. unset($this->file);
  26. }
  27. /**
  28. * @ORM\postRemove
  29. */
  30. public function removeUpload()
  31. {
  32. if ($file = $this->getAbsolutePath()) {
  33. unlink($file);
  34. }
  35. }
  36. // ...

La classe fait maintenant tout ce qu'il faut: elle génère un nom de fichier unique avant la persistance, déplace le fichier après la persistance, et supprime le fichier si l'entité est déjà supprimée. Maintenant que le déplacement du fichier est géré atomiquement par l'entité, il faut supprimer le code que nous avons ajouté plus tôt dans le contrôleur pour gérer l'upload:

 
  1. // src/Ens/JobeetBundle/Controller/JobController.php
  2. // ...
  3. public function createAction()
  4. {
  5. $entity = new Job();
  6. $request = $this->getRequest();
  7. $form = $this->createForm(new JobType(), $entity);
  8. $form->bindRequest($request);
  9. if ($form->isValid()) {
  10. $em = $this->getDoctrine()->getEntityManager();
  11. $em->persist($entity);
  12. $em->flush();
  13. return $this->redirect($this->generateUrl('ens_job_show', array(
  14. 'company' => $entity->getCompanySlug(),
  15. 'location' => $entity->getLocationSlug(),
  16. 'id' => $entity->getId(),
  17. 'position' => $entity->getPositionSlug()
  18. )));
  19. }
  20. return $this->render('EnsJobeetBundle:Job:new.html.twig', array(
  21. 'entity' => $entity,
  22. 'form' => $form->createView()
  23. ));
  24. }
  25. // ...

Le template du formulaire

Maintenant que la classe du formulaire a été personnalisée, nous avons besoin de l'afficher. Ouvrez le template new.html.twig et modifiez-le:

 
  1. <!-- /src/Ens/JobeetBundle/Resources/views/Job/new.html.twig -->
  2. {% extends 'EnsJobeetBundle::layout.html.twig' %}
  3. {% form_theme form _self %}
  4. {% block field_errors %}
  5. {% spaceless %}
  6. {% if errors|length > 0 %}
  7. <ul class="error_list">
  8. {% for error in errors %}
  9. <li>{{ error.messageTemplate|trans(error.messageParameters, 'validators') }}</li>
  10. {% endfor %}
  11. </ul>
  12. {% endif %}
  13. {% endspaceless %}
  14. {% endblock field_errors %}
  15. {% block stylesheets %}
  16. {{ parent() }}
  17. <link rel="stylesheet" href="{{ asset('bundles/ensjobeet/css/job.css') }}" type="text/css" media="all" />
  18. {% endblock %}
  19. {% block content %}
  20. <h1>Job creation</h1>
  21. <form action="{{ path('ens_job_create') }}" method="post" {{ form_enctype(form) }}>
  22. <table id="job_form">
  23. <tfoot>
  24. <tr>
  25. <td colspan="2">
  26. <input type="submit" value="Preview your job" />
  27. </td>
  28. </tr>
  29. </tfoot>
  30. <tbody>
  31. <tr>
  32. <th>{{ form_label(form.category) }}</th>
  33. <td>
  34. {{ form_errors(form.category) }}
  35. {{ form_widget(form.category) }}
  36. </td>
  37. </tr>
  38. <tr>
  39. <th>{{ form_label(form.type) }}</th>
  40. <td>
  41. {{ form_errors(form.type) }}
  42. {{ form_widget(form.type) }}
  43. </td>
  44. </tr>
  45. <tr>
  46. <th>{{ form_label(form.company) }}</th>
  47. <td>
  48. {{ form_errors(form.company) }}
  49. {{ form_widget(form.company) }}
  50. </td>
  51. </tr>
  52. <tr>
  53. <th>{{ form_label(form.file) }}</th>
  54. <td>
  55. {{ form_errors(form.file) }}
  56. {{ form_widget(form.file) }}
  57. </td>
  58. </tr>
  59. <tr>
  60. <th>{{ form_label(form.url) }}</th>
  61. <td>
  62. {{ form_errors(form.url) }}
  63. {{ form_widget(form.url) }}
  64. </td>
  65. </tr>
  66. <tr>
  67. <th>{{ form_label(form.position) }}</th>
  68. <td>
  69. {{ form_errors(form.position) }}
  70. {{ form_widget(form.position) }}
  71. </td>
  72. </tr>
  73. <tr>
  74. <th>{{ form_label(form.location) }}</th>
  75. <td>
  76. {{ form_errors(form.location) }}
  77. {{ form_widget(form.location) }}
  78. </td>
  79. </tr>
  80. <tr>
  81. <th>{{ form_label(form.description) }}</th>
  82. <td>
  83. {{ form_errors(form.description) }}
  84. {{ form_widget(form.description) }}
  85. </td>
  86. </tr>
  87. <tr>
  88. <th>{{ form_label(form.how_to_apply) }}</th>
  89. <td>
  90. {{ form_errors(form.how_to_apply) }}
  91. {{ form_widget(form.how_to_apply) }}
  92. </td>
  93. </tr>
  94. <tr>
  95. <th>{{ form_label(form.token) }}</th>
  96. <td>
  97. {{ form_errors(form.token) }}
  98. {{ form_widget(form.token) }}
  99. </td>
  100. </tr>
  101. <tr>
  102. <th>{{ form_label(form.is_public) }}</th>
  103. <td>
  104. {{ form_errors(form.is_public) }}
  105. {{ form_widget(form.is_public) }}
  106. <br /> Whether the job can also be published on affiliate websites or not.
  107. </td>
  108. </tr>
  109. <tr>
  110. <th>{{ form_label(form.email) }}</th>
  111. <td>
  112. {{ form_errors(form.email) }}
  113. {{ form_widget(form.email) }}
  114. </td>
  115. </tr>
  116. </tbody>
  117. </table>
  118. {{ form_rest(form) }}
  119. </form>
  120. {% endblock %}

Nous pourrions afficher le formulaire en utilisant simplement la ligne de code suivante, mais comme nous avons besoin de plus de personnalisation, nous avons choisi d'afficher chaque champ de formulaire à la main.

{{ form_widget(form) }}

En appelant form_widget(form), chaque champ du formulaire est affiché, accompagné d'un message d'erreur et d'un label (s'il en existe un). Aussi facile que cela soit, ce n'est pas très souple. Habituellement, vous aurez envie de rendre chaque champ de formulaire individuellement, de sorte de pouvoir contrôler la façon dont le formulaire ressemble.

Nous avons également utilisé une technique appelée Form Theming pour personnaliser la façon dont les erreurs de formulaire seront affichées. Vous pouvez en lire plus à ce sujet dans la documentation officielle de Symfony2.

Faites la même chose avec le template edit.html.twig:

 
  1. <!-- /src/Ens/JobeetBundle/Resources/views/Job/edit.html.twig -->
  2. {% extends 'EnsJobeetBundle::layout.html.twig' %}
  3. {% form_theme edit_form _self %}
  4. {% block field_errors %}
  5. {% spaceless %}
  6. {% if errors|length > 0 %}
  7. <ul>
  8. {% for error in errors %}
  9. <li>{{ error.messageTemplate|trans(error.messageParameters, 'validators') }}</li>
  10. {% endfor %}
  11. </ul>
  12. {% endif %}
  13. {% endspaceless %}
  14. {% endblock field_errors %}
  15. {% block stylesheets %}
  16. {{ parent() }}
  17. <link rel="stylesheet" href="{{ asset('bundles/ensjobeet/css/job.css') }}" type="text/css" media="all" />
  18. {% endblock %}
  19. {% block content %}
  20. <h1>Job edit</h1>
  21. <form action="{{ path('ens_job_update', { 'id': entity.id }) }}" method="post" {{ form_enctype(edit_form) }}>
  22. <table id="job_form">
  23. <tfoot>
  24. <tr>
  25. <td colspan="2">
  26. <input type="submit" value="Preview your job" />
  27. </td>
  28. </tr>
  29. </tfoot>
  30. <tbody>
  31. <tr>
  32. <th>{{ form_label(edit_form.category) }}</th>
  33. <td>
  34. {{ form_errors(edit_form.category) }}
  35. {{ form_widget(edit_form.category) }}
  36. </td>
  37. </tr>
  38. <tr>
  39. <th>{{ form_label(edit_form.type) }}</th>
  40. <td>
  41. {{ form_errors(edit_form.type) }}
  42. {{ form_widget(edit_form.type) }}
  43. </td>
  44. </tr>
  45. <tr>
  46. <th>{{ form_label(edit_form.company) }}</th>
  47. <td>
  48. {{ form_errors(edit_form.company) }}
  49. {{ form_widget(edit_form.company) }}
  50. </td>
  51. </tr>
  52. <tr>
  53. <th>{{ form_label(edit_form.file) }}</th>
  54. <td>
  55. {{ form_errors(edit_form.file) }}
  56. {{ form_widget(edit_form.file) }}
  57. </td>
  58. </tr>
  59. <tr>
  60. <th>{{ form_label(edit_form.url) }}</th>
  61. <td>
  62. {{ form_errors(edit_form.url) }}
  63. {{ form_widget(edit_form.url) }}
  64. </td>
  65. </tr>
  66. <tr>
  67. <th>{{ form_label(edit_form.position) }}</th>
  68. <td>
  69. {{ form_errors(edit_form.position) }}
  70. {{ form_widget(edit_form.position) }}
  71. </td>
  72. </tr>
  73. <tr>
  74. <th>{{ form_label(edit_form.location) }}</th>
  75. <td>
  76. {{ form_errors(edit_form.location) }}
  77. {{ form_widget(edit_form.location) }}
  78. </td>
  79. </tr>
  80. <tr>
  81. <th>{{ form_label(edit_form.description) }}</th>
  82. <td>
  83. {{ form_errors(edit_form.description) }}
  84. {{ form_widget(edit_form.description) }}
  85. </td>
  86. </tr>
  87. <tr>
  88. <th>{{ form_label(edit_form.how_to_apply) }}</th>
  89. <td>
  90. {{ form_errors(edit_form.how_to_apply) }}
  91. {{ form_widget(edit_form.how_to_apply) }}
  92. </td>
  93. </tr>
  94. <tr>
  95. <th>{{ form_label(edit_form.token) }}</th>
  96. <td>
  97. {{ form_errors(edit_form.token) }}
  98. {{ form_widget(edit_form.token) }}
  99. </td>
  100. </tr>
  101. <tr>
  102. <th>{{ form_label(edit_form.is_public) }}</th>
  103. <td>
  104. {{ form_errors(edit_form.is_public) }}
  105. {{ form_widget(edit_form.is_public) }}
  106. <br /> Whether the job can also be published on affiliate websites or not.
  107. </td>
  108. </tr>
  109. <tr>
  110. <th>{{ form_label(edit_form.email) }}</th>
  111. <td>
  112. {{ form_errors(edit_form.email) }}
  113. {{ form_widget(edit_form.email) }}
  114. </td>
  115. </tr>
  116. </tbody>
  117. </table>
  118. {{ form_rest(edit_form) }}
  119. </form>
  120. {% endblock %}

Les actions du formulaire

Nous avons maintenant une classe de formulaire et un template qui l'affiche. Maintenant, il est temps de le faire réellement fonctionner avec certaines actions. Le formulaire d'offre est géré par quatre méthodes dans JobController:

  • - newAction: Affiche un formulaire vierge pour créer une nouvelle offre
  • - createAction: Traite le formulaire (validation, repopulation du formulaire), et crée une nouvelle offre avec les valeurs soumises par l'utilisateur
  • - editAction: Affiche un formulaire pour modifier une offre existante
  • - updateAction: Traite le formulaire (validation, repopulation du formulaire), et met à jour une offre avec les valeurs soumises par l'utilisateur

Lorsque vous accédez à la page /job/new, une instance du formulaire pour un nouvel objet Job est créée en appelant la méthode createForm et transmise au template (newAction).

Lorsque l'utilisateur soumet le formulaire (createAction), le formulaire est lié (méthode bindRequest()) aux valeurs soumises par l'utilisateur et la validation est déclenchée.

Une fois que le formulaire est lié, il est possible de vérifier sa validité en utilisant la méthode isValid():

  • - Si le formulaire est valide (retourne true), l'offre est enregistrée dans la BDD ($em->persist($entity)), et l'utilisateur est redirigé vers la page de prévisualisation de l'offre
  • - Sinon, le template new.html.twig s'affiche à nouveau avec les valeurs soumises par l'utilisateur et les messages d'erreur associés

La modification d'une offre existante est assez similaire. La seule différence entre les actions new et edit est que l'objet Job à modifier est passé en second argument de la méthode createForm. Cet objet sera utilisé pour les valeurs par défaut du widget dans le template.

Vous pouvez également définir des valeurs par défaut pour le formulaire de création. Pour cela, nous allons passer un objet Job pré-modifié à la méthode createForm pour définir la valeur de type par défaut à temps plein:

 
  1. // src/Ens/JobeetBundle/Controller/JobController.php
  2. // ...
  3. public function newAction()
  4. {
  5. $entity = new Job();
  6. $entity->setType('full-time');
  7. $form = $this->createForm(new JobType(), $entity);
  8. return $this->render('EnsJobeetBundle:Job:new.html.twig', array(
  9. 'entity' => $entity,
  10. 'form' => $form->createView()
  11. ));
  12. }
  13. // ...

Tout doit fonctionner correctement maintenant. Désormais, l'utilisateur doit saisir le jeton pour l'offre. Mais comme nous ne voulons pas compter sur l'utilisateur pour fournir un jeton unique, le jeton de l'offre doit être généré automatiquement quand une nouvelle offre est créée. Créez une nouvelle méthode setTokenValue() de l'entité Job pour ajouter la logique qui génère le jeton avant qu'une nouvelle offre ne soit enregistrée:

 
  1. // src/Ens/JobeetBundle/Entity/Job.php
  2. // ...
  3. public function setTokenValue()
  4. {
  5. if(!$this->getToken())
  6. {
  7. $this->token = sha1($this->getEmail().rand(11111, 99999));
  8. }
  9. }
  10. // ...

Ajoutez cette méthode au lifecycleCallbacks PrePersist pour l'entité Job:

 
  1. # src/End/JobeetBundle/Resources/config/doctrine/Job.orm.yml
  2. # ...
  3. lifecycleCallbacks:
  4. prePersist: [ setTokenValue, preUpload, setCreatedAtValue, setExpiresAtValue ]
  5. # ...

Régénérez les entités Doctrine pour appliquer cette modification:

php app/console doctrine:generate:entities EnsJobeetBundle

Vous pouvez maintenant supprimer le champ token du formulaire:

 
  1. // src/Ens/JobeetBundle/Form/JobType.php
  2. // ...
  3. public function buildForm(FormBuilder $builder, array $options)
  4. {
  5. $builder->add('category');
  6. $builder->add('type', 'choice', array('choices' => Job::getTypes(), 'expanded' => true));
  7. $builder->add('company');
  8. $builder->add('file', 'file', array('label' => 'Company logo', 'required' => false));
  9. $builder->add('url');
  10. $builder->add('position');
  11. $builder->add('location');
  12. $builder->add('description');
  13. $builder->add('how_to_apply', null, array('label' => 'How to apply?'));
  14. $builder->add('is_public', null, array('label' => 'Public?'));
  15. $builder->add('email');
  16. }
  17. // ...

Supprimez-le aussi des templates new.html.twig et edit.html.twig:

 
  1. <!-- src/Ens/JobeetBundle/Resources/views/Job/new.html.twig -->
  2. <tr>
  3. <th>{{ form_label(form.token) }}</th>
  4. <td>
  5. {{ form_errors(form.token) }}
  6. {{ form_widget(form.token) }}
  7. </td>
  8. </tr>
 
  1. <!-- src/Ens/JobeetBundle/Resources/views/Job/edit.html.twig -->
  2. <tr>
  3. <th>{{ form_label(edit_form.token) }}</th>
  4. <td>
  5. {{ form_errors(edit_form.token) }}
  6. {{ form_widget(edit_form.token) }}
  7. </td>
  8. </tr>

Et dans le fichier validation.yml:

 
  1. # src/Ens/JobeetBundle/Resources/config/validation.yml
  2. # ...
  3. token:
  4. - NotBlank: ~

Si vous vous souvenez des scénarios du deuxième chapitre, une offre ne peut être modifiée que si l'utilisateur connaît le jeton associé. À l'heure actuelle, il est assez facile de modifier ou de supprimer n'importe quelle offre en essayant de deviner l'URL. C'est parce que l'URL saisie ressemble à /job/ID/edit, où ID est la clé primaire de l'offre.

Nous allons changer les routes de sorte que vous puissiez modifier ou supprimer une tâche uniquement si vous connaissez le jeton secret:

 
  1. # src/End/JobeetBundle/Resources/config/routing/job.yml
  2. # ...
  3. ens_job_edit:
  4. pattern: /{token}/edit
  5. defaults: { _controller: "EnsJobeetBundle:Job:edit" }
  6. ens_job_update:
  7. pattern: /{token}/update
  8. defaults: { _controller: "EnsJobeetBundle:Job:update" }
  9. requirements: { _method: post }
  10. ens_job_delete:
  11. pattern: /{token}/delete
  12. defaults: { _controller: "EnsJobeetBundle:Job:delete" }
  13. requirements: { _method: post }

Maintenant, modifiez JobController pour utiliser le jeton à la place de l'ID:

 
  1. // src/Ens/JobeetBundle/Controller/JobController.php
  2. // ...
  3. public function editAction($token)
  4. {
  5. $em = $this->getDoctrine()->getEntityManager();
  6. $entity = $em->getRepository('EnsJobeetBundle:Job')->findOneByToken($token);
  7. if (!$entity) {
  8. throw $this->createNotFoundException('Unable to find Job entity.');
  9. }
  10. $editForm = $this->createForm(new JobType(), $entity);
  11. $deleteForm = $this->createDeleteForm($token);
  12. return $this->render('EnsJobeetBundle:Job:edit.html.twig', array(
  13. 'entity' => $entity,
  14. 'edit_form' => $editForm->createView(),
  15. 'delete_form' => $deleteForm->createView(),
  16. ));
  17. }
  18. public function updateAction($token)
  19. {
  20. $em = $this->getDoctrine()->getEntityManager();
  21. $entity = $em->getRepository('EnsJobeetBundle:Job')->findOneByToken($token);
  22. if (!$entity) {
  23. throw $this->createNotFoundException('Unable to find Job entity.');
  24. }
  25. $editForm = $this->createForm(new JobType(), $entity);
  26. $deleteForm = $this->createDeleteForm($token);
  27. $request = $this->getRequest();
  28. $editForm->bindRequest($request);
  29. if ($editForm->isValid()) {
  30. $em->persist($entity);
  31. $em->flush();
  32. return $this->redirect($this->generateUrl('ens_job_edit', array('token' => $token)));
  33. }
  34. return $this->render('EnsJobeetBundle:Job:edit.html.twig', array(
  35. 'entity' => $entity,
  36. 'edit_form' => $editForm->createView(),
  37. 'delete_form' => $deleteForm->createView(),
  38. ));
  39. }
  40. public function deleteAction($token)
  41. {
  42. $form = $this->createDeleteForm($token);
  43. $request = $this->getRequest();
  44. $form->bindRequest($request);
  45. if ($form->isValid()) {
  46. $em = $this->getDoctrine()->getEntityManager();
  47. $entity = $em->getRepository('EnsJobeetBundle:Job')->findOneByToken($token);
  48. if (!$entity) {
  49. throw $this->createNotFoundException('Unable to find Job entity.');
  50. }
  51. $em->remove($entity);
  52. $em->flush();
  53. }
  54. return $this->redirect($this->generateUrl('ens_job'));
  55. }
  56. private function createDeleteForm($token)
  57. {
  58. return $this->createFormBuilder(array('token' => $token))
  59. ->add('token', 'hidden')
  60. ->getForm()
  61. ;
  62. }

Dans le template d'offre show.html.twig, modifiez le paramètre de route ens_job_edit:

 
  1. <a href="{{ path('ens_job_edit', { 'token': entity.token }) }}">

Faites de même pour la route ens_job_update dans le template d'offre edit.html.twig:

 
  1. <form action="{{ path('ens_job_update', { 'token': entity.token }) }}" method="post" {{ form_enctype(edit_form) }}>

Maintenant, toutes les routes liées aux offres, à l'exception de job_show_user, incorporent le jeton. Par exemple, la route pour modifier une offre est maintenant sur le schéma suivant:

http://jobeet.local/job/TOKEN/edit

La page de prévisualisation

La page de prévisualisation est la même que l'affichage de la page d'offre. La seule différence est que la page de prévisualisation sera accessible à l'aide du jeton de l'offre plutôt que l'ID de l'offre:

 
  1. # src/End/JobeetBundle/Resources/config/routing/job.yml
  2. # ...
  3. ens_job_show:
  4. pattern: /{company}/{location}/{id}/{position}
  5. defaults: { _controller: "EnsJobeetBundle:Job:show" }
  6. requirements:
  7. id: \d+
  8. ens_job_preview:
  9. pattern: /{company}/{location}/{token}/{position}
  10. defaults: { _controller: "EnsJobeetBundle:Job:preview" }
  11. requirements:
  12. token: \w+
  13. # ...

L'action preview (ici la différence avec l'action show, c'est que l'offre est récupérée à partir de la BDD en utilisant le jeton fourni à la place de l'ID):

 
  1. // src/Ens/JobeetBundle/Controller/JobController.php
  2. // ...
  3. public function previewAction($token)
  4. {
  5. $em = $this->getDoctrine()->getEntityManager();
  6. $entity = $em->getRepository('EnsJobeetBundle:Job')->findOneByToken($token);
  7. if (!$entity) {
  8. throw $this->createNotFoundException('Unable to find Job entity.');
  9. }
  10. $deleteForm = $this->createDeleteForm($entity->getId());
  11. return $this->render('EnsJobeetBundle:Job:show.html.twig', array(
  12. 'entity' => $entity,
  13. 'delete_form' => $deleteForm->createView(),
  14. ));
  15. }
  16. // ...

Si l'utilisateur va à l'URL avec le jeton, nous allons ajouter une barre d'administration dans la partie supérieure. Au début du template show.html.twig, incluez un template pour mettre la barre d'administration et supprimez le lien Modifier en bas:

 
  1. <!-- /src/Ens/JobeetBundle/Resources/views/Job/show.html.twig -->
  2. <!-- ... -->
  3. {% block content %}
  4. {% if app.request.get('token') %}
  5. {% include 'EnsJobeetBundle:Job:admin.html.twig' with {'job': entity} %}
  6. {% endif %}
  7. <!-- ... -->
  8. {% endblock %}

Ensuite, créez le template admin.html.twig:

 
  1. <!-- /src/Ens/JobeetBundle/Resources/views/Job/admin.html.twig -->
  2. <div id="job_actions">
  3. <h3>Admin</h3>
  4. <ul>
  5. {% if not job.isActivated %}
  6. <li><a href="{{ path('ens_job_edit', { 'token': job.token }) }}">Edit</a></li>
  7. <li><a href="{{ path('ens_job_edit', { 'token': job.token }) }}">Publish</a></li>
  8. {% endif %}
  9. <li>
  10. <form action="{{ path('ens_job_delete', { 'token': job.token }) }}" method="post">
  11. {{ form_widget(delete_form) }}
  12. <button type="submit" onclick="if(!confirm('Are you sure?')) { return false; }">Delete</button>
  13. </form>
  14. </li>
  15. {% if job.isActivated %}
  16. <li {% if job.expiresSoon %} class="expires_soon" {% endif %}>
  17. {% if job.isExpired %}
  18. Expired
  19. {% else %}
  20. Expires in <strong>{{ job.getDaysBeforeExpires }}</strong> days
  21. {% endif %}
  22. {% if job.expiresSoon %}
  23. - <a href="">Extend</a> for another 30 days
  24. {% endif %}
  25. </li>
  26. {% else %}
  27. <li>
  28. [Bookmark this <a href="{{ url('ens_job_preview', { 'token': job.token, 'company': job.companyslug, 'location': job.locationslug, 'position': job.positionslug }) }}">URL</a> to manage this job in the future.]
  29. </li>
  30. {% endif %}
  31. </ul>
  32. </div>

Il y a beaucoup de code, mais la plupart du code est simple à comprendre.

Pour rendre le template plus lisible, nous avons ajouté un ensemble de raccourcis de méthodes dans la classe d'entité Job:

 
  1. // src/Ens/JobeetBundle/Entity/Job.php
  2. // ...
  3. public function isExpired()
  4. {
  5. return $this->getDaysBeforeExpires() < 0;
  6. }
  7. public function expiresSoon()
  8. {
  9. return $this->getDaysBeforeExpires() < 5;
  10. }
  11. public function getDaysBeforeExpires()
  12. {
  13. return ceil(($this->getExpiresAt()->format('U') - time()) / 86400);
  14. }

La barre d'administration affiche les différentes actions en fonction du statut de l'offre:

Nous allons maintenant rediriger les actions create et update de JobController ver la nouvelle page de prévisualisation:

 
  1. // src/Ens/JobeetBundle/Controller/JobController.php
  2. // ...
  3. public function createAction()
  4. {
  5. // ...
  6. if ($form->isValid()) {
  7. // ...
  8. return $this->redirect($this->generateUrl('ens_job_preview', array(
  9. 'company' => $entity->getCompanySlug(),
  10. 'location' => $entity->getLocationSlug(),
  11. 'token' => $entity->getToken(),
  12. 'position' => $entity->getPositionSlug()
  13. )));
  14. }
  15. // ...
  16. }
  17. public function updateAction($token)
  18. {
  19. // ...
  20. if ($editForm->isValid()) {
  21. // ...
  22. return $this->redirect($this->generateUrl('ens_job_preview', array(
  23. 'company' => $entity->getCompanySlug(),
  24. 'location' => $entity->getLocationSlug(),
  25. 'token' => $entity->getToken(),
  26. 'position' => $entity->getPositionSlug()
  27. )));
  28. }
  29. // ...
  30. }

Activation et publication des offres

Dans la section précédente, il y a un lien pour publier l'offre. Le lien doit être modifié pour pointer vers une nouvelle action publish. Pour cela, nous allons créer une nouvelle route:

 
  1. # src/Ens/JobeetBundle/Resources/config/routing/job.yml
  2. # ...
  3. ens_job_publish:
  4. pattern: /{token}/publish
  5. defaults: { _controller: "EnsJobeetBundle:Job:publish" }
  6. requirements: { _method: post }

Nous pouvons maintenant modifier le lien "Publish" (nous allons utiliser un formulaire ici, comme lors de la suppression d'une offre, nous aurons donc une requête POST):

 
  1. <!-- src/Ens/JobeetBundle/Resources/views/job/admin.html.twig -->
  2. <!-- ... -->
  3. {% if not job.isActivated %}
  4. <li><a href="{{ path('ens_job_edit', { 'token': job.token }) }}">Edit</a></li>
  5. <li>
  6. <form action="{{ path('ens_job_publish', { 'token': job.token }) }}" method="post">
  7. {{ form_widget(publish_form) }}
  8. <button type="submit">Publish</button>
  9. </form>
  10. </li>
  11. {% endif %}
  12. <!-- ... -->

La dernière étape consiste à créer l'action publish, le formulaire publish et modifier l'action preview pour envoyer le formulaire publish vers le template:

 
  1. // src/Ens/JobeetBundle/Controller/JobController.php
  2. // ...
  3. public function previewAction($token)
  4. {
  5. // ...
  6. $deleteForm = $this->createDeleteForm($entity->getId());
  7. $publishForm = $this->createPublishForm($entity->getToken());
  8. return $this->render('EnsJobeetBundle:Job:show.html.twig', array(
  9. 'entity' => $entity,
  10. 'delete_form' => $deleteForm->createView(),
  11. 'publish_form' => $publishForm->createView(),
  12. ));
  13. }
  14. public function publishAction($token)
  15. {
  16. $form = $this->createPublishForm($token);
  17. $request = $this->getRequest();
  18. $form->bindRequest($request);
  19. if ($form->isValid()) {
  20. $em = $this->getDoctrine()->getEntityManager();
  21. $entity = $em->getRepository('EnsJobeetBundle:Job')->findOneByToken($token);
  22. if (!$entity) {
  23. throw $this->createNotFoundException('Unable to find Job entity.');
  24. }
  25. $entity->publish();
  26. $em->persist($entity);
  27. $em->flush();
  28. $this->get('session')->setFlash('notice', 'Your job is now online for 30 days.');
  29. }
  30. return $this->redirect($this->generateUrl('ens_job_preview', array(
  31. 'company' => $entity->getCompanySlug(),
  32. 'location' => $entity->getLocationSlug(),
  33. 'token' => $entity->getToken(),
  34. 'position' => $entity->getPositionSlug()
  35. )));
  36. }
  37. private function createPublishForm($token)
  38. {
  39. return $this->createFormBuilder(array('token' => $token))
  40. ->add('token', 'hidden')
  41. ->getForm()
  42. ;
  43. }
  44. // ...

La méthode publishAction() utilise une nouvelle méthode publish() qui peut être définie comme suit:

 
  1. // src/Ens/JobeetBundle/Entity/Job.php
  2. // ...
  3. public function publish()
  4. {
  5. $this->setIsActivated(true);
  6. }
  7. // ...

Vous pouvez maintenant tester la nouvelle fonctionnalité de publication dans votre navigateur.

Mais nous avons encore quelque chose à corriger. Les emplois non-activés ne doivent pas être accessibles, ce qui signifie qu'ils ne doivent pas apparaître sur la page d'accueil Jobeet et ne doivent pas être accessibles via leur URL. Nous devons modifier les méthodes de JobRepository pour ajouter cette exigence:

 
  1. // src Ens/JobeetBundle/Repository/JobRepository.php
  2. namespace Ens\JobeetBundle\Repository;
  3. use Doctrine\ORM\EntityRepository;
  4. class JobRepository extends EntityRepository
  5. {
  6. public function getActiveJobs($category_id = null, $max = null, $offset = null)
  7. {
  8. $qb = $this->createQueryBuilder('j')
  9. ->where('j.expires_at > :date')
  10. ->setParameter('date', date('Y-m-d H:i:s', time()))
  11. ->andWhere('j.is_activated = :activated')
  12. ->setParameter('activated', 1)
  13. ->orderBy('j.expires_at', 'DESC');
  14. if($max)
  15. {
  16. $qb->setMaxResults($max);
  17. }
  18. if($offset)
  19. {
  20. $qb->setFirstResult($offset);
  21. }
  22. if($category_id)
  23. {
  24. $qb->andWhere('j.category = :category_id')
  25. ->setParameter('category_id', $category_id);
  26. }
  27. $query = $qb->getQuery();
  28. return $query->getResult();
  29. }
  30. public function countActiveJobs($category_id = null)
  31. {
  32. $qb = $this->createQueryBuilder('j')
  33. ->select('count(j.id)')
  34. ->where('j.expires_at > :date')
  35. ->setParameter('date', date('Y-m-d H:i:s', time()))
  36. ->andWhere('j.is_activated = :activated')
  37. ->setParameter('activated', 1);
  38. if($category_id)
  39. {
  40. $qb->andWhere('j.category = :category_id')
  41. ->setParameter('category_id', $category_id);
  42. }
  43. $query = $qb->getQuery();
  44. return $query->getSingleScalarResult();
  45. }
  46. public function getActiveJob($id)
  47. {
  48. $query = $this->createQueryBuilder('j')
  49. ->where('j.id = :id')
  50. ->setParameter('id', $id)
  51. ->andWhere('j.expires_at > :date')
  52. ->setParameter('date', date('Y-m-d H:i:s', time()))
  53. ->andWhere('j.is_activated = :activated')
  54. ->setParameter('activated', 1)
  55. ->setMaxResults(1)
  56. ->getQuery();
  57. try {
  58. $job = $query->getSingleResult();
  59. } catch (\Doctrine\Orm\NoResultException $e) {
  60. $job = null;
  61. }
  62. return $job;
  63. }
  64. }

Pareil pour la méthode getWithJobs() de CategoryRepository:

 
  1. // src/Ens/JobeetBundle/Repository/CategoryRepository.php
  2. namespace Ens\JobeetBundle\Repository;
  3. use Doctrine\ORM\EntityRepository;
  4. class CategoryRepository extends EntityRepository
  5. {
  6. public function getWithJobs()
  7. {
  8. $query = $this->getEntityManager()->createQuery(
  9. 'SELECT c FROM EnsJobeetBundle:Category c LEFT JOIN c.jobs j WHERE j.expires_at > :date AND j.is_activated = :activated'
  10. )->setParameter('date', date('Y-m-d H:i:s', time()))->setParameter('activated', 1);
  11. return $query->getResult();
  12. }
  13. }

Vous pouvez maintenant le tester dans votre navigateur. Tous les emplois non-activés ont disparu de la page d'accueil, même si vous connaissez son URL, ils ne sont plus accessibles. Ils sont cependant accessibles si l'on connaît le jeton de l'offre. Dans ce cas, l'aperçu de l'offre sera affiché avec la barre d'administration.

Voir les commentaires

Les tests unitaires

Publié le par Penda Jose

Les tests dans Symfony2

Il existe deux types de tests automatisés dans Symfony: les tests unitaires et les tests fonctionnels. Les tests unitaires vérifient que chaque méthode et fonction fonctionne correctement. Chaque test doit être aussi indépendant que possible des autres. D'autre part, les tests fonctionnels vérifient que l'application résultante se comporte correctement dans son ensemble.

Les tests unitaires seront couverts dans ce chapitre, alors que le prochain chapitre sera consacré aux tests fonctionnels.

Symfony2 s'intègre à une bibliothèque indépendante, PHPUnit, pour vous donner un framework de tests riche. Pour exécuter des tests, vous devrez installer PHPUnit 3.5.11 ou une version ultérieure.

Chaque test - qu'il s'agisse d'un test unitaire ou un test fonctionnel - est une classe PHP qui doit se situer dans un sous-répertoire Tests/ de vos paquets. Si vous suivez cette règle, vous pouvez exécuter tous les tests de votre application avec la commande suivante:

$ phpunit -c app/

L'option -c indique à PHPUnit de chercher un fichier de configuration dans le répertoire app/. Si vous êtes curieux de connaître les options de PHPUnit, consultez le fichier app/phpunit.xml.dist.

Tests unitaires

Un test unitaire est généralement un test contre une classe PHP spécifique. Commençons par écrire des tests pour la méthode Jobeet::slugify().

Créez un nouveau fichier, JobeetTest.php, dans le répertoire src/Ens/JobeetBundle/Tests/Utils. Par convention, le sous-répertoire Tests/ doit répliquer le répertoire de votre paquet. Donc, quand nous testons une classe dans le répertoire Utils/ de notre paquet, nous avons mis le test dans le répertoire Tests/Utils/:

 
  1. // src/Ens/JobeetBundle/Tests/Utils/JobeetTest.php
  2. namespace Ens\JobeetBundle\Tests\Utils;
  3. use Ens\JobeetBundle\Utils\Jobeet;
  4. class JobeetTest extends \PHPUnit_Framework_TestCase
  5. {
  6. public function testSlugify()
  7. {
  8. $this->assertEquals('sensio', Jobeet::slugify('Sensio'));
  9. $this->assertEquals('sensio-labs', Jobeet::slugify('sensio labs'));
  10. $this->assertEquals('sensio-labs', Jobeet::slugify('sensio labs'));
  11. $this->assertEquals('paris-france', Jobeet::slugify('paris,france'));
  12. $this->assertEquals('sensio', Jobeet::slugify(' sensio'));
  13. $this->assertEquals('sensio', Jobeet::slugify('sensio '));
  14. }
  15. }

Pour exécuter ce test, vous pouvez utiliser la commande suivante:

phpunit -c app/ src/Ens/JobeetBundle/Tests/Utils/JobeetTest

Comme tout devrait fonctionner correctement, vous devriez obtenir le résultat suivant:

 
  1. PHPUnit 3.6.10 by Sebastian Bergmann.
  2. Configuration read from /home/dragos/work/jobeet/app/phpunit.xml.dist
  3. .
  4. Time: 0 seconds, Memory: 3.50Mb
  5. OK (1 test, 6 assertions)

Pour une liste complète des assertions, vous pouvez consulter la documentation de PHPUnit.

Ajout de tests pour de nouvelles fonctionnalités

Le jeton pour une chaîne vide est une chaîne vide. Vous pouvez le tester, ça va marcher. Mais une chaîne vide dans une URL n'est pas une bonne idée. Nous allons changer la méthode slugify() de sorte qu'elle retourne la chaîne "n-a" dans le cas d'une chaîne vide.

Vous pouvez écrire le premier test, puis mettre à jour la méthode, ou l'inverse. C'est vraiment une question de goût mais l'écriture du test en premier vous donne l'assurance que votre code implémente réellement ce que vous avez prévu:

 
  1. // src/Ens/JobeetBundle/Tests/Utils/JobeetTest.php
  2. // ...
  3. $this->assertEquals('n-a', Jobeet::slugify(''));
  4. // ...

Maintenant, si nous réexécutons le test, nous aurons un échec:

 
  1. PHPUnit 3.6.10 by Sebastian Bergmann.
  2. Configuration read from /home/dragos/work/jobeet/app/phpunit.xml.dist
  3. F
  4. Time: 0 seconds, Memory: 3.50Mb
  5. There was 1 failure:
  6. 1) Ens\JobeetBundle\Tests\Utils\JobeetTest::testSlugify
  7. Failed asserting that two strings are equal.
  8. --- Expected
  9. +++ Actual
  10. @@ @@
  11. -'n-a'
  12. +''
  13. /home/dragos/work/jobeet/src/Ens/JobeetBundle/Tests/Utils/JobeetTest.php:17
  14. FAILURES!
  15. Tests: 1, Assertions: 7, Failures: 1.

Maintenant, modifiez la classe Jobeet et ajoutez la condition suivante au début:

 
  1. // src/Ens/JobeetBundle/Utils/Jobeet.php
  2. // ...
  3. static public function slugify($text)
  4. {
  5. if (empty($text))
  6. {
  7. return 'n-a';
  8. }
  9. // ...
  10. }

Le test doit maintenant passer comme prévu, et vous pourrez profiter de la barre verte.

Ajout de tests à cause d'un bug

Disons que le temps a passé et l'un de vos utilisateurs vous rapporte un bug bizarre: certains liens des offres pointent vers une page d'erreur 404. Après quelques recherches, vous trouverez que, pour une raison quelconque, ces offres ont un jeton société, intitulé ou lieu vide.

Comment est-ce possible?

Vous regardez à travers les enregistrements de la BDD et les colonnes ne sont absolument pas vide. Vous y réfléchissez un moment, et hop, vous trouvez la cause. Lorsqu'une chaîne ne contient que des caractères non-ASCII, la méthode slugify() la convertit en une chaîne vide. Je suis si heureux d'avoir trouvé la cause, vous ouvrez la classe Jobeet et corrigez le problème tout de suite. C'est une mauvaise idée. Tout d'abord, nous allons ajouter un test:

$this->assertEquals('n-a', Jobeet::slugify(' - '));

Après avoir vérifié que le test ne passe pas, modifiez la classe Jobeet et déplacez la vérification de la chaîne vide à la fin de la méthode:

 
  1. static public function slugify($text)
  2. {
  3. // ...
  4. if (empty($text))
  5. {
  6. return 'n-a';
  7. }
  8. return $text;
  9. }

Le nouveau test passe désormais, comme tous les autres. La méthode slugify() a eu un bug en dépit de notre couverture à 100%.

Vous ne pouvez pas penser à tous les cas lors de l'écriture des tests, et c'est très bien. Mais quand vous en découvrez un, vous devez écrire un test avant de corriger votre code. Cela signifie également que votre code va s'améliorer au fil du temps, ce qui est toujours une bonne chose.

Vers une meilleure méthode slugify()

Vous savez probablement que Symfony a été créé par des français, nous allons donc ajouter un test avec un mot français qui contient un accent:

$this->assertEquals('developpeur-web', Jobeet::slugify('Développeur Web'));

Le test doit échouer. Au lieu de remplacer é par e, la méthode slugify() l'a remplacé par un tiret (-). C'est un problème compliqué, appelé translittération. Heureusement, la bibliothèque iconv, si elle est installée, va faire le travail pour nous. Remplacez le code de la méthode slugify() par ce qui suit:

 
  1. static public function slugify($text)
  2. {
  3. // replace non letter or digits by -
  4. $text = preg_replace('#[^\\pL\d]+#u', '-', $text);
  5. // trim
  6. $text = trim($text, '-');
  7. // transliterate
  8. if (function_exists('iconv'))
  9. {
  10. $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
  11. }
  12. // lowercase
  13. $text = strtolower($text);
  14. // remove unwanted characters
  15. $text = preg_replace('#[^-\w]+#', '', $text);
  16. if (empty($text))
  17. {
  18. return 'n-a';
  19. }
  20. return $text;
  21. }

N'oubliez pas de sauvegarder tous vos fichiers PHP avec l'encodage UTF-8, car c'est l'encodage par défaut de Symfony, et celui utilisé par iconv pour faire la translitération.

Modifiez également le fichier de test pour n'exécuter le test que si iconv est disponible:

 
  1. if (function_exists('iconv'))
  2. {
  3. $this->assertEquals('developpeur-web', Jobeet::slugify('Développeur Web'));
  4. }

Voir les commentaires

Aller plus loin avec le Modèle

Publié le par Penda Jose

Aller plus loin avec le Modèle

L'objet Query de Doctrine

Scénario du deuxième chapitre: "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:

 
  1. // src/Ens/JobeetBundle/Controller/JobController.php
  2. // ...
  3. class JobController extends Controller
  4. {
  5. public function indexAction()
  6. {
  7. $em = $this->getDoctrine()->getEntityManager();
  8. $entities = $em->getRepository('EnsJobeetBundle:Job')->findAll();
  9. return $this->render('EnsJobeetBundle:Job:index.html.twig', array(
  10. 'entities' => $entities
  11. ));
  12. // ...
  13. }

Une offre active est une offre de moins de 30 jours. La méthode $entities = $em->getRepository('EnsJobeetBundle: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:

 
  1. public function indexAction()
  2. {
  3. $em = $this->getDoctrine()->getEntityManager();
  4. $query = $em->createQuery(
  5. 'SELECT j FROM EnsJobeetBundle:Job j WHERE j.created_at > :date'
  6. )->setParameter('date', date('Y-m-d H:i:s', time() - 86400 * 30));
  7. $entities = $query->getResult();
  8. return $this->render('EnsJobeetBundle:Job:index.html.twig', array(
  9. 'entities' => $entities
  10. ));
  11. }

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):

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 deuxième chapitre: "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 troisième chapitre, 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:

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

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

php app/console doctrine:generate:entities EnsJobeetBundle

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

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

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

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

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:

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

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

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/Ens/JobeetBundle/Resources/config/doctrine/Job.orm.yml et ajoutez ce qui suit:

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

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 EnsJobeetBundle

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).

 
  1. // src/Ens/JobeetBundle/Repository/JobRepository.php
  2. namespace Ens\JobeetBundle\Repository;
  3. use Doctrine\ORM\EntityRepository;
  4. class JobRepository extends EntityRepository
  5. {
  6. public function getActiveJobs($category_id = null)
  7. {
  8. $qb = $this->createQueryBuilder('j')
  9. ->where('j.expires_at > :date')
  10. ->setParameter('date', date('Y-m-d H:i:s', time()))
  11. ->orderBy('j.expires_at', 'DESC');
  12. if($category_id)
  13. {
  14. $qb->andWhere('j.category = :category_id')
  15. ->setParameter('category_id', $category_id);
  16. }
  17. $query = $qb->getQuery();
  18. return $query->getResult();
  19. }
  20. }

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

 
  1. // src/Ens/JobeetBundle/Controller/JobController.php
  2. // ...
  3. public function indexAction()
  4. {
  5. $em = $this->getDoctrine()->getEntityManager();
  6. $entities = $em->getRepository('EnsJobeetBundle:Job')->getActiveJobs();
  7. return $this->render('EnsJobeetBundle:Job:index.html.twig', array(
  8. 'entities' => $entities
  9. ));
  10. }
  11. // ...

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 chapitre, 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:

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

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

php app/console doctrine:generate:entities EnsJobeetBundle

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

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

Modifiez l'action index en conséquence:

 
  1. public function indexAction()
  2. {
  3. $em = $this->getDoctrine()->getEntityManager();
  4. $categories = $em->getRepository('EnsJobeetBundle:Category')->getWithJobs();
  5. foreach($categories as $category)
  6. {
  7. $category->setActiveJobs($em->getRepository('EnsJobeetBundle:Job')->getActiveJobs($category->getId()));
  8. }
  9. return $this->render('EnsJobeetBundle:Job:index.html.twig', array(
  10. 'categories' => $categories
  11. ));
  12. }

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

 
  1. // src/Ens/JobeetBundle/Entity/Category.php
  2. namespace Ens\JobeetBundle\Entity;
  3. use Doctrine\ORM\Mapping as ORM;
  4. class Category
  5. {
  6. // ...
  7. private $active_jobs;
  8. // ...
  9. public function setActiveJobs($jobs)
  10. {
  11. $this->active_jobs = $jobs;
  12. }
  13. public function getActiveJobs()
  14. {
  15. return $this->active_jobs;
  16. }
  17. }

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

 
  1. <!-- src/Ens/JobeetBundle/Resources/views/Job/index.html.twig -->
  2. <!-- ... -->
  3. {% block content %}
  4. <div id="jobs">
  5. {% for category in categories %}
  6. <div>
  7. <div class="category">
  8. <div class="feed">
  9. <a href="">Feed</a>
  10. </div>
  11. <h1>{{ category.name }}</h1>
  12. </div>
  13. <table class="jobs">
  14. {% for entity in category.activejobs %}
  15. <tr class="{{ cycle(['even', 'odd'], loop.index) }}">
  16. <td class="location">{{ entity.location }}</td>
  17. <td class="position">
  18. <a href="{{ path('ens_job_show', { 'id': entity.id, 'company': entity.companyslug, 'location': entity.locationslug, 'position': entity.positionslug }) }}">
  19. {{ entity.position }}
  20. </a>
  21. </td>
  22. <td class="company">{{ entity.company }}</td>
  23. </tr>
  24. {% endfor %}
  25. </table>
  26. </div>
  27. {% endfor %}
  28. </div>
  29. {% 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()

 
  1. public function getActiveJobs($category_id = null, $max = null)
  2. {
  3. $qb = $this->createQueryBuilder('j')
  4. ->where('j.expires_at > :date')
  5. ->setParameter('date', date('Y-m-d H:i:s', time()))
  6. ->orderBy('j.expires_at', 'DESC');
  7. if($max)
  8. {
  9. $qb->setMaxResults($max);
  10. }
  11. if($category_id)
  12. {
  13. $qb->andWhere('j.category = :category_id')
  14. ->setParameter('category_id', $category_id);
  15. }
  16. $query = $qb->getQuery();
  17. return $query->getResult();
  18. }

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

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

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 de 10 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:

 
  1. # app/config/config.yml
  2. # ...
  3. parameters:
  4. max_jobs_on_homepage: 10

Cela est maintenant accessible dans un contrôleur:

 
  1. // src/Ens/JobeetBundle/Controller/JobController
  2. // ...
  3. public function indexAction()
  4. {
  5. $em = $this->getDoctrine()->getEntityManager();
  6. $categories = $em->getRepository('EnsJobeetBundle:Category')->getWithJobs();
  7. foreach($categories as $category)
  8. {
  9. $category->setActiveJobs($em->getRepository('EnsJobeetBundle:Job')->getActiveJobs($category->getId(), $this->container->getParameter('max_jobs_on_homepage')));
  10. }
  11. return $this->render('EnsJobeetBundle:Job:index.html.twig', array(
  12. 'categories' => $categories
  13. ));
  14. }

Fixtures dynamiques

Pour l'instant, vous ne verrez aucune différence parce que nous avons 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:

 
  1. // src/Ens/JobeetBundle/DataFixtures/ORM/LoadJobData.php
  2. // ...
  3. public function load(ObjectManager $em)
  4. {
  5. // ...
  6. for($i = 100; $i <= 130; $i++)
  7. {
  8. $job = new Job();
  9. $job->setCategory($em->merge($this->getReference('category-programming')));
  10. $job->setType('full-time');
  11. $job->setCompany('Company '.$i);
  12. $job->setPosition('Web Developer');
  13. $job->setLocation('Paris, France');
  14. $job->setDescription('Lorem ipsum dolor sit amet, consectetur adipisicing elit.');
  15. $job->setHowToApply('Send your resume to lorem.ipsum [at] dolor.sit');
  16. $job->setIsPublic(true);
  17. $job->setIsActivated(true);
  18. $job->setToken('job_'.$i);
  19. $job->setEmail('job@example.com');
  20. $em->persist($job);
  21. }
  22. $em->flush();
  23. }
  24. // ...

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:

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:

 
  1. // src/Ens/JobeetBundle/Repository/JobRepository.php
  2. // ...
  3. public function getActiveJob($id)
  4. {
  5. $query = $this->createQueryBuilder('j')
  6. ->where('j.id = :id')
  7. ->setParameter('id', $id)
  8. ->andWhere('j.expires_at > :date')
  9. ->setParameter('date', date('Y-m-d H:i:s', time()))
  10. ->setMaxResults(1)
  11. ->getQuery();
  12. try {
  13. $job = $query->getSingleResult();
  14. } catch (\Doctrine\Orm\NoResultException $e) {
  15. $job = null;
  16. }
  17. return $job;
  18. }

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:

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

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

Voir les commentaires

1 2 3 4 5 6 7 8 9 10 > >>