Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Easyadmin Tree Bundle Laravel Package

umanit/easyadmin-tree-bundle

Umanit EasyAdmin Tree Bundle adds hierarchical tree views to Symfony EasyAdmin. Display parent-child entities as an expandable tree, reorder nodes with drag and drop, and manage nested records directly in your admin backend. Ideal for categories, pages, menus, and other self-referencing entities.

View on GitHub
Deep Wiki
Context7

Getting Started

What you get: a tree view for hierarchical entities (categories, menu items, pages, org units) inside EasyAdmin, typically replacing the flat index list with a collapsible/expandable tree and drag-and-drop reordering or re-parenting. It is a small bundle: one field/type, some Twig templates, and a JS asset that talks to an endpoint.

Prerequisites

  • Symfony 5.4/6.x, PHP 8.0+, EasyAdmin 4, Doctrine ORM.
  • Your entity must be self-referencing: a parent ManyToOne to itself and a children OneToMany (adjacency list). A position/sortOrder integer is strongly recommended so ordering is stable.
  • Implement __toString() on the entity — the tree renders node labels from it.

Minimal steps

composer require umanit/easyadmin-tree-bundle
# Flex usually registers it; otherwise add to config/bundles.php:
# Umanit\EasyAdminTreeBundle\UmanitEasyAdminTreeBundle::class => ['all' => true],

php bin/console assets:install public   # if not using AssetMapper
php bin/console cache:clear
// src/Entity/Category.php
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'children')]
private ?self $parent = null;

#[ORM\OneToMany(mappedBy: 'parent', targetEntity: self::class)]
#[ORM\OrderBy(['position' => 'ASC'])]
private Collection $children;

#[ORM\Column]
private int $position = 0;

Where to look first

  1. The package README — it is short and contains the exact field class name and the configureFields() snippet.
  2. vendor/umanit/easyadmin-tree-bundle/src/ — check Field/ for the tree field and Controller/ for the move/AJAX endpoint. The public surface is tiny, so reading it takes minutes.
  3. vendor/umanit/easyadmin-tree-bundle/templates/ — the markup you will likely override.

First use case: a CategoryCrudController whose index page shows the category hierarchy instead of a paginated flat list, with nodes linking to edit, plus drag-to-move.

public function configureFields(string $pageName): iterable
{
    // index: tree field; detail/edit: normal fields
    if ($pageName === Action::INDEX) {
        yield TreeField::new('name', 'Category'); // confirm class name in src/Field
        return;
    }

    yield TextField::new('name');
    yield AssociationField::new('parent')->setQueryBuilder(
        fn (QueryBuilder $qb) => $qb->andWhere('entity.id != :self')->setParameter('self', $this->getContext()->getEntity()->getInstance()?->getId())
    );
    yield IntegerField::new('position');
}

Implementation Patterns

Pattern 1 — Tree only on index, standard CRUD everywhere else. This is the lowest-risk integration. Keep the generated EasyAdmin CRUD controller, use the bundle's field in Action::INDEX, and leave new/edit/detail untouched. You get the visual tree without changing forms or persistence.

Pattern 2 — Drag-and-drop re-parenting with server validation. The JS posts the moved node and its new parent/position to the bundle endpoint. In practice you should:

  • Add a Doctrine lifecycle listener or an explicit flush() in the endpoint to persist parent and position.
  • Reject moves that place a node under itself or one of its descendants (walk the parent chain server-side; never trust the client).
  • Wrap the endpoint in a voter check (IS_AUTHENTICATED, role, or a CategoryVoter::MOVE).
  • If the bundle's route lacks CSRF protection, add it in a security.yaml access_control rule or decorate the controller.
// Example guard before persisting a move
for ($p = $newParent; $p !== null; $p = $p->getParent()) {
    if ($p->getId() === $node->getId()) {
        throw new \InvalidArgumentException('Cannot move a node under its own descendant.');
    }
}

Pattern 3 — Scoped trees (multi-tenant / multiple roots). Override createIndexQueryBuilder() to filter by tenant or root, and make sure the same filter is applied to the children query the tree uses. A common bug is the index being scoped but the recursive children fetch returning other tenants' nodes; if the bundle builds children queries itself, add a Doctrine filter on the entity or a repository method the template calls.

