selective/validation
Selective Validation is a Laravel package for validating only the request fields you choose. It simplifies partial updates, conditional forms, and API endpoints by applying rules to selected attributes instead of the full payload, with an easy, lightweight integration.
Install it with Composer and confirm the PHP floor in composer.json (the package targets modern PHP 7.4/8.x, with 2.3.0 adding explicit PHP 8.5 support):
composer require selective/validation
The public surface is deliberately tiny, so read these four files first, in this order:
src/Validation.php — the accumulator object (addError(), fails(), getErrors()).src/ValidationError.php — the value object for a single error (getField(), getMessage()).src/Exception/ValidationException.php — the throwable wrapper used at boundaries.src/Factory/ValidationFactory.php — the DI-friendly way to obtain a fresh Validation.First use case: validating a decoded request payload in a service/action class and returning a 422 if anything is wrong.
use Selective\Validation\Factory\ValidationFactory;
$factory = new ValidationFactory();
$validation = $factory->createValidation();
$data = $request->all();
if (empty($data['username'])) {
$validation->addError('username', 'Username is required');
}
if (!filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL)) {
$validation->addError('email', 'Invalid email address');
}
if ($validation->fails()) {
$errors = [];
foreach ($validation->getErrors() as $error) {
$errors[$error->getField()][] = $error->getMessage();
}
return response()->json(['message' => 'Validation failed', 'errors' => $errors], 422);
}
Notice the key difference from Laravel's validator: nothing throws on the first failure. You collect every problem, then decide what to do — that is the notification pattern, and it is the reason to reach for this package.
One validator class per input. Wrap the checks in a small, injectable class so controllers stay thin and the logic is unit-testable without HTTP:
final class CreateUserValidator
{
public function __construct(private ValidationFactory $factory) {}
public function validate(array $data): Validation
{
$v = $this->factory->createValidation();
if (empty($data['username'])) {
$v->addError('username', 'Username is required');
}
if (strlen($data['password'] ?? '') < 8) {
$v->addError('password', 'Password must be at least 8 characters');
}
return $v;
}
}
Throw at the boundary, catch centrally. Inside domain/service code, throw ValidationException (wrapping the Validation) and let a single handler render it. In Laravel, register it in app/Exceptions/Handler.php:
use Selective\Validation\Exception\ValidationException;
public function render($request, Throwable $e)
{
if ($e instanceof ValidationException) {
$errors = [];
foreach ($e->getValidation()->getErrors() as $error) {
$errors[$error->getField()][] = $error->getMessage();
}
return response()->json(['message' => 'Validation failed', 'errors' => $errors], 422);
}
return parent::render($request, $e);
}
For Slim/PSR-15 projects, the same behaviour is a three-line middleware that catches ValidationException and emits the JSON body — no framework coupling needed.
Bind the factory in the container. In AppServiceProvider::register(), bind ValidationFactoryInterface to ValidationFactory; then validators are autowired and easy to fake in tests:
$this->app->bind(
\Selective\Validation\Factory\ValidationFactoryInterface::class,
\Selective\Validation\Factory\ValidationFactory::class
);
Where it actually earns its keep in Laravel. Use it for domain-level and non-HTTP validation: console commands, queued jobs, importers, and DTOs where FormRequest is unavailable or where you want to report all import row errors rather than abort on the first. Keep FormRequest for HTTP shape/auth concerns; don't duplicate the same rules in both.
Translation. Messages are plain strings passed to addError(). If you need i18n, route them through a translator (the package ships a TranslatorInterface abstraction) or build a field→message catalog in your validator class. Because the package does not own the message text, you control tone and locale completely.
Testing. Assert on fails() and inspect getErrors() directly — no HTTP layer required:
$validation = (new CreateUserValidator(new ValidationFactory()))->validate([]);
$this->assertTrue($validation->fails());
$this->assertSame('username', $validation->getErrors()[0]->getField());
Merging results. There is no built-in merge(); to compose sub-validators, iterate getErrors() on each child and re-addError() the field/message onto the parent. It is three lines and keeps composition explicit.
?? defaults and cast before validating. strlen($data['password']) on a missing key is a fatal error, not a validation error.required/nullable semantics — it is a bag of errors. Decide explicitly whether a field is absent, empty, or invalid, or you will emit misleading messages.items.0.name) so the client can map errors back to inputs.ValidationError (or wrap it in your own DTO) rather than parsing message strings.ValidationException is for crossing layers. Do not use it for loop control inside validators; return the Validation object instead.^2.3 keeps that predictable. The upside: the whole library is a few hundred lines, so vendoring or replacing it later is trivial. Wrap it behind your own Validator interface if that risk matters to you.$validation->getErrors() (field + message pairs) rather than var_dump-ing the object; and when a ValidationException bubbles up uncaught, check your handler registration first — a missing catch is the usual cause of a 500 instead of a 422.ValidationFactoryInterface for pre-seeded validations, a custom translator for locales, and middleware/exception handlers for transport. That covers essentially every integration need without forking.unique/exists, conditional rules, nested arrays, and localization for free. Reach for selective/validation when the notification pattern or framework independence is the point — typically action classes, importers, and shared domain code.How can I help you explore Laravel packages today?