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

Request Object Resolver Bundle Laravel Package

mops1k/request-object-resolver-bundle

A Symfony bundle that resolves typed request objects from incoming HTTP requests and injects them into controller arguments. It keeps controllers clean by moving request parsing, mapping, and validation into dedicated request classes for simpler, safer endpoints.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check first: this is a Symfony bundle (mops1k/request-object-resolver-bundle), not a Laravel package. As a Laravel developer you cannot composer require it into an app/ and expect it to work — there is no service provider, no config/app.php entry, and it hooks into Symfony's ArgumentResolver / kernel.controller pipeline. If you are assessing it because you are moving into a Symfony codebase (or maintaining one alongside a Laravel app), read on — the mental model transfers almost 1:1 to Laravel FormRequests. If you are looking for the Laravel equivalent, stop reading and reach for FormRequest + $request->validated(), or spatie/laravel-data if you want typed DTOs.

Minimal steps (in a Symfony app):

  1. composer require mops1k/request-object-resolver-bundle — you want v2.0.0 or later; v2 is the first release with attribute support and a pluggable resolver chain. Check the README for the exact bundle class name and attribute namespace before writing code; the package is small and docs are thin, so the source is your spec.
  2. Enable the bundle (Symfony Flex normally edits config/bundles.php automatically).
  3. Make sure symfony/serializer and symfony/validator are installed — the bundle deserializes and validates through them.
  4. Create a DTO with a constructor or public typed properties and add #[Assert\*] constraints.
  5. Type-hint the DTO in a controller action with the bundle's request-object attribute on the parameter.

Where to look first:

  • The README's single usage example — that is effectively the whole public API.
  • The ArgumentResolver class (usually under src/), because that is where deserialization, validation, and error handling actually happen.
  • The attribute class itself — its constructor arguments tell you what is configurable (serialization groups, validation groups, source of data).
  • New in v2: the resolve-chain wiring. This is where each resolver is registered, prioritized, and asked in turn — read it before you try to debug "my DTO is empty", because the answer is almost always "an earlier resolver in the chain claimed it."

First use case: a POST /api/orders endpoint where you currently do Request $request and a pile of $request->request->get(...) / $request->toArray() calls. Replace it with one typed DTO parameter. That single swap is the entire value proposition: the controller signature becomes the contract.

#[Route('/api/orders', methods: ['POST'])]
public function create(
    #[RequestObject] CreateOrderRequest $payload, // verify attribute name in README
): JsonResponse {
    $order = $this->orders->create($payload);
    return $this->json($order, 201);
}
final class CreateOrderRequest
{
    public function __construct(
        #[Assert\NotBlank, Assert\Email]
        public readonly string $email,
        #[Assert\Count(min: 1), Assert\Valid]
        public readonly array $items,
    ) {}
}

Implementation Patterns

DTO-as-contract, one per action. Resist reusing a single OrderRequest across create/update/delete. Separate CreateOrderRequest / UpdateOrderRequest classes make the validation rules and required-ness obvious at the call site. This is the same reason FormRequest subclasses are per-action in Laravel.

Push validation into the DTO, not the controller. Constraints as attributes on properties/constructor params mean validation runs as part of resolution and the controller never sees an invalid object. If the bundle surfaces failures as an HttpException (typically 400), you get a free fail-fast layer — no if ($validator->fails()) boilerplate.

Nested and collection payloads. For {"items": [{"sku": "A", "qty": 2}]}, define an OrderItemInput class and annotate the array with #[Assert\Valid]. Without Valid, nested objects are deserialized but never validated — a silent hole that ships to production surprisingly often.

