gal-digital-gmbh/pimcore-navigation
A navigation bundle for Pimcore that helps developers build and manage site menus and navigation structures. Integrates with Pimcore documents and supports configurable, template-friendly navigation rendering.
This is not a Laravel package. It's a Pimcore bundle built on Symfony, so the mental model shifts: no Eloquent models (documents are Pimcore\Model\Document instances), no Blade (Pimcore uses Twig), no app() helper for services (use constructor injection or bin/console debug:container).
Minimal steps
composer require gal-digital-gmbh/pimcore-navigation:^3.1.1 — the 3.x line is current, and 3.1.1 is the first 3.x release carrying the menu renderer fix that had previously only shipped on the 2.x branch. Don't start on an older 3.x patch.config/bundles.php (Pimcore 11 / Symfony 6 layout):
GalDigitalGmbh\PimcoreNavigation\PimcoreNavigationBundle::class => ['all' => true],
bin/console cache:clear and bin/console pimcore:cache:clearbin/console config:dump-reference pimcore_navigation (adjust the alias to whatever src/DependencyInjection/Configuration.php defines) and inspect services with bin/console debug:container --tag=twig.extension.Where to look first
src/ — the navigation service/builder and any Twig extension. This is the real API surface; the README on a small bundle is often thinner than the code.config/services.php (or .yaml) — service IDs and autowiring aliases you'll inject.README.md — install snippet and the one canonical example.CHANGELOG.md / GitHub releases — you need this more than usual here, because fixes occasionally land on one major branch and get ported later (see Gotchas).First use case
Render a primary menu in your site layout from the children of the home document:
{# templates/layout.html.twig #}
<nav>
{{ pimcore_navigation(document, { depth: 2 }) }} {# check the actual function name in the Twig extension #}
</nav>
If no Twig function exists, inject the service into your controller instead — the point is to get one working menu before wiring breadcrumbs and sitemaps.
Pattern 1 — Root-driven menus
Bind a menu to a specific branch by passing the root Document, not the current page. This keeps the menu identical across the subtree and avoids accidental "menu of whatever page I'm on" bugs.
Pattern 2 — Active state and trail
Active/highlight state needs the current document passed explicitly (document in Twig, or $request->attributes->get('_document')). Expect the bundle to compute an active trail; if not, walk getParent() up from the current page and compare document IDs against the menu items.
Pattern 3 — Respect Pimcore's built-in navigation properties
Pimcore documents already carry navigation_name, navigation_title, navigation_target, and navigation_exclude. Verify the bundle reads these; if it doesn't, layer them on in your template:
{% if not item.document.getProperty('navigation_exclude') %}
<a href="{{ item.uri }}" target="{{ item.document.getProperty('navigation_target') }}">
{{ item.document.getProperty('navigation_name') ?: item.label }}
</a>
{% endif %}
Pattern 4 — Filtering by document type
Folders, snippets, and links behave differently in menus. Filter on $doc->getType() (page, link, folder, snippet) and on $doc->getPublished() before handing documents to the builder. Links especially: they need getHref(), not getRealFullPath().
Pattern 5 — Caching Navigation is the classic N+1. For anything past trivial sites:
pimcore.document.postUpdate / postDelete events (or a tag-collection per root).Pattern 6 — Recursive Twig macro
For depth > 2, a recursive macro is cleaner than nested for loops:
{% macro render(items) %}
<ul>
{% for item in items %}
<li class="{{ item.active ? 'active' }}">
<a href="{{ item.uri }}">{{ item.label }}</a>
{% if item.pages %}{{ _self.render(item.pages) }}{% endif %}
</li>
{% endfor %}
</ul>
{% endmacro %}
Integration tips for Laravel devs
app() — inject via constructor or use AbstractController::getContainer.{{ }} auto-escapes, like Blade; use |raw sparingly for pre-rendered HTML from the bundle's renderer.Package maturity is still the biggest risk — and the version graph is now the specific trap. It's a small, low-dependency-count package, but it is not a single-release project: there is a maintained 2.x line and a 3.x line, and 3.1.1 exists purely to migrate a menu renderer bugfix that had shipped in 2.1.0 on the older branch. That tells you two things: (a) fixes sometimes land on one branch and are ported to the other later, and (b) if you adopted 3.0.x or 3.1.0 you were running a renderer with a known, already-fixed-elsewhere bug. Pin an exact version, read the changelog on every bump, and vendor-review src/ before adopting. There is no community to catch regressions for you.
Never assume a 2.x fix is in your 3.x install. Before upgrading, or when you hit a rendering oddity (wrong nesting, missing wrapper markup, malformed <ul>), check whether a 2.x patch exists that hasn't been migrated to the branch you're on. 3.1.1 is exactly that scenario resolved — treat it as the floor for 3.x, not as a nice-to-have.
Bugfix-only releases still need a full redeploy cycle. php-cs-fixer noise in the 3.1.1 notes is cosmetic, but the renderer change is behavioural. Run bin/console cache:clear and bin/console pimcore:cache:clear, and invalidate any nav tree you've cached, before concluding the upgrade "did nothing".
Pimcore/Symfony version drift. Check composer.json constraints against your Pimcore major (10 vs 11 imply Symfony 5 vs 6, and different bundle registration styles). Mismatches surface as container compile errors, not runtime ones. Note that branch 2.x vs 3.x may not track the same Pimcore majors — confirm which branch your Pimcore version targets before pinning.
It is not Laravel. Every Laravel instinct — Facades, Artisan, Eloquent relationships, php artisan vendor:publish — has no equivalent. Use bin/console, Pimcore's Element\Service, and Symfony config trees.
Unpublished/hidden documents vanish silently. Menus empty out for no visible reason when a parent folder is unpublished. Debug by dumping $doc->getChildren(true) vs ->getChildren() in a controller before blaming the bundle.
Service and Twig function naming collisions. Pimcore core ships Pimcore\Navigation\Builder and menu renderers. If you have both, debug:twig to see which function wins — aliases are resolved by registration order, and this bites quietly. This matters more now that the bundle's renderer has been patched: if core's renderer is winning, the 3.1.1 fix is simply not in play.
Current-document context is not magically resolved. If active states are all false, nine times out of ten the current document wasn't passed in, or _document isn't set on the request in that context (e.g. inside a snippet rendered from a block).
Debugging checklist
bin/console debug:container gal — confirm the service is registered and its alias.bin/console debug:twig — confirm the extension and function names, and that the bundle's renderer (not core's) is the one wired up.bin/console pimcore:documents:list or a dump() in a throwaway controller to inspect the tree.bin/console cache:clear after editing any bundle config or upgrading — Symfony container caching is aggressive.composer show gal-digital-gmbh/pimcore-navigation) against the changelog before filing a renderer bug.Extension points. Decorate the bundle's service (services.yaml with decorates:) rather than editing vendor code; override Twig templates by placing same-named files in templates/bundles/PimcoreNavigationBundle/; hook document save events for cache invalidation. If the bundle lacks the flexibility you need, wrapping Pimcore\Navigation\Builder directly is often less work than bending a small package — and it frees you from waiting on cross-branch bugfix ports.
How can I help you explore Laravel packages today?