Jobeet FR

Le tutoriel pour Symfony2 en français

Pleine page

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.


Installation du paquet Admin

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

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
{
 
}

Création de la classe Admin

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.


Configuration des classes Admin

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

  • - La liste des objets est paginée
  • - La liste peut être triée
  • - La liste peut être filtrée
  • - Les objets peuvent être créés, modifiés et supprimés
  • - Les objets sélectionnés peuvent être supprimés par lot
  • - La validation du formulaire est activée
  • - Les messages flash donnent une rétroaction immédiate à l'utilisateur

Actions par lot (batch)

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.


Chapitre précédent Chapitre suivant


Une question ? Une réaction ?

comments powered by Disqus