Пакетное действие SonataAdminBundle + Select2

от автора

Всем доброго времени суток!

Нынче многие бросают свой взгляд в сторону EasyAdminBundle, но я до сих пор использую и предпочитаю одну из лучших панелей администратора для Symfony.

SonataAdminBundle

Система очень гибкая и многими недооценённая, мол Sonata ограничивает администратора в действиях (представляет малый функционал панели администратора). Если Вам нужно что-то иное, всегда можно дополнить или модернизировать уже существующие методы.

Недавно мне нужно было произвести пакетное действие для продуктов и после выполненной работы захотелось рассказать об этом Вам. Вдобавок прикреплю Select2, ибо у меня возникали некие трудности, офф.документация ограничена в информации.

Итак, начнем сразу с чистого проекта. На момент создания хаба актуальные версии пакетов:

При установке никаких проблем особо возникать не должно, в ином случае в интернете полно информации об установке SonataAdminBundle.

Для демонстративной работы пакетного действия нам понадобится 2 сущности, со связью M2M.

Создаём сущность Product:

#src/Entity/Product.php <?php  namespace App\Entity;  use App\Repository\ProductRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM;  #[ORM\Entity(repositoryClass: ProductRepository::class)] class Product {     #[ORM\Id]     #[ORM\GeneratedValue]     #[ORM\Column]     private ?int $id = null;      #[ORM\Column(length: 255, nullable: true)]     private ?string $name = null;      #[ORM\Column(nullable: true)]     private ?int $price = null;      #[ORM\ManyToMany(targetEntity: Category::class, mappedBy: 'products')]     private Collection $categories;          public function __construct()     {         $this->products = new ArrayCollection();     }          public function __toString(): string     {         return $this->name;     }          //getters and setters }

Также создаём Category:

#src/Entity/Category.php <?php  namespace App\Entity;  use App\Repository\CategoryRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM;  #[ORM\Entity(repositoryClass: CategoryRepository::class)] class Category {     #[ORM\Id]     #[ORM\GeneratedValue]     #[ORM\Column]     private ?int $id = null;      #[ORM\Column(length: 255, nullable: true)]     private ?string $name = null;      #[ORM\ManyToMany(targetEntity: Product::class, inversedBy: 'categories')]     private Collection $products;          public function __construct()     {         $this->products = new ArrayCollection();     }          public function __toString(): string     {         return $this->name;     }          //getters and setters }

В обе сущности требуется добавить метод возврата строки __toString
Обновим таблицы в БД:

bin/console doctrine:schema:update --force

Теперь создадим файлы для панели администратора:

#src/Admin/ProductAdmin.php <?php  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\Show\ShowMapper;  class ProductAdmin extends AbstractAdmin {     protected function configureFormFields(FormMapper $form): void     {         if ($this->getSubject()->getId()) {             $form                 ->add('price')                 ->add('categories')             ;         }else {             $form                 ->add('name')                 ->add('price')                 ->add('categories')             ;         }     }      protected function configureDatagridFilters(DatagridMapper $filter): void     {         $filter             ->add('name')             ->add('price')             ->add('categories')         ;     }      protected function configureListFields(ListMapper $list): void     {         $list             ->addIdentifier('name')             ->add('price')             ->add('categories')             ->add('_action', 'actions',[                 'actions' => [                     'edit' => [],                     'delete' => [],                 ]             ])         ;     }      protected function configureShowFields(ShowMapper $show): void     {         $show             ->with('Product')                 ->add('name')                 ->add('price')             ->end()             ->with('Categories')                 ->add('categories')             ->end()         ;     } }
#src/Admin/CategoryAdmin.php <?php  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\Show\ShowMapper;  class CategoryAdmin extends AbstractAdmin {     protected function configureFormFields(FormMapper $form): void     {         $form             ->add('name')             ->add('products')         ;     }      protected function configureDatagridFilters(DatagridMapper $filter): void     {         $filter             ->add('name')             ->add('products')         ;     }      protected function configureListFields(ListMapper $list): void     {         $list             ->addIdentifier('name')             ->add('products')         ;     }      protected function configureShowFields(ShowMapper $show): void     {         $show             ->with('Category')                 ->add('name')             ->end()             ->with('Products')                 ->add('products')             ->end()         ;     } }

