kevintweber/ktw-database-menu-bundle
A Symfony bundle for creating database-backed menus. Store menu items as Doctrine entities, manage navigation without code changes, and render dynamic menus in Twig templates. Useful for CMS-style applications with frequently updated navigation.
Read this first: it is a Symfony bundle, not a Laravel package. It extends KnpMenuBundle and persists the menu tree through Doctrine ORM. There is no Laravel service provider, no Eloquent model, no Blade view. If you are in a Laravel codebase, your "day-to-day leverage" is limited to reading it as a reference implementation for a schema plus a tree provider — you cannot composer require it. The Laravel equivalents you'd actually adopt are spatie/laravel-menu or a small first-party build.
If you are on a Symfony app (realistically 2.x/3.x vintage, given the bundle's era), the minimal path is:
composer require kevintweber/ktw-database-menu-bundle alongside knplabs/knp-menu-bundle and Doctrine ORM. Expect constraint conflicts on a modern Symfony — resolve by pinning, or accept that you are fork-and-owning it.app/AppKernel.php (or config/bundles.php on Symfony 4).label, routeName, routeParams, url, position, parent, enabled. Nothing about your navigation should live in the bundle's own columns.doctrine:schema:update --force or a hand-written migration.The first real use case: a main menu with two levels where a content editor can reorder items without a deploy. Get that working end to end — entity extension → migration → one seeded tree → knp_menu_render('main') in a Twig layout — before touching anything else. That loop is the whole product; everything else is polish.
Extend the entity, don't fight it. The bundle's value is that your columns are yours. Add:
class MenuItem extends BaseMenuItem
{
/** @ORM\Column(type="string", length=120) */
private $label;
/** @ORM\Column(type="string", length=120, nullable=true) */
private $routeName;
/** @ORM\Column(type="json_array", nullable=true) */
private $routeParams = [];
/** @ORM\Column(type="integer") */
private $position = 0;
/** @ORM\ManyToOne(targetEntity="MenuItem", inversedBy="children") */
private $parent;
/** @ORM\Column(type="boolean") */
private $enabled = true;
}
Keeping routeName and routeParams separate from a free-text url is the single highest-value decision. You generate links through Symfony's UrlGenerator at build time, so changing a route slug does not silently break the navigation.
Ordering. A position integer per parent is enough for editorial drag-and-drop. Always add id as a tiebreaker in the ORDER BY — two items at the same position otherwise render in non-deterministic order and your reorder tests flake.
Active state and the current URI. KnpMenu's isCurrent only works when the menu is built with the request context. Make sure whatever wires the provider passes the current URI/request (setCurrentUri on the root item, or the standard knp_menu Twig extension path). Verify "active trail" on a two-level menu explicitly — it is the most common "why is nothing highlighted" bug report.
Route vs URL resolution. Resolve route names to URLs in a service that takes all rows for a menu, builds an id-indexed array, and hydrates the tree in memory. Do not let the renderer call the URL generator per node inside a recursive template — that is where N+1 creeps in for larger trees.
Caching. Menu trees are tiny, read on every page, and change rarely. Cache the built tree per menu key (locale/tenant variants included in the key) and bust on write. A Doctrine postPersist/postUpdate/postRemove listener on the menu-item entity is the least error-prone invalidation point — editors reorder items through three different code paths, and only the ORM layer sees them all.
Admin surface. In practice the bundle gives you the schema and provider; the CRUD is yours. If you already run EasyAdmin or SonataAdmin, a menu-item CRUD with parent select and a position field is an afternoon. Drag-and-drop reordering is the part that eats the time — budget for it. If you skip the admin UI, you have not actually delivered the feature, just moved the hardcoding into SQL inserts.
Multi-tree / multi-locale. There is no first-class concept of named menus or translations. Add menuKey (and locale, tenantId if relevant) columns yourself and scope the provider by them. Prefer a translation-key column for labels over per-locale rows unless labels genuinely differ per locale.
@Entity) produces confusing Doctrine errors at cache warmup rather than at the point of the mistake. Run doctrine:schema:validate after wiring.url column at all, validate it on save. An invalid route name throws RouteNotFoundException at render time — which means a 500 on every page of the site, triggered by an editor's typo.renderDepth in the Twig call is cheap insurance against a deep or wide tree you did not anticipate.raw and escaping; the default KnpMenu template escapes. Do not flip to raw globally just to get an icon in.postUpdate will silently serve an old order. Include a manual "rebuild navigation" action as an escape hatch.enabled, date window) without touching the entity. KnpMenu's MenuItem::setDisplay() / setDisplayChildren() lets you filter nodes during a menu-building event, which is the cleanest place to put authorization-aware visibility rather than in the template.How can I help you explore Laravel packages today?