Symfony 4 / Sonata: Crear una interfaz de configuración.

Una de las necesidades recurrentes de un proyecto web es la necesidad de parámetros globales para su aplicación. Generalmente, estos se almacenan en un archivo yml y eso es todo lo que necesitas. Pero se complica cuando el cliente pide poder tener control sobre ellos.
¿Darles acceso FTP/SSH para modificarlos? No, definitivamente no. Especialmente si se trata de delegar esta tarea a un becario.

Necesitarás proporcionarles una interfaz de administración con un formulario para poder modificar estos parámetros.

En nuestro caso, solo necesitamos unos pocos parámetros, pero esta interfaz nos permitirá gestionar tantos parámetros como deseemos.

Creando nuestra tabla

Creamos una tabla simple con MySQLWorkbench, o con make:entity. La elección es tuya.
En nuestro ejemplo, usaremos solo 2 campos de fecha para el inicio y el fin de una oferta.
Necesitamos un campo clave, que contendrá la palabra clave que se usará para referenciarla. Y un campo de valor, que contendrá una cadena con el valor serializado.
Y un último campo con la fecha de la última actualización.

Sélection_219

Creando nuestra interfaz de administración

Una vez que nuestra tabla está creada con MySQLWorkbench o PhpMyAdmin, creamos nuestra entidad y repositorio

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

Y para que nuestro repositorio sea creado, necesitaremos agregar la siguiente anotación y regenerar todo

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

Para dar esto:

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

Regeneramos los archivos

php bin/console make:entity --regenerate App

Creamos nuestro admin:

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

Y ahí lo tienes, en esta etapa hemos creado nuestra interfaz de administración para configurar parámetros, con un CRUD simple.

image-5


Excepto que esto no es lo que queremos, aunque seguiría funcionando.
Vamos a crear una página de administración donde el usuario no pueda crear y modificar lo que quiera, sino solo nuestros 2 parámetros.

Por defecto, nuestra interfaz apunta a la vista de lista. Esa es la acción que usaremos.
Y para sobrescribirla necesitaremos crear un controlador.

Sélection_216

Luego lo agregamos a la configuración de nuestro servicio de administración agregando el controlador (App\Controller\ParametresController) a los argumentos:

    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

A continuación, necesitamos cambiar la extensión de nuestro controlador para que utilice CRUDController en lugar de AbstractController. También necesitamos agregar nuestro método listAction que apunta a su propia plantilla.

<?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',
        ]);
    }
}

Instalando un datePicker

Dado que nuestro parámetro es una fecha, necesitaremos instalar un datepicker. La documentación habla sobre eonasdan bootstrap-datetimepicker

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

Agregamos un tema a twig

# config/packages/twig.yaml.yml

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

Y agregamos la referencia a nuestros assets en 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

Creando nuestro formulario

Primero, crearemos un método para actualizar nuestros valores de acuerdo con nuestra clave de configuración. Este método se colocará en nuestro repositorio.

<?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();
    }
}

A continuación, queremos almacenar valores. Pueden ser de cualquier naturaleza. Por lo tanto, almacenaremos elementos serializados.
Necesitamos modificar nuestro getter y setter de nuestra entidad Parametres. Y cambiar nuestro campo de fecha a un tipo de cadena para facilitar su uso.

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

Entonces podemos crear nuestro formulario, con su verificación en nuestro controlador. Aún agregamos algunas funciones para gestionar nuestras fechas.

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

Nuestro último paso es dar estilo a nuestra página en nuestra plantilla twig que colocamos en 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 %}

Y aquí está el renderizado de nuestra página

image-6

``` Tenga en cuenta que los marcadores de posición como `{{figure_placeholder_X}}` y `{{code_placeholder_X}}` se han mantenido en su forma original, ya que parecen ser marcadores de plantilla o del sistema destinados a la inserción de contenido dinámico y no forman parte del texto traducible.