Hành Động Tùy Chỉnh Sonata

Theo bài viết của Thomas Bourdin SYMFONY / SONATA: THÊM CHỨC NĂNG NHÂN BẢN VÀO CRUD, chúng tôi sẽ chỉ bạn cách thêm các hành động tùy chỉnh vào giao diện một cách đơn giản.

Trên Bảng điều khiển:

image-23

Nhưng chúng tôi cũng sẽ xem xét cách tùy chỉnh đơn giản và chung chung các hành động tùy chỉnh của danh sách, tiêu đề CRUD của bạn và cuối cùng là cách thêm hành động cho xử lý hàng loạt.

Với ví dụ của tôi, tôi bắt đầu với một bảng đơn giản gọi là Title là entity ở đây. Không cần thiết cho bạn phải khởi chạy dự án với những tệp này nhưng nó thiết lập ngữ cảnh cho bài viết. Vì vậy, chúng tôi có các trường id, title, slug giúp chúng tôi có nội dung.

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use App\Repository\TitlesRepository;
/**
 * Projets
 *
 * @ORM\Table(name="titles")
 * @ORM\Entity(repositoryClass=TitlesRepository::class)
 */
class SlugTitles
{
    /**
     * @ORM\Column(name="id", type="integer", nullable=false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private ?int $id = null;


    /**
     * @ORM\Column(name="title", type="string", length=350, nullable=true)
     */
    private ?string $title = null;

    /**
     * @ORM\Column(name="slug", type="string", length=350, nullable=true)
     */
    private ?string $slug = null;

    /**
     * @return ?int
     */
    public function getId(): ?int
    {
        return $this->id;
    }

    /**
     * @param int $id
     * @return $this
     */
    public function setId(int $id): self
    {
        $this->id = $id;
        return $this;
    }

    /**
     * @return string|null
     */
    public function getTitle(): ?string
    {
        return $this->title;
    }

    /**
     * @param string|null $title
     * @return $this
     */
    public function setTitle(?string $title): self
    {
        $this->title = $title;
        return $this;
    }

    /**
     * @return string|null
     */
    public function getSlug(): ?string
    {
        return $this->slug;
    }

    /**
     * @param string|null $slug
     * @return $this
     */
    public function setSlug(?string $slug): self
    {
        $this->slug = $slug;
        return $this;
    }

    public function __toString(): string
    {
        return (string) $this->title;
    }

}

Về phần cấu hình, chúng tôi khai báo admin của mình (config/packages/sonata_admin.yaml:29,34):

sonata_admin:
    title: 'Sonata Admin'
    title_logo: /bundles/sonataadmin/images/logo_title.png
    show_mosaic_button: false
    security:
        handler: sonata.admin.security.handler.role
    options:
        default_admin_route: edit
        html5_validate: false
    global_search:
        admin_route: edit
    breadcrumbs:
        child_admin_route: edit
    dashboard:
        groups:
            runroom:
            users:
                label: Users
                icon: <i class="fa fa-users"></i>
                on_top: true
                items:
                    - sonata.user.admin.user
            media:
                label: Media
                icon: <i class="fa fa-photo"></i>
                on_top: true
                items:
                    - sonata.media.admin.media
            slug_titles:
                label: "Titles"
                icon: <i class="fa fa-users"></i>
                on_top: true
                items:
                    - admin.titles
        blocks:
            - { type: sonata.admin.block.admin_list, position: left })

sonata_block:
    blocks:
        sonata.admin.block.admin_list:
            contexts: [admin]

và chúng tôi khai báo nó như một Dịch vụ để có thể tiêm các tham số. Điểm quan trọng ở đây là chúng tôi kết hợp một Controller bổ sung với các tham số thông thường (config/services.yaml:20,24):

parameters:

services:
    # default configuration for services in *this* file
    _defaults:
        autowire: true      # Automatically injects dependencies in your services.
        autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.

    # makes classes in src/ available to be used as services
    # this creates a service per class whose id is the fully-qualified class name
    App\:
        resource: '../src/'
        exclude:
            - '../src/DependencyInjection/'
            - '../src/Entity/'
            - '../src/Kernel.php'

    # add more service definitions when explicit configuration is needed
    # please note that last definitions always *replace* previous ones
    admin.titles:
        class: App\Admin\SlugTitlesAdmin
        tags:
            - { name: sonata.admin, model_class: App\Entity\SlugTitles, controller: App\Controller\SlugTitlesAdminController, manager_type: orm, group: admin , label: "Administrations de vos titres de pages" }
        arguments: [ ~, ~, ~]

Bạn sẽ nhận thấy rằng chúng tôi tự động kết hợp một Controller \App\Controller\SlugTitlesAdminController với khai báo trang quản trị của chúng tôi. Giai đoạn này rất quan trọng trong việc thiết lập một trang quản trị. Nó sẽ cho phép chúng tôi chơi với phép màu của Sonata. Thực sự, Sonata sẽ kết hợp một hành động tùy chỉnh trực tiếp với Controller,
Controller sẽ phụ trách phân phối hành động của chúng tôi giữa mô hình và các dịch vụ khác nhau mà chúng tôi có thể cần.

<?php

declare(strict_types=1);

namespace App\Controller;

use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\AdminBundle\Controller\CRUDController;
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

final class SlugTitlesAdminController extends CRUDController
{
    public function batchActionClone( ProxyQueryInterface $selectedModelQuery, AdminInterface $admin, Request $request): RedirectResponse
    {
        return new RedirectResponse(
            $admin->generateUrl('list', [
                 'filter' => $this->admin->getFilterParameters()
            ])
        );
    }

    /**
     * @param $id
     */
    public function cloneAction($id): Response
    {
        return new RedirectResponse(
            $admin->generateUrl('list', [
                 'filter' => $this->admin->getFilterParameters()
            ])
        );
    }

}

Để kết thúc, đây là bản vỏ khá trống rỗng của trang quản trị của chúng tôi.

<?php
declare(strict_types=1);

namespace App\Admin;

use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Route\RouteCollectionInterface;
use Sonata\AdminBundle\Show\ShowMapper;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\TextType;

final class SlugTitlesAdmin extends AbstractAdmin
{
    /**
     * @param string|null $code
     * @param string|null $class
     * @param string|null $baseControllerName
     */
    public function __construct(?string $code, ?string $class,?string  $baseControllerName)
    {
        parent::__construct($code, $class, $baseControllerName);
    }

    /**
     * @param DatagridMapper $filter
     * @return void
     */
    protected function configureDatagridFilters(DatagridMapper $filter): void
    {
        $filter
            ->add('id')
            ->add('title')
            ->add('slug')
        ;
    }

    /**
     * @param ListMapper $list
     * @return void
     */
    protected function configureListFields(ListMapper $list): void
    {
        $list
            ->add('title')
            ->add('slug')
            ->add(ListMapper::NAME_ACTIONS, null, [
                'actions' => [
                    'show' => [],
                    'edit' => [],
                    'delete' => []
                 ]
            ]);
        ;
    }

    /**
     * @param FormMapper $form
     * @return void
     */
    protected function configureFormFields(FormMapper $form): void
    {
        $form
            ->add('title', TextType::class, ['help' => 'Renseignez le titre de votre article'])
            ->add('slug', HiddenType::class)
        ;
    }

    /**
     * @param ShowMapper $show
     * @return void
     */
    protected function configureShowFields(ShowMapper $show): void
    {
        $show
            ->add('id')
            ->add('title')
            ->add('slug')
        ;
    }

}

Điều đó thiết lập ngữ cảnh. Bây giờ chúng tôi có thể thêm các yếu tố khác nhau của giao diện của chúng tôi.

Hành động Tùy chỉnh trên Bảng điều khiển

Nhưng Bảng điều khiển là gì? Đó là trang chủ của chúng tôi. Chính xác hơn, ở đây:

image-26

Chúng tôi sẽ thêm một nút nhập nhỏ. Quan trọng là phải đặt mình vào tình huống :).

Tôi không thích lặp lại mã của mình, càng không khi nói đến Template. Vì vậy, cho hướng dẫn này, tôi đã chuẩn bị một nút nhỏ chung chung mà chúng tôi có thể sử dụng cho mỗi việc thêm một hành động vào trang này.

