S
S
svscorp2013-08-17 18:27:32
symfony
svscorp, 2013-08-17 18:27:32

How to organize dynamic lists of categories and subcategories using forms in Symfony2?

Greetings.
Colleagues, I ran into a small problem, for which I can not find a solution. Participate in a brainstorming session, maybe someone has met a similar task.
I am using Symfony2.3. We have 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>

In addition, we have a form type:
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>

And the ItemController controller:
// 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>

Categories contain subcategories, which in turn also contain subcategories, and so on. Nested tree implementation from doctrine extensions.
What do I want to get?:
1) When I add a new item and go to select the categories it belongs to, I want to get the following behavior (screenshot mine, in English): category select view .
Those. I select a category, subcategory, etc. until the category has no children (for example: Weapons-Magical-Staffs). The 1st category to which the item belongs is Staves. You can add up to N categories. By clicking "add category", a zero-level drop-down list appears and the dynamic appearance of subcategories occurs as described earlier.
2) When I edit an item, I expect to see already expanded categories down to the last one, with the ability to edit them.
Of course, I can implement this using javascript, get the list of categories in the controller, the path to the root of each and render this case in the twig template. But I would like to find a best practice solution.
Most likely I missed something, tk. the component in a Form in Symfony2 is quite extensive. Maybe the desired behavior in my case can help organize some kind of Bundle? But then again, so far, I haven't found anything.

Answer the question

In order to leave comments, you need to log in

3 answer(s)
S
Sergey, 2013-08-17
Protko @Fesor

And why is the “write everything in JS” option bad? Just wrap the logic in your FormType, add a DataTransformer and a ViewTransformer, and you're good-practice. Again, you can do with just expanding the collection type and replacing the widget. There are a lot of options, see what will be easier for you to do.

M
multifinger, 2013-08-17
@multifinger

and here there is no other option - only handles, only js
it is better to unload the entire set of categories into a js-array, which is then fed to the object that implements work with the tree (parent-children), and then it will be convenient to work with this object, animating the buttons

S
svscorp, 2013-09-08
@svscorp

I deal with DataTransformers and custom types.
Main form:

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) {
      ...
   }
}

Here's what I get:
Although, I expect to see 3 selectboxes in 1 group and 3 in the second (data transformer returns an array of the form:
array(
[0] => array( ObjectCategory#..., ObjectCategory#..., ObjectCategory#..., ),
[1] => array( ObjectCategory#..., ObjectCategory#..., ObjectCategory#..., )

Does anyone have any ideas what is wrong here?

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question