alengo / sulu-redirect-bundle
CSV-driven permanent redirects for Symfony/Sulu sites: map legacy URLs to their new location at kernel.request.
Package info
github.com/alengodev/SuluRedirectBundle
Type:symfony-bundle
pkg:composer/alengo/sulu-redirect-bundle
Requires
- php: ^8.1
- sulu/sulu: ^2.6 || ^3.0
- symfony/cache-contracts: ^2.5 || ^3.0
- symfony/config: ^6.4 || ^7.0
- symfony/dependency-injection: ^6.4 || ^7.0
- symfony/event-dispatcher: ^6.4 || ^7.0
- symfony/http-foundation: ^6.4 || ^7.0
- symfony/http-kernel: ^6.4 || ^7.0
Requires (Dev)
- php-cs-fixer/shim: ^3.93
- rector/rector: ^2.3
README
CSV-driven permanent redirects for Symfony / Sulu sites. Maps legacy URLs to their new
location at kernel.request, before routing and security run. Built for site migrations
where a list of old → new URLs has to be honoured with proper 301 responses.
How it works
A single kernel listener (priority 40, before the router at 32) resolves the incoming
host to a Sulu webspace via WebspaceManager (using the webspaces.xml config for the
current environment). It then looks up the domainless request URI in that webspace's CSV
(config/app/{webspaceKey}_redirects.csv) and, on a match, issues a redirect to the same
host + target path. Unknown hosts and unmatched URIs fall through to normal routing.
The CSV is parsed into an in-memory hash map (O(1) lookup) and cached across requests via
cache.app, keyed by the file's modification time — editing a CSV invalidates it
automatically, no cache:clear needed.
Environments (wildcard hosts)
In dev/stage/test, Sulu webspaces typically declare the {host} wildcard, so
any host resolves to the webspace and redirects fire regardless of the incoming
domain. This is safe by construction: the redirect target is always built on the
incoming host ($request->getSchemeAndHttpHost() . $target), so a match on
foo.example.com redirects to foo.example.com, never to a different domain. In prod,
webspaces pin concrete hosts, so only real production hosts match and everything else
falls through to normal routing (typically a 404).
Behind a CDN / reverse proxy (trusted proxies)
The listener reads the request through standard Symfony accessors — getHost() (webspace
resolution), getRequestUri() (CSV lookup) and getSchemeAndHttpHost() (redirect target).
These are the same accessors Sulu itself uses to resolve webspaces and generate URLs,
so the bundle introduces no proxy requirement beyond what the Sulu site already needs — but
it does inherit it: the emitted Location is only correct when the app sees the real
public scheme and host.
Behind a CDN or reverse proxy (Bunny, Cloudflare, Fastly, Varnish, an Apache/nginx front, …) that means one of:
- Trusted proxies — set Symfony's
trusted_proxiesto the proxy IPs and trust at leastX-Forwarded-Proto(for the scheme) and, if the proxy forwards the public host that way,X-Forwarded-Host. Without this the forwarded headers are ignored and the redirect can come out ashttp://…, adding an extrahttp → httpshop. - or an origin-level host rewrite — some setups don't forward the public host as a header
at all (the origin serves its own vhost and would answer the public host with
421). There the public host is presented to the app at the web-server level (e.g. ApacheRequestHeader set Host "www.example.com"beforemod_rewrite), and the origin connection itself is HTTPS so the scheme is already correct.X-Forwarded-Hostis then intentionally not trusted.
Consequence either way: whatever host + scheme the app resolves is exactly where a matched
legacy URL redirects to. If the front-end terminates TLS and talks HTTP to the origin,
trust X-Forwarded-Proto or the Location will be http. There is no open-redirect risk
— the target is always built on the app-visible host, never on an attacker-supplied one.
Installation
composer require alengo/sulu-redirect-bundle
With Symfony Flex the bundle is registered automatically. Otherwise add it to
config/bundles.php:
return [ // ... Alengo\SuluRedirectBundle\AlengoRedirectBundle::class => ['all' => true], ];
Configuration
Create config/packages/alengo_redirect.yaml. All keys are optional; defaults shown:
alengo_redirect: enabled: true csv_dir: '%kernel.project_dir%/config/app' # directory holding the per-webspace CSVs csv_pattern: '{webspace}_redirects.csv' # {webspace} → webspace key delimiter: ';' status_code: 301 # 301 permanent, 302 temporary priority: 40 # must stay > 32 (before the router)
Allowed domains are not configured — the incoming host is matched against Sulu's webspaces.xml for the current environment. A host that belongs to no webspace is ignored.
The CSV
One CSV per webspace, named after the webspace key (e.g. hasslacher_redirects.csv). One
redirect per line, source and target separated by the delimiter. Both columns are
domainless — an absolute path (with optional query), starting with /. Lines whose
source does not start with / (comments, blanks) are skipped. Matching is exact against
the incoming request URI (path + query), and the redirect keeps the incoming host.
/data/_dateimanager/produkte/HNT-Produkthinweise-DE.pdf;/media/2979/download/Produkthinweise.pdf?v=1
/cli/xml_sitemap.php?LNG=de;/de
License
MIT © alengo