Как организовать динамические списки категорий и субкатегорий с использованием форм в Symfony2?

Приветствую.



Коллеги, столкнулся с небольшой задачкой, решение для которой никак не найду. Поучаствуйте в мозговом штурме, может кто-то встречал похожую задачу.



Я использую Symfony2.3. Мы имеем, Entity:



Category:

// src/Acme/DemoBundle/Entity/Category.php<br><br>
    namespace Acme\DemoBundle\Entity;<br><br>
    use Gedmo\Mapping\Annotation as Gedmo;<br>
    use Doctrine\ORM\Mapping as ORM;<br><br>
    /**<br>
     * @Gedmo\Tree(type="nested")<br>
     * @ORM\Table(name="categories")<br>
     * use repository for handy tree functions<br>
     * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository")<br>
     */<br>
    class Category<br>
    {<br>
        /**<br>
         * @ORM\Column(name="id", type="integer")<br>
         * @ORM\Id<br>
         * @ORM\GeneratedValue<br>
         */<br>
        private $id;<br><br>
        /**<br>
         * @Gedmo\Translatable<br>
         * @ORM\Column(name="name", type="string", length=64)<br>
         */<br>
        private $name;<br><br>
        /**<br>
         * @Gedmo\TreeLeft<br>
         * @ORM\Column(name="lft", type="integer")<br>
         */<br>
        private $lft;<br><br>
        /**<br>
         * @Gedmo\TreeLevel<br>
         * @ORM\Column(name="lvl", type="integer")<br>
         */<br>
        private $lvl;<br><br>
        /**<br>
         * @Gedmo\TreeRight<br>
         * @ORM\Column(name="rgt", type="integer")<br>
         */<br>
        private $rgt;<br><br>
        /**<br>
         * @Gedmo\TreeRoot<br>
         * @ORM\Column(name="root", type="integer", nullable=true)<br>
         */<br>
        private $root;<br><br>
        /**<br>
         * @Gedmo\TreeParent<br>
         * @ORM\ManyToOne(targetEntity="Category", inversedBy="children")<br>
         * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE")<br>
         */<br>
        private $parent;<br><br>
        /**<br>
         * @ORM\OneToMany(targetEntity="Category", mappedBy="parent")<br>
         * @ORM\OrderBy({"lft" = "ASC"})<br>
         */<br>
        private $children;<br>




Item:

// src/Acme/DemoBundle/Entity/Item.php<br>
    namespace Acme\DemoBundle\Entity;<br><br>
    use DoctrineExtensions\Taggable\Taggable;<br>
    use Doctrine\Common\Collections\ArrayCollection;<br>
    use Doctrine\ORM\Mapping as ORM;<br>
    use Doctrine\Common\Collections\Collection;<br><br>
    /**<br>
     * Item entity<br>
     *<br>
     * @ORM\Table(name="items")<br>
     * @ORM\HasLifecycleCallbacks<br>
     * @ORM\Entity<br>
     */<br>
    class Item implements Taggable<br>
    {<br>
        /**<br>
         * @ORM\Id<br>
         * @ORM\Column(type="integer")<br>
         * @ORM\GeneratedValue(strategy="AUTO")<br>
         */<br>
        protected $id;<br><br>
        /**<br>
         * @ORM\Column(name="name", type="string", length=64, nullable=true)<br>
         */<br>
        protected $name;<br><br>
        /**<br>
         * @ORM\ManyToMany(targetEntity="Category", inversedBy="items")<br>
         * @ORM\JoinColumn(name="category_id", referencedColumnName="id"),<br>
         *      inverseJoinColumn=(name="item_id", referencedColumnName="id")<br>
         *<br>
         **/<br>
        protected $categories;<br><br>
         public function getCategories(){<br>
            return $this->categories;<br>
        }<br><br>
        public function setCategories($categories){<br>
            $this->categories = $categories;<br><br>
            return $this->categories;<br>
        }<br>




Кроме этого, у нас есть тип формы:

AddItemForm:

// src/Acme/DemoBundle/Form/AddItemForm.php<br>
    namespace Acme\DemoBundle\Form;<br><br>
    use Symfony\Component\Form\AbstractType;<br>
    use Symfony\Component\Form\FormBuilderInterface;<br>
    use Symfony\Component\OptionsResolver\OptionsResolverInterface;<br>
    use Doctrine\ORM\EntityRepository;<br>
    use Symfony\Component\Validator\Constraints\Collection;<br><br>
    /**<br>
     * Add item form<br>
     *<br>
     */<br>
    class AddModelForm extends AbstractType<br>
    {    public function buildForm(FormBuilderInterface $builder, array $options) {<br>
            $builder->add('categories', 'collection', array(<br>
                                                            'type' => 'entity',<br>
                                                            'allow_add' => true,<br>
                                                            'allow_delete' => true,<br>
                                                            'prototype' => true,<br>
                                                            'show_legend' => false,<br>
                                                            'data' => array(''),<br>
                                                            'widget_add_btn' => array('label' => _('Добавить категорию')),<br>
                                                            'options' => array(<br>
                                                                            'widget_control_group' => false,<br>
                                                                            'label_render' => false,<br>
                                                                            'class' => 'AcmeDemoBundle:Category',<br>
                                                                            'query_builder' => function(EntityRepository $er) {<br>
                                                                                return $er->createQueryBuilder('c')<br>
                                                                                    ->where('c.lvl = 0')<br>
                                                                                    ->orderBy('c.id', 'ASC');<br>
                                                                            },<br>
                                                                            'property' => 'name',<br>
                                                                            'empty_value' => _('Choose category'),<br>
                                                                        ),<br><br>
                                                        )<br>
                                  );<br>
        }<br>
    }<br>




И контроллер ItemController:

// src/Acme/DemoBundle/Controller/ItemController.php<br>
    ...<br>
    public function editAction($itemId) {<br>
        $item= $em->getRepository('AcmeDemoBundle:Item')<br>
                      ->findOneById($itemId);<br><br>
        $form = $this->createForm(new AddItemForm(), $item);<br>
    }<br><br>
    public function addAction() {<br>
        $item = new Item();<br><br>
        $form = $this->createForm(new AddItemForm(), $item);<br>
    }<br>
    ...<br>




Категории содержат подкатегории, которые, в свою очередь, тоже содержат подкатегории и т.д. Реализация nested tree из doctrine extensions.

Что я хочу получить?:



1) Когда я добавляю новый предмет и перехожу к выбору категорий, к которым он принадлежит, я хочу получить следующее повидение (скриншот мой, на английском): вид выбора категорий.



Т.е. я выбираю категорию, подкатегорию и т.д. до момента, когда у категории не будет потомков (например: Оружие-Магическое-Посохи). 1-я категория, к которой принадлежит предмет — Посохи. Можно добавить до N категорий. Нажминая «добавить категорию», появляется выпадающий список нулевого уровня и динамическое появление подкатегорий происходит как было описано ранее.



2) Когда я редактирую предмет, я ожидаю увидеть уже раскрытые категории до последней, с возможностью редактировать их.



