nowo-tech/routing-kit-bundle
A lightweight routing kit bundle for PHP applications that simplifies route definition, request matching, and URL generation. It offers a clean, extensible API for organizing routing logic and adding reusable routing features to Symfony-style projects.
1. Install and register
composer require nowo-tech/routing-kit-bundle
// config/bundles.php
return [
// ...
NowoTech\RoutingKitBundle\RoutingKitBundle::class => ['all' => true],
];
Confirm the package namespace and service IDs against src/ before copying snippets — treat the class names below as illustrative.
2. Where to look first, in order
| File / area | Why |
|---|---|
README.md |
Install steps, minimal config, the exact route/cache commands |
src/DependencyInjection/Configuration.php |
The real config tree: storage service alias, locales list, default locale, prefix behaviour |
src/DependencyInjection/*Extension.php |
Which services are aliased, tagged, or made public |
| `src/Resources/config/routing*.xml | yaml` |
| Storage interface + default implementation | The one extension point you are most likely to replace |
src/Resources/views/ |
The Twig CRUD panel — where you will add access control |
tests/ |
Working examples of routing assertions against a real kernel |
3. Minimal config
# config/packages/routing_kit.yaml
routing_kit:
locales: [en, fr, de]
default_locale: en
# storage: app.routing_kit.storage # alias your own implementation here
4. Import the routing loader
# config/routes.yaml
routing_kit:
resource: .
type: routing_kit
If the loader is not registered, /foo may still resolve via your normal controllers while /{locale}/foo silently 404s — this is the single most common first-run failure.
5. First use case — end to end
product_show, path /products/widget, locale fr.bin/console cache:clear (or hit the bundle's invalidation path) and verify:
GET /fr/products/widget → 200, correct locale in the requestGET /products/widget → 200 (dual access) with your canonical tag emittedpath().That is the whole loop: admin defines path → storage persists → router matches → template emits canonical URL.
Storage swap (pluggable storage)
The bundle is DB-driven by design, but the persistence layer is an interface. The two realistic patterns:
routing_kit.storage at it. The CRUD panel becomes read-only or irrelevant — that is fine, the routing side still works.final readonly class LegacyPathStorage implements PathStorageInterface
{
public function __construct(private Connection $db) {}
public function findCanonical(string $route, string $locale): ?string
{
// return '/products/widget' or null
}
}
Keep return types identical to the interface; strict typing means a nullable drift breaks the router silently at match time, not at compile time.
Dual access + canonical strategy
The /foo and /{locale}/foo duality is a feature, not a redirect engine — pick a policy and enforce it in one place:
<link rel="canonical"> and hreflang point at the locale-prefixed URL. Cheapest, safest.Put the policy in a kernel.request listener rather than in templates, otherwise you repeat it in every layout.
Twig integration
Use the bundle's helper for all outbound localized links — never concatenate '/' ~ locale ~ path(...):
<a href="{{ routing_kit_path('product_show', {slug: product.slug}, app.request.locale) }}">
Centralising this means a change in prefix behaviour (e.g. dropping /en/) is a config change, not a grep across templates.
Cache invalidation on admin writes
Wire invalidation to the write, not to the deploy:
postFlush).cache:clear.public function postUpdate(LifecycleEventArgs $args): void
{
if ($args->getObject() instanceof CanonicalPath) {
$this->routeCacheInvalidator->invalidate();
}
}
SeoKitBundle bridge
When both bundles are installed, the path bridge feeds canonical paths into SEO path generation (sitemaps, hreflang sets). Pattern: define canonical paths first, then let SeoKit consume them; the reverse order produces sitemap entries pointing at 404s.
# ensure the bridge is enabled rather than hand-rolling sitemap URLs
seo_kit:
routing_kit_bridge: true
Integration tips
{slug} route in config/routes.yaml, otherwise the locale-prefixed URL is swallowed by the generic route.Route cache is the number one source of "it didn't work"
Editing rows directly in the database does not invalidate the compiled route cache. Always go through the CRUD panel or the invalidation service. On deploy, cache:clear and cache:warmup are mandatory — a warmed cache built before migrations will bake in an empty path table.
Locale prefix shadowing real routes
If you have a real route at /de/about (unlikely) or any first segment matching a locale code, the locale loader will claim it. Constrain the locale segment with a requirements regex built from your configured locale list, and keep that list in config — not duplicated in the routing YAML.
Duplicate content penalty
Dual access means two URLs, one page. If you ship this without canonical/hreflang tags you have created an SEO problem while solving one. Make the canonical tag part of the acceptance criteria, not a follow-up ticket.
CRUD panel ships unsecured
The Twig CRUD routes are ordinary routes; nothing stops an anonymous user from rewriting your URLs. Add access_control entries or a firewall role before enabling in any non-local environment. Also validate submitted paths server-side: leading slash required, no query strings or fragments, no .. segments.
Silent 404s from storage mismatches
A path stored with a trailing slash, a missing leading slash, or the wrong route name will persist fine and simply never match. Add a validation step in the CRUD form and a bin/console sanity check that every stored row resolves through the router.
_locale default interaction
Symfony populates _locale from the request when a route has the parameter. If your controllers read $request->getLocale() without a LocaleListener priority consideration, unprefixed requests can fall back to the framework default rather than default_locale from the bundle config. Assert the resolved locale in a test for both URL forms.
Extension points, ranked by safety
Debugging checklist
bin/console debug:router | grep -i routing_kit # is the loader even loaded?
bin/console debug:container --tag=routing_kit.* # are your services tagged?
bin/console router:match /fr/products/widget # what actually matched?
Then confirm the storage layer returns the row you expect for (route, locale) — in most reports, the router is fine and the storage lookup key is wrong.
Upgrade notes
Symfony 8.x requires PHP 8.4+. If your CI runs a single PHP version, a composer update will resolve to 7.4-era packages and mask the 8.1 incompatibilities you will hit in production. Pin the matrix early.
Contribution expectations (if you patch the bundle)
The repo enforces final classes, strict types, constructor injection, English PHPDoc, no BC breaks on config keys/service IDs/Twig blocks, and no agent co-author trailers in commits. Smallest viable patch plus the nearest test is the accepted shape.
How can I help you explore Laravel packages today?