Skip to content

Instantly share code, notes, and snippets.

@mdzzohrabi
Last active February 14, 2024 17:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save mdzzohrabi/784034549f2217badf96e844df2792e5 to your computer and use it in GitHub Desktop.
Save mdzzohrabi/784034549f2217badf96e844df2792e5 to your computer and use it in GitHub Desktop.
Symfony Entity Tree ( Hierarchy ) Form Type
<?php
/**
* (c) Masoud Zohrabi <mdzzohrabi@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mdzzohrabi\Form;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Class EntityTreeType
* @package Mdzzohrabi\Form
*/
class EntityTreeType extends AbstractType {
public function buildView( FormView $view , FormInterface $form , array $options ) {
$choices = [];
foreach ( $view->vars['choices'] as $choice ) {
if ( $choice->data->getParent() === null )
$choices[ $choice->value ] = $choice->data;
}
$choices = $this->buildTreeChoices( $choices );
$view->vars['choices'] = $choices;
}
/**
* @param object[] $choices
* @param int $level
* @return array
*/
protected function buildTreeChoices( $choices , $level = 0 ) {
$result = array();
foreach ( $choices as $choice ){
$result[ $choice->getId() ] = new ChoiceView(
$choice,
(string)$choice->getId(),
str_repeat( '--' , $level ) . ' ' . $choice->getName(),
[]
);
if ( !$choice->getChilds()->isEmpty() )
$result = array_merge(
$result,
$this->buildTreeChoices( $choice->getChilds() , $level + 1 )
);
}
return $result;
}
public function getParent() {
return EntityType::class;
}
}
@Ma7454
Copy link

Ma7454 commented Apr 29, 2017

Hi

Thanks a lot for this it work fine fo me ! I need to do the same for an expended and multiple choices and has it used subform I'm a little bit confused on how I can adapt your code to the $listview for this case

Thanks a lot for your help !

Ma

@kl3sk
Copy link

kl3sk commented Dec 2, 2019

Thank you for this snippet, I update this to have more control over method names and prefix:

<?php
/**
 * (c) Masoud Zohrabi <mdzzohrabi@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * Updated by klesk <klesk44@gmail.com>
 * Added:
 *      Parent / Child method name
 *      Prefix config
 */

namespace App\Form\Type;

use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Class EntityTreeType
 *
 * @package Mdzzohrabi\Form
 */
class EntityTreeType extends AbstractType
{

    /**
     * @param OptionsResolver $resolver
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'parent_method_name' => 'getParent',
            'children_method_name' => 'getChildren',
            'prefix' => '>',
        ]);

    }

    /**
     * @param FormView      $view
     * @param FormInterface $form
     * @param array         $options
     */
    public function buildView(FormView $view, FormInterface $form, array $options)
    {
        $choices = [];

        $parent_method_name = $options['parent_method_name'];
        foreach ($view->vars['choices'] as $choice) {
            if ($choice->data->$parent_method_name() === null)
                $choices[$choice->value] = $choice->data;
        }

        $choices = $this->buildTreeChoices($choices, $options);

        $view->vars['choices'] = $choices;

    }

    /**
     * @param object[] $choices
     * @param array    $options
     * @param int      $level
     *
     * @return array
     */
    protected function buildTreeChoices($choices, array $options, $level = 0)
    {

        $result = [];
        $children_method_name = $options['children_method_name'];


        foreach ($choices as $choice) {

            $result[$choice->getId()] = new ChoiceView(
                $choice,
                (string)$choice->getId(),
                str_repeat($options['prefix'], $level) . ' ' . $choice->getName(),
                []
            );

            if (!$choice->$children_method_name()->isEmpty())
                $result = array_merge(
                    $result,
                    $this->buildTreeChoices($choice->$children_method_name(), $options, $level + 1)
                );

        }

        return $result;

    }

    /**
     * @return string|null
     */
    public function getParent()
    {
        return EntityType::class;
    }
}

Use like this:

            $builder->add('company', EntityTreeType::class, [
                'class' => YourClassName::class,
                'children_method_name' => 'myCustomParentFunctionName', // default to getParent
                'parent_method_name' => 'myCustomChildrenFunctionName', // default to getChildren
            ])

@mdzzohrabi
Copy link
Author

@kl3sk Thanks for update

@teemuparkkinen
Copy link

teemuparkkinen commented Mar 27, 2020

Is it possible to use query_builder with this?

This is not working. It shows all choices even if I don't search all in the query.
->add('category', EntityTreeType::class, [ 'class' => Category::class, 'query_builder' => ...

@mdzzohrabi
Copy link
Author

@teemuparkkinen Doctrine Query builder only apply conditions on root nodes , you must customize your getChilds() methods in your Entity Class or create a new method for get childs and use updated version of EntityTreeType class by @kl3sk and pass you customized get childs methods name into 'children_method_name' option.

@tony5Dim
Copy link

tony5Dim commented Feb 8, 2021

myCustomParentFunctionName and myCustomChildrenFunctionName are both supposed to return a string ?
I have an error Object of class App\Entity\Theme could not be converted to string.
My getParent and getChildren method returns objects (and a collection for the getChildren) and I don't know how to do.
Any tips ?

Edit: My bad, noob error... I added the __toString() method and it worked. I also used the last version of kl3sk's fork (for the entity_method_name option)

@kl3sk
Copy link

kl3sk commented Feb 8, 2021

@tony5Dim implement __toString() method in your entity that return the name for example.

Edit : sorry I didn’t see that you’ve ever made that ^^

@arvodia
Copy link

arvodia commented Mar 6, 2021

FIX BUG

$result = array_merge(
        $result,
        [new ChoiceView(
                $choice,
                (string)$choice->getId(),
                str_repeat($options['prefix'], $level) . ' ' . $choice->getName(),
                []
            )]
        );
if (!$choice->$children_method_name()->isEmpty())
    $result = array_merge(
        $result,
        $this->buildTreeChoices($choice->$children_method_name(), $options, $level + 1)
    );

@josequinta-pixelarus
Copy link

Hello!
Thank you for share this code!

It works fine when "multiple" and "expanded" are false, but:

'multiple' => true,
'expanded' => true,

Choices generated by this class are overridden by original EntityType class, losing hierarchy and order.
How can we do?
Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment