Sonata Admin et Symfony

Illustration article du blog

Prérequis

Si une gestion des traductions est nécessaire, et que vous voulez utiliser la traduction par défaut du bundle, vérifiez que la config suivante est activée dans app/config/config.yml :

framework:
    translator: ~

Installation

Télécharger les dépendances

[SonataAdminBundle]
    git=http://github.com/sonata-project/SonataAdminBundle.git
 target=/bundles/Sonata/AdminBundle
 version=origin/2.0
 [SonataBlockBundle]
    git=http://github.com/sonata-project/SonataBlockBundle.git
 target=/bundles/Sonata/BlockBundle
[SonataCacheBundle]
    git=http://github.com/sonata-project/SonataCacheBundle.git
 target=/bundles/Sonata/CacheBundle
[SonatajQueryBundle]
    git=http://github.com/sonata-project/SonatajQueryBundle.git
 target=/bundles/Sonata/jQueryBundle
[KnpMenuBundle]git=http://github.com/KnpLabs/KnpMenuBundle.git target=/bundles/Knp/Bundle/MenuBundle[KnpMenu]git=http://github.com/KnpLabs/KnpMenu.git target=/knp/menu
[Exporter]
    git=http://github.com/sonata-project/exporter.git
 target=/exporter
[SonataDoctrineORMAdminBundle]
    git=http://github.com/sonata-project/SonataDoctrineORMAdminBundle.git
 target=/bundles/Sonata/DoctrineORMADminBundle

Si vous rencontrez le problème suivant lors de l’exécution de php bin/vendors install, suivez ces lignes :

[SymfonyComponentConfigDefinitionExceptionInvalidConfigurationException]
The child node “default_contexts” at path “sonata_block” must be configured.

Modifier app/config/config.yml et ajouter les lignes suivantes (en théorie, seules les 2 premières lignes sont utiles, mais sans confirmation, il vaut mieux faire ce que la doc officielle conseille) :

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:

Relancer alors php bin/vendor install. Il est aussi possible de rencontrer des problèmes suite au changement de version Symfony2 vers Symfony2.1. Voir ce ticket : http://sonata-project.org/blog/2012/3/15/moving-to-symfony2-1

Enregistrer les namespaces

Dans le fichier autoloader, ajouter les lignes suivantes au registerNamespaces :

$loader->registerNamespaces(array(
    // ...
    'Sonata'     => __DIR__.'/../vendor/bundles',
    'Exporter'   => __DIR__.'/../vendor/exporter/lib',
'KnpBundle'=>__DIR__.'/../vendor/bundles','KnpMenu'=>__DIR__.'/../vendor/knp/menu/src',
    // ...
));

Enregistrer le bundle

Dans appKernel.php :

public function registerBundles()
{
    $bundles = array(
        // ...
        new SonataAdminBundleSonataAdminBundle(),
        new SonataBlockBundleSonataBlockBundle(),
        new SonataCacheBundleSonataCacheBundle(),
        new SonatajQueryBundleSonatajQueryBundle(),
newKnpBundleMenuBundleKnpMenuBundle(),
 new SonataDoctrineORMAdminBundleSonataDoctrineORMAdminBundle(),
    );
    // ...
)

Maintenant, il faut installer les assets des bundles et supprimer le cache :

php app/console assets:install web
php app/console cache:clear

Configurer les routes

Les routes sont toutes inclues dans un fichier de routing qu’il faut référencer dans le routing principal app/config/routing.yml :

admin:
    resource: '@SonataAdminBundle/Resources/config/routing/sonata_admin.xml'
    prefix: /admin
_sonata_admin:
    resource: .
    type: sonata_admin
    prefix: /admin

Dès l’instant que les routes sont en place, l’admin est accessible via http://localhost/admin/dashboard

Configurer le service de persistance (ORM, ODM…)

SonataAdminBundle n’impose pas le service de persistance, qui permet d’utiliser et contrôler les modèles, mais au cas où vous en utilisiez vous pouvez installer un des bundles officiels comme SonataDoctrineORMAdminBundle, SonataDoctrineMongoDBAdminBundle ou SonataDoctrinePhpcrAdminBundle. Dans notre exemple actuel, nous avons installé SonataDoctrineORMAdminBundle.

