pugx/extravalidator-bundle
PUGXExtraValidatorBundle extends Symfony's Validator component with a set of additional, ready-to-use constraints, letting you validate common real-world data—such as locale-specific identifiers and anti-spam checks—with simple annotations or YAML rules.
This bundle adds a handful of constraints that Symfony's core validator historically didn't ship — the classic case being "require at least N checkboxes" on multi-choice form fields. It is a thin Symfony bundle: constraint classes, validator classes, and a service definition that tags the validators with validator.constraint_validator. There is no bundle configuration to speak of, so "getting started" is really "install, register, use a constraint on a field."
1. Install (and accept the caveat)
composer require pugx/extravalidator-bundle
The package is archived. There will be no bug fixes or Symfony-version compatibility work. Pin an explicit tag/commit in composer.json so a future composer update on a fresh machine resolves the exact code you tested against.
2. Register the bundle (Symfony 2.x style)
// app/AppKernel.php
public function registerBundles()
{
return [
// ...
new PUGX\ExtraValidatorBundle\PUGXExtraValidatorBundle(),
];
}
No config.yml entry, no extension parameters — the bundle's job is just to load its validator services.
3. Where to look first
Before writing a line of code, read the source, not the README:
Validator/Constraints/ — the constraint classes. This is the authoritative list of what the bundle actually offers for your pinned commit; names and option sets differ between tags.Validator/ — the matching ConstraintValidator implementations. Read 2–3 of them; they tell you exact option names, default messages, and — critically — what getTargets() returns (property vs. class constraint).Resources/config/ — confirm the XML/YAML service file tags each validator with validator.constraint_validator and an alias. If you later get "validator not found" errors, this file is the reason.Tests/ — usually the best documentation of real usage, including edge cases (empty arrays, null values) the README omits.4. First use case: a checkbox group with a minimum selection
use PUGX\ExtraValidatorBundle\Validator\Constraints as PugxAssert;
$builder->add('roles', 'choice', [
'choices' => $roles,
'multiple' => true,
'expanded' => true,
'constraints' => [
new PugxAssert\MinSelected(['min' => 2]), // verify exact class/option names in Validator/Constraints
],
]);
Then in the controller — the validator runs inside the normal form validation, so there is nothing extra to call:
if ($form->isValid()) {
// ...
}
If the selected count is below the threshold, the error lands on that field and $form->get('roles')->getErrors() contains the message. That is the whole workflow: install once, then treat the constraints exactly like NotBlank or Length.
Attach constraints in the form type, not in entity mapping. These constraints are form-shaped (checkbox counts, field comparisons). Keeping them in FormType::buildForm() means they can't accidentally fire during entity persistence or API deserialization, where "at least 2 roles selected" may be a UI rule rather than a domain invariant.
Use class-level constraints for cross-field rules. If the constraint's validator returns Constraint::CLASS_CONSTRAINT from getTargets(), it must be attached to the object, not the field:
$builder->add('startDate', 'date')
->add('endDate', 'date');
$resolver->setDefaults([
'constraints' => [new PugxAssert\DateAfter(['firstField' => 'startDate', 'secondField' => 'endDate'])],
]);
Attaching a class-level constraint to a property throws a ConstraintDefinitionException at mapping time — that's your signal you picked the wrong target.
Reuse via a shared form-type base or trait. Projects typically end up applying the same checkbox/date rule across several forms. Wrap them:
trait ExtraValidatorConstraints
{
protected function minSelection($min)
{
return new PugxAssert\MinSelected(['min' => $min]);
}
}
This also gives you one place to swap the implementation when you migrate off the bundle.
Drive behaviour with validation groups. Extra constraints are a natural fit for multi-step wizards and partial saves:
$form = $this->createForm(MyType::class, $data, ['validation_groups' => ['Default', 'publish']]);
Put the strict rules (minimum selections, date ordering) in a publish group so drafts can be saved half-filled and only the final submit enforces them.
Test constraints directly, not through the form. Instantiate the validator service and call validate() with a crafted value. In a Symfony 2-era codebase, extend the bundle's/validator's ConstraintValidatorTest base class; this is dramatically faster than booting a kernel and submitting a form, and it pins the behaviour you rely on if you later replace the constraint with a core one.
Treat it as a library of examples as much as a dependency. Each constraint is ~30 lines: a Constraint subclass with public options and messages, plus a validator with isValid(). Copying the pattern into your own AppBundle\Validator\Constraints for the next bespoke rule is often better than adding another vendor dependency — you get the same behaviour with no archived-package risk.
Archived means frozen in the Symfony 2 era. Expect friction on Symfony 3+ (constraint API removals, setDefaultOption/getDefaultOption deprecations becoming removals, AbstractConstraintValidator/ConstraintValidator naming changes, service tag and validator.constraint_validator alias behaviour). If the bundle installs but validators are never invoked, suspect class-name/API drift before suspecting your form. Do not upgrade Symfony "around" this bundle silently — verify each constraint with a unit test.
Replace rather than revive where core now covers you. Most of the bundle's value has been absorbed by Symfony core:
multiple choice fields → Count (with min/max) or Choice with min/max options.GreaterThan, LessThan, Range.Callback constraint on the class, or Expression.Expression.A short deprecation sweep replacing each bundle constraint with its core equivalent removes the dependency entirely, which is almost always the right call for a 16-star archived package.
Empty values are usually skipped. Like core constraints, most validators return early on null/''/[]. A required checkbox group submitted with nothing checked may therefore bypass a "min selected" rule silently. Pair the extra constraint with NotBlank (or NotNull) and test the empty-submit path explicitly — this is the single most common production bug with selection-count validators.
Check the default message parameters and translation domain. Messages often interpolate {{ limit }}-style parameters. If a message renders with a literal {{ limit }}, your options key doesn't match what the validator reads (min vs limit vs count). Also confirm the messages are translatable in your locale; the bundle may ship a Resources/translations/validators.* file covering only a few languages, in which case you override in app/Resources/translations/validators.en.yml.
Debug the wiring before debugging the logic. To confirm the validators are registered as services:
# Symfony 2.x / 3.x
app/console debug:container --tag=validator.constraint_validator
# Symfony 4+
bin/console debug:container --tag=validator.constraint_validator
If the alias is missing, the constraint's validatedBy() will resolve to nothing and you'll get an RuntimeException about a validator not being found. Note this is a runtime failure on first validation, not a boot-time failure — tests that never hit that constraint won't catch it.
Inspect errors properly. $form->getErrors() only returns errors attached at that level. For a class-level constraint, errors land on the form root, not the field:
foreach ($form->getErrors(true) as $error) {
echo $error->getOrigin()->getName() . ': ' . $error->getMessage();
}
Extension point: write your own, don't patch vendor code. The clean extension path is to add your own validator service tagged validator.constraint_validator with a unique alias — the bundle doesn't need to know about it, and your rule survives when the bundle is eventually removed. Never edit vendor/pugx/...; the next composer install eats it.
How can I help you explore Laravel packages today?