Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Rewrite Bundle Laravel Package

atoolo/rewrite-bundle

A Symfony bundle for flexible URL rewriting and cleanup, letting you define and apply rules to transform incoming URLs. Documented, PHPStan level 9 tested, and supports PHP 8.2 through 8.4.

View on GitHub
Deep Wiki
Context7

Getting Started

Be clear-eyed before writing any code: atoolo/rewrite-bundle is a Symfony bundle, not a Laravel package. There is no service provider, no facade, no config/rewrite.php, and nothing in the repo targets Laravel. The realistic "getting started" for a Laravel developer is a time-boxed spike in a Symfony harness, not composer require in your laravel/laravel project.

Step 1 — Read the actual docs, not the README. The README is ~10 lines and links out to https://sitepark.github.io/atoolo-docs/develop/bundles/rewrite/. That page is the only place the configuration tree and handler contract are described. Budget your first hour there and in src/ — specifically look for:

  • the bundle class and its DependencyInjection/Configuration.php (this is the config reference — the docs lag it),
  • the rewrite handler interface (the thing SameNavigationUrlRewriteHandler implements),
  • UrlRewriteContext and UrlRewriteOptions (the two DTOs you will touch most),
  • Resources/config/services.yaml to see what is public, tagged, or ordered.

Step 2 — Stand up a throwaway Symfony 7 app. Do not try to weld this into Laravel first; you will spend the day debugging container mismatch instead of evaluating whether the rewrite logic is worth porting.

composer create-project symfony/skeleton:^7.0 rewrite-spike
cd rewrite-spike
composer require atoolo/rewrite-bundle
// config/bundles.php
Atoolo\Rewrite\RewriteBundle::class => ['all' => true],

Watch the transitive installs — the bundle pulls atoolo/resource-bundle, which is a non-trivial dependency with its own configuration. If Composer resolves that cleanly you have learned something useful about coupling; if it fights you, that is a signal too.

Step 3 — Pick the smallest possible first use case. Do not start with locale redirects or microsites. Start with a single normalization rule you can verify with curl -I: /About/ → /about. This exercises the full path (request → rewrite decision → Symfony routing) without touching the context/microsite machinery, which the 1.5.0 "pparam / microsites" bugfix suggests is the rougher edge.

Step 4 — Decide, explicitly, between two outcomes. After the spike you are choosing between:

  1. Port the idea — implement the handler chain natively in Laravel middleware (recommended at this maturity level), or
  2. Run it as a sidecar — a small Symfony service in front of / beside the Laravel app that only does URL rewriting, which is the only integration shape that does not require a Symfony kernel inside a Laravel request.

If neither is appealing, stop here — the bundle is ~100 LOC of logic and 0 stars; you are paying integration tax for very little borrowed code.

Implementation Patterns

Treat handlers as a chain, and own the ordering. The bundle's design is a set of small handler classes (SameNavigationUrlRewriteHandler is the only one named in public releases) operating on a request via a shared context. The day-to-day pattern is: one handler per rule family, registered in priority order, each returning either a rewritten target or a pass-through. When you port this to Laravel, replicate the chain — do not build a monolithic RewriteMiddleware with a 200-line match statement. That is the one genuinely reusable idea here.

Pass state through the context object, not through config lookups. UrlRewriteContext (added in 1.1.0) exists so handlers can carry per-request decisions — current microsite, whether this is a navigation link, resolved locale — without each handler re-querying the container. In a Laravel port, this maps naturally to a request-scoped singleton or a Request attribute bag:

// app/Rewrite/RewriteContext.php
final class RewriteContext
{
    public function __construct(
        public readonly string $path,
        public ?string $locale = null,
        public ?string $microsite = null,
        public bool $isNavigationLink = false,
    ) {}
}

Bind it as scoped so Octane and queued jobs do not leak state between requests.

Use the options DTO for output shape, not input. UrlRewriteOptions with its lang attribute (1.0.0) is the "what should the result look like" object — it is where you set the target language prefix rather than string-munging the path in the handler. Follow that split in your port: handlers decide whether and where, the options object decides how it is rendered. It keeps redirect logic testable without HTTP.

Locale prefixes are conditional, and that condition is config-driven. The 1.4.0 fix — "lang-prefix rewriting only with translation-locales" — tells you the intended workflow: you configure a set of translation locales, and prefix rewriting is active only for those. Mirror this with an explicit allowlist in config/rewrite.php rather than deriving locale support from app()->getLocales(), so a stray locale cannot silently generate redirects:

// config/rewrite.php
return [
    'translation_locales' => ['en', 'de', 'es'],
    'normalize_case'      => true,
    'strip_trailing_slash' => true,
];

