Inline Rules with the Callback Constraint

Javier Eguiluz
Contributed by Javier Eguiluz in #64779

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

FORT Pierre-Louis
Contributed by FORT Pierre-Louis in #48951

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

MD. Saiful Islam Feroz
Contributed by MD. Saiful Islam Feroz in #65110

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

Pascal CESCON
Contributed by Pascal CESCON in #64367

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

naxvog Pascal CESCON
Contributed by naxvog and Pascal CESCON in #63557 and #64641

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

Nicolas Grekas Antonio Pauletich
Contributed by Nicolas Grekas and Antonio Pauletich in #65104 and #66134

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.

Published in #Living on the edge