Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 30b6b05

Browse filesBrowse 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
Copy full SHA for 30b6b05

File tree

5 files changed

+211
-0
lines changed
Filter options

5 files changed

+211
-0
lines changed

‎src/Symfony/Component/Validator/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/Validator/CHANGELOG.md
+1Lines 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
---
+39Lines 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+
}
+47Lines 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+
}
+47Lines 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+
}
+77Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.