ruwork/route-optional-prefix
A Laravel package for route groups with optional prefixes, letting a single route definition match URLs both with and without a given path segment. Useful for locale, region, or section prefixes without duplicating routes.
ruwork/route-optional-prefix is a routing helper that lets a path segment (almost always /{_locale}) be omitted while still resolving to the same controller — so /about and /fr/about both hit AboutController, with _locale defaulting when absent. It does the duplicate-route bookkeeping Symfony's routing component won't do for you.
Because this is a subtree-split read-only mirror, the canonical docs and tests live in the main Ruwork monorepo, not in the split repo.
Install (the package is archived, so pin intentionally):
composer require ruwork/route-optional-prefix
If a release tag no longer resolves against your Symfony version, inspect the latest tag's composer.json constraints before forcing dev-master.
Register it only if it ships a bundle (Ruwork\RouteOptionalPrefix\…Bundle) — add it to config/bundles.php. Many Ruwork packages are bundle-less and expose plain classes instead; check src/ first.
Read these first, in order:
src/ — you'll see either a RouteCollection subclass/extension, a routing loader decorator, or a Route option (commonly _optional_prefix). That tells you whether configuration is YAML-level or code-level.tests/ — the fastest source of truth for the exact option names and expected route table after load.ruvents/ruwork repo README for the usage snippet that got stripped during the split.First use case — optional locale prefix:
# config/routes.yaml
controllers:
resource: ../src/Controller/
type: attribute
prefix: /{_locale}
defaults: { _locale: en }
requirements: { _locale: en|fr|de }
After loading, verify both variants exist:
bin/console debug:router | grep about
# about /about
# about.fr /fr/about
Then confirm resolution: bin/console router:match /about should match with _locale=en.
Two-route fan-out is the core pattern. For every route behind the prefix, expect the loader to emit a prefixed and an unprefixed route. Design your controllers to read _locale from the request the same way in both cases — never from the URL manually.
Collection-level application (best for whole resource blocks). Apply the optional prefix once to an imported controller resource rather than per-action. This is where the package pays for itself: one declaration covers dozens of routes, and the prefix logic stays out of your controllers.
Per-action overrides. Most implementations let a single route opt out via a route option, e.g. leaving the locale-prefixed variant off for health-check or webhook endpoints that should never be localized:
health:
path: /health
controller: App\Controller\HealthController
options: { _optional_prefix: false } # confirm option name in src/
URL generation. The prefixed route is usually registered with a name suffix, so generation is explicit:
$url = $this->generateUrl('about', ['_locale' => 'fr']); // /fr/about
$url = $this->generateUrl('about'); // /about
If your app uses named routes via Route attributes, prefer generating by the base name and let the package's default decide, then assert the output in a test — URL generation ordering is the most common place this breaks.
Canonical-locale redirect middleware. A practical combination: let the optional prefix match both forms, then add a listener that 301s /en/about → /about (or the reverse) for SEO. Do the redirect on kernel.request after routing so you can inspect the matched route name and _locale.
Locale switching without session state. Because the locale lives in the path, build a language switcher that regenerates the current route with a different _locale:
$switchUrl = $this->generateUrl(
$request->attributes->get('_route'),
['_locale' => $target] + $request->attributes->get('_route_params')
);
This works cleanly only if every route behind the switcher has the optional prefix applied.
Sitemap and cache keying. With two URL forms per route, decide which is canonical before you generate <loc> entries or key any HTTP cache — otherwise you'll store duplicates under different keys.
Archived and unmaintained. With zero stars, zero dependents, and an archived flag, assume no upstream fixes. Vendor the source or add compatibility shims yourself if a Symfony upgrade changes RouteCollection/loader signatures. Read the full diff of src/ before adopting — it's small.
Duplicate route names. The fan-out means your route table roughly doubles for prefixed resources. Watch for debug:router output showing overwritten names; most implementations suffix the prefixed variant, but a per-action Route(name: ...) that matches the suffix pattern will collide.
Ordering and first-match wins. Symfony matches in insertion order. If you have a catch-all or a /{slug} route, it can shadow the unprefixed variant of another route. Run bin/console router:match /about after every routing change.
Requirements must be anchored and non-empty. _locale is a path segment; if your requirement regex permits an empty string, the unprefixed variant degenerates into double slashes or an ambiguous match. Use en|fr|de, not .*.
Always supply a default for the optional segment. Without defaults: { _locale: en }, the unprefixed route matches but $request->getLocale() and _locale are null, and every locale-aware service downstream misbehaves in a way that only shows up in production behind the short URL.
Clear the router cache every time. bin/console cache:clear (or at minimum rm -rf var/cache/*/url_matching_routes.php) — routing config changes silently don't apply otherwise, and you'll spend twenty minutes debugging a change that was never loaded.
Watch the leading slash. The prefix value must include it (/ {_locale}), and the route path must not. Double-slash URLs match inconsistently across web servers and break signed URLs.
Don't also read locale from _locale in the query string. If you support both path and query locales, decide precedence explicitly in a kernel.request listener; otherwise users get cached pages in the wrong language.
Extension point: decorate the loader. If you need behavior the package doesn't expose (custom naming, per-locale trailing slash rules), decorate Symfony's routing loader rather than forking — pull the RouteCollection out of the loader's return, post-process, and hand it back. That keeps you upgradable if the package ever revives.
Pin and test on upgrade. Add a routing smoke test that asserts the exact path set for a sample resource:
$this->assertSame(
['/about', '/fr/about'],
array_column($router->getRouteCollection()->all(), 'path')
);
With an abandoned dependency, this test is your only early warning when Symfony's routing internals shift.
How can I help you explore Laravel packages today?