<a class="btn btn-link btn-flat" href="{{ admin.generateUrl(action.route) }}">
    <i class="fas {{ action.icon | default('')}}"></i>
    {{ action.label | default('') | trans({}, 'default') }}
</a>

Không có gì ngạc nhiên trong template này ngoại trừ một thông tin thú vị. Ở cấp độ TWIG, chúng tôi có quyền truy cập vào $context[‘actions’] sẽ cho phép chúng tôi dễ dàng truyền thông tin từ trang của chúng tôi src/Admin/SlugTitlesAdmin.php đến template của chúng tôi templates/Sonata/default_dashboard_button.html.twig

Đây là ảnh chụp màn hình của debugger khi chúng tôi đặt một điểm dừng trong file templates/Sonata/default_dashboard_button.html.twig

image-27

Quay lại file quản trị của chúng tôi src/Admin/SlugTitlesAdmin.php, chúng tôi sẽ thêm phương thức configureRoutes()

Thực sự, chúng tôi sẽ cần giải thích cho Sonata và do đó là Symfony về các hành động và đường dẫn liên quan mà chúng tôi muốn thiết lập. Điều này quan trọng nếu bạn muốn tùy chỉnh URL.

    /**
     * @param RouteCollectionInterface $collection
     * @return void
     */
    protected function configureRoutes(RouteCollectionInterface $collection): void
    {
        $collection
            ->add('clone', $this->getRouterIdParameter().'/clone')
            ->add('import');
    }

Như bạn thấy, chúng tôi có hai đường dẫn. Nhìn nhanh vào giao diện và bạn sẽ hiểu rằng chúng tôi có quyền truy cập vào hầu như mọi thứ trên Route của chúng tôi.

namespace Sonata\AdminBundle\Route;

use Symfony\Component\Routing\Route;

/**
 * @author Jordi Sala <jordism91@gmail.com>
 */
interface RouteCollectionInterface
{
    /**
     * @param array<string, mixed>  $defaults
     * @param array<string, string> $requirements
     * @param array<string, mixed>  $options
     * @param string[]              $schemes
     * @param string[]              $methods
     *
     * @return static
     */
    public function add(
        string $name,
        ?string $pattern = null,
        array $defaults = [],
        array $requirements = [],
        array $options = [],
        string $host = '',
        array $schemes = [],
        array $methods = [],
        string $condition = ''
    ): self;

Bạn thấy hành động import được để trống. SonataSymfony làm việc cho chúng tôi và đó là điều tốt.

Vẫn trong file cấu hình trang quản trị của chúng tôi, chúng tôi sẽ thêm phương thức configureDashboardActions(). Không có gì phức tạp ở đây. Chúng tôi truyền Template sẽ chăm sóc mọi thứ. Về phần mình, tôi thêm thông tin như nhãn, biểu tượng, và đường dẫn liên quan.

    /**
     * @param array $actions
     * @return array|\mixed[][]
     */
    protected function configureDashboardActions(array $actions): array
    {
        $actions['import'] = [
            'template' => 'Sonata/default_dashboard_button.html.twig',
            'label'    => 'Import',
            'icon'     => 'fa-level-up-alt',
            'route'    => 'import'
        ];


        return $actions;
    }

Điều này trực quan cho chúng tôi cái này:

image-28

Một khi nhấp vào, điều gì sẽ xảy ra? Đó là nơi phép màu xảy ra. Đối với bài viết, điều chúng tôi xem xét là đến một Controller, sử dụng một Service để lấy dữ liệu, chèn nó vào cơ sở dữ liệu, và trực tiếp đến trang của chúng tôi với danh sách các tiêu đề của chúng tôi.

Trong đời thực, bạn sẽ muốn đi qua một trang trung gian với chắc chắn là một xác nhận, một biểu mẫu với các tùy chọn, v.v. Ở đây chúng tôi không ở trong đời thực. Chúng tôi chỉ muốn cho thấy rằng hành động của chúng tôi sẽ tự động đi đến một Controller, rằng trong Controller này bạn có thể lấy một Service (cảm ơn autowiring!) và truy cập kho lưu trữ thực thể thông qua modelManager.

Về phần mã, tôi chỉ thêm một tệp ImportSevice mà không cần cấu hình YAML. Nó xảy ra tự nhiên. Không có gì bất thường trong phương thức getDatas() chúng tôi chỉ trả về một mảng với chỉ một dữ liệu.

<?php

namespace App\Service;

use App\Repository\TitlesRepository;

class ImportService
{