Pattern 4 — Coexistence with Gedmo NestedSet / existing tree libraries. If you already use gedmo/doctrine-extensions Tree (nested set), do not let two systems both write parent/lft/rgt. Either (a) let the bundle manage adjacency and add a listener that syncs nested-set columns, or (b) skip the bundle's move endpoint and implement your own reorder action that calls $repo->moveUp()/moveDown(), keeping the bundle only for rendering.

Pattern 5 — Customising the node rendering. Override templates by placing files under templates/bundles/UmanitEasyAdminTreeBundle/. Add badges (status, item count), custom action links, or icons by extending the node partial. This is preferable to editing vendor code when the bundle has no new releases.

Pattern 6 — Replacing the JS, keeping the backend. If the drag library is outdated or conflicts with your CSP, keep the server endpoint and swap the front-end for SortableJS or a simple <select> "move to" form. The bundle's contract is just "persist parent/position"; the rendering is replaceable.

Pattern 7 — Functional test the move path. Because the feature is driven by an AJAX endpoint, a small WebTestCase that logs in as an admin, POSTs a move, and asserts the new parent is the highest-value test you can write:

$client->request('POST', '/admin/category/'.$id.'/move', ['parent' => $newParentId, 'position' => 0]);
self::assertResponseIsSuccessful();
$em->refresh($node);
self::assertSame($newParentId, $node->getParent()->getId());

Integration tips

  • Pair with an AssociationField::new('parent') on the form so users can also fix hierarchy without dragging.
  • Use setQueryBuilder() on the parent association to exclude the current node (and its descendants) from the dropdown.
  • If you need breadcrumbs or "full path" columns, add a computed getPath() on the entity; the tree field itself does not give you that.

Gotchas and Tips

Maintenance risk is the headline. Last release was 2022-10-28 with 23 stars. EasyAdmin 4 has changed field/asset APIs repeatedly since then. Pin easycorp/easyadmin-bundle to a version known to work (composer why-not umanit/easyadmin-tree-bundle is your friend), and budget for a fork if you upgrade Symfony to 7.

Version and PHP compatibility. Expect deprecation notices on PHP 8.2/8.3 (dynamic properties, return types). If composer require resolves an old constraint, check composer show umanit/easyadmin-tree-bundle and test the install in a branch before committing.

Asset loading is the most common "it renders as a flat list" cause. Without assets:install, the JS 404s and the tree degrades silently to nested <ul>s. Open the browser console and Network tab; a missing tree.js is usually the whole problem. With AssetMapper, add the bundle's asset path explicitly.

Index-only field. EasyAdmin fields are page-specific; using the tree field on edit/detail will not render a picker. Keep your normal AssociationField for forms and only use the tree on index.

Ordering is not magic. If you omit a position column (or don't order children by it), sibling order is whatever the DB returns — often insertion order, which changes after edits. Add #[ORM\OrderBy(['position' => 'ASC'])] and persist position on move.

Circular references. Drag-and-drop makes it trivial to create a cycle. Guard on the server; a cycle will cause infinite recursion in Twig and a white screen, which is painful to debug in production.

Authorization on the move endpoint. EasyAdmin's dashboard ACL does not automatically protect custom routes the bundle registers. Check php bin/console debug:router | grep -i tree and add access_control entries; otherwise any authenticated user can re-parent categories by crafting a POST.

N+1 queries on deep trees. Each node may lazy-load children. For shallow category trees this is fine; for hundreds of nodes, either eager-load with a recursive CTE / fetchJoin, or switch to a nested-set strategy. Watch the Doctrine profiler on the index page.

Caching and config changes. After changing the bundle config or templates, run cache:clear; in prod, Twig template overrides are cached aggressively and a stale template is a frequent false alarm.

Debugging the move. The endpoint may return 200 with an HTML error page on failure (EasyAdmin's exception page), so a JS "success" callback can hide a failed save. Inspect the response body, and log the incoming parent/position values on the server for the first few days.

Extension points, in order of safety: (1) template overrides, (2) setQueryBuilder/repository scoping, (3) decorating the field configurator or controller, (4) replacing the JS while keeping the endpoint, (5) forking. Given the release cadence, plan for (4) or (5) rather than upstream fixes.

When not to use it. If you only need a read-only hierarchy, a recursive Twig macro over an adjacency list is ~20 lines, has no maintenance risk, and avoids the asset/version issues. The bundle earns its place mainly for the drag-and-drop UX and the endpoint wiring you would otherwise hand-roll.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle