Inline Rules with the Callback Constraint
The Callback constraint is the usual way to define custom validation rules,
but it requires a lot of code for simple rules. In Symfony 8.2, the Callback
constraint adds a message option. When it's set, the callback only has to
return a boolean and Symfony creates the violation when it returns false.
Combined with PHP 8.5, which allows static closures in constant expressions, you can now define the whole rule inside the attribute:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
class Order
{
#[Assert\Callback(
static function (\DateTimeImmutable $value): bool {
return $value > new \DateTimeImmutable('today');
},
message: 'The delivery date must be in the future.',
)]
public \DateTimeImmutable $deliveryDate;
public int $subtotal;
// the closure gets the same arguments as before, so it can access the object
#[Assert\Callback(
static function (int $value, ExecutionContextInterface $context): bool {
return $value >= $context->getObject()->subtotal;
},
message: 'The total must be greater than or equal to the subtotal.',
)]
public int $total;
}
Unique Constraint with Object Properties
The fields option of the Unique constraint checks the uniqueness of a
collection based only on some of its fields. However, it only worked for
collections of arrays. In Symfony 8.2, it also works for collections of objects:
1 2 3 4 5 6 7 8
use Symfony\Component\Validator\Constraints as Assert;
class Cart
{
/** @var CartItem[] */
#[Assert\Unique(fields: ['sku'])]
public array $items = [];
}
Public properties are read directly and the rest of the values are read with the PropertyAccess component, so getters and property paths work too.
Arbitrary Precision Numbers
PHP 8.4 added the BcMath\Number class to work with arbitrary precision
numbers, which is common in financial applications. In Symfony 8.2, the Range
constraint and the comparison constraints (EqualTo, NotEqualTo,
GreaterThan, GreaterThanOrEqual, LessThan and LessThanOrEqual)
support these values:
1 2 3 4 5 6 7 8 9 10 11
use BcMath\Number;
use Symfony\Component\Validator\Constraints as Assert;
class Payment
{
#[Assert\Range(min: 0.01, max: 10000)]
public Number $amount;
#[Assert\GreaterThanOrEqual('0.0001')]
public Number $exchangeRate;
}
Comparing a BcMath\Number with a float in PHP truncates the float to an integer
(new Number('10.2') > 10.5 is true). That's why Symfony converts the limits
to BcMath\Number objects before comparing them, so the precision is kept on
both sides.
Non-String Violation Parameters
When creating violations in custom constraint validators, the setParameter()
method only accepted strings, while setParameters() accepted any value. This
was inconsistent, especially when using the ICU message format in your
translations, which can format numbers and dates. In Symfony 8.2, setParameter()
accepts string, int, float, \Stringable and \DateTimeInterface
values:
1 2
# translations/validators+intl-icu.en.yaml
booking.too_late: 'Bookings must be made before {deadline, date, long}.'
1 2 3
$this->context->buildViolation('booking.too_late')
->setParameter('deadline', $booking->getEvent()->getStartsAt())
->addViolation();
Group Sequence Improvements
Group sequences validate the groups of a class step by step. In the sequence,
the group of the class itself had to be written as its short class name (e.g.
'Order'), a plain string that is easy to miss when renaming the class. In
Symfony 8.2, you can use the ::class constant of the class instead.
In addition, when a class defines a group sequence, the objects referenced with
Valid are validated only against the Default group, so their constraints
in the other groups of the sequence never run. The new cascadeCurrentGroup
option validates those objects against the current group of the sequence too:
1 2 3 4 5 6 7 8 9 10 11 12
use Symfony\Component\Validator\Constraints as Assert;
#[Assert\GroupSequence([Order::class, 'Shipping'], cascadeCurrentGroup: true)]
class Order
{
// the Address constraints that belong to the 'Shipping' group
// are now checked in the 'Shipping' step of the sequence
#[Assert\Valid]
public Address $shippingAddress;
// ...
}
This option is also available in the YAML and XML formats, and in the
#[Assert\GroupSequenceProvider] attribute for classes that compute their
sequence dynamically.
More Flexible Cascade Validation
The groups option of the Valid constraint does two things at once: it
decides when to validate the nested object and it restricts which of its
constraints are checked. This makes it hard to create reusable nested objects,
because the parent object decides which constraints run on them. In Symfony 8.2,
set the new restrictGroups option to false to use the groups only to
trigger the cascade:
1 2 3 4 5 6 7 8 9 10 11
use Symfony\Component\Validator\Constraints as Assert;
class Order
{
#[Assert\Valid(groups: ['alternateInvoiceAddress'], restrictGroups: false)]
public Address $invoiceAddress;
}
// the address is only validated when 'alternateInvoiceAddress' is one of the
// validated groups, and then it's checked against all of them
$validator->validate($order, null, ['Default', 'alternateInvoiceAddress']);
The other improvement is that Valid can now be used inside the
Sequentially constraint. This is useful for values that can have more than
one shape and should only be cascaded after checking some other constraint first:
1 2 3 4 5 6 7 8 9 10
use Symfony\Component\Validator\Constraints as Assert;
class ImportRequest
{
#[Assert\Sequentially([
new Assert\Type(Product::class),
new Assert\Valid(),
])]
public mixed $payload;
}
The nested Valid constraint can't define its own groups; set them in the
Sequentially constraint instead.
This is amazing! Thank you much!