The Compound constraint allows you to group other constraints into a single reusable constraint to apply the same validation in different parts of your application. For example, you can validate your password policy by checking all conditions with a set of constraints like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Validator/MatchPasswordPolicy.php
use Symfony\Component\Validator\Constraints as Assert;

#[\Attribute]
class MatchPasswordPolicy extends Assert\Compound
{
    protected function getConstraints(array $options): array
    {
        return [
            new Assert\NotBlank(allowNull: false),
            new Assert\Length(min: 8, max: 255),
            new Assert\NotCompromisedPassword(),
            new Assert\Type('string'),
            new Assert\Regex('/[A-Z]+/'),
            // ...
        ];
    }
}

Then, in your User entity, you can apply this compound constraint like this:

1
2
3
4
5
6
7
8
9
10
11
12
// src/Entity/User.php
namespace App\Entity\User;

use App\Validator\Constraints as Assert;

class User
{
    #[Assert\MatchPasswordPolicy]
    public string $plainPassword;

    // ...
}

In Symfony 7.2 we've added some features to make working with compound constraints easier.

Easier Compound Constraint Tests

Alexandre Daubois
Contributed by Alexandre Daubois in #49547

Testing compound constraints used to be unnecessarily complex. That's why we're introducing a new CompoundConstraintTestCase class to simplify those tests. This is an abstract class that requires defining a single method:

1
2
3
4
5
6
abstract class CompoundConstraintTestCase extends TestCase
{
    abstract protected function createCompound(): Compound;

    // ...
}

Using the same MatchPasswordPolicy compound constraint shown earlier, you can now write a test for it like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// tests/Validator/MatchPasswordPolicyTest.php
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Test\CompoundConstraintTestCase;

class MatchPasswordPolicyTest extends CompoundConstraintTestCase
{
    public function createCompound(): Assert\Compound
    {
        return new MatchPasswordPolicy();
    }

    /**
     * @dataProvider provideInvalidPasswords
     */
    public function testInvalid(mixed $password, string $code): void
    {
        $this->validateValue($password);

        $this->assertViolationIsRaisedByCompound($code);
    }

    public static function provideInvalidPasswords(): \Generator
    {
        yield 'Blank' => ['', Assert\NotBlank::IS_BLANK_ERROR];
        yield 'Too short' => ['a', Assert\Length::TOO_SHORT_ERROR];
        yield 'Not a string' => [1, Assert\Type::INVALID_TYPE_ERROR];
        // ...
    }

    public function testValid(): void
    {
        $this->validateValue('VeryStr0ngP4$$wOrD');

        $this->assertNoViolation();
    }
}

Add Groups and Payload Constructor Arguments

Alexander M. Turek
Contributed by Alexander M. Turek in #58062

A while ago, we added the $groups and $payload arguments to the constructor of many composite constraints, but we didn't add them to the compound constraint. In Symfony 7.2, we're adding those arguments so you can pass them in the constructor:

1
2
3
4
5
6
7
8
9
10
class User
{
    #[Assert\MatchPasswordPolicy(
        groups: ['registration'],
        payload: ['severity' => 'error'],
    )]
    public string $plainPassword;

    // ...
}
Published in #Living on the edge