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

Welcome.

Colleagues, faced with a small problem, the solution for which I can't find. Participate in brainstorming, maybe someone met similar problem.

I use Symfony2.3. We have Entity:

Category:
 // src/Acme/DemoBundle/Entity/Category.php

 namespace Acme\DemoBundle\Entity;

 use Gedmo\Mapping\Annotation as Gedmo;
 use Doctrine\ORM\Mapping as ORM;

/**
 * @Gedmo\Tree(type="nested")
 * @ORM\Table(name="categories")
 * use repository for handy tree functions
 * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository")
*/
 class Category
{
/**
 * @ORM\Column(name="id" type="integer")
 * @ORM\Id
 * @ORM\GeneratedValue
*/
 private $id;

/**
 * @Gedmo\Translatable
 * @ORM\Column(name="name" type="string", length=64)
*/
 private $name;

/**
 * @Gedmo\TreeLeft
 * @ORM\Column(name="lft", type="integer")
*/
 private $lft;

/**
 * @Gedmo\TreeLevel
 * @ORM\Column(name="lvl", type="integer")
*/
 private $lvl;

/**
 * @Gedmo\TreeRight
 * @ORM\Column(name="rgt", type="integer")
*/
 private $rgt;

/**
 * @Gedmo\TreeRoot
 * @ORM\Column(name="root" type="integer", nullable=true)
*/
 private $root;

/**
 * @Gedmo\TreeParent
 * @ORM\ManyToOne(targetEntity="Category", inversedBy="children")
 * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE")
*/
 private $parent;

/**
 * @ORM\OneToMany(targetEntity="Category", mappedBy="parent")
 * @ORM\OrderBy({"lft" = "ASC"})
*/
 private $children;


Item:
// src/Acme/DemoBundle/Entity/Item.php
 namespace Acme\DemoBundle\Entity;

 use DoctrineExtensions\Taggable\Taggable;
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\ORM\Mapping as ORM;
 use Doctrine\Common\Collections\Collection;

/**
 * Item entity
*
 * @ORM\Table(name="items")
 * @ORM\HasLifecycleCallbacks
 * @ORM\Entity
*/
 class Item implements Taggable
{
/**
 * @ORM\Id
 * @ORM\Column(type="integer")
 * @ORM\GeneratedValue(strategy="AUTO")
*/
 protected $id;

/**
 * @ORM\Column(name="name" type="string", length=64, nullable=true)
*/
 protected $name;

/**
 * @ORM\ManyToMany(targetEntity="Category", inversedBy="items")
 * @ORM\JoinColumn(name="category_id", referencedColumnName="id"),
 * inverseJoinColumn=(name="item_id", referencedColumnName="id")
*
**/
 protected $categories;

 public function getCategories(){
 return $this->categories;
}

 public function setCategories($categories){
 $this->categories = $categories;

 return $this->categories;
}


In addition, we have the form type:
AddItemForm:
 // src/Acme/DemoBundle/Form/AddItemForm.php
 namespace Acme\DemoBundle\Form;

 use Symfony\Component\Form\AbstractType;
 use Symfony\Component\Form\FormBuilderInterface;
 use Symfony\Component\OptionsResolver\OptionsResolverInterface;
 use Doctrine\ORM\EntityRepository;
 use Symfony\Component\Validator\Constraints\Collection;

/**
 * Add item form
*
*/
 class AddModelForm extends AbstractType
 { public function buildForm(FormBuilderInterface $builder, array $options) {
 $builder->add('categories', 'collection', array(
 'type' => 'entity',
 'allow_add' => true,
 'allow_delete' => true,
 'prototype' => true,
 'show_legend' => false,
 'data' => array("),
 'widget_add_btn' => array('label' => _('Add category')),
 'options' => array(
 'widget_control_group' => false,
 'label_render' => false,
 'class' => 'AcmeDemoBundle:Category',
 'query_builder' => function(EntityRepository $er) {
 return $er->createQueryBuilder('c')
 ->where('c.lvl = 0')
 ->orderBy('c.id', 'ASC');
},
 'property' => 'name',
 'empty_value' => _('Choose category'),
),

)
);
}
}


The controller ItemController:
 // src/Acme/DemoBundle/Controller/ItemController.php
...
 public function editAction($itemId) {
 $item= $em->getRepository('AcmeDemoBundle:Item')
->findOneById($itemId);

 $form = $this->createForm(new AddItemForm(), $item);
}

 public function addAction() {
 $item = new Item();

 $form = $this->createForm(new AddItemForm(), $item);
}
...


Categories contain subcategories, which, in turn, also contain sub-categories, etc. implementation of the nested tree of the doctrine extensions.
What I want to?:

1) When I add a new item and go to select the categories to which it belongs, I want to get the next bearing is (a screenshot of mine, in English): the choice of categories.

Ie I choose a category, subcategory etc. before the moment when the category would not be descendants (e.g.: Weapon-Magical-Staves). Category 1, which belongs to the object — Rods. You can add up to N categories. Namina "add category", there is a drop down list of zero level and the dynamic emergence of subcategories occurs as described earlier.

2) When I edit the item, I expect to see already opened the category up to the last, with the ability to edit them.

Of course, I can implement this by using javascript in the controller to obtain a list of categories, way up to the root and in each twig template to render this. But I would like to find a best practice solution.

It is likely I missed something because the Form component in Symfony2 is quite extensive. Maybe that's the desired behavior in my case, can help to organize any Bundle? But then again, so far, I found nothing.
October 3rd 19 at 04:15
3 answers
October 3rd 19 at 04:17
What's wrong with the option "write everything in JS"? Just wrap the logic in your FormType, add a DataTransformer and ViewTransformer, and you get quite a best-practice. Again, you can do an expansion of the Taipa collection and replacement of the widget. The mass of options, see what you will be able to do.
I will try this option, however, haven't figured out DataTransformer and ViewTransformer. And about the extension of Taipa collection — more? Or a link to an example of some.

Thank you! - Lavada_Schmeler4 commented on October 3rd 19 at 04:20
October 3rd 19 at 04:19
and there is another option and will not work — only handles only js

the entire set of categories is better to upload in a js array, which is then fed to an object implementing the tree (parent-children), and then have this object be convenient to work making buttons
OK, about JS — understood. What about the form and generation of categories in the view? - Lavada_Schmeler4 commented on October 3rd 19 at 04:22
October 3rd 19 at 04:21
Deal with Datatransformer 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' => _('Add category')),
 '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' => _('Select a category'),
),

)
);


}

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


DataTransformer:

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


That's what I get:
Although, expect to see 3 select boxes 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 idea what's wrong.?

Find more questions by tags ProgrammingPHPSymfony