Symfony 4 / Sonata: Tạo giao diện cấu hình.

Một trong những nhu cầu thường xuyên của dự án web là cần có các tham số toàn cục cho ứng dụng của bạn. Thông thường, chúng được lưu trữ trong một tệp yml và đó là tất cả những gì bạn cần. Nhưng mọi thứ trở nên phức tạp khi khách hàng yêu cầu được kiểm soát chúng.
Cho họ quyền truy cập FTP/SSH để chỉnh sửa chúng? Không, tuyệt đối không. Đặc biệt nếu nhiệm vụ này được giao cho một thực tập sinh.

Bạn cần phải cung cấp cho họ một giao diện quản trị với một biểu mẫu để có thể chỉnh sửa các tham số này.

Trong trường hợp của chúng tôi, chúng tôi chỉ cần một vài tham số, nhưng giao diện này sẽ cho phép chúng tôi quản lý nhiều tham số hơn nếu muốn.

Tạo bảng của chúng tôi

Chúng tôi tạo một bảng đơn giản với MySQLWorkbench, hoặc với make:entity. Lựa chọn là của bạn.
Trong ví dụ của chúng tôi, chúng tôi chỉ sử dụng 2 trường ngày cho ngày bắt đầu và kết thúc của một ưu đãi.
Chúng tôi cần một trường khóa, sẽ chứa từ khóa được sử dụng để tham chiếu đến nó. Và một trường giá trị, sẽ chứa một chuỗi với giá trị được tuần tự hóa.
Và cuối cùng là một trường với ngày cập nhật gần nhất.

Sélection_219

Tạo giao diện quản trị của chúng tôi

Sau khi bảng của chúng tôi được tạo với MySQLWorkbench hoặc PhpMyAdmin, chúng tôi tạo thực thể và kho lưu trữ của mình

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

Và để kho lưu trữ của chúng tôi được tạo ra, chúng tôi cần thêm chú thích sau và tái tạo tất cả mọi thứ

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

Để cho điều này:

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

Chúng tôi tái tạo các tệp

php bin/console make:entity --regenerate App

Chúng tôi tạo quản trị viên của mình:

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

Và đó bạn có nó, ở giai đoạn này chúng tôi đã tạo giao diện quản trị của mình để thiết lập các tham số, với một CRUD đơn giản.

image-5


Nhưng đó không phải là những gì chúng tôi muốn, mặc dù nó vẫn sẽ hoạt động.
Chúng tôi sẽ tạo một trang quản trị mà người dùng không thể tạo và chỉnh sửa bất cứ thứ gì họ muốn, nhưng chỉ có 2 tham số của chúng tôi.

Theo mặc định, giao diện của chúng tôi chỉ đến chế độ xem danh sách. Đó là hành động chúng tôi sẽ sử dụng.
Và để ghi đè nó chúng tôi cần tạo một bộ điều khiển.

Sélection_216

Sau đó thêm nó vào cấu hình của dịch vụ quản trị của chúng tôi bằng cách thêm bộ điều khiển (App\Controller\ParametresController) vào các đối số:

    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

Tiếp theo, chúng tôi cần thay đổi mở rộng của bộ điều khiển của mình để nó sử dụng CRUDController thay vì AbstractController. Chúng tôi cũng cần thêm phương thức listAction của mình mà chỉ đến mẫu riêng của nó.

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

Cài đặt một bộ chọn ngày

Vì tham số của chúng tôi là một ngày, chúng tôi sẽ cần cài đặt một bộ chọn ngày. Tài liệu nói về eonasdan bootstrap-datetimepicker

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

Chúng tôi thêm một chủ đề vào twig

# config/packages/twig.yaml.yml

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

Và chúng tôi thêm tham chiếu đến tài sản của mình trong 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

Tạo biểu mẫu của chúng tôi

Đầu tiên, chúng tôi sẽ tạo một phương thức để cập nhật các giá trị của chúng tôi theo khóa cài đặt của chúng tôi. Phương thức này sẽ được đặt trong kho lưu trữ của chúng tôi.

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

Tiếp theo, chúng tôi muốn lưu trữ các giá trị. Chúng có thể là bất kỳ bản chất nào. Do đó, chúng tôi sẽ lưu trữ các phần tử được tuần tự hóa.
Chúng tôi cần chỉnh sửa getter và setter của thực thể Parametres của chúng tôi. Và thay đổi trường ngày của chúng tôi thành kiểu chuỗi để dễ sử dụng hơn.

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

Chúng tôi sau đó có thể tạo biểu mẫu của mình, với việc xác minh của nó trong bộ điều khiển của chúng tôi. Chúng tôi vẫn thêm một số chức năng để quản lý ngày của chúng tôi.

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

Bước cuối cùng của chúng tôi là phong cách trang của chúng tôi trong mẫu twig của chúng tôi mà chúng tôi đặt trong 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 %}

Và đây là kết quả trang của chúng tôi

image-6