Symfony 4 / Sonata: Create a configuration interface.

One of the recurring needs of a web project is the need for global parameters for your application. Generally, these are stored in a yml file and that's all you need. But it gets complicated when the client asks to be able to have control over them.
Give them FTP/SSH access to modify them? No, definitely not. Especially if it’s to delegate this task to an intern.

You will need to provide them with an admin interface with a form to be able to modify these parameters.

In our case, we only need a few parameters, but this interface will allow us to manage as many parameters as we wish.

Creating our table

We create a simple table with MySQLWorkbench, or with make:entity. The choice is yours.
In our example, we will just use 2 date fields for the start and end of an offer.
We need a key field, which will contain the keyword that will be used to refer to it. And a value field, which will contain a string with the serialized value.
And one last field with the last update date.

Sélection_219

Creating our admin interface

Once our table is created with MySQLWorkbench or PhpMyAdmin, we create our entity and repository

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

And for our repository to be created, we will need to add the following annotation and regenerate everything

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

To give this:

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

We regenerate the files

php bin/console make:entity --regenerate App

We create our admin:

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

And there you have it, at this stage we have created our admin interface for setting parameters, with a simple CRUD.

image-5


Except that this is not what we want, even if it would still work.
We are going to create an admin page where the user cannot create and modify whatever they want, but only our 2 parameters.

By default, our interface points to the list view. That's the action we will use.
And to override it we will need to create a controller.

Sélection_216

Then add it to the configuration of our admin service by adding the controller (App\Controller\ParametresController) to the 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

Next, we need to change the extend of our controller so that it uses CRUDController instead of AbstractController. We also need to add our listAction method which points to its own 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',
        ]);
    }
}

Installing a datePicker

Since our parameter is a date, we will need to install a datepicker. The documentation talks about eonasdan bootstrap-datetimepicker

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

We add a theme to twig

# config/packages/twig.yaml.yml

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

And we add the reference to our assets in 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

Creating our form

First, we will create a method to update our values according to our settings key. This method will be placed in our 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();
    }
}

Next, we want to store values. They can be of any nature. Therefore, we will store serialized elements.
We need to modify our getter and setter of our Parametres entity. And change our date field to a string type for ease of use.

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

We can then create our form, with its verification in our controller. We still add some functions to manage our 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;
    }
}

Our last step is to style our page in our twig template that we placed in 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 %}

And here is the rendering of our page

image-6