fsi/admin-tree-bundle
FSI Admin Tree Bundle adds hierarchical tree management to PHP admin interfaces, helping you organize and reorder nested records such as categories, menus, and parent-child data through a lightweight Symfony bundle.
Reality check first. This is a single-purpose, zero-star, zero-dependent bundle whose last release was February 2020. Before writing any code, budget an hour to read the source — you are adopting it as a reference implementation as much as a dependency, and you should expect to vendor-patch or fork it.
Install and inspect:
composer require fsi/admin-tree-bundle
If resolution fails, check composer.json in the packagist metadata: a 2020 FSi bundle will constrain Symfony to ^3.4|^4.4 and PHP to ^7.x. On Symfony 5.4+/6+/7+ or PHP 8.2+, expect to fork, bump constraints, and run the test suite yourself. Do not let Composer silently downgrade a shared Symfony runtime.
Where to look first, in this order:
vendor/fsi/admin-tree-bundle/composer.json — actual supported stack and which sibling FSi/Sonata packages it expects.src/FSi/Bundle/AdminTreeBundle/Resources/config/ — every service it registers, plus routing.FSi\Bundle\AdminTreeBundle\FSiAdminTreeBundle) — what it adds to the DI container.Register the bundle in config/bundles.php (or AppKernel on legacy Symfony):
return [
// ...
FSi\Bundle\AdminBundle\FSiAdminBundle::class => ['all' => true],
FSi\Bundle\AdminTreeBundle\FSiAdminTreeBundle::class => ['all' => true],
];
Confirm the routes exist — this is the fastest sanity check that wiring succeeded:
bin/console debug:router | grep -i tree
First use case: a category/taxonomy entity shown in a FSi (Sonata-based) admin data grid, where editors need explicit Move up / Move down buttons instead of drag-and-drop — typically because the frontend renders a nested menu or an ordered list from a position column. The bundle's job is exactly: render two row actions, dispatch a move on the entity/repository, redirect back to the grid.
The bundle needs a way to reorder siblings. Two shapes work; pick one deliberately:
Adjacency list + integer position — simplest, but you own the renumbering:
class Category
{
/** @ORM\Column(type="integer") */
private int $position = 0;
/** @ORM\ManyToOne(targetEntity="Category", inversedBy="children") */
private ?Category $parent = null;
public function moveUp(): void { $this->position--; }
public function moveDown(): void { $this->position++; }
}
Doctrine tree (Gedmo Tree / NestedTreeRepository) — the safer bet if the bundle was written with moveUp/moveDown repository calls in mind. Verify by grepping the vendor source for the method names it actually calls:
grep -rn "moveUp\|moveDown\|getTree\|sort" vendor/fsi/admin-tree-bundle/src
Match your entity to whatever it calls; do not assume.
The integration point is the data grid action column. In FSi/Sonata style:
$datagrid->addColumn('move', new Column\ActionColumn([
'label' => '',
'actions' => [
'move_up' => new \FSi\Bundle\AdminTreeBundle\Action\MoveUpAction(),
'move_down' => new \FSi\Bundle\AdminTreeBundle\Action\MoveDownAction(),
],
]));
Class names and option keys vary by version — read Resources/views for the Twig partial that renders them, then mirror its expectations exactly.
position ascending in the grid's data source query, otherwise the buttons appear to do nothing.clear()/refresh() the related entities afterwards.Maintenance risk is the headline. Zero stars, zero dependents, no release since 2020 means: no CVE triage, no Symfony-version catch-up, no Stack Overflow answers. Treat the dependency as frozen. Fork it into your VCS, tag it 1.0.0-fork.1, and use repositories: { vcs: ... } in composer.json so the rest of your team installs your patched copy. Add the fork to your dependency-review watchlist.
Version constraints are the first wall. Verify with composer why-not fsi/admin-tree-bundle before assuming it's installable. If you are on Symfony 6/7, porting is usually small (bundle class, DI config, a Twig include, one action class) — often faster than making the package resolve.
Nested-set vs adjacency list confusion is the top functional bug. With Gedmo Tree, "up/down" means moving among siblings of the same parent; with a plain position column it means swapping numbers. Mixing the two (e.g. position field plus a lft/rgt tree) produces duplicate positions. Pick one and delete the other from the entity.
Pagination breaks the mental model. On page 2, "move up" moves the row into page 1. Editors report this as a bug. Either disable pagination for the tree grid, or show a "first page only" hint, or implement move-across-boundary handling in your action.
GET-based actions and CSRF. Row action links are almost always plain anchors. If the bundle's actions are GET routes that mutate, you have a CSRF-by-link issue (crawlers, prefetchers, browser back-buttons). Either accept it (internal admin, auth required) or, better, wrap moves in a POST form rendered in the action column.
Missing/stale position data. On legacy rows where position is null or duplicated, moves appear random. Run a one-off backfill script before enabling the buttons, and add a DB unique index on (parent_id, position) if your workflow allows it.
Sibling scope and multi-root trees. If your entity has two or more "container" types (e.g. Categories and Tags both ordered), make sure each action is registered only on the right grid and that the move query filters by the correct parent/root.
Translations and translatable fields. FSi bundles often pair with prezent/doctrine-translatable. Moving a translated entity is fine, but if your grid sorts by a translated label, the visible order won't match position. Sort by position only.
Extension points you actually have:
templates/bundles/FSiAdminTreeBundle/...) to change icon markup or hide disabled buttons.tree.moved event from the action so frontend caches and search indexes invalidate.Permissions. Check whether the actions implement getRole() (FSi's action interface does). If not, add one in your subclass — moving nodes is a write operation and should require EDIT, not just view access.
Debugging recipe when a move does nothing: (1) confirm the route fires in the profiler; (2) dump the entity's position and parent before/after in the action; (3) check the grid's ORDER BY; (4) check the entity manager's unit of work — nested-set bundles frequently need an explicit flush() plus clear() before the redirect.
How can I help you explore Laravel packages today?