Конечно, я могу реализовать это с помощью javascript, в контроллере получить список категорий, путь до корня каждой и в twig шаблоне рендерить это дело. Но хотелось бы найти best practice решение.



Скоре всего я что-то упустил, т.к. компонент в Form в Symfony2 достаточно обширный. Может, нужное поведение в моем случае может помочь организовать какой-либо Bundle? Но опять же, пока, я ничего не нашел.
  • Вопрос задан
  • 7907 просмотров
Пригласить эксперта
Ответы на вопрос 3
Fesor
@Fesor
Full-stack developer (Symfony, Angular)
А чем плох вариант с «написать все на JS»? Просто заверните логику в свой FormType, добавьте DataTransformer и ViewTransformer, и получится вполне себе best-practice. Опять же можно обойтись только расширением тайпа collection и заменой виджета. Вариантов масса, смотрите что вам будет проще сделать.
Ответ написан
multifinger
@multifinger
а тут другого варианта и не получиться — только ручками, только js

весь набор категорий лучше выгрузить в js-массив, который потом скормить объекту, реализующему работу с деревом (parent-children), и дальше уже с этим объектом удобно будте работать, оживляя кнопки
Ответ написан
svscorp
@svscorp Автор вопроса
Разбираюсь с ДатаТрансформерами и кастомными типами.

Основная форма:
class CategoryForm extends AbstractType
{
    private $em;

    public function __construct($em) {
       $this->em = $em;
    }

    public function buildForm(FormBuilderInterface $builder, array $options) {
      $transformer = new CategoryToChoiceTransformer($this->em);

      $builder->add(
               $builder->create('categories', 'collection',
                   array(
                      'type' => new CategoryCollectionType($this->em)
                   )
               )->addModelTransformer($transformer));



        $builder->add('save', 'submit');
    }

    public function getDefaultOptions(array $options)
    {
        $resolver->setDefaults(array(
                'data_class' => 'Acme\DemoBundle\Entity\Item',
        ));
    }


CategoryCollectionType:
class CategoryCollectionType extends AbstractType
{
    private $em;

    public function __construct($em) {
        $this->em = $em;
    }

    public function buildForm(FormBuilderInterface $builder, array $options) {

        $builder->add('categories', 'collection', array(
                                                        'type' => 'entity',
                                                        'allow_add' => true,
                                                        'allow_delete' => true,
                                                        'prototype' => false,
                                                        'show_legend' => true,
                                                        'widget_add_btn' => array('label' => _('Добавить категорию')),
                                                        'options' => array(
                                                                        'widget_control_group' => false,
                                                                        'label_render' => false,
                                                                        'show_legend' => false,
                                                                        'class' => 'AcmeDemoBundle:Category',
                                                                        'query_builder' => function(EntityRepository $er) {
                                                                            return $er->createQueryBuilder('c')
                                                                                ->orderBy('c.id', 'ASC');
                                                                        },
                                                                        'property' => 'name',
                                                                        'empty_value' => _('Выберите категорию'),
                                                                    ),

                                                    )
                              );


    }

    public function getName()
    {
        return 'category_collection_type';
    }
}


DataTransformer:

class CategoryToChoiceTransformer implements DataTransformerInterface
{
    /**
     * @var ObjectManager
     */
    private $om;

    /**
     * @param ObjectManager $om
     */
    public function __construct(ObjectManager $om)
    {
        $this->om = $om;
    }

    /**
     *
     * @param  Issue|null $issue
     * @return string
     */
    public function transform($categories)
    {
        $categoriesExpanded = new ArrayCollection();

        if (!empty($categories)) {
            $categoryRepository = $this->om->getRepository('AcmeDemoBundle:Category');

            foreach ($categories as $category) {
                $path = new ArrayCollection($categoryRepository->getPath($category));
                $categoriesExpanded->add($path);
            }
        }

        return $categoriesExpanded;
    }

    public function reverseTransform($f) {
      ...
   }
}


Вот, что я получаю:
Хотя, ожидаю увидеть 3 селектбокса в 1 группе и 3 во второй (data transformer возвращает массив вида:
array(
[0] => array( ObjectCategory#..., ObjectCategory#..., ObjectCategory#..., ),
[1] => array( ObjectCategory#..., ObjectCategory#..., ObjectCategory#..., )


Есть у кого идеи, что здесь не верно?
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы