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

Symfony Request Entity Bundle Laravel Package

halloverden/symfony-request-entity-bundle

A Symfony bundle that maps incoming request data directly onto entity objects, simplifying form-free request handling and reducing boilerplate in controllers.

View on GitHub
Deep Wiki
Context7

Getting Started

What this package is (and isn't). halloverden/symfony-request-entity-bundle is a small Symfony bundle that removes the two most repetitive lines in a controller: fetching a Doctrine entity from a route parameter, and hydrating an entity from request data. It works through Symfony's argument resolver / value resolver mechanism, so the payoff shows up as typed controller arguments that are already persisted-or-404'd (and optionally validated) before your action body runs. It is a thin convenience layer — it is not a serialization framework, not an API Platform replacement, and it has zero dependents on Packagist, so treat it as vendor code you may need to read and patch rather than a stable community primitive.

Minimal setup.

composer require halloverden/symfony-request-entity-bundle

Symfony Flex will not have a recipe for a package this small. Register it manually in config/bundles.php:

return [
    // ...
    Halloverden\SymfonyRequestEntityBundle\SymfonyRequestEntityBundle::class => ['all' => true],
];

If the class name differs from the halloverden namespace convention, find it with:

grep -r "extends Bundle" vendor/halloverden/symfony-request-entity-bundle/src

Where to look first. In order:

  1. vendor/halloverden/symfony-request-entity-bundle/README.md — usage snippets; for a 0-star package this is often the only complete documentation.
  2. src/Attribute/ (or src/Annotation/) — the attribute class is the public API surface. Its constructor signature tells you everything about supported options (entity class, route param name, mapping source, validation groups).
  3. src/ValueResolver/ or src/ArgumentResolver/ — this is where the actual resolution logic lives; read it before trusting it.
  4. src/DependencyInjection/Configuration.php — whether configuration is available at all, and whether the attribute can be used without any config/packages/*.yaml entry.

First use case. Replace the classic findOrFail boilerplate. Before:

#[Route('/orders/{id}', methods: ['GET'])]
public function show(int $id, OrderRepository $orders): Response
{
    $order = $orders->find($id) ?? throw $this->createNotFoundException();
    return $this->render('order/show.html.twig', ['order' => $order]);
}

After (adjust the attribute name to whatever the repo actually exports):

#[Route('/orders/{id}', methods: ['GET'])]
public function show(#[RequestEntity(Order::class)] Order $order): Response
{
    return $this->render('order/show.twig', ['order' => $order]);
}

If that works, you have validated the bundle end to end: container compiles, the resolver is registered, and it converts a missing entity into a 404. Everything else is variation on this theme.

Implementation Patterns

Pattern 1 — Route param to entity (read endpoints). This is the bread and butter. The route placeholder name usually must match either the entity's identifier or an explicit param: option on the attribute. Convention over configuration here means: name your placeholder {id} for the primary key, and {slug} (with mappedBy: 'slug') for human-readable routes. Use it on every GET/PATCH/DELETE detail route and delete the repository calls from your actions entirely.

Pattern 2 — Payload to new entity (create endpoints). The more interesting half of the package: constructing an entity from request body rather than looking one up. Typical shape for form-encoded or JSON:

#[Route('/orders', methods: ['POST'])]
public function create(#[RequestEntity(Order::class, source: 'request')] Order $order): Response
{
    $this->em->persist($order);
    $this->em->flush();
    return $this->json($order, 201);
}

Keep the mapping shallow: let the bundle set scalar fields and let your action or a domain service handle nested collections and associations. Mass-assignment of a $customer relation from raw payload is a security hole, not a feature.

Pattern 3 — Update in place (PATCH/PUT). Combine lookup and hydration by resolving the existing entity first, then mapping request data onto it:

#[Route('/orders/{id}', methods: ['PATCH'])]
public function update(#[RequestEntity(Order::class, mapRequest: true)] Order $order): Response
{
    $this->em->flush(); // entity is already managed
    return $this->json($order);
}

Because the resolved entity comes from the EntityManager, it is already managed — you only need flush(), not persist(). This is a genuinely nice ergonomic win and mirrors Symfony's own #[MapEntity] + #[MapRequestPayload] combination in 6.3+/7.x.

Pattern 4 — Validation at the boundary. If the bundle accepts validation groups, wire them to Symfony's validator and let constraint violations surface as 422 rather than a 500 or a silently-persisted invalid entity. Prefer a dedicated group (order:write) so entities used elsewhere with different constraints don't reject your payload for unrelated reasons.

Pattern 5 — Composition with security. Resolve the entity first, then guard with a voter in the action rather than pushing authorization into the resolver:

$this->denyAccessUnlessGranted('ORDER_EDIT', $order);

This keeps the resolver a pure data-mapping concern and makes the authorization testable in isolation. If you find yourself wanting a #[IsGranted] shortcut on the controller class plus attribute-level entity resolution, that ordering works — class-level security runs before argument resolution in the HttpKernel flow.

Pattern 6 — Testing. Because the behaviour is a resolver, you can unit test it against a Request without booting a kernel, and integration test controllers with WebTestCase using the normal $client->request(). Assert three things per route: 404 on missing identity, 422 on invalid payload, 200/201 with the expected serialized shape. Those three assertions catch almost every regression when you upgrade the bundle.

Integration tip. In a larger codebase, restrict this bundle to your "thin CRUD" controllers and use hand-written actions or Messenger command handlers for anything with multi-step invariants. The bundle is a mapping convenience; it is not a transaction boundary.

Gotchas and Tips

Zero dependents means zero upgrade insurance. The package has no dependents and no ecosystem pressure on its maintainers, so semantic versioning is aspirational. Pin it in composer.json ("halloverden/symfony-request-entity-bundle": "0.*" or an exact constraint), read the diff before every bump, and be prepared to vendor-fork it. Budget for the possibility that it is abandoned; a fork of ~200 lines is cheaper than it looks.

Resolver priority conflicts with Symfony core. Modern Symfony ships #[MapEntity] and #[MapRequestPayload] (EntityValueResolver / RequestPayloadValueResolver). If both your bundle's resolver and the core resolver claim the same argument, you get either a "cannot autowire" error or a confusing double-resolution. Check the bundle's tag priority in its service definition (debug:container --tag=controller.argument_value_resolver) and, where possible, standardise on one mechanism per controller rather than mixing. For read endpoints, core #[MapEntity] already does 90% of this; the bundle's differentiated value is the payload-to-entity side.

Route param naming is load-bearing. The overwhelmingly common failure is #[Route('/orders/{orderId}')] paired with a resolver that looks for id. The resolver silently returns null and Symfony either 404s or falls through to autowiring an empty entity — which looks like a mapping bug. Confirm the parameter convention from src/ before debugging anything else, and prefer {id} universally.

Type coercion and "not found" semantics. Route params arrive as strings. A resolver doing a naive find($id) will throw a Doctrine ORMException/DBAL type error for non-numeric input rather than a 404 — which means GET /orders/abc returns 500 in production. Decide deliberately whether non-numeric identifiers should be 404 (preferred) and verify the bundle does it; wrap the resolver or add a route requirement if it doesn't: requirements: ['id' => '\d+'].

Silent nulls are the dangerous failure mode. If resolution fails and returns null, a nullable controller argument passes through and your action runs with a half-built entity. Never type the argument as nullable unless you actually want that. Add a smoke test per controller asserting the 404 path, as above.

Content-type quirks for payload mapping. Hydration from $request->request->all() only sees application/x-www-form-urlencoded and multipart/form-data. A JSON body needs $request->toArray() or json_decode($request->getContent(), true). Many small bundles handle only the former — test with curl -H 'Content-Type: application/json' early, because PHPUnit's $client->request('POST', $url, $params) uses form encoding by default and will hide the bug entirely.

Doctrine identity and association proxies. For update flows, confirm the resolver uses EntityManager::find() (managed, change-tracking, flush() works) and not a hand-rolled new $class() + reflection hydration. A detached entity in an update action produces confusing "no changes detected" or duplicate-row behaviour depending on whether you persist().

Extension point. The right place to customise behaviour is your own class implementing ValueResolverInterface (or the older ArgumentValueResolverInterface) plus an attribute of your own, registered with controller.argument_value_resolver — not editing the vendor tree. Decorate the bundle's resolver via services.yaml decorates: if you only need one behaviour changed (e.g. forcing 404 over null).

Debugging recipes. With the resolution invisible in the action, use: bin/console debug:container --tag=controller.argument_value_resolver to confirm registration; bin/console debug:router to confirm placeholder names; and a temporary dump() inside a small decorated resolver to see what the bundle hands you. If nothing is registered at all, the bundle's DependencyInjection/Extension.php likely requires a config key to activate the service — an empty config/packages/symfony_request_entity.yaml is a cheap first fix.

When to skip it. If you are on Symfony 6.3+, #[MapEntity] and #[MapRequestPayload] cover the read and create paths with first-party support and a real BC policy. Adopt this bundle only if its payload-to-entity (rather than payload-to-DTO) convenience is worth an unmaintained dependency — and for most teams, the honest answer is to prototype with it, then port the two or three patterns you liked onto core Symfony.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource