swp/menu-bundle
SWP Menu Bundle offers a flexible way to define, organize, and render menus in Symfony applications. Build menu trees in PHP or configuration, integrate with Twig, and handle active states and access control with minimal setup.
What it actually is. swp/menu-bundle is a READ-ONLY subtree split of the src/SWP/Bundle/MenuBundle directory from the Superdesk Web Publisher monorepo. It is a Symfony bundle, not a Laravel package — no service provider, no facades, no Eloquent. It provides the persistence + service layer for hierarchical navigation menus in SWP: Menu and MenuItem Doctrine entities, repositories, factories, a tree-building service, and REST controllers for a headless menu API.
Reality check before you start. With 0 stars, 0 dependents, and a last release in June 2018, treat this as an internal component and a reference implementation, not a library you composer require into an app. Expect the split's composer.json to be incomplete or to point at sibling SWP bundles (swp/common-bundle, swp/route-bundle, swp/content-bundle) that you'll also have to pull from the monorepo.
Minimal steps (Symfony context).
path or vcs repository entry, since the split tag is unlikely to resolve cleanly on its own:
"repositories": [{ "type": "vcs", "url": "https://github.com/SuperdeskWebPublisher/SWP" }],
"require": { "swp/menu-bundle": "dev-master" }
SWP\Bundle\MenuBundle\SWPMenuBundle in AppKernel/bundles.php.Resources/config/routing.yml) only if you want the REST endpoints.Resources/config/doctrine/*.orm.yml and that the menu / menu_item tables are migrated.Where to look first (in this order):
Entity/Menu.php and Entity/MenuItem.php — the domain model, and the only thing you truly need to understand.Resources/config/doctrine/ — the actual column/relation definitions; more reliable than the PHP docblocks.Factory/ — how a menu and its items are constructed (this is the pattern to copy).Service/ — tree assembly and lookup logic.DependencyInjection/Configuration.php — the config tree, if any, and the service aliases you can override.First use case. Create a main menu, attach three items (one pointing at a route, one at an article/content object, one at an external URL), flush, then walk the tree ordered by position and render it in a template.
Create-then-attach. The dominant workflow is: build a Menu, build MenuItems via the factory, attach them to the menu, set parent for nesting, then persist the menu (cascade) rather than each item individually. One flush() per menu, not per item.
Model your items by target type. SWP menus are polymorphic in spirit: an item can reference a Route entity, a content/article entity, or carry a raw URI for external links. In practice you'll write a small factory or enum switch on your side that maps "route / article / external" to the correct set of fields, because the entity itself doesn't enforce that invariant.
Ordering is manual. position is a plain integer the bundle does not auto-maintain. The reliable pattern is to sort children in PHP (usort by position) or to use a repository method with an explicit ORDER BY position ASC. Do not trust insertion order.
Tree building. Two viable approaches:
findOneBy(['name' => 'main']) and recurse over getChildren(). Simple, but N+1 queries on deep trees.parentId and build the nested array yourself. This is what you want for anything rendered on every page.Rendering. If the bundle's Twig helper (swp_menu_render / a MenuExtension) fits your markup, use it; otherwise render a recursive Twig partial over the array tree from the flat-fetch approach. Building a DTO/array tree instead of passing entities to the template also dodges the serializer recursion problem below.
API surface. The controllers expose menus as a REST resource. This is genuinely useful if you're going headless: point your SPA at /api/menus/{name} and let the bundle handle serialization. Configure serialization groups so children is included but parent is not.
Cache invalidation. Menus change rarely and render on every page. Cache the serialized tree keyed by menu name, and invalidate from a Doctrine postPersist/postUpdate/postRemove listener on MenuItem (or a postFlush listener) rather than from the service layer, so changes made through the API also bust the cache.
Extension points. Decorate the menu service rather than editing it; add fields via a Doctrine single_table inheritance subclass of MenuItem if you need per-item metadata (icon, CSS class, visibility flag); add a compiler pass if you need to replace the Twig extension.
Read-only split — no PRs here. Any fix belongs in the monorepo. The split's history and releases are frozen; there will be no bugfix release. Pin to a commit, not a version range.
It will not run on a modern Symfony. The 2018 release targets Symfony 3.4/4.0-era APIs, PHP 7.1, and old Doctrine. Expect deprecation noise at best and hard failures at worst on Symfony 5/6/7. If you're evaluating this for anything new, port the model and tree-building logic, don't install the bundle.
It is not usable from Laravel. No service provider, no config publishing, no Eloquent. A Laravel developer's realistic move is to read Entity/MenuItem.php and Service/*, then reimplement the tree as a menus + menu_items migration with a self-referencing parent_id, an order column, and a MenuTree service — maybe an hour of work versus days of fighting bundle wiring.
Missing siblings cause container compile errors. The bundle assumes swp/common-bundle (translatable/timestampable traits, base entities) and route/content bundles are present. If bin/console cache:clear fails on a missing service, it's almost always a sibling dependency, not this bundle.
Bidirectional parent/children will blow up your serializer. JMS Serializer and Symfony's serializer both recurse infinitely on the parent→children→parent cycle. Use explicit serialization groups, @MaxDepth, or serialize to DTOs. This is the single most common failure when exposing the API.
Cascade on delete is likely not configured. Deleting a parent MenuItem can leave orphaned children with a dangling foreign key. Handle deletion yourself: delete the subtree bottom-up, or add orphanRemoval/onDelete: CASCADE in an override mapping.
position collisions produce unstable ordering. Two items with the same position will sort differently across queries. Add a tiebreaker (position, id) in every query that orders.
YAML mappings, not attributes. The mapping lives in Resources/config/doctrine/. If you add fields to a subclass, you override mappings in your app's config — do not edit the vendor directory, because composer update will silently wipe it.
No documentation, thin test coverage in the split. The authoritative "docs" are the monorepo's functional tests and the SWP templates that consume menus. Search the monorepo for MenuItemFactory usages to see real construction sequences.
Debugging tip. When a menu renders empty, check menu_id consistency first: children must belong to the same Menu as their parent, and the bundle does not enforce it. Dump the raw rows grouped by parent_id — nine times out of ten the tree is fine and the query filter is wrong.
How can I help you explore Laravel packages today?