    /**
     * @return array
     */
    public function getDatas(): array
    {
        $data = [
            'title' => 'titre factice : ' . (new \DateTime())->format(\DateTimeInterface::RSS),
            'slug' => null
        ];
        return [$data];
    }
}

Trong Controller của chúng tôi, cũng không có nhiều:

    /**
     * @param ImportService $importService
     * @return RedirectResponse
     * @throws \Sonata\AdminBundle\Exception\ModelManagerThrowable
     */
    public function importAction(ImportService $importService): RedirectResponse
    {
        $datas = $importService->getDatas();
        foreach($datas as $data){
            $newEntity = new SlugTitles();
            $newEntity->setTitle($data['title']);
            $newEntity->setSlug($data['slug']);
            $this->admin->create($newEntity);
        }

        return new RedirectResponse($this->admin->generateUrl('list'));
    }

Như bạn thấy, chúng tôi tự động có quyền truy cập vào ImportService mà không cần làm gì. Vì Controller của chúng tôi mở rộng CRUDController chúng tôi có quyền truy cập trực tiếp vào $this->admin không phải là gì khác ngoài một thể hiện của \Sonata\AdminBundle\Admin\AdminInterface mà chính nó mở rộng \Sonata\AdminBundle\Admin\LifecycleHookProviderInterface cho phép chúng tôi truy cập vào các phương thức update(), create(), delete().

<?php

declare(strict_types=1);

/*
 * This file is part of the Sonata Project package.
 *
 * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Sonata\AdminBundle\Admin;

use Sonata\AdminBundle\Exception\LockException;
use Sonata\AdminBundle\Exception\ModelManagerThrowable;

/**
 * This interface can be implemented to provide hooks that will be called
 * during the lifecycle of the object.
 *
 * @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
 *
 * @phpstan-template T of object
 */
interface LifecycleHookProviderInterface
{
    /**
     * @throws ModelManagerThrowable
     * @throws LockException
     *
     * @phpstan-param T $object
     * @phpstan-return T $object
     */
    public function update(object $object): object;

    /**
     * @throws ModelManagerThrowable
     *
     * @phpstan-param T $object
     * @phpstan-return T $object
     */
    public function create(object $object): object;

    /**
     * @throws ModelManagerThrowable
     *
     * @phpstan-param T $object
     */
    public function delete(object $object): void;
}

Vì vậy, nếu bạn đang tìm cách chuyển một số logic vào một Repository hoặc một Service bạn sẽ cần chơi với $this->admin.

Điều tuyệt vời là bây giờ chúng tôi có toàn bộ logic của mình. Và Controller của chúng tôi sẽ được sử dụng cho các hành động mà chúng tôi sẽ thêm vào tiêu đề và trên mỗi dòng của bảng của chúng tôi.

Hành động Tùy chỉnh trên Tiêu đề CRUD

image-29

Thực tế, không có gì đơn giản hơn.

    protected function configureActionButtons(array $buttonList, string $action, ?object $object = null): array
    {
        $buttonList['import'] = [
            'template' => 'Sonata/default_header_action.html.twig',
            'label'    => 'Import',
            'icon'     => 'fa-level-up-alt',
            'route'    => 'import'
        ];
        return $buttonList;
    }
<li>
    <a class="sonata-action-element" href="{{ admin.generateUrl(item.route) }}">
        <i class="fas {{ item.icon | default('')}}"></i>
        {{ item.label | default('') | trans({}, 'default') }}
    </a>
</li>

Bạn sẽ thấy rằng trong mẫu của chúng tôi, không còn là action mà chúng ta có quyền truy cập mà là item.

image-30

Bạn nhấp chuột. Nó trực tiếp gửi hành động đến phương thức của chúng tôi \App\Controller\SlugTitlesAdminController::importAction.

Hành Động Tùy Chỉnh Trên Mỗi Dòng Của Bảng

Về chủ đề này, tôi khuyến nghị đọc bài viết của Thomas Bourdin SYMFONY / SONATA: THÊM CHỨC NĂNG NHÂN BẢN CHO CRUD để có thông tin bổ sung.

Mã lệnh cũng đơn giản như những mã khác.