Utilisation

Créer les entities

Si vous n’en avez pas, créez tout d’abord le bundle et les entités, puis mappez les (astuce : créez vos entités et exécutez php app/console doctrine:generate:entities AcseoTestBundle pour créer les getters et setters, puis php app/console doctrine:schema:update –force pour créer les tables).

Définir le routing

Oh joie, les routes sont automatiquement créées. Nous avons donc list, create, batch, update, edit et delete par défaut !

Créer le contrôleur CRUD

Cette étape n’est pas nécessaire. Si vous décidez de le créer, au cas où vous auriez d’autres actions à créer dedans, il doit étendre de CRUDController pour utiliser les méthodes de CRUD de Sonata. Si aucun contrôleur n’est créé, Sonata utilisera par défaut le sien. On doit donc avoir un contrôleur comme suit, si vous désirez créer un contrôleur :

namespace AcseoTestBundleController;
use SonataAdminBundleControllerCRUDController as Controller;
class CommentAdminController extends Controller
{
}

Créer la classe d’admin

Ressource http://sonata-project.org/bundles/doctrine-orm-admin/master/doc/tutorial/creating_your_first_admin_class/installation.html Cette classe contient toutes les informations requises pour générer l’interface CRUD. Par convention, nous créons un dossier Admin dans chaque bundle, dans lequel nous mettrons les classes EntityAdmin.php. En voici un rapide exemple, pour une entité ayant 2 champs : title et content.

namespace TutorialBlogBundleAdmin;
use SonataAdminBundleAdminAdmin;
use SonataAdminBundleFormFormMapper;
use SonataAdminBundleDatagridDatagridMapper;
use SonataAdminBundleDatagridListMapper;
use SonataAdminBundleShowShowMapper;
use KnpMenuItemInterface as MenuItemInterface;
use AcseoTestBundleEntityPage;
class PageAdmin extends Admin
{
    /**
 * @param SonataAdminBundleShowShowMapper $showMapper
 *
 * @return void
 */
    protected function configureShowField(ShowMapper $showMapper)
    {
        $showMapper
            ->add('title')
            ->add('content')
        ;
    }
    /**
 * @param SonataAdminBundleFormFormMapper $formMapper
 *
 * @return void
 */
    protected function configureFormFields(FormMapper $formMapper)
    {
        $formMapper
            ->with('General')
                ->add('title')
                ->add('content')
            ->end()
        ;
    }
    /**
 * @param SonataAdminBundleDatagridListMapper $listMapper
 *
 * @return void
 */
    protected function configureListFields(ListMapper $listMapper)
    {
        $listMapper
            ->addIdentifier('title')
            ->add('content')
            ->add('_action', 'actions', array(
                'actions' => array(
                    'view' => array(),
                    'edit' => array(),
                    'delete' => array(),
                )
            ))
        ;
    }
    /**
 * @param SonataAdminBundleDatagridDatagridMapper $datagridMapper
 *
 * @return void
 */
    protected function configureDatagridFilters(DatagridMapper $datagridMapper)
    {
        $datagridMapper
            ->add('title')
        ;
    }
}

Puis il faut enregistrer le service dans le fichier services.xml du bundle : Notons que group sera le nom visible de notre module dans l’interface d’admin, et label sera le nom visible de l’entité, il est donc recommandé de les écrire avec majuscules, espaces et tout caractère nécessaire à la bonne compréhension par tous.

<service id="acseo.test.admin.page" class="AcseoTestBundleAdminPageAdmin">
    <tag name="sonata.admin" manager_type="orm" group="Ceci est un test ACSEO" label="Page"/>

Nous avons donc maintenant un backoffice listant chacun de nos bundles, contenant chacun leurs entités, administrables via des CRUD !

Customisation de l’interface

On peut modifier l’affichage de notre interface d’admin via le fichier app/config/config.yml. On peut aussi customiser les champs.

sonata_admin:
    title:      Administration de notre application
    title_logo: /bundles/sonataadmin/logo_title.png
    templates:
        # default global templates
        layout:  SonataAdminBundle::standard_layout.html.twig
        ajax:    SonataAdminBundle::ajax_layout.html.twig