Groups for shape-shifting payloads. If the same DTO serves a create (email required) and a patch (email optional), use serialization/validation groups on the attribute (e.g. #[RequestObject(groups: ['create'])]) rather than duplicating classes. Verify the exact option names against the attribute constructor — this is the one place the thin README is likely to under-document.

Custom resolvers — the v2 headline. The resolver chain is now extensible, which unlocks the pattern you actually want in a mature app: one controller signature style that reads from the right source per parameter. Typical custom resolvers:

  • Route-parameter → DTO. Resolve a OrderId or OrderFilter DTO from {id} / query string instead of the body, so GET /orders/{id} and POST /orders both use typed parameters.
  • Header / tenant context → DTO. Populate a TenantContext from a header or the authenticated user, validated like any other DTO.
  • Entity → DTO. Reuse an existing EntityValueResolver-ish resolver but map onto your input type, keeping controllers free of findOrFail.

Register yours with a tag and a priority so it runs before the generic body resolver — otherwise the body resolver claims the parameter first and hands you an empty object. Keep each resolver narrow and single-source; a resolver that inspects the request and guesses is much harder to debug than two resolvers with explicit ordering.

Composite payloads from multiple sources. With a chain in place you can split a request into #[RequestObject] CreateOrderRequest $body and OrderId $id on the same action, each resolved by a different link in the chain. This is where the bundle starts to genuinely beat hand-rolled $request->toArray().

Snake-case wire format. JSON APIs frequently use snake_case while PHP properties are camelCase. Configure a CamelCaseToSnakeCaseNameConverter on the serializer in config/packages/framework.yaml rather than naming PHP properties $first_name. Do it app-wide so DTOs stay idiomatic.

Testing is where the pattern pays off. Functional tests become short and declarative:

$client->request('POST', '/api/orders', server: ['CONTENT_TYPE' => 'application/json'], content: json_encode($body));
self::assertResponseStatusCodeSame(422);

Because the DTO is a plain class, unit tests can new CreateOrderRequest(...) and hand it to the handler directly — no HTTP layer, no mocks of Request. This is the biggest day-to-day win, and it's identical to why Laravel devs like ->validated() arrays being replaced by DTOs.

Integration tip / bridge pattern. If you genuinely maintain both stacks, keep the DTO classes in a framework-agnostic namespace (no Symfony or Laravel imports beyond validator attributes) and hydrate them from Laravel via a small FormRequest::toDto() method. That gives you one shared contract definition and two thin adapters, instead of two divergent request shapes.

Gotchas and Tips

  • v2.0.0 is a major bump on a one-person, nine-star project. It's the first release in well over a year and it changes how attributes and resolution are wired — assume breaking changes to the attribute signature and to anything you customised around the resolver. Pin the exact version, read the commit diff (not just the two-line changelog), and budget for a fork. If you are on Symfony 6.3+, compare against native #[MapRequestPayload] before adopting — the bundle's original "better alternative" pitch is now largely moot, and its main remaining edge is the pluggable chain.
  • Upgrading from v1: the old attribute usage may not be source-compatible. Grep your codebase for every attribute usage and every DTO type-hint in a controller, upgrade in a branch, and run your 422-status functional tests — those are the cheapest detector of behavioural drift in a resolver.
  • Content-Type is load-bearing. These resolvers typically deserialize a JSON body. Form-encoded (application/x-www-form-urlencoded) or multipart/form-data payloads will not populate the DTO the way you expect. File uploads need a different approach entirely.
  • Empty or missing body usually produces a deserialization exception rather than a clean validation error. Test POST with {} and with no body at all before you ship; wire a kernel.exception listener if the default response shape isn't your API's error envelope.
  • Unknown keys. The Serializer's ALLOW_EXTRA_ATTRIBUTES default behavior matters: extra JSON fields either throw or are silently dropped. Decide deliberately — silently dropping is friendlier for clients but hides typos.
  • Type coercion surprises. JSON true → bool works; "true" → bool depends on the denormalizer. "1" into an int is fine; "abc" into an int throws a NotNormalizableValueException that often surfaces as a 500 rather than a 422. Add a test for one bad-type payload per DTO.
  • Validation only runs if the bundle is configured to run it. If constraints appear to be ignored, you're likely missing symfony/validator or a config flag. Confirm by deliberately violating a NotBlank in a test.
  • Debugging path: php bin/console debug:event-dispatcher kernel.controller (or debug:container --tag=controller.argument_value_resolver) to confirm the resolver is registered and at what priority. With a resolver chain this is now the first place to look — dump the registered resolvers and their priorities, and check whether an earlier one is claiming your parameter. A DTO arriving as an empty object means a resolver ran; which one is the question.
  • Custom resolver ordering is not optional. Tag priorities decide who wins. Test each new resolver in isolation (assert the DTO is populated from the intended source) and in combination with the body resolver, or you'll get intermittent empties that look like serialization bugs.
  • Warm the serializer cache in CI/production (cache:clear, cache:warmup), otherwise the first request per DTO pays denormalization metadata cost.
  • Extension points worth knowing: a custom Denormalizer for value objects (Money, Uuid), a custom constraint + validator for cross-field rules (startDate < endDate — property-level attributes can't express that easily), a kernel.exception listener to normalize validation errors into {errors: {field: [...]}}, and now a custom resolver for non-body sources. Those four cover nearly every real-world gap.
  • The Laravel takeaway: the pattern — typed request DTO, validation declared on the object, controller signature as documentation — is worth copying even though the code isn't. FormRequest::toDto() plus readonly constructor-promoted DTOs gets you 90% of the ergonomics with zero third-party risk. If you specifically want the resolver half (typed, validated parameters from body, route, headers), that maps onto Laravel's own route-model binding + a small ResolvesFromRequest concern.
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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package