    /**
     * @param ListMapper $list
     * @return void
     */
    protected function configureListFields(ListMapper $list): void
    {
        $list
            ->add('title')
            ->add('slug')
            ->add(ListMapper::NAME_ACTIONS, null, [
                'actions' => [
                    'show' => [],
                    'edit' => [],
                    'delete' => [],
                    'clone' => [
                        'template' => 'Sonata/default_filed_button_action.html.twig',
                        'label' => 'Clone',
                        'route' => 'clone',
                        'icon_class' => 'fas fa-clone',
                        'ask_confirmation' => true,
                        'confirmation_message' => 'êtes vous sûr de blahblah cette action ????? '
                    ],
                    'import' => ['template' => 'Sonata/custom_field_button.html.twig']
                ],
            ]);
        ;
    }

Dưới đây là hai mẫu được sử dụng.

<a href="{{ admin.generateObjectUrl(actions.route, object) }}"
   class="btn btn-sm btn-default"
   {% if actions.ask_confirmation is defined and actions.ask_confirmation == true %}
        data-confirm="{{ actions.confirmation_message }}"
    {% endif %}
    >
    <i class="{{ actions.icon_class | default('') }} " aria-hidden="true"></i>
    {{ actions.label | default('') | trans({}, 'default') }}
</a>
<a class="btn btn-sm btn-default" href="{{ admin.generateUrl('import') }}">
    <i class="fas fa-level-up-alt"></i> {{ 'Import'|trans({}, 'SonataAdminBundle') }}
</a>

Cuối cùng, không có gì mới trong hai hành động này. Một hành động khác biệt với hành động kia ở chỗ tôi đã thêm hai tham số vào Mẫu: ask_confirmation, confirmation_message. Không, đây không phải là các đối số bản địa của Sonata. Tôi chỉ thêm một JavaScript để kích hoạt một xác nhận. Không phức tạp. Dưới đây là JavaScript sẽ tự lo liệu mọi thứ.

sonata_admin:
    assets:
        extra_javascripts:
            - '/Sonata/custom_field_action_ask_confirmation.js'
$(document).ready(function() {
    $('a[data-confirm]').click(function(ev) {
        var href = $(this).attr('href');

        if (!$('#dataConfirmModal').length) {
            $('body').append('' +
                '<div id="dataConfirmModal" class="modal fade" role="dialog" aria-labelledby="dataConfirmLabel" aria-hidden="true">' +
                '<div class="modal-dialog modal-lg">' +
                '<div class="modal-content">' +
                '<div class="modal-header">' +
                '<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>' +
                '<h4 class="modal-title"></h4>' +
                '</div>' +
                '' +
                '<div class="modal-body">' +
                '</div>' +
                '<div class="modal-footer">' +
                '<button class="btn" data-dismiss="modal" aria-hidden="true">Cancel</button>' +
                '<a class="btn btn-primary" id="dataConfirmOK">OK</a>' +
                '</div>' +
                '</div>' +
                '</div>' +
                '</div>');
        }
        $('#dataConfirmModal').find('.modal-body').text($(this).attr('data-confirm'));
        $('#dataConfirmOK').attr('href', href);
        $('#dataConfirmModal').modal({show:true});
        return false;
    });
});

Tất cả những điều này để nói rằng không tồn tại xác nhận tự động cho các hành động tùy chỉnh ở cấp độ các thực thể CRUD. Bạn sẽ chọn thực hiện thao tác trực tiếp trong Controller của mình, hoặc tìm cách sử dụng JavaScript như trong ví dụ.

Dưới đây là phương thức cloneAction()