dashboard: SonataAdminBundle:Core:dashboard.html.twig
       # default actions templates, should extend a global templates
        list:    SonataAdminBundle:CRUD:list.html.twig
        show:    SonataAdminBundle:CRUD:show.html.twig
        edit:    SonataAdminBundle:CRUD:edit.html.twig
     dashboard:
        blocks:
            # display a dashboard block
            - { position: left, type: sonata.admin.block.admin_list }
            # Customize this part to add new block configuration
            - { position: right, type: sonata.block.service.text, settings: { content: "
Welcome
totheSonataAdmin
This
isasonata.block.service.textfromtheBlockBundle,youcancreateandaddnewblockintheseareabyconfiguringthesonata_adminsection.<br/>Forinstance,hereaRSSfeedparser(sonata.block.service.rss):"} }
            - { position: right, type: sonata.block.service.rss, settings: { title: Sonata Project's Feeds, url: http://sonata-project.org/blog/archive.rss }}
        groups:
            default: ~
            # on peut aussi afficher les groupes que l'on veut via BlockBundle
# on peut aussi customiser les champs. Les champs par défaut.
sonata_doctrine_orm_admin:
    templates:
        types:
            list:
                array:      SonataAdminBundle:CRUD:list_array.html.twig
                boolean:    SonataAdminBundle:CRUD:list_boolean.html.twig
                date:       SonataAdminBundle:CRUD:list_date.html.twig
                time:       SonataAdminBundle:CRUD:list_time.html.twig
                datetime:   SonataAdminBundle:CRUD:list_datetime.html.twig
                text:       SonataAdminBundle:CRUD:base_list_field.html.twig
                trans:      SonataAdminBundle:CRUD:list_trans.html.twig
                string:     SonataAdminBundle:CRUD:base_list_field.html.twig
                smallint:   SonataAdminBundle:CRUD:base_list_field.html.twig
                bigint:     SonataAdminBundle:CRUD:base_list_field.html.twig
                integer:    SonataAdminBundle:CRUD:base_list_field.html.twig
                decimal:    SonataAdminBundle:CRUD:base_list_field.html.twig
                identifier: SonataAdminBundle:CRUD:base_list_field.html.twig
            show:
                array:      SonataAdminBundle:CRUD:show_array.html.twig
                boolean:    SonataAdminBundle:CRUD:show_boolean.html.twig
                date:       SonataAdminBundle:CRUD:show_date.html.twig
                time:       SonataAdminBundle:CRUD:show_time.html.twig
                datetime:   SonataAdminBundle:CRUD:show_datetime.html.twig
                text:       SonataAdminBundle:CRUD:base_show_field.html.twig
                trans:      SonataAdminBundle:CRUD:show_trans.html.twig
                string:     SonataAdminBundle:CRUD:base_show_field.html.twig
                smallint:   SonataAdminBundle:CRUD:base_show_field.html.twig
                bigint:     SonataAdminBundle:CRUD:base_show_field.html.twig
                integer:    SonataAdminBundle:CRUD:base_show_field.html.twig
                decimal:    SonataAdminBundle:CRUD:base_show_field.html.twig

Sécurité

La dernière chose importante est la sécurité. Par défaut il n’y a aucun système de gestion des utilisateurs, mais il existe SonataUserBundle qui intègre FOSUserBundle.

Configuration avancée

Chaque classe admin d’entité possède 4 fonctions :

  • configureShowField
  • configureFormFields
  • configureListFields
  • configureDatagridFilters

Types de champs disponibles :

  • boolean
  • datetime
  • decimal
  • identifier
  • integer
  • many_to_one
  • string
  • text
  • date
  • time

Si aucun type n’est défini, la classe Admin utilisera le type défini dans la définition du mapping Doctrine.

Configuration des actions dans configureListFields

Par défaut les actions edit et delete sont activées, mais on peut ajouter nos propres actions (dans notre exemple on rajoute delete). Le fichier de template appelé par défaut est SonataAdminBundle:CRUD:list_action[ACTION_NAME].html.twig

Configuration des filtres dans configureDatagridFilters

Il est possible de modifier le label, mais aussi d’ajouter un filtre en callback. Pour créer un filtre en callback, il est nécessaire d’implémenter 2 méthodes : une pour définir le type de champ, et une autre pour définir comment la valeur du champ est utilisée. Cette dernière doit retourner si le filtre est appliqué ou non au queryBuilder. Dans notre exemple, getWithOpenCommentField et getWithOpenCommentFilter implémentent cette fonctionnalité.

protected function configureDatagridFilters(DatagridMapper $datagrid)
{
    $datagrid
        ->add('tags', null, array('label' => 'les tags'), null, array('expanded' => true, 'multiple' => true) # ajout d'un label
       # ajout d'un callback
        ->add('with_open_comments', 'doctrine_orm_callback', array(
// 'callback' => array($this, 'getWithOpenCommentFilter'),
              'callback' => function($queryBuilder, $alias, $field, $value) {
                    if (!$value) return;
                    $queryBuilder->leftJoin(sprintf('%s.comments', $alias), 'c');
                    $queryBuilder->andWhere('c.status = :status');
                    $queryBuilder->setParameter('status', Comment::STATUS_MODERATE);
                    return true;
                }, 'field_type' => 'checkbox'
        ))
    ;
}
public function getWithOpenCommentFilter($queryBuilder, $alias, $field, $value)
{
 if (!$value) return;
    $queryBuilder->leftJoin(sprintf('%s.comments', $alias), 'c');
    $queryBuilder->andWhere('c.status = :status');
    $queryBuilder->setParameter('status', Comment::STATUS_MODERATE);
    return true;
}

Configuration des champs des formulaires dans configureFormFields

Les champs sont requis par défaut.

    protected function configureFormFields(FormMapper $formMapper)
    {
        $formMapper
            ->add('author', 'sonata_type_model', array(), array('edit' => 'list'))
            ->add('enabled')
            ->add('title')
            ->add('abtract', null, array('required' => false))
            ->add('content')
            ->add('binaryContent', 'file', array('required' => false)); // fichier binaire
           // you can define help messages like this
            ->setHelps(array(
               'title' => $this->trans('help_post_title')
            ));
    }
    public function validate(ErrorElement $errorElement, $object)
    {
        // conditional validation, see the related section for more information
        if ($object->getEnabled()) {
            // abstract cannot be empty when the post is enabled
            $errorElement
                ->with('abtract')
                    ->assertNotBlank()
                    ->assertNotNull()
                ->end()
            ;
        }
    }

Dans le cas où il y a une relation many-to-one, nous avons 3 options à disposition :

  • standard : valeur par défaut, listing des user dans un widget de sélection
  • list : la liste de user est dans un modèle où on peut chercher et sélectionner un utilisateur
  • inline : embarqué dans le formulaire user dans le formulaire post. Pratique pour du one-to-one ou si vous voulez donner le droit à l’admin de modifier les informations sur les user
    $formMapper
    ->with('General')
        ->add('enabled', null, array('required' => false))
        ->add('author', 'sonata_type_model', array(), array('edit' => 'list'))
        ->add('title')
        ->add('abstract')
        ->add('content')
    ->end()
    ->with('Tags')
        ->add('tags', 'sonata_type_model', array('expanded' => true))
    ->end()
    ->with('Options', array('collapsed' => true))
        ->add('commentsCloseAt')
        ->add('commentsEnabled', null, array('required' => false))
        ->add('commentsDefaultStatus', 'choice', array('choices' => Comment::getStatusList()))
    ->end()
    ;

    Dans le cas où il y a une relation one-to-many nous avons 3 options à disposition. Prenons une Gallery qui lie à plusieurs Media via une table intermédiaire galleryHasMedias. On peut ajouter une nouvelle ligne galleryHasMedias via ces options :

  • edit : inline|standard, plus de lignes avec le mode inline
  • inline : table|standard, les champs sont présentés sous forme de tableau
  • sortable : si le modèle a un champ position, vous pouvez activer un effet de tri par drag&drop en mettant sortabl=field_name
    $formMapper
    ->add('code')
    ->add('enabled')
    ->add('name')
    ->add('defaultFormat')
    ->add('galleryHasMedias', 'sonata_type_collection', array(), array(
        'edit' => 'inline',
        'inline' => 'table',
        'sortable'  => 'position'
    ))
    ;