jrm/request-bundle
jrm/request-bundle is a Symfony bundle that streamlines HTTP request handling, giving you a structured way to validate incoming data, map parameters to typed objects, and keep controllers thin. Drop-in integration and sensible defaults make it easy to adopt in existing projects.
Caveat before step one: jrm/request-bundle is a Symfony bundle, not a Laravel package. Its composer manifest pulls Symfony HTTP-kernel/DI components, and its value comes from Symfony's bundle boot + argument-resolver machinery. A Laravel app will not execute that boot logic, so composer require jrm/request-bundle in a Laravel project gets you a dependency, not a feature. Read this as "what it does, and how the pattern maps to Laravel" rather than an install-and-go guide.
New in 0.3.3 — check your Symfony version first. The release is a compatibility narrowing: Symfony 7.2 and higher only. Earlier lines supported 6.4/7.0/7.1; 0.3.3 does not. Before anything else run composer show jrm/request-bundle --all and read the require block, then composer why-not jrm/request-bundle 0.3.3 if resolution fails. This matters even for a Laravel app: Illuminate pins Symfony components transitively, so Laravel 10 (Symfony 6.x components) will hard-conflict with this constraint, and anything else in your tree still holding a symfony/*: ^6.4 or ^7.0 floor can block the upgrade.
Minimal steps if you are in a Symfony 7.2+ app:
composer require jrm/request-bundle (expect ^7.2 on symfony/http-kernel + friends; if you are on 7.1, stay on 0.3.2).config/bundles.php (Flex usually does this).DependencyInjection/*Extension.php and Resources/config/services.*.yaml — that is where the request factory / argument resolver is wired. Confirm whether the bundle expects you to configure a request class or type-hint a subclass.App\Http\AppRequest extends Symfony\Component\HttpFoundation\Request with your helpers (->user(), ->wantsJson(), ->inputOr($key, $default)).AppRequest in a controller action where you previously had Request.First meaningful use case: stop repeating $request->query->get(...) ?? $request->request->get(...) and JSON-detection boilerplate in every action. Put it on one custom request class and let the resolver inject it.
In Laravel the equivalent first step is not this bundle — it is php artisan make:request StoreOrderRequest (a FormRequest) or binding a Request subclass in a service provider via $this->app->bind(Request::class, AppRequest::class) with a factory. Start there before pulling a third-party abstraction.
Subclass-and-type-hint. The core pattern the bundle enables: one AppRequest subclass per app (or per domain), injected by type-hint. Keep it thin — helpers that read from query, request, headers, files, or attributes. Don't put business logic in it; it is a transport object with ergonomics.
final class AppRequest extends Request
{
public function user(): ?User { return $this->attributes->get('user'); }
public function inputOr(string $key, mixed $default = null): mixed
{
return $this->request->get($key) ?? $this->query->get($key) ?? $default;
}
}
Populate once, read everywhere. If the bundle provides a factory, populate attributes with the authenticated user, tenant id, and request id once in a listener/middleware, then read from the injected request in controllers, subscribers, and services. This is the pattern that actually saves lines of code day-to-day.
Hydrate a DTO from the request. Pair the custom request with a small DTO: StoreOrderData::fromRequest(AppRequest $r). Controllers become public function store(AppRequest $r, StoreOrderData $d). This is the closest analogue to Laravel's FormRequest::validated() and is worth copying even if you never install the bundle.
Testing. Because it extends Request, build instances with Request::create('/orders', 'POST', [...]) and pass them to the controller directly, or configure the test container's request factory. Do not mock Request — it is cheap to construct.
Laravel mapping, if you are porting the idea:
FormRequest for validation; a Request subclass + container binding for read helpers.Illuminate\Http\Request into controller methods; override the binding to swap in your subclass.$request->merge([...]) or setting attributes via $request->attributes->set(...).Ecosystem mismatch is the biggest one. Tiny download footprint, effectively zero dependents, and a release cadence driven by one maintainer bumping Symfony version floors rather than adding features — 0.3.3 is only a constraint change. You are the first production user. Pin the exact version, vendor it if you must, and budget for it being abandoned. For a Laravel codebase, the honest recommendation is: don't install it, copy the pattern.
0.3.3 is a silent breaking change, not a feature release. "Symfony 7.2 and higher only" means: (a) existing installs on Symfony 6.4/7.0/7.1 cannot upgrade without a framework upgrade first, and (b) the patch-style version number (0.3.3) hides that from composer update reflexes. Pin to 0.3.2 or 0.3.3 explicitly — never ^0.3 — and read the diff of composer.json between tags, since the release notes are a one-liner and the constraint is what actually changed. Also check whether the Symfony floor was raised only or whether transitive pins (e.g. symfony/http-foundation, symfony/dependency-injection) moved with it; a partial bump can leave you with a resolver from one minor and a request from another.
Argument-resolver ordering. In Symfony, custom ValueResolvers run in tag-priority order. If Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver fires first, you get the base Request, not your subclass, and the type-hint silently doesn't match — you get a 500 about an unresolvable argument or, worse, the wrong object. Check debug:container --tag=controller.argument_value_resolver and adjust priority. Symfony 7.2 shuffled several HttpKernel internals, so re-verify priorities after any Symfony minor bump, not just after bundle bumps.
Subclassing Request is a footgun in Symfony. HttpFoundation creates the request from globals via Request::createFromGlobals(), which returns a base Request. If you subclass, either the bundle provides a replacement factory or you must set it up yourself (Request::setFactory() historically, or overriding the service). Verify which the bundle does before assuming the type-hint works — and re-verify after a Symfony upgrade, because setFactory() behaviour has changed across majors.
Don't hold state on the request object. Requests are per-request; caching them in singletons or static properties leaks between requests in long-running workers (RoadRunner, Swoole, FrankenPHP). Same warning applies to Laravel Octane.
Config quirks. Symfony bundles usually expose config under a short alias (jrm_request:). Run bin/console config:dump-reference jrm_request — if the command errors, the bundle has no extension or the alias differs from the package name. Read *Extension::load() to find the real key. A bundle this small may well ship no extension at all, in which case there is nothing to configure and everything is service-definition override.
Debugging checklist. composer why symfony/http-kernel (what pulled the 7.2 floor?), bin/console debug:container AppRequest (is it a service?), debug:event-dispatcher (is a listener populating attributes?), and a dd($request::class) at the top of the action (is the resolver doing its job?). Nine times out of ten the issue is registration or version resolution, not logic.
Extension points worth knowing. The attributes bag is the safest place to stash cross-cutting data (user, tenant, request id) — it survives middleware, is not sent to the client, and is not confused with input. Reach for it before merge() or query->set().
Laravel equivalents to reach for instead: FormRequest for validation + authorization, Request::macro() for small helpers without a subclass, a global middleware for population, and $this->app->bind(...) if you truly need a custom Request class everywhere. All four are first-party, documented, won't fight your Symfony constraint set, and won't strand you on an unmaintained bundle.
How can I help you explore Laravel packages today?