    /**
     * @param $id
     * @param CloneService $cloneService
     * @return Response
     */
    public function cloneAction($id, CloneService $cloneService): Response
    {
        $object = $this->admin->getSubject();

        if (!$object) {
            throw new NotFoundHttpException(sprintf('unable to find the object with id: %s', $id));
        }
        $clonedResult = $cloneService->clone($this->admin->getModelManager(), $object);

        if($clonedResult){
            $this->addFlash('sonata_flash_success', 'Cloned successfully');
        }else{
            $this->addFlash('sonata_flash_error', 'Clone Error');
        }

        return new RedirectResponse($this->admin->generateUrl('list', ['filter' => $this->admin->getFilterParameters()]));
    }

Controller sẽ lấy $id làm đối số, được Sonata tự động truyền. Nó tương ứng với id của thực thể của bạn. Đối số thứ hai là Dịch Vụ mà bạn có thể muốn sử dụng. Nó được tự động dây chuyền qua Symfony. Bạn có thể thêm bao nhiêu Dịch Vụ tùy ý vào Controller của mình.

$this->admin->getSubject() cho phép bạn trực tiếp truy cập thực thể đang được nói đến. Tiện lợi!

image-31

Dưới đây là dịch vụ cho thông tin của bạn.

<?php

namespace App\Service;

use App\Entity\SlugTitles;
use Sonata\DoctrineORMAdminBundle\Model\ModelManager;

Class CloneService {

    /**
     * @param CRUDController $crudController
     * @param SlugTitles $slugTitle
     * @return bool
     */
    public function clone(ModelManager $modelManager, SlugTitles $slugTitle): bool
    {
        try{
            $clonedObject = clone $slugTitle;
            $clonedObject->setTitle($slugTitle->getTitle().' (Clone)');
            $modelManager->create($clonedObject);
        }catch (\Exception $e){
            return new \Exception('Erreur lors du clonage : ' . $e->getMessage());
        }

        return true;
    }
}

Như bạn có thể thấy qua $this->admin->getModelManager() chúng tôi lấy lại tương đương với kho lưu trữ của thực thể sẽ cho phép chúng tôi ghi trực tiếp vào dịch vụ của mình.

Bước cuối cùng, hành động chế độ lô.

Hành Động Lô Tùy Chỉnh

image-32

Vẫn đơn giản như những cái khác với Sonata và phép màu đi kèm với nó.

    /**
     * @param array $actions
     * @return array|\mixed[][]
     */
    protected function configureBatchActions(array $actions): array
    {
        if (
            $this->hasRoute('edit') && $this->hasAccess('edit') &&
            $this->hasRoute('delete') && $this->hasAccess('delete')
        ) {
            $actions['clone'] = [
                'ask_confirmation' => true,
            ];
        }

        return $actions;
    }

Lần này xác nhận là bản địa. Hoàn hảo!

Ở phía Controller, chúng tôi sẽ chờ một phương thức kiểu batchActionClone() cho một hành động có tên Nhân Bản.

Giống như hành động đơn giản lấy int $id làm tham số đầu tiên, batchActionClone'ActionName'() mong đợi một thể hiện của ProxyQueryInterface chứa một thể hiện của QueryBuilder. Cuối cùng, một ProxyQueryInterface->execute() đơn giản là đủ để truy cập vào tất cả các thực thể được chọn.

image-33
    public function batchActionClone(ProxyQueryInterface $selectedModelQuery, CloneService $cloneService): RedirectResponse
    {
        $entityList = $selectedModelQuery->execute();
        
        try {
            foreach ($entityList->getIterator() as $entity) {
                $clonedResult = $cloneService->clone($this->admin->getModelManager(), $entity);
                if($clonedResult){
                    $this->addFlash('sonata_flash_success', 'Cloned successfully: ' . $entity->getTitle());
                }else{
                    $this->addFlash('sonata_flash_error', 'Clone Error' . $entity->getTitle());
                }
            }

        } catch (\Exception $e) {
            $this->addFlash('sonata_flash_error', 'Quelques erreurs sont survenues lors de l\'execution des taches groupés');
        } finally {
            return new RedirectResponse(
                $this->admin->generateUrl('list', [
                    'filter' => $this->admin->getFilterParameters()
                ])
            );
        }
    }

Mã lệnh khá đơn giản và thể hiện rõ lô-gíc đằng sau nó.

image-34
image-35

Và đó là tất cả. Bây giờ bạn đã có cái nhìn tổng quan về các hành động khác nhau có thể từ trang quản lý thực thể của bạn.