Skip to content

Commit 30b6b05

Browse files
Add Slug validation constraint and tests
Introduce Slug constraint class for validating strings as slugs. Implement SlugValidator to check against slug pattern. Add unit tests to verify correct behavior for valid and invalid slugs.
1 parent d83167d commit 30b6b05

File tree

5 files changed

+211
-0
lines changed

5 files changed

+211
-0
lines changed

src/Symfony/Component/Validator/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ CHANGELOG
1313
* Add the `Week` constraint
1414
* Add `CompoundConstraintTestCase` to ease testing Compound Constraints
1515
* Add context variable to `WhenValidator`
16+
* Add the `Slug` constraint
1617

1718
7.1
1819
---
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
public const SLUG_PATTERN = '/^[a-z0-9]+(?:-[a-z0-9]+)*$/';
26+
27+
public string $message = 'This value is not a valid slug.';
28+
29+
public function __construct(
30+
?array $options = null,
31+
?string $message = null,
32+
?array $groups = null,
33+
mixed $payload = null
34+
) {
35+
parent::__construct($options, $groups, $payload);
36+
37+
$this->message = $message ?? $this->message;
38+
}
39+
}
Lines changed: 47 additions & 0 deletions
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(preg_match(Slug::SLUG_PATTERN, $value) === 0) {
41+
$this->context->buildViolation($constraint->message)
42+
->setParameter('{{ value }}', $this->formatValue($value))
43+
->setCode(Slug::NOT_SLUG_ERROR)
44+
->addViolation();
45+
}
46+
}
47+
}
Lines changed: 47 additions & 0 deletions
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+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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+
}

0 commit comments

Comments
 (0)