Нужно их зарегистрировать:

#config/services.yaml services:     App\Admin\ProductAdmin:         arguments: [ ~, App\Entity\Product, ~ ]         tags:             - { name: sonata.admin, manager_type: orm, group: Content, label: Product }      App\Admin\CategoryAdmin:         arguments: [ ~, App\Entity\Category, ~ ]         tags:             - { name: sonata.admin, manager_type: orm, group: Content, label: Category }

Добавим кастомное поле, где будет отображаться скидка и цена со скидкой.
Саму скидку запишем в параметры контейнера:

#config/services.yaml parameters:     discount: 15

Теперь передадим этот параметр в качестве аргумента:

#config/services.yaml services:     App\Admin\ProductAdmin:         arguments: [ ~, App\Entity\Product, ~, '%discount%' ]

Принимаем параметр и создаём само поле:

#src/Admin/ProductAdmin.php <?php     private ?int $discount;          public function __construct(         ?string $code = null,          ?string $class = null,          ?string $baseControllerName = null,          ?int $discount = null     )     {         parent::__construct($code, $class, $baseControllerName);         $this->discount = $discount;     }       protected function configureListFields(ListMapper $list): void     {         $list             ->add('discountPrice', null,[                 'template' => 'SonataAdmin/price.html.twig',                 'discount' => $this->discount,              ])         ;     }

Чтобы не вызывало ошибку о несуществующем методе, создадим метод в сущности продукта. Не важно что он будет возвращать.

#src/Entity/Product.php <?php     public function getDiscountPrice(): int     {         return 1;     }

Нам теперь нужно создать шаблон, который будет отображать поле

#templates/SonataAdmin/price.html.twig {% extends '@SonataAdmin/CRUD/base_list_field.html.twig' %}  {% block field %}     DiscountPrice: {{ (object.price/100*(100-field_description.options.discount))|round }}<br>     Discount: {{ field_description.options.discount }} {% endblock %}

Добавим тройку тестовых записей продуктов, заодно сразу тройку категорий.

Проверяем.

Теперь появилось поле, в котором отображается цена со скидкой и сама скидка
Теперь появилось поле, в котором отображается цена со скидкой и сама скидка

Можно создавать пакетное действие. Нужно создать метод configureBatchActions

#src/Admin/ProductAdmin.php <?php     protected function configureBatchActions(array $actions): array     {         $actions['add_category'] = [             'ask_confirmation' => true, //можно убрать, если не нужно подтверждение действия         ];         return $actions;     }

Проверим отображение

Как мы видим, наше действие уже отображается.
Как мы видим, наше действие уже отображается.

Попробуем добавить поле выбора категории. Для этого я решил выбрать Select2 с подгрузкой названий с помощью Ajax. Создадим новый шаблон. Метод пакетного удаления мне не нужен, поэтому я удалил возможность выбора и всегда будет использоваться только наш метод:

#templates/SonataAdmin/list.html.twig {% extends '@SonataAdmin/CRUD/base_list.html.twig' %}  {% block batch_actions %}     <label class="checkbox" for="{{ admin.uniqid }}_all_elements">         <input type="checkbox" name="all_elements" id="{{ admin.uniqid }}_all_elements">         {{ 'all_elements'|trans({}, 'SonataAdminBundle') }}         ({{ admin.datagrid.pager.countResults() }})     </label>     <input id="action" name="action" value="addCategory" style="display: none">     <select id="category" name="category" style="width: 150px"></select>     <script>         $(function (){             $('#category').select2({                 ajax:{                     url: '/find_category_ajax',                     dataType: 'json',                     processResults: function (data) {                         return {                             results: data                         };                     },                 },                 minimumInputLength: 3,             })         })     </script> {% endblock %}

Установим новый шаблон для нашего списка

#config/services.yaml services:     App\Admin\ProductAdmin:         calls:             - [ setTemplate, [ list, SonataAdmin/list.html.twig ] ]

Теперь у нас поле выбора категории, но ещё не настроен ответ Ajax.

Категории мы уже создали.

Нужно создать запрос query в CategoryRepository, который будет возвращать сущности с похожим набором символов в имени.

