Symfony 4 / Sonata : Créer une interface de paramétrage.

Un des besoins récurent d’un projet web, est d’avoir besoin de paramètres globaux pour votre application. Généralement, on stock ça dans un fichier yml et le tour est joué. Mais cela se complique lorsque le client demande à pouvoir avoir la main dessus.
Lui donner les accès FTP/SSH pour les modifier ? Non, certainement pas. Surtout si c’est pour refiler cette tache au stagiaire.

Il faudra lui mettre à disposition une interface d’admin avec un formulaire pour pouvoir les modifier.

Dans notre cas, nous avons besoin de juste de quelques paramètres, mais cette interface permettra de gérer autant de paramètres que nous souhaitons.

Création de notre table

On créé une table toute bête avec mysqlWorkbench, ou avec make:entity. Au choix.
Dans notre exemple, nous allons juste utiliser 2 champs date pour un début et fin d’une offre.
Nous avons besoin d’un champs clef, qui contiendra le mot clef qui permettra de pointer dessus. Et un champs valeur, qui contiendra une chaîne avec la valeur sérialisée.
Et un dernier champs avec la dernière date de mise à jour.

Sélection_219

Création de notre interface d’admin

Une fois notre table créé avec MysqlWorkbench ou PhpMyAdmin, on crée notre entité et repository

php bin/console doctrine:mapping:import "App\Entity" annotation --path=src/Entity

Et pour que notre repository soit créé, nous faudra rajouter l’annotation suivante et régénérer le tout

@ORM\Entity(repositoryClass="App\Repository\parametresRepository")

Pour donner ceci :

/**
 * Parametres
 *
 * @ORM\Table(name="parametres", indexes={@ORM\Index(name="clef", columns={"clef"})})
 * @ORM\Entity(repositoryClass="App\Repository\parametresRepository")
 */
class Parametres
{

On régénère les fichiers

php bin/console make:entity --regenerate App

On créé notre admin :

php bin/console make:sonata:admin
image-4

Et voila, à ce stade nous avons créé notre interface d’admin de paramétrage, avec un CRUD simple.

image-5


Sauf, que ce n’est pas ce qu’on veux, même si ça fonctionnerais quand même.
On va donc créer une page d’admin où l’utilisateur ne pourras pas créer et modifier ce qu’il veux, mais uniquement nos 2 paramètres.

Par défaut, notre interface pointe sur la vue list. c’est cette action que nous allons utiliser.
Et pour la surcharger il va falloir créer un controller.

Sélection_216

Puis l’ajouter dans la configuration notre service admin en rajoutant le controller (App\Controller\ParametresController) dans les arguments :

    admin.parametres:
        class: App\Admin\ParametresAdmin
        arguments: [~, App\Entity\Parametres, App\Controller\ParametresController]
        tags:
            - { name: sonata.admin, manager_type: orm, group: admin, label: Parametres }
        public: true

Ensuite, nous devons changer l’extend de notre controller pour qu’il utilise CRUDController au lieux de AbstractController. Nous devons aussi ajouter notre méthode listAction qui pointe sur son propre template.

<?php

namespace App\Controller;

#use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Sonata\AdminBundle\Controller\CRUDController;

class ParametresController extends CRUDController
{
    
    public function listAction()
    {
        
        
        if (false === $this->admin->isGranted('LIST')) {
            throw new AccessDeniedException();
        }
        
        return $this->render('Admin/parametres/listAction.html.twig', [
            'controller_name' => 'ParametresController',
        ]);
    }
}

Installer un datePicker

Comme notre paramètre est une date. Il va falloir que nous installions un datepicker. La documentation parle de eonasdan bootstrap-datetimepicker

composer require eonasdan/bootstrap-datetimepicker
php bin/console assets:install

On rajoute un thème à twig

# config/packages/twig.yaml.yml

twig:
    form_themes:
        - '@SonataCore/Form/datepicker.html.twig'

Et on ajoute la référence a nos assets dans sonata_admin.yaml

    assets:
        extra_stylesheets:
            - bundles/sonataformatter/markitup/skins/sonata/style.css
            - bundles/sonataformatter/markitup/sets/markdown/style.css
            - bundles/sonataformatter/markitup/sets/html/style.css
            - bundles/sonataformatter/markitup/sets/textile/style.css
            - css/admin.css
            - build/admin.css
            - css/fontawesome/css/all.css
            - bundles/sonatacore/vendor/eonasdan-bootstrap-datetimepicker/build/css/bootstrap-datetimepicker.min.css


        extra_javascripts:
            - bundles/fosckeditor/ckeditor.js
            - bundles/sonataformatter/vendor/markitup-markitup/markitup/jquery.markitup.js
            - bundles/sonataformatter/markitup/sets/markdown/set.js
            - bundles/sonataformatter/markitup/sets/html/set.js
            - bundles/sonataformatter/markitup/sets/textile/set.js
            - bundles/pixsortablebehavior/js/jquery-ui.min.js
            - bundles/pixsortablebehavior/js/init.js
            - bundles/sonatacore/vendor/eonasdan-bootstrap-datetimepicker/build/js/bootstrap-datetimepicker.min.js
            - js/admin.js

Création de notre formulaire

En premier lieu nous allons créer une méthode pour mettre à jour nos valeurs selon nos clefs de notre paramètre. Cette méthode sera placée dans notre repository.

<?php

namespace App\Repository;

use App\Entity\Parametres;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;


class ParametresRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        $this->registry=$registry;
        $this->connection=$this->registry->getManager()->getConnection();

        parent::__construct($registry, Parametres::class);
    }

    public function updateConfig($clef,$valeur){
        $em = $this->registry->getManager();
        $item = $this->createQueryBuilder('p')
        ->andWhere('p.clef = :val')
        ->setParameter('val', $clef)
        ->getQuery()
        ->getOneOrNullResult();
        if (!$item) {
            $item = new Parametres();
            $item->setValeur($valeur);
            $item->setClef($clef);
            $item->setUpdatedAt(date("Y-m-d H:i:s",strtotime('now')));
            $em->persist($item);
        }else{
            $item->setValeur($valeur);
            $item->setUpdatedAt(date("Y-m-d H:i:s",strtotime('now')));
        }
       $em->flush();
    }
}

Ensuite, nous voulons stoker des valeurs. Elles peuvent être de toutes natures. Nous allons donc stocker des éléments sérialisés.
Il faut donc que nous modifions notre getter et notre setter de notre entity Parametres. Et mettre notre champs date en type string pour plus de facilités.

    public function getValeur(): ?string
    {
        return unserialize($this->valeur);
    }

    public function setValeur(?string $valeur): self
    {
        $this->valeur = serialize($valeur);

        return $this;
    }

    public function getUpdatedAt(): ?string
    {
        return $this->updatedAt;
    }

    public function setUpdatedAt(?string $updatedAt): self
    {
        $this->updatedAt = $updatedAt;

        return $this;
    }

On peu ensuite créer notre formulaire, avec sa vérification dans notre controller. On rajoute tout de même quelques fonctions pour gérer nos dates.

<?php

namespace App\Controller;

use Symfony\Component\Routing\Annotation\Route;
use Sonata\AdminBundle\Controller\CRUDController;
use App\Entity\Parametres;

use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\HttpFoundation\Request;
use Sonata\CoreBundle\Form\Type\DatePickerType;
use Sonata\CoreBundle\Form\Type\DateTimePickerType;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Validator\Context\ExecutionContextInterface;

class ParametresController extends CRUDController
{
    
    private function getConfigData() {
        $em = $this->container->get('doctrine.orm.entity_manager');
        $config = $em->getRepository(Parametres::class)->findAll();
        $data=[];
        foreach($config as $c){
            $data[$c->getClef()]=$c->getValeur();
        }
        return $data;
    }
    
