petroslabs/fr-validator-symfony
Symfony Validator constraints for French data: validate SIRET, SIREN, VAT numbers, postal codes, and phone formats with drop-in constraint classes. Integrates with Symfony's validator service and works in Laravel apps built on the Symfony Validator component.
This package is a thin adapter layer: it turns each of the 9 checks in petroslabs/fr-validator (Nir, Rib, Siren, Siret, Iban, Bic, Vat, Naf, Phone) into a Symfony Validator constraint, and surfaces the core library's typed failure reason as both a message parameter ({{ reason }}-style) and the violation code. It is not a Laravel-native validation rule — Laravel's Illuminate\Validation stack is separate from Symfony's Validator. Plan your first use around that reality.
Minimal steps:
composer require petroslabs/fr-validator-symfony-validator (this pulls in petroslabs/fr-validator and requires symfony/validator). Confirm the exact package name against the repo's composer.json before adding it to require in a project.Siret or Iban, the two highest-traffic checks in French business apps — and at how the constraint exposes the failure reason (message parameter) and the violation code.ValidatorBuilder directly:use Symfony\Component\Validator\Validation;
use Petroslabs\FrValidator\Symfony\Constraint\Siret;
$validator = Validation::createValidator();
$violations = $validator->validate($siret, [new Siret()]);
foreach ($violations as $violation) {
dump($violation->getCode()); // typed failure reason from fr-validator
dump($violation->getParameters()); // includes the reason as a message parameter
}
If you only ever live inside Laravel controllers/FormRequests, jump straight to the "bridging" pattern in the next section rather than building a parallel validator service.
Attribute-driven DTO validation. Apply constraints as attributes on a request DTO and validate the DTO at the edge of your bounded context. This is the pattern the package is designed for:
class CompanyDto
{
#[Siret]
public string $siret;
#[Iban]
public string $iban;
#[Bic]
public string $bic;
}
Bridge into Laravel's validator via a custom Rule. Because Laravel does not consume Symfony constraints natively, wrap them. This keeps the failure code and message parameters available to your validation response:
class SymfonyConstraintRule implements \Illuminate\Contracts\Validation\ValidationRule
{
public function __construct(private object $constraint) {}
public function validate(string $attribute, mixed $value, \Closure $fail): void
{
foreach (Validation::createValidator()->validate($value, [$this->constraint]) as $v) {
$fail($v->getMessage())->translate(); // or map $v->getCode() to your own message
}
}
}
// FormRequest
public function rules(): array
{
return [
'siret' => ['required', new SymfonyConstraintRule(new Siret())],
'iban' => ['required', new SymfonyConstraintRule(new Iban())],
'bic' => ['required', new SymfonyConstraintRule(new Bic())],
];
}
Use violation codes, not messages, for API error mapping. The typed failure reason exposed as code is the stable machine-readable contract. Map it in a single place (e.g., an enum-backed ReasonCode => HTTP/error-key map) so user-facing text and API error keys stay decoupled from the core library's enum:
$violations->get(0)->getCode(); // e.g. a fr-validator reason case -> your error key
Localize via message parameters. The reason is passed as a message parameter, so customise the constraint's message and translate the injected reason. Prefer overriding messages per-constraint at construction (new Siret(message: '...')) or in a translation catalogue rather than string-matching the default message.
Group checks by field, not field type. Siren vs Siret differ by length/format; a form field may accept either depending on the entity (company vs establishment). Compose constraints deliberately and test both branches rather than assuming one constraint covers the field.
Cross-field checks for Iban/Bic/Vat. These are inherently relational (IBAN↔BIC, VAT↔country). Run the single-field constraints first, then a second pass for cross-field consistency to produce one clear error instead of several.
Testing. Symfony's Validation::createValidator() makes these trivially unit-testable without booting Laravel — write value-object/DTO tests here instead of HTTP feature tests.
Illuminate\Validation\Rule. Nothing auto-registers with Laravel. Plan for the bridge rule (above) or route these checks through a Symfony validator service intentionally.CHANGELOG.md on every bump — constraint namespaces, constructor signatures, and the reason-as-code mapping are all fair game to change before 1.0.code comes from the core library's failure-reason enum/cases. Great for mapping, but if fr-validator renames a case, your map breaks. Keep the mapping in one adapter class and cover it with tests.getMessage() raw you'll get a generic string — translate/format with the parameters, or override message at construction.+33, foreign IBANs, and EU VAT formats before relying on the constraint as a hard reject.codes to decide precedence.message per constraint instance, the reason parameter for templating, and wrapping the constraint in your own Laravel Rule so you control message/attribute naming (:attribute) and error keys centrally.$violation->getParameters() and $violation->getCode() together — the parameters tell you what failed (reason), the code tells you why in a form you can branch on. If a value passes unexpectedly, reproduce it against petroslabs/fr-validator directly to isolate adapter vs core behaviour.How can I help you explore Laravel packages today?