8000 feature #58542 [Validator] Add `Slug` constraint (raffaelecarelle) · symfony/symfony@18d08ff · GitHub
[go: up one dir, main page]

Skip to content

Commit 18d08ff

Browse files
committed
feature #58542 [Validator] Add Slug constraint (raffaelecarelle)
This PR was squashed before being merged into the 7.3 branch. Discussion ---------- [Validator] Add `Slug` constraint | Q | A | ------------- | --- | Branch? | 7.3 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | | License | MIT Introduce Slug constraint class for validating strings as slugs. Add unit tests to verify correct behavior for valid and invalid slugs. Commits ------- 6233382 [Validator] Add `Slug` constraint
2 parents 8c069cd + 6233382 commit 18d08ff

File tree

5 files changed

+242
-0
lines changed

5 files changed

+242
-0
lines changed

src/Symfony/Component/Validator/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Add support for ratio checks for SVG files to the `Image` constraint
8+
* Add the `Slug` constraint
89

910
7.2
1011
---
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Constraints;
13+
14+
use Symfony\Component\Validator\Constraint;
15+
16+
/**
17+
* Validates that a value is a valid slug.
18+
*
19+
* @author Raffaele Carelle <raffaele.carelle@gmail.com>
20+
*/
21+
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
22+
class Slug extends Constraint
23+
{
24+
public const NOT_SLUG_ERROR = '14e6df1e-c8ab-4395-b6ce-04b132a3765e';
25+
26+
public string $message = 'This value is not a valid slug.';
27+
public string $regex = '/^[a-z0-9]+(?:-[a-z0-9]+)*$/';
28+
29+
public function __construct(
30+
?array $options = null,
31+
?string $regex = null,
32+
?string $message = null,
33+
?array $groups = null,
34+
mixed $payload = null,
35+
) {
36+
parent::__construct($options, $groups, $payload);
37+
38+
$this->message = $message ?? $this->message;
39+
$this->regex = $regex ?? $this->regex;
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Constraints;
13+
14+
use Symfony\Component\Validator\Constraint;
15+
use Symfony\Component\Validator\ConstraintValidator;
16+
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
17+
use Symfony\Component\Validator\Exception\UnexpectedValueException;
18+
19+
/**
20+
* @author Raffaele Carelle <raffaele.carelle@gmail.com>
21+
*/
22+
class SlugValidator extends ConstraintValidator
23+
{
24+
public function validate(mixed $value, Constraint $constraint): void
25+
{
26+
if (!$constraint instanceof Slug) {
27+
throw new UnexpectedTypeException($constraint, Slug::class);
28+
}
29+
30+
if (null === $value || '' === $value) {
31+
return;
32+
}
33+
34+
if (!\is_scalar($value) && !$value instanceof \Stringable) {
35+
throw new UnexpectedValueException($value, 'string');
36+
}
37+
38+
$value = (string) $value;
39+
40+
if (0 === preg_match($constraint->regex, $value)) {
41+
$this->context->buildViolation($constraint->message)
42+
->setParameter('{{ value }}', $this->formatValue($value))
43+
->setCode(Slug::NOT_SLUG_ERROR)
44+
->addViolation();
45+
}
46+
}
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Tests\Constraints;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Validator\Constraints\Slug;
16+
use Symfony\Component\Validator\Mapping\ClassMetadata;
17+
use Symfony\Component\Validator\Mapping\Loader\AttributeLoader;
18+
19+
class SlugTest extends TestCase
20+
{
21+
public function testAttributes()
22+
{
23+
$metadata = new ClassMetadata(SlugDummy::class);
24+
$loader = new AttributeLoader();
25+
self::assertTrue($loader->loadClassMetadata($metadata));
26+
27+
[$bConstraint] = $metadata->properties['b']->getConstraints();
28+
self::assertSame('myMessage', $bConstraint->message);
29+
self::assertSame(['Default', 'SlugDummy'], $bConstraint->groups);
30+
31+
[$cConstraint] = $metadata->properties['c']->getConstraints();
32+
self::assertSame(['my_group'], $cConstraint->groups);
33+
self::assertSame('some attached data', $cConstraint->payload);
34+
}
35+
}
36+
37+
class SlugDummy
38+
{
39+
#[Slug]
40+
private $a;
41+
42+
#[Slug(message: 'myMessage')]
43+
private $b;
44+
45+
#[Slug(groups: ['my_group'], payload: 'some attached data')]
46+
private $c;
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Tests\Constraints;
13+
14+
use Symfony\Component\Validator\Constraints\Slug;
15+
use Symfony\Component\Validator\Constraints\SlugValidator;
16+
use Symfony\Component\Validator\Exception\UnexpectedValueException;
17+
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
18+
19+
class SlugValidatorTest extends ConstraintValidatorTestCase
20+
{
21+
protected function createValidator(): SlugValidator
22+
{
23+
return new SlugValidator();
24+
}
25+
26+
public function testNullIsValid()
27+
{
28+
$this->validator->validate(null, new Slug());
29+
30+
$this->assertNoViolation();
31+
}
32+
33+
public function testEmptyStringIsValid()
34+
{
35+
$this->validator->validate('', new Slug());
36+
37+
$this->assertNoViolation();
38+
}
39+
40+
public function testExpectsStringCompatibleType()
41+
{
42+
$this->expectException(UnexpectedValueException::class);
43+
$this->validator->validate(new \stdClass(), new Slug());
44+
}
45+
46+
/**
47+
* @testWith ["test-slug"]
48+
* ["slug-123-test"]
49+
* ["slug"]
50+
*/
51+
public function testValidSlugs($slug)
52+
{
53+
$this->validator->validate($slug, new Slug());
54+
55+
$this->assertNoViolation();
56+
}
57+
58+
/**
59+
* @testWith ["NotASlug"]
60+
* ["Not a slug"]
61+
* ["not-á-slug"]
62+
* ["not-@-slug"]
63+
*/
64+
public function testInvalidSlugs($slug)
65+
{
66+
$constraint = new Slug([
67+
'message' => 'myMessage',
68+
]);
69+
70+
$this->validator->validate($slug, $constraint);
71+
72+
$this->buildViolation('myMessage')
73+
->setParameter('{{ value }}', '"'.$slug.'"')
74+
->setCode(Slug::NOT_SLUG_ERROR)
75+
->assertRaised();
76+
}
77+
78+
/**
79+
* @testWith ["test-slug", true]
80+
* ["slug-123-test", true]
81+
*/
82+
public function testCustomRegexInvalidSlugs($slug)
83+
{
84+
$constraint = new Slug(regex: '/^[a-z0-9]+$/i');
85+
86+
$this->validator->validate($slug, $constraint);
87+
88+
$this->buildViolation($constraint->message)
89+
->setParameter('{{ value }}', '"'.$slug.'"')
90+
->setCode(Slug::NOT_SLUG_ERROR)
91+
->assertRaised();
92+
}
93+
94+
/**
95+
* @testWith ["slug"]
96+
* @testWith ["test1234"]
97+
*/
98+
public function testCustomRegexValidSlugs($slug)
99+
{
100+
$constraint = new Slug(regex: '/^[a-z0-9]+$/i');
101+
102+
$this->validator->validate($slug, $constraint);
103+
104+
$this->assertNoViolation();
105+
}
106+
}

0 commit comments

Comments
 (0)
0