kreyu/nip-validator-bundle
A Symfony bundle that adds validation for Polish NIP tax identification numbers. It integrates with Symfony’s Validator component, providing a constraint and validator to verify NIP format and checksum in forms and entities.
Install and register
composer require kreyu/nip-validator-bundle:^2.1
Pin ^2.1 specifically if you intend to use the constraint as a PHP attribute — that support is the headline change in v2.1.0. On earlier tags the class is a plain Constraint and #[Nip] will blow up at class-load time (see Gotchas).
The bundle targets Symfony ≥ 6.1. If there is no Flex recipe for it (likely given the package's size), register it manually in config/bundles.php:
return [
// ...
Kreyu\Bundle\NipValidatorBundle\KreyuNipValidatorBundle::class => ['all' => true],
];
If Symfony Flex auto-registered it, this entry will already exist — don't add it twice.
Where to look first
src/Validator/Nip.php — the constraint. As of 2.1.0 it carries an #[Attribute(...)] declaration, which is what makes #[Nip] legal. Check the declared targets before putting it on a method or a promoted constructor argument.src/Validator/NipValidator.php — the checksum logic itself; a few lines, worth reading once so you know whether it normalizes separators.src/Resources/translations/ — the error message IDs, so you know what to override or translate.composer.json — confirm the PHP and symfony/validator ranges against your project. Attribute support implies PHP ≥ 8.0.First use case
Validate a Polish NIP on a DTO used by a form:
use Kreyu\Bundle\NipValidatorBundle\Validator\Nip;
use Symfony\Component\Validator\Constraints as Assert;
final class CompanyDto
{
#[Assert\NotBlank]
#[Nip]
public ?string $nip = null;
}
Attributes work on typed properties and on constructor-promoted properties. Then either let Symfony Forms handle it (constraints on the form field, or data_class + valid), or run it through the validator directly:
$violations = $this->validator->validate($dto);
If count($violations) > 0, render the message from the bundle's translation catalogue.
Constraint on an entity property
The simplest day-to-day pattern. Annotate the persisted field and rely on Doctrine lifecycle validation (or Symfony's Valid in forms):
#[ORM\Column(length: 10, nullable: true)]
#[Assert\NotBlank(groups: ['registration'])]
#[Nip(groups: ['registration'])]
private ?string $nip = null;
Named arguments for options
Attributes are instantiated via reflection, so the constraint's constructor is the API. Pass options by name to stay readable and resilient to signature changes:
#[Nip(message: 'company.nip.invalid', groups: ['billing'])]
public ?string $nip = null;
Positional usage (#[Nip('company.nip.invalid')]) works if $message is the first parameter, but it silently breaks if the constructor is reordered in a future release.
DTO + Symfony Form
Keep constraints on the DTO, not the form type, so validation works in controllers, CLI commands, and messenger handlers that never touch Form:
$form = $this->createForm(CompanyType::class, $dto);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// $dto->nip is a valid NIP
}
Form types should only define widgets (TextType with a null transformer) — let the validator own the rule.
Standalone validator service
For imports, seeders, or API payloads outside a form — the pattern that mattered most before 2.1.0, and still the right one for non-object input:
if (count($this->validator->validate($nip, new Nip())) > 0) {
// reject
}
Validation groups
Register the constraint only in the groups where it matters (e.g., registration, billing) so partial updates or PATCH endpoints don't fail on missing NIPs:
#[Nip(groups: ['billing'])]
Remember: Symfony defaults to the Default group, so if you set explicit groups you must call validate($object, null, ['billing']) or configure the form's validation_groups. Attribute support does not change group resolution.
Reusable form type
Bundle the field + constraint in one form type to avoid drift across forms:
final class NipType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'constraints' => [new Assert\NotBlank(), new Nip()],
]);
}
public function getParent(): string
{
return TextType::class;
}
}
Testing
Unit-test your domain logic with the real validator, not by mocking it:
$validator = Validation::createValidator();
$violations = $validator->validate('1234563218', new Nip());
self::assertCount(0, $violations);
Since attributes are now supported, you can also assert mapping directly without booting a kernel:
$attributes = (new \ReflectionProperty(CompanyDto::class, 'nip'))
->getAttributes(Nip::class);
self::assertCount(1, $attributes);
Keep a small fixture set of valid and invalid NIPs (including dashed and padded versions) so regressions are obvious.
Attribute support is version-gated
#[Nip] only works on v2.1.0+. On 1.x/2.0 you get a fatal at metadata load — typically Cannot instantiate ... as attribute or Attempting to use a non-attribute class. Before blaming your autoloader, run:
composer show kreyu/nip-validator-bundle
If you can't upgrade, drop back to YAML/XML mapping with the FQCN, or new Nip() in code.
Don't nest it under Assert
The class lives in the bundle's namespace, not Symfony's:
use Kreyu\Bundle\NipValidatorBundle\Validator\Nip; // correct
// use Symfony\Component\Validator\Constraints\Nip; // does not exist
#[Assert\Nip] is a common copy-paste error and fails at container/metadata warmup, not at the call site.
Clear the cache after editing attributes
Constraint metadata is cached. Add or remove an attribute and the old mapping can survive:
php bin/console cache:clear
If a newly added #[Nip] "does nothing," this is the first thing to try.
Normalization is on you unless the bundle says otherwise
Polish NIPs are frequently written as 123-456-32-18 or with spaces. Many small validator bundles do not strip separators — they validate the raw string. Verify NipValidator and, if it doesn't normalize, either:
Callback/Regex constraint or DataTransformer before the Nip constraint, orpreg_replace('/\D/', '', $value)).Don't assume a hyphenated NIP will pass just because the checksum is correct.
Null is not invalid
Like all Symfony constraints, null/empty is skipped by the validator. If the field is required, pair Nip with NotBlank; otherwise an empty NIP passes silently.
Bundle registration failures
If you see "cannot find constraint" or the validator service is missing, first check config/bundles.php and run:
php bin/console debug:container --tag=validator.constraint_validator
If the validator isn't listed, the bundle wasn't registered or its services weren't autoconfigured — still the most common first-run error. Note that attribute support makes mapping errors look different (class-load failure vs. "constraint not run"), so confirm registration before chasing the attribute syntax.
Version and maintenance risk
NipValidator before adopting it in a payments/tax flow; the logic audits in a few minutes.composer.json and note it in your own docs so a future composer update doesn't surprise you.6,5,7,2,3,4,5,6,7, last digit must match sum % 11 (and reject % 11 === 10). Keep that escape hatch in mind if the bundle goes quiet again.Framework scope
This is a Symfony bundle — there's no Laravel service provider or Validator::extend hook. If you're consuming it from Laravel you'd have to pull symfony/validator in alongside it, which rarely pays off; implement the checksum inline instead.
Symfony and PHP version mismatch
The bundle requires Symfony ≥ 6.1, and attribute usage requires PHP ≥ 8.0. On 5.4/6.0, or on PHP 7.4, you'll get resolver/attribute errors:
php bin/console --version
composer show symfony/validator
php -v
Message overrides and localization
Override the bundle's message key in translations/validators.pl.yaml (or your locale) rather than editing vendor files. If the message renders as a raw key, translations weren't loaded — clear cache.
Debugging mapping
When a constraint "doesn't run," inspect the actual metadata:
php bin/console debug:validator 'App\Dto\CompanyDto'
This shows which constraints and groups are attached — the fastest way to distinguish a mapping problem from a checksum failure, and to confirm that an attribute was actually picked up.
Extension points
message, groups, payload) rather than subclassing; with attributes, named arguments are the intended configuration surface.How can I help you explore Laravel packages today?