Avec ce que nous avons ajouté au chapitre 11, l'application est maintenant pleinement utilisable par les chercheurs d'emploi et les recruteurs. Il est temps de parler un peu de la section admin de notre application. Aujourd'hui, grâce au paquet SonataAdminBundle, nous allons développer une interface complète d'administration pour Jobeet en moins d'une heure.
Ouvrez votre fichier deps et ajoutez ces lignes:
[SonataCacheBundle] git=http://github.com/sonata-project/SonataCacheBundle.git target=/bundles/Sonata/CacheBundle version=origin/2.0 [SonataBlockBundle] git=http://github.com/sonata-project/SonataBlockBundle.git target=/bundles/Sonata/BlockBundle version=origin/2.0 [SonatajQueryBundle] git=http://github.com/sonata-project/SonatajQueryBundle.git target=/bundles/Sonata/jQueryBundle [KnpMenu] git=https://github.com/KnpLabs/KnpMenu.git version=v1.1.2 [KnpMenuBundle] git=https://github.com/KnpLabs/KnpMenuBundle.git target=bundles/Knp/Bundle/MenuBundle version=v1.1.0 [Exporter] git=http://github.com/sonata-project/exporter.git target=/exporter [SonataDoctrineORMAdminBundle] git=http://github.com/sonata-project/SonataDoctrineORMAdminBundle.git target=/bundles/Sonata/DoctrineORMAdminBundle version=origin/2.0 [SonataAdminBundle] git=git://github.com/sonata-project/SonataAdminBundle.git target=/bundles/Sonata/AdminBundle version=origin/2.0
Exécutez le script vendors pour télécharger les paquets:
php bin/vendors install --reinstall
Activez les paquets dans votre autoload.php:
// app/autoload.php $loader->registerNamespaces(array( // ... 'Sonata' => __DIR__.'/../vendor/bundles', 'Exporter' => __DIR__.'/../vendor/exporter/lib', 'Knp\Bundle' => __DIR__.'/../vendor/bundles', 'Knp\Menu' => __DIR__.'/../vendor/KnpMenu/src', // ... ));
et AppKernel.php:
// app/AppKernel.php public function registerBundles() { return array( // ... new Sonata\AdminBundle\SonataAdminBundle(), new Sonata\BlockBundle\SonataBlockBundle(), new Sonata\CacheBundle\SonataCacheBundle(), new Sonata\jQueryBundle\SonatajQueryBundle(), new Sonata\DoctrineORMAdminBundle\SonataDoctrineORMAdminBundle(), new Knp\Bundle\MenuBundle\KnpMenuBundle(), // ... ); }
Vous aurez aussi besoin de modifier votre fichier app/config/config.yml. Ajoutez ce qui suit à la fin:
# app/config/config.yml sonata_admin: title: Jobeet Admin sonata_block: default_contexts: [cms] blocks: sonata.admin.block.admin_list: contexts: [admin] sonata.block.service.text: sonata.block.service.action: sonata.block.service.rss:
Aussi, recherchez l'entrée translator et décommentez-la si elle est commentée:
framework: translator: { fallback: %locale% }
Maintenant, installez les assets des paquets:
php app/console assets:install web
N'oubliez pas de vider le cache:
php app/console cache:clear --env=dev php app/console cache:clear --env=prod
Maintenant importez les routes d'admin dans le fichier de routage de l'application:
# app/config/routing.yml admin: resource: '@SonataAdminBundle/Resources/config/routing/sonata_admin.xml' prefix: /admin _sonata_admin: resource: . type: sonata_admin prefix: /admin # ...
Vous devriez maintenant être en mesure d'accéder à l'interface d'administration en utilisant l'URL suivante:
http://jobeet.local/app_dev.php/admin/dashboard
Le contrôleur CRUD contient les actions de base CRUD. Elle est liée à une classe Admin en mappant le nom du contrôleur à l'instance Admin valide. Toutes ou partie des actions peuvent être remplacées en fonction des besoins du projet. Le contrôleur utilise la classe Admin pour construire les différentes actions. A l'intérieur du contrôleur, l'objet Admin est accessible via la propriété de configuration.
Maintenant créez un contrôleur pour chaque entité. D'abord pour Category:
// src/Ens/JobeetBundle/Controller/CategoryAdminController.php namespace Ens\JobeetBundle\Controller; use Sonata\AdminBundle\Controller\CRUDController as Controller; class CategoryAdminController extends Controller { }
Et maintenant, pour Job:
// src/Ens/JobeetBundle/Controller/JobAdminController.php namespace Ens\JobeetBundle\Controller; use Sonata\AdminBundle\Controller\CRUDController as Controller; class JobAdminController extends Controller { }
La classe Admin représente la cartographie de votre modèle et sections d'admininstration (formulaires, listes, affichages). La meilleure façon de créer une classe d'administration de votre modèle est d'étendre la classe Sonata\AdminBundle\Admin\Admin. Nous allons créer des classes d'admin dans le dossier admin de notre paquet. Pour les catégories:
// src/Ens/JobeetBundle/Admin/CategoryAdmin.php namespace Ens\JobeetBundle\Admin; use Sonata\AdminBundle\Admin\Admin; use Sonata\AdminBundle\Datagrid\ListMapper; use Sonata\AdminBundle\Datagrid\DatagridMapper; use Sonata\AdminBundle\Validator\ErrorElement; use Sonata\AdminBundle\Form\FormMapper; class CategoryAdmin extends Admin { }
Et pour les offres:
// src/Ens/JobeetBundle/Admin/JobAdmin.php namespace Ens\JobeetBundle\Admin; use Sonata\AdminBundle\Admin\Admin; use Sonata\AdminBundle\Datagrid\ListMapper; use Sonata\AdminBundle\Datagrid\DatagridMapper; use Sonata\AdminBundle\Validator\ErrorElement; use Sonata\AdminBundle\Form\FormMapper; class JobAdmin extends Admin { }
Maintenant, nous devons ajouter chaque classe admin dans le fichier de configuration services.yml:
# src/Ens/JobeetBundle/Resources/config/services.yml services: ens.jobeet.admin.category: class: Ens\JobeetBundle\Admin\CategoryAdmin tags: - { name: sonata.admin, manager_type: orm, group: jobeet, label: Categories } arguments: [null, Ens\JobeetBundle\Entity\Category, EnsJobeetBundle:CategoryAdmin] ens.jobeet.admin.job: class: Ens\JobeetBundle\Admin\JobAdmin tags: - { name: sonata.admin, manager_type: orm, group: jobeet, label: Jobs } arguments: [null, Ens\JobeetBundle\Entity\Job, EnsJobeetBundle:JobAdmin]
A ce stade, nous pouvons voir dans le tableau de bord le groupe Jobeet et à l'intérieur les modules Job et Category, avec leurs liens respectifs d'ajout et de liste.
À ce stade, si l'on suit n'importe quel lien rien ne se passera. C'est parce que nous n'avons pas configuré les champs qui appartiennent à la liste et au formulaire. Faisons une configuration de base, d'abord pour les catégories:
// src Ens/JobeetBundle/Admin/CategoryAdmin.php namespace Ens\JobeetBundle\Admin; use Sonata\AdminBundle\Admin\Admin; use Sonata\AdminBundle\Datagrid\ListMapper; use Sonata\AdminBundle\Datagrid\DatagridMapper; use Sonata\AdminBundle\Validator\ErrorElement; use Sonata\AdminBundle\Form\FormMapper; class CategoryAdmin extends Admin { // setup the default sort column and order protected $datagridValues = array( '_sort_order' => 'ASC', '_sort_by' => 'name' ); protected function configureFormFields(FormMapper $formMapper) { $formMapper ->add('name') ->add('slug') ; } protected function configureDatagridFilters(DatagridMapper $datagridMapper) { $datagridMapper ->add('name') ; } protected function configureListFields(ListMapper $listMapper) { $listMapper ->addIdentifier('name') ->add('slug') ; } }
Et maintenant pour les offres:
// src Ens/JobeetBundle/Admin/JobAdmin.php namespace Ens\JobeetBundle\Admin; use Sonata\AdminBundle\Admin\Admin; use Sonata\AdminBundle\Datagrid\ListMapper; use Sonata\AdminBundle\Datagrid\DatagridMapper; use Sonata\AdminBundle\Validator\ErrorElement; use Sonata\AdminBundle\Form\FormMapper; use Sonata\AdminBundle\Show\ShowMapper; use Ens\JobeetBundle\Entity\Job; class JobAdmin extends Admin { // setup the defaut sort column and order protected $datagridValues = array( '_sort_order' => 'DESC', '_sort_by' => 'created_at' ); protected function configureFormFields(FormMapper $formMapper) { $formMapper ->add('category') ->add('type', 'choice', array('choices' => Job::getTypes(), 'expanded' => true)) ->add('company') ->add('file', 'file', array('label' => 'Company logo', 'required' => false)) ->add('url') ->add('position') ->add('location') ->add('description') ->add('how_to_apply') ->add('is_public') ->add('email') ->add('is_activated') ; } protected function configureDatagridFilters(DatagridMapper $datagridMapper) { $datagridMapper ->add('category') ->add('company') ->add('position') ->add('description') ->add('is_activated') ->add('is_public') ->add('email') ->add('expires_at') ; } protected function configureListFields(ListMapper $listMapper) { $listMapper ->addIdentifier('company') ->add('position') ->add('location') ->add('url') ->add('is_activated') ->add('email') ->add('category') ->add('expires_at') ->add('_action', 'actions', array( 'actions' => array( 'view' => array(), 'edit' => array(), 'delete' => array(), ) )) ; } protected function configureShowField(ShowMapper $showMapper) { $showMapper ->add('category') ->add('type') ->add('company') ->add('webPath', 'string', array('template' => 'EnsJobeetBundle:JobAdmin:list_image.html.twig')) ->add('url') ->add('position') ->add('location') ->add('description') ->add('how_to_apply') ->add('is_public') ->add('is_activated') ->add('token') ->add('email') ->add('expires_at') ; } }
Pour l'action show, nous avons utilisé un template personnalisé pour afficher le logo de l'entreprise:
<!-- src/Ens/JobeetBundle/Resources/views/JobAdmin/list_image.html.twig --> <tr> <th>Logo</th> <td><img src="{{ asset(object.webPath) }}" /></td> </tr>
Grâce à cela, nous avons créé un module d'administration de base avec les opérations CRUD pour nos offres et catégories. Parmi les fonctionnalités que vous trouverez lors de son utilisation:
Les actions batch sont des actions déclenchées sur un ensemble de modèles sélectionnés (tous ou seulement un sous-ensemble spécifique). Vous pouvez facilement ajouter une action batch personnalisée dans la liste. Par défaut, l'action delete permet de supprimer plusieurs entrées à la fois.
Pour ajouter une nouvelle action batch, nous devons remplacer la méthode getBatchActions de la classe Admin. Nous allons définir ici une nouvelle action extend:
// src/Ens/JobeetBundle/Admin/JobAdmin.php // ... public function getBatchActions() { // retrieve the default (currently only the delete action) actions $actions = parent::getBatchActions(); // check user permissions if($this->hasRoute('edit') && $this->isGranted('EDIT') && $this->hasRoute('delete') && $this->isGranted('DELETE')) { $actions['extend'] = array( 'label' => 'Extend', 'ask_confirmation' => true // If true, a confirmation will be asked before performing the action ); } return $actions; }
La méthode batchActionExtend de JobAdminController sera exécutée pour atteindre la logique de base. Les modèles sélectionnés sont transmis à la méthode par le biais d'un argument requête qui les récupére. Si, pour une raison quelconque, il est logique d'effectuer votre action batch sans la méthode de sélection par défaut (par exemple vous avez défini une autre façon, au niveau du template, pour sélectionner le modèle à moindre granularité), la requête passée est null.
// src/Ens/JobeetBundle/Controller/JobAdminController.php namespace Ens\JobeetBundle\Controller; use Sonata\AdminBundle\Controller\CRUDController as Controller; use Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery as ProxyQueryInterface; use Symfony\Component\HttpFoundation\RedirectResponse; class JobAdminController extends Controller { public function batchActionExtend(ProxyQueryInterface $selectedModelQuery) { if ($this->admin->isGranted('EDIT') === false || $this->admin->isGranted('DELETE') === false) { throw new AccessDeniedException(); } $request = $this->get('request'); $modelManager = $this->admin->getModelManager(); $selectedModels = $selectedModelQuery->execute(); try { foreach ($selectedModels as $selectedModel) { $selectedModel->extend(); $modelManager->update($selectedModel); } } catch (\Exception $e) { $this->get('session')->setFlash('sonata_flash_error', $e->getMessage()); return new RedirectResponse($this->admin->generateUrl('list',$this->admin->getFilterParameters())); } $this->get('session')->setFlash('sonata_flash_success', sprintf('The selected jobs validity has been extended until %s.', date('m/d/Y', time() + 86400 * 30))); return new RedirectResponse($this->admin->generateUrl('list',$this->admin->getFilterParameters())); } }
Ajoutons une nouvelle action batch qui va supprimer toutes les offres qui n'ont pas été activées par le recruteur depuis plus de 60 jours. Pour cette action, nous n'avons pas besoin de sélectionner des offres d'emploi de la liste parce que la logique de l'action sera de rechercher les enregistrements correspondants et les supprimer.
// src/Ens/JobeetBundle/Admin/JobAdmin.php // ... public function getBatchActions() { // retrieve the default (currently only the delete action) actions $actions = parent::getBatchActions(); // check user permissions if($this->hasRoute('edit') && $this->isGranted('EDIT') && $this->hasRoute('delete') && $this->isGranted('DELETE')){ $actions['extend'] = array( 'label' => 'Extend', 'ask_confirmation' => true // If true, a confirmation will be asked before performing the action ); $actions['deleteNeverActivated'] = array( 'label' => 'Delete never activated jobs', 'ask_confirmation' => true // If true, a confirmation will be asked before performing the action ); } return $actions; }
En plus de créer l'action batchActionDeleteNeverActivated, nous allons créer une nouvelle méthode dans JobAdminController, batchActionDeleteNeverActivatedIsRelevant, qui est exécuté avant toute confirmation, pour s'assurer qu'y a quelque chose pour confirmer. Dans notre cas elle renverra toujours true parce que le choix des offres qui seront supprimées est géré par la logique dans la méthode JobRepository::cleanup()
// src/Ens/JobeetBundle/Controller/JobAdminController.php // ... public function batchActionDeleteNeverActivatedIsRelevant() { return true; } public function batchActionDeleteNeverActivated() { if ($this->admin->isGranted('EDIT') === false || $this->admin->isGranted('DELETE') === false) { throw new AccessDeniedException(); } $em = $this->getDoctrine()->getEntityManager(); $nb = $em->getRepository('EnsJobeetBundle:Job')->cleanup(60); if ($nb) { $this->get('session')->setFlash('sonata_flash_success', sprintf('%d never activated jobs have been deleted successfully.', $nb)); } else { $this->get('session')->setFlash('sonata_flash_info', 'No job to delete.'); } return new RedirectResponse($this->admin->generateUrl('list',$this->admin->getFilterParameters())); }
Dans le prochain chapitre, nous allons voir comment sécuriser la section admin avec un nom d'utilisateur et un mot de passe. Ce sera l'occasion de parler de la sécurité Symfony2.