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.

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

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

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.

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
