gepur-it/request-validator-bundle
A Symfony bundle that validates incoming request data against declarative rules, with clean error responses and easy integration into controllers.
Reality check first. gepur-it/request-validator-bundle is a Symfony bundle — there is no Laravel service provider, facade, or config/request-validator.php. Its day-to-day value for a Laravel developer is therefore threefold: (a) if you maintain any Symfony services alongside a Laravel app, it gives you FormRequest-style validation there; (b) it is a useful reference implementation for how to structure request-DTO validation; (c) it is a candidate for "should we adopt this?" review. With 1 star, 0 dependents, and a near-zero score, treat it as unvetted third-party code, not infrastructure.
Minimal setup (Symfony app):
composer require gepur-it/request-validator-bundle
// config/bundles.php
GepurIt\RequestValidatorBundle\RequestValidatorBundle::class => ['all' => true],
Then, in order:
src/ before the README. The README is likely thin. Look for the attribute/annotation class, the exception class, and the event subscriber/listener that turns violations into an HTTP response. Those three files are the entire contract of the package.composer.json constraints. Confirm it supports your Symfony and PHP versions. A 1-star bundle is very often pinned to symfony/*: ^4.4|^5.0 and will silently conflict or break on 6.x/7.x.framework.validation is enabled — the bundle almost certainly delegates to Symfony Validator rather than implementing constraints itself. If validation is off, you get zero violations and a false sense of safety.First use case — replace this:
// before: controller does everything
$data = $request->request->all(); // or json_decode($request->getContent())
if (!isset($data['email'])) { return new JsonResponse([...], 400); }
// ... 40 more lines of hand-rolled checks
with a DTO + constraints, and let the bundle validate and reject:
// before writing this, verify the exact attribute name in src/
#[RequestValidator(MyPayload::class)]
public function create(Request $request): JsonResponse { /* $request now validated */ }
Rough Laravel mental mapping:
| Laravel | This bundle / Symfony |
|---|---|
FormRequest |
Request DTO + bundle attribute |
rules() |
Constraint attributes on DTO properties |
ValidationException → 422 |
Bundle's exception → handled response |
prepareForValidation() |
DTO constructor / normalizer |
$request->validated() |
The typed DTO handed to your controller |
Handler::render customization |
Custom exception listener / normalizer |
One DTO per endpoint, not per entity. CreateOrderRequest, UpdateOrderRequest, SearchOrderRequest. Sharing a DTO across create and update forces you into conditional rules; use validation groups (create, update) instead, keeping one class with explicit group membership.
Let the DTO be the boundary object. Constraints live on the DTO; the controller receives a validated, typed object and never touches $request again. This is the single biggest win and the main reason to bother with the bundle at all.
class CreateOrderRequest
{
#[Assert\NotBlank]
#[Assert\Email]
public string $email;
#[Assert\Positive]
public int $quantity = 1;
/** @var OrderLine[] */
#[Assert\Valid]
#[Assert\Count(min: 1, max: 50)]
public array $lines = [];
}
Normalize the error envelope once. Whatever the bundle's default error body is, wrap it in your own listener so clients see a consistent shape across validated and non-validated endpoints. If you're exposing this to a Laravel frontend, map to Laravel's { "message": ..., "errors": { "field": [...] } } so a single JS error handler works on both sides.
Validate the payload shape before the content. Typed properties mean a string sent to an int property fails during hydration, not during constraint checking. Make sure your listener catches hydration/deserialization errors too, otherwise you get a 500 for what is plainly a 400 — this is the most common production surprise with this class of bundle.
Use Expression/Callback constraints sparingly. Cross-field rules (startDate < endDate, discount only when coupon present) are tempting to express in constraints but become unreadable. Keep at most one or two; move anything involving a DB lookup or business policy into a service and call it from an explicit guard.
Strangler adoption. Introduce the bundle on one endpoint (usually a POST with a real failure history), keep manual validation everywhere else. Add a contract test asserting the 400/422 body shape and the status code; that test is what protects you if you later swap the bundle for plain Symfony Validator, or for Laravel FormRequest if the service is ever ported.
Pin hard. Exact version in composer.json ("gepur-it/request-validator-bundle": "1.0.3", whatever the current one is), committed lockfile, and a CI job that runs the full functional suite on the minimum and maximum supported Symfony versions. With zero dependents, you are the only one discovering breakage.
Maintenance risk is the headline. 1 star / 0 dependents / score ~0.005 means: no community bug reports, no security triage, no upgrade path. Before committing, check the last commit date, open issues, and whether the author responds. Plan for one of three exits: fork it into your monorepo, copy the ~200 lines of logic into your own bundle, or skip it and write a 20-line argument resolver on top of symfony/validator. Because the bundle is thin, all three exits are cheap — which is precisely why adopting it is defensible, but only with the exit pre-planned. Never let business rules accumulate inside the package's extension points.
Version constraint roulette. The most likely failure is composer require resolving to an old Symfony branch or refusing to install. If it conflicts, don't force it with --ignore-platform-reqs; that defers the breakage to runtime.
"Valid" is not "sanitized." The bundle validates; it does not trim, cast, or reject extra fields. Enable strict/whitelist mode if the bundle exposes it (Symfony's allowExtraFields = false) — otherwise unknown keys pass through silently and can surprise you downstream. Also handle "" vs null: NotBlank rejects "", NotNull does not, and clients rarely distinguish.
Booleans and numbers arrive as strings. Form-encoded bodies give you "false" and "3". Decide once whether the bundle coerces (filter_var) or whether your DTO uses a custom setter. Test with Content-Type: application/x-www-form-urlencoded, application/json, and empty bodies — three different code paths, three different failure modes.
Nested collections need #[Assert\Valid] on every level. A missing Valid on array $lines validates the array's count but never the objects inside. Silent, and easy to ship.
Error messages leak if you let them. Symfony validator messages are developer-facing. Don't echo constraint messages containing class names or internal IDs to clients; map to your own catalogue, and localize via the validators translation domain.
Debugging. If validation "doesn't fire," check in this order: (1) bundle registered, (2) framework.validation enabled, (3) the listener is actually subscribed with a priority that beats Symfony's default error handler — a lower priority means your nice 400 is replaced by a generic 500, (4) the attribute is on the routed controller method (not a parent class method, if annotations aren't inherited). Dump the raw ConstraintViolationList in a test rather than the profiler; with no community, your test is the documentation.
Extension points worth knowing. A custom Constraint + ConstraintValidator pair for domain rules (e.g. a valid internal SKU), and a custom exception normalizer for the error envelope. Both live in your code, not the bundle, so they survive a swap.
Not its job. Authorisation, rate limiting, idempotency, and CSRF are out of scope. A validated payload says nothing about whether the caller may perform the action — keep that in your voters/policies, and never infer permissions from a successfully-validated field.
How can I help you explore Laravel packages today?