jackai/symfony-validator
Integrates Symfony Validator with Laravel, letting you use Symfony constraints and validation rules in Laravel apps, reuse validator services, and share validation logic across Symfony and Laravel projects.
This package gives you a typed, constraint-based validation layer on top of Laravel's native validator. As of v1.09 it explicitly supports PHP 8.0, so before anything else confirm your runtime and composer.json platform constraint match (php: ^8.0). If you are still on PHP 7.x, pin to the previous release.
providers entry should be needed unless your app uses a custom bootstrap. Run php artisan vendor:publish --tag=<package>-config if a config file ships with it, then php artisan config:clear.in:, enum, typed scalar checks) so you know what to replace.src/Constraints/ (or equivalent) for the base constraint class. This is the extension point introduced/expanded in this release. Understanding the constructor signature here is the difference between a five-minute rule and an afternoon of debugging.FormRequest and replace a hand-rolled Rule::in([...]) or a custom closure with the package's default type rule. Verify with a Pest/PHPUnit feature test that the validation message is what you expect before touching anything else.Start with a single FormRequest, not a shared base request class, so you can compare native and package behaviour side by side.
Default types in FormRequests. The new default types are designed to be dropped into the rules() array like any string rule. Prefer them over inline in: lists because they keep the set of allowed values in one place:
public function rules(): array
{
return [
'status' => ['required', new OrderStatusType()], // default type, no manual in: list
'channel' => ['required', 'string', new SalesChannelType()],
];
}
Custom constraints with field-name injection. The headline feature in v1.09 is that a custom constraint receives the name of the field currently being validated. This removes the most common reason people abandon constraint classes and go back to closures — you no longer need to hardcode the attribute name or pass it through the constructor:
class UniqueWithinTenant extends Constraint
{
public function __construct(protected string $fieldName) {}
// $this->fieldName is resolved per-attribute, so one class
// can serve 'email', 'phone', 'tax_id' without duplication
}
Register it once and reuse it across many attributes; the field name is supplied by the validator, not by you. This is the pattern to reach for whenever a rule's message or query depends on which attribute is being checked.
Two-tier validation strategy. Use the package's default types for the "shape" of a value (enumerations, typed identifiers) and reserve custom constraints for the "context" checks that need the field name or the request payload. Mixing both in one rules() array is the intended usage; do not try to express a cross-field rule as a default type.
Integration with Validator::make(). For non-HTTP validation (console commands, imports), the same constraints instantiate cleanly:
$validator = Validator::make($payload, [
'role' => ['required', new UserRoleType()],
]);
Keep a single rules() definition in the FormRequest and call the FormRequest from the command if you can, rather than duplicating rule arrays.
Centralise messages. Pair each constraint with a messages() entry keyed by attribute.rule so your API error format stays stable when you swap a native rule for a package constraint. Attribute-name translation via attributes() matters more now that field names flow into constraints automatically.
Testing pattern. Extend your base TestCase with a helper that asserts a payload fails with a specific rule key. Because field names are injected, a test that passes email and one that passes phone through the same constraint class are both worth having — that is the whole point of the feature.
composer.json require block before upgrading; if it moved to ^8.0, your CI matrix and any platform overrides in composer.json must move with it or composer update will silently resolve an older tag.required, string, in), the native rule wins or the behaviour becomes ambiguous. Namespace or prefix your type names, and prefer object instances (new FooType()) over bare strings to make resolution explicit.php artisan config:cache will freeze the old set. If a new default type "doesn't exist," clear config before debugging the class itself.items.*.sku) arrive with the wildcard expanded or not depending on the validator path. Assert on the exact string in a test; do not assume items.0.sku and items.*.sku are interchangeable in your message templates.How can I help you explore Laravel packages today?