Middleware placement matters more than handler design. In Laravel, rewrite-and-redirect must run before the router resolves the route but after TrustProxies/TrimStrings, otherwise you rewrite a path built from the wrong host or a path that still has whitespace. Register it in the web group ahead of SubstituteBindings:

// bootstrap/app.php (Laravel 11+)
->withMiddleware(function (Middleware $middleware) {
    $middleware->web(prepend: [
        \App\Http\Middleware\RewriteUrl::class, // before routing
    ]);
})

Separate "redirect" from "rewrite" from the very first commit. The package name is ambiguous and it will bite you. A rewrite is internal (same URL, different route); a redirect is a 301/302 the client sees. SEO intent (canonical, lowercase, trailing-slash) requires a redirect. Language negotiation and microsite resolution usually want a rewrite or a redirect depending on whether the URL is user-visible. Model this as an explicit result type:

enum RewriteResult {
    case Pass;                          // continue to router
    case Redirect;                      // 301 to $target
    case Rewrite;                       // re-dispatch with $target
}

Integration tip for a sidecar. If you keep the Symfony bundle running standalone, make it stateless and config-only: it receives a path + host + Accept-Language and returns a target + status code. That is a trivially testable contract and it insulates you from atoolo/resource-bundle entirely.

Gotchas and Tips

The Symfony dependency is the whole story. atoolo/resource-bundle is a hard requirement and it wants Symfony's Config, DependencyInjection, and HttpKernel. There is no version of "just use this one class" that avoids that graph. Before you sell anyone on integration, run composer why atoolo/resource-bundle in the spike and read what it drags in. If the answer is "a lot," the port-to-middleware option wins by default.

Zero stars and zero dependents is a real (if soft) risk. Not a reason to reject the logic — it is small and readable — but it means: no Stack Overflow answers, no blog posts, no third-party bug reports to search when something behaves oddly. You are the first integrator. Pin the version in composer.json, and if you fork for the port, keep the fork in-house so upstream churn cannot break you.

The docs are thin and the releases are the real changelog. Every public-API fact you can rely on is currently a release note. Skim 1.0.0 → 1.6.0 before adopting; the bugfix titles are unusually informative about design intent (e.g. 1.3.0 "SameNavigationUrlRewriteHandler only for links" tells you that handler used to over-apply — if you hit unexpected rewrites on non-link URLs, this is the historical shape of that bug).

The pparam/microsite code path is the least-tested corner. 1.5.0 shipped a fix for "get param for microsites." In any port, treat microsite resolution as a separate, individually-tested concern. Write a test with two hosts resolving to two different rule sets before you ship — this is the classic place for cross-tenant URL leakage, and the bundle's own history says it is fragile.

Watch the PHP floor and ceiling. The bundle declares 8.1–8.5, and 8.1/8.2 are already outside Laravel 11/12's comfortable support window depending on your version. It is not a blocker for a port (you are copying logic, not requiring it), but it is a blocker if you go the hybrid/sidecar route — that service must run on a PHP your infra supports, which may force you off 8.1.

Handler ordering bugs look like "the rewrite is ignored." With a chain, the most common failure is that an earlier handler already consumed the request or returned a context that makes a later handler's guard false. Add a debug log line at entry and exit of each handler with the path and the decision; if you cannot see the decision, you will waste an afternoon on the router.

Guard against circular rewrites at the framework level, not in each handler. /a → /b plus /b → /a will produce an infinite redirect chain that only shows up in a browser, never in curl -I. Cap rewrite depth (one hop, or three) in the middleware and log when the cap is hit. This is the single highest-value thing to port even if you port nothing else.

Clear the container cache after every config change. Standard Symfony pain, but relevant here: rewrite configuration is compiled into the container, so bin/console cache:clear before you re-test, or you will conclude your edit did nothing. In the Laravel port, the equivalent is php artisan config:clear — same class of bug, so do not be smug about it.

Extension point to prefer: a custom handler, not a config hack. If you need business logic (A/B paths, user-specific destinations), implement a new handler in a Symfony spike and see whether the interface is genuinely small. If it is, that interface is the thing you copy into app/Rewrite/Contracts/RewriteHandler.php — and you get the bundle's whole value proposition in ~40 lines of your own code.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr
laravel2580/idehelpers
delatbabel/viewpages
anavel/gettext
muhamadrezaar/highcharts
delatbabel/keylists
frankkessler/salesforce-laravel-oauth2-rest
anavel/foundation
wdmtech/enviricons
eii/laravel-captcha
muka/shape-reader
tdt/json