    public function listAction()
    {
        
        
        if (false === $this->admin->isGranted('LIST')) {
            throw new AccessDeniedException();
        }
        
        $request = $this->getRequest();
        $locale = $this->getRequest()->getLocale();
        
        $data = $this->getConfigData();
       
        
        $formBuilder = $this->createFormBuilder(null, [
            'constraints' => [new Callback([$this, 'formValidate'])]
        ]);
        $formBuilder->add("offre_date_debut", DatePickerType::class, [
            'required' => true,
            'dp_use_current' => false,
            'dp_min_date' => new \DateTime("2020-01-01"),
            'data' => isset($data['offre_date_debut']) ? new \DateTime('@'.$this->parseDate($data['offre_date_debut'],$locale)) : new \DateTime(),
            'mapped' => false,
        ])
        ->add("offre_date_fin", DatePickerType::class, [
            'required' => true,
            'dp_use_current' => false,
            'dp_min_date' => new \DateTime("2020-01-01"),
            'data' => isset($data['offre_date_fin']) ? new \DateTime('@'.$this->parseDate($data['offre_date_fin'],$locale)) : new \DateTime(),
            'mapped' => false,
        ])
        ->add('submit', SubmitType::class, [
            'label' => $this->get('translator')->trans('Valider')
        ]);

        $form = $formBuilder->getForm();
        $form->handleRequest($request);
        
        if ($form->isSubmitted() && $form->isValid()) {
            $formData = $this->getRequest()->request->get('form');
            $ParametresRepositoty = $this->container->get('doctrine.orm.entity_manager')->getRepository(Parametres::class);
            $ParametresRepositoty->updateConfig('offre_date_debut', date("Y-m-d", $this->parseDate($formData['offre_date_debut'],$locale)) );
            $ParametresRepositoty->updateConfig('offre_date_fin',date("Y-m-d",$this->parseDate($formData['offre_date_fin'],$locale)));
            $this->addFlash('success', $this->get('translator')->trans('Parametres sauvegardés.'));
        }
        
        
        return $this->render('Admin/parametres/listAction.html.twig', [
            'controller_name' => 'ParametresController',
            'form' => $form->createView()
        ]);
    }
    

    
    public function formValidate($data, ExecutionContextInterface $context) {
    
        $data = $this->getRequest()->request->get('form');
        $locale = $this->getRequest()->getLocale();
        
        if (isset($data['offre_date_debut'])) {
            $offre_date_debut = $this->parseDate($data['offre_date_debut'], $locale);
            $offre_date_debut = new \DateTime("@$offre_date_debut");
        }
        if (isset($data['offre_date_fin'])) {
            $offre_date_fin = $this->parseDate($data['offre_date_fin'], $locale);
            $offre_date_fin = new \DateTime("@$offre_date_fin");
        }
    }
    
    
    public function parseDate($date, $locale, $format = 'dd LLL. y') {
        $fmt = \IntlDateFormatter::create(
            $locale,
            \IntlDateFormatter::FULL,
            \IntlDateFormatter::FULL,
            'Etc/UTC',
            \IntlDateFormatter::GREGORIAN,
            $format
            );
        if (isset($date)) {
            $parse_date = $fmt->parse($date);
            return $parse_date;
        }
        return null;
    }
}

Notre dernière étape consiste à mettre en forme notre page dans notre template twig que nous avons placé dans templates/Admin/parametres/listAction.html.twig

{% extends '@SonataAdmin/standard_layout.html.twig' %}

{% block notice %}
    {{ parent() }}
{% endblock %}

{% form_theme form _self %}

{# form_errors.html.twig #}
{% block form_errors %}
    {% spaceless %}
        {% if errors|length > 0 %}
            {% for error in errors %}
            <div class="alert alert-danger alert-dismissable">
            <button type="button" class="close" data-dismiss="alert" aria-hidden="true" aria-label="Fermer">×</button>
                {{ error.message }}
            </div>
            {% endfor %}
        {% endif %}
    {% endspaceless %}
{% endblock form_errors %}

{% block sonata_admin_content %}
    {% include 'SonataCoreBundle:FlashMessage:render.html.twig' %}

    <div>
        {{ form_errors(form) }}
        <h2 class="title-border">{{ 'Paramétrage'|trans }}</h2>
        <p>{{ 'Modifiaction des parametres transverse de l\'application'|trans }}</p>
        <div class="sonata-ba-form">
        {{ form_start(form, { attr: { class: 'form-setting-general form-theme' }}) }}
        <div class="box-body container-fluid">
            <div class="sonata-ba-collapsed-fields form-theme-hoz">
                <div class="row">
                    <div class="">
                        <div class="box box-primary">
                            <div class="box-header with-border">
                                <h4 class="box-title">{{ 'Dates par défaut des offres'|trans }}</h4>
                            </div>
                            <div class="box-body">
                            
                                <div class="sonata-ba-collapsed-fields">
                                    <div class="form-group">
                                        <label class="control-label required" for="form_date_manifestation">{{ 'Date de début'|trans }}</label>
                                        <div class="sonata-ba-field sonata-ba-field-standard-natural">
                                            {{ form_widget(form.offre_date_debut, { attr: { class: 'sonata-medium-date form-control' }}) }}
                                        </div>
                                    </div>
                                </div>
                                
                                <div class="sonata-ba-collapsed-fields">
                                    <div class="form-group">
                                        <label class="control-label required" for="form_date_manifestation">{{ 'Date de fin'|trans }}</label>
                                        <div class="sonata-ba-field sonata-ba-field-standard-natural">
                                            {{ form_widget(form.offre_date_fin, { attr: { class: 'sonata-medium-date form-control' }}) }}
                                        </div>
                                    </div>
                                </div>
                                
                                
                            </div>
                        </div>
                    </div>

                    <div class="sonata-ba-form-actions well well-small form-actions">
                                   {{ form_widget(form.submit, { attr: { class: 'btn btn-success' }}) }}                                                                                                                
                     </div>


                </div>
            </div>
        </div>
                                                                                         
        {{ form_end(form) }}
        </div>
    </div>
{% endblock %}

Et voici le rendu de notre page

image-6