#src/Repository/CategoryRepository.php <?php  namespace App\Repository;  use App\Entity\Category; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry;  /**  * @extends ServiceEntityRepository<Category>  *  * @method Category|null find($id, $lockMode = null, $lockVersion = null)  * @method Category|null findOneBy(array $criteria, array $orderBy = null)  * @method Category[]    findAll()  * @method Category[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)  */ class CategoryRepository extends ServiceEntityRepository {     public function __construct(ManagerRegistry $registry)     {         parent::__construct($registry, Category::class);     }      public function add(Category $entity, bool $flush = false): void     {         $this->getEntityManager()->persist($entity);          if ($flush) {             $this->getEntityManager()->flush();         }     }      public function remove(Category $entity, bool $flush = false): void     {         $this->getEntityManager()->remove($entity);          if ($flush) {             $this->getEntityManager()->flush();         }     }      public function findToName(string $value)     {         return $this->createQueryBuilder('c')             ->andWhere('LOWER(c.name) LIKE :val')             ->setParameter('val', '%'.$value.'%')             ->setMaxResults(5)             ->getQuery()             ->getResult()         ;     } }

Создадим контроллер, который будет принимать запрос от Ajax.

#src/Controller/Admin/CategoryAjaxController.php <?php  namespace App\Controller\Admin;  use App\Entity\Category; use Doctrine\Persistence\ManagerRegistry; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response;  class CategoryAjaxController extends AbstractController {     public function findCategoryAjax(ManagerRegistry $doctrine, Request $request): Response     {         $string = $request->query->get('q');         $categories = $doctrine->getRepository(Category::class)->findToName($string);         $result = array();         foreach ($categories as $category)         {             $result[] = [                 'id' => $category->getId(),                 'text' => $category->getName(),             ];         }         return new Response(json_encode($result));     } }

Настроим маршрут для контроллера

#config/routes.yaml find_category_ajax:     path: /find_category_ajax     controller: App\Controller\Admin\CategoryAjaxController::findCategoryAjax

Теперь можно проверить работу нашего Select2

Осталось только настроить контроллер, который будет принимать категорию и добавлять её к выделенным продуктам.

Нам нужен CRUD контроллер и метод с названием нашего пакетного действия:

#src/Controller/Admin/ProductAdminController.php <?php  namespace App\Controller\Admin;  use App\Entity\Category; use Sonata\AdminBundle\Controller\CRUDController; use Sonata\AdminBundle\Admin\AdminInterface; use Sonata\AdminBundle\Datagrid\ProxyQueryInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Doctrine\Persistence\ManagerRegistry;  class ProductAdminController extends CRUDController {     private ManagerRegistry $doctrine;      public function __construct(ManagerRegistry $doctrine)     {         $this->doctrine = $doctrine;     }      public function batchActionAddCategory(         ProxyQueryInterface $query,         AdminInterface $admin,     ): RedirectResponse     {         $id = json_decode($_POST['data'])->category;         $category = $this->doctrine->getRepository(Category::class)->find($id);          $entity = $this->doctrine->getManager();         $products = $query->execute();         foreach ($products as $product)         {             $product->addCategory($category);             $entity->persist($product);         }         $entity->persist($category);         $entity->flush();          $this->addFlash(             'sonata_flash_success',             'Successfully added to "'.$category->getName().'" category'         );          return new RedirectResponse(             $admin->generateUrl('list',[                 'filter' => $admin->getFilterParameters()             ])         );     } }

Теперь передадим в наш сервис контроллер в качестве аргумента:

#config/services.yaml services:         App\Admin\ProductAdmin:         arguments: [ ~, App\Entity\Product, App\Controller\Admin\ProductAdminController, '%discount%']

Попробуем добавить категорию к нескольким продуктам

Как мы видим, теперь всё работает и работает всё как нам нужно и к отмеченным продуктам прикрепилась выбранная категория.

Заключение

Я хотел вам показать, что SonataAdminBundle не ограничивает Вас в действиях. Вы можете сделать любое действие, которое Вам требуется, нужно лишь знать как и где это править.

Буду рад любым замечаниям и комментариям, также постараюсь ответить на вопросы, если у Вас они возникнут.


ссылка на оригинал статьи https://habr.com/ru/post/687632/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *