Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.idea
/vendor/*
42 changes: 31 additions & 11 deletions Controller/TreeCrudController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,31 @@

namespace Umanit\EasyAdminTreeBundle\Controller;

use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection;
use EasyCorp\Bundle\EasyAdminBundle\Collection\FilterCollection;
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
use EasyCorp\Bundle\EasyAdminBundle\Config\Assets;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Config\KeyValueStore;
use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
use EasyCorp\Bundle\EasyAdminBundle\Dto\SearchDto;
use EasyCorp\Bundle\EasyAdminBundle\Event\AfterCrudActionEvent;
use EasyCorp\Bundle\EasyAdminBundle\Event\BeforeCrudActionEvent;
use EasyCorp\Bundle\EasyAdminBundle\Exception\ForbiddenActionException;
use EasyCorp\Bundle\EasyAdminBundle\Factory\ActionFactory;
use EasyCorp\Bundle\EasyAdminBundle\Factory\EntityFactory;
use EasyCorp\Bundle\EasyAdminBundle\Factory\FieldFactory;
use EasyCorp\Bundle\EasyAdminBundle\Factory\FilterFactory;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
use EasyCorp\Bundle\EasyAdminBundle\Security\Permission;
use Gedmo\Tree\Entity\Repository\NestedTreeRepository;
use LogicException;

abstract class TreeCrudController extends AbstractCrudController
{
Expand All @@ -29,7 +38,20 @@ public function __construct(ManagerRegistry $doctrine)
$this->doctrine = $doctrine;
}

public function index(AdminContext $context)
public function createIndexQueryBuilder(SearchDto $searchDto, EntityDto $entityDto, FieldCollection $fields,
FilterCollection $filters): QueryBuilder {
/** @var NestedTreeRepository $repository */
$repository = $this->doctrine->getRepository($entityDto->getFqcn());

$queryBuilder = $repository
->createQueryBuilder('entity')
->orderBy('entity.root, entity.lft', 'ASC');

return $queryBuilder;
}


public function index(AdminContext $context)
{
$event = new BeforeCrudActionEvent($context);
$this->container->get('event_dispatcher')->dispatch($event);
Expand All @@ -46,19 +68,17 @@ public function index(AdminContext $context)
$context->getCrud()->setFieldAssets($this->getFieldAssets($fields));
$filters = $this->container->get(FilterFactory::class)->create($context->getCrud()->getFiltersConfig(), $fields, $context->getEntity());

$repository = $this->doctrine->getRepository($context->getEntity()->getFqcn());

$queryBuilder = $repository
->createQueryBuilder('entity')
->orderBy('entity.root, entity.lft', 'ASC')
;
$queryBuilder = $this->createIndexQueryBuilder($context->getSearch(), $context->getEntity(), $fields, $filters);

$this->doctrine->getManager()->getConfiguration()->addCustomHydrationMode('tree', 'Gedmo\Tree\Hydrator\ORM\TreeObjectHydrator');
$entities = $queryBuilder->getQuery()->getResult();
$query = $queryBuilder->getQuery()->setHint(Query::HINT_INCLUDE_META_COLUMNS, true);
$query->getResult("tree");
$entities = $query->getResult();
$entities = $this->container->get(EntityFactory::class)->createCollection($context->getEntity(), $entities);

$this->container->get(EntityFactory::class)->processFieldsForAll($entities, $fields);
$actions = $this->container->get(EntityFactory::class)->processActionsForAll($entities, $context->getCrud()->getActionsConfig());

$this->container->get(FieldFactory::class)->processFieldsForAll($entities, $fields);
$actions = $this->container->get(ActionFactory::class)->processGlobalActionsAndEntityActionsForAll($entities, $context->getCrud()->getActionsConfig());

$responseParameters = $this->configureResponseParameters(KeyValueStore::new([
'pageName' => Crud::PAGE_INDEX,
Expand Down Expand Up @@ -100,7 +120,7 @@ public function configureCrud(Crud $crud): Crud

public static function getEntityFqcn(): string
{
throw new \LogicException('Override this method in child class');
throw new LogicException('Override this method in child class');
}

public function configureAssets(Assets $assets): Assets
Expand Down
3 changes: 1 addition & 2 deletions DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ class Configuration implements ConfigurationInterface
/**
* {@inheritDoc}
*/
public function getConfigTreeBuilder()
{
public function getConfigTreeBuilder(): TreeBuilder {
$treeBuilder = new TreeBuilder('umanit_easy_admin_tree');
$rootNode = $treeBuilder->getRootNode();

Expand Down
34 changes: 10 additions & 24 deletions Entity/AbstractTreeItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,6 @@
#[ORM\MappedSuperclass]
abstract class AbstractTreeItem
{

#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
protected int $id;

#[ORM\Column(length: 255)]
protected string $name;

#[Gedmo\TreeLeft]
#[ORM\Column(name: 'lft', type: Types::INTEGER)]
protected $lft;
Expand All @@ -42,29 +33,22 @@ abstract class AbstractTreeItem
#[ORM\OrderBy(['lft' => 'ASC'])]
protected $children;

public function getId(): int
{
return $this->id;
}
abstract public function getId();

public function getName(): string
{
return $this->name;
}
abstract public function getName(): string;

public function setName(string $name): void
{
$this->name = $name;
}
abstract public function setName(string $name): static;

public function getRoot(): ?self
public function getRoot(): ?static
{
return $this->root;
}

public function setParent(self $parent = null): void
public function setParent(self $parent = null): static
{
$this->parent = $parent;

return $this;
}

public function getParent(): ?self
Expand All @@ -77,9 +61,11 @@ public function getChildren()
return $this->children;
}

public function setChildren($children): void
public function setChildren($children): static
{
$this->children = $children;

return $this;
}

public function getLevel()
Expand Down
12 changes: 12 additions & 0 deletions Field/Configurator/TreeConfigurator.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Umanit\EasyAdminTreeBundle\Field\Configurator;

use Doctrine\ORM\EntityRepository;
use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext;
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldConfiguratorInterface;
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
Expand All @@ -19,5 +20,16 @@ public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $c
{
$targetEntityFqcn = $field->getDoctrineMetadata()->get('targetEntity');
$field->setFormTypeOptionIfNotSet('class', $targetEntityFqcn);
$field->setFormTypeOptionIfNotSet('query_builder', static function (EntityRepository $repository) use ($field) {
// TODO: should this use `createIndexQueryBuilder` instead, so we get the default ordering etc.?
// it would then be identical to the one used in autocomplete action, but it is a bit complex getting it in here
if (null !== $queryBuilderCallable = $field->getCustomOption(TreeField::OPTION_QUERY_BUILDER_CALLABLE)) {
$queryBuilder = $queryBuilderCallable($repository);
} else {
$queryBuilder = $repository->createQueryBuilder('entity');
}

return $queryBuilder;
});
}
}
9 changes: 9 additions & 0 deletions Field/TreeField.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Umanit\EasyAdminTreeBundle\Field;

use Closure;
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldInterface;
use EasyCorp\Bundle\EasyAdminBundle\Field\FieldTrait;
use Umanit\EasyAdminTreeBundle\Form\Type\TreeFieldType;
Expand All @@ -11,6 +12,7 @@ class TreeField implements FieldInterface
use FieldTrait;

public const OPTION_CLASS = 'class';
public const OPTION_QUERY_BUILDER_CALLABLE = 'queryBuilderCallable';

public static function new(string $propertyName, ?string $label = null)
{
Expand All @@ -20,6 +22,13 @@ public static function new(string $propertyName, ?string $label = null)
->addFormTheme('@UmanitEasyAdminTreeBundle/form/themes/tree.html.twig')
->setFormType(TreeFieldType::class)
->addCssFiles('bundles/umaniteasyadmintree/css/tree-field.css')
->setCustomOption(self::OPTION_QUERY_BUILDER_CALLABLE, null)
;
}

public function setQueryBuilder(Closure $queryBuilderCallable): self {
$this->setCustomOption(self::OPTION_QUERY_BUILDER_CALLABLE, $queryBuilderCallable);

return $this;
}
}
73 changes: 68 additions & 5 deletions Form/Type/TreeFieldType.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,95 @@

namespace Umanit\EasyAdminTreeBundle\Form\Type;

use Doctrine\ORM\EntityRepository;
use App\Repository\Client\CategoryRepository;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\Parameter;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
use Gedmo\Tree\Hydrator\ORM\TreeObjectHydrator;
use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader;
use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\ChoiceList;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use TypeError;

class TreeFieldType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void

public function __construct(private readonly ManagerRegistry $registry) {}

public function configureOptions(OptionsResolver $resolver): void
{

$choiceLoader = function (Options $options) {
// Unless the choices are given explicitly, load them on demand
if (null === $options['choices']) {
// If there is no QueryBuilder we can safely cache
$vary = [$options['em'], $options['class']];

// also if concrete Type can return important QueryBuilder parts to generate
// hash key we go for it as well, otherwise fallback on the instance
if ($options['query_builder']) {
$vary[] = $this->getQueryBuilderPartsForCachingHash($options['query_builder'])??$options['query_builder'];
}

return ChoiceList::loader($this, new DoctrineChoiceLoader(
$options['em'],
$options['class'],
$options['id_reader'],
new ORMQueryBuilderLoader(
$options['query_builder']??$options['em']->getRepository($options['class'])->createQueryBuilder('e')
)
), $vary);
}

return null;
};
$resolver->setDefaults([
'expanded' => true,
'block_name' => 'umanit_easyadmin_tree',
'query_builder' => function (EntityRepository $er) {
'query_builder' => function (CategoryRepository $er) {
return $er
->createQueryBuilder('entity')
->orderBy('entity.root, entity.lft', 'ASC')
;
},
"choice_loader" => $choiceLoader,
'choice_attr' => function ($choice, $key, $value) {
return ['data-level' => $choice->getLevel(), 'data-has-child' => !$choice->getChildren()->isEmpty()];
},
'placeholder' => 'umanit.easyadmin.tree.form-field.placeholder',
// 'placeholder' => 'umanit.easyadmin.tree.form-field.placeholder',
]);

}

public function getParent(): string
public function getQueryBuilderPartsForCachingHash(object $queryBuilder): ?array {
if (!$queryBuilder instanceof QueryBuilder) {
throw new TypeError(sprintf('Expected an instance of "%s", but got "%s".', QueryBuilder::class, get_debug_type($queryBuilder)));
}

$this->registry->getManager()->getConfiguration()->addCustomHydrationMode('tree', TreeObjectHydrator::class);
$query = $queryBuilder->getQuery()->setHint(Query::HINT_INCLUDE_META_COLUMNS, true);
$query->getResult("tree");

return [
$query->getSQL(),
array_map($this->parameterToArray(...), $queryBuilder->getParameters()->toArray()),
];
}

/**
* Converts a query parameter to an array.
*/
private function parameterToArray(Parameter $parameter): array {
return [$parameter->getName(), $parameter->getType(), $parameter->getValue()];
}


public function getParent(): string
{
return EntityType::class;
}
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ doctrine:
default:
mappings:
gedmo_tree:
type: annotation
type: attribute
prefix: Gedmo\Tree\Entity
dir: "%kernel.project_dir%/vendor/gedmo/doctrine-extensions/src/Tree/Entity"
alias: GedmoTree # (optional) it will default to the name set for the mapping
Expand Down
4 changes: 3 additions & 1 deletion Resources/config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ services:
autowire: true
tags:
- 'ea.field_configurator'

Umanit\EasyAdminTreeBundle\Form\Type\TreeFieldType:
autowire: true
autoconfigure: true
arguments:
$registry: '@doctrine'

Umanit\EasyAdminTreeBundle\Twig\TreeExtension:
autowire: true
Expand Down
2 changes: 1 addition & 1 deletion Resources/views/form/themes/tree.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<div class="umanit_easyadmintree_tree_field-item {% if child.vars.checked %}umanit_easyadmintree_tree_field-item--active{% endif %}">
<input type="radio" id="{{ child.vars.id }}" name="{{ child.vars.full_name }}" class="form-check-input" value="{% if child.vars.value is defined %}{{ child.vars.value }}{% endif %}" {% if child.vars.checked %} checked="checked"{% endif %} />
<label class="form-check-label umanit_easyadmintree_tree_field-item-label" for="{{ child.vars.id }}">
{% if loop.first %}
{% if child.vars.label == "None" %}
{{ 'umanit.easyadmin.tree.form-field.placeholder' | trans }}
{% else %}
{{ child.vars.label }}
Expand Down
14 changes: 7 additions & 7 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@
"php": "^8.0",
"doctrine/dbal": "^3.4",
"doctrine/doctrine-bundle": "^2.7",
"doctrine/orm": "^2.13",
"doctrine/orm": "^2.13|^3.1",
"easycorp/easyadmin-bundle": "^4.4",
"gedmo/doctrine-extensions": "^3.9",
"stof/doctrine-extensions-bundle": "^1.7",
"symfony/config": "^5.4|^6.0",
"symfony/dependency-injection": "^5.4|^6.0",
"symfony/doctrine-bridge": "^5.4|^6.0",
"symfony/form": "^5.4|^6.0",
"symfony/options-resolver": "^5.4|^6.0",
"symfony/translation": "^5.4|^6.0"
"symfony/config": "^5.4|^6.0|^7.0",
"symfony/dependency-injection": "^5.4|^6.0|^7.0",
"symfony/doctrine-bridge": "^5.4|^6.0|^7.0",
"symfony/form": "^5.4|^6.0|^7.0",
"symfony/options-resolver": "^5.4|^6.0|^7.0",
"symfony/translation": "^5.4|^6.0|^7.0"
},
"minimum-stability": "stable",
"autoload": {
Expand Down