Skip to content

Commit 454d531

Browse files
committed
Added new CssColor constraint
1 parent 5010ebd commit 454d531

File tree

8 files changed

+586
-1
lines changed

8 files changed

+586
-1
lines changed

src/Symfony/Component/Validator/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
5.4
55
---
66

7+
* Add a `CssColor` constraint to validate hexadecimal colors
78
* Add support for `ConstraintViolationList::createFromMessage()`
89

910
5.3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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\Exception\InvalidArgumentException;
16+
17+
/**
18+
* @Annotation
19+
* @Target({"PROPERTY", "METHOD", "ANNOTATION"})
20+
*
21+
* @author Mathieu Santostefano <msantostefano@protonmail.com>
22+
*/
23+
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
24+
class CssColor extends Constraint
25+
{
26+
public const VALIDATION_MODE_HTML5 = 'html5';
27+
public const VALIDATION_MODE_LONG = 'css_long';
28+
public const VALIDATION_MODE_SHORT = 'css_short';
29+
public const VALIDATION_MODE_NAMED_COLORS = 'named_colors';
30+
public const INVALID_FORMAT_ERROR = '454ab47b-aacf-4059-8f26-184b2dc9d48d';
31+
32+
protected static $errorNames = [
33+
self::INVALID_FORMAT_ERROR => 'STRICT_CHECK_FAILED_ERROR',
34+
];
35+
36+
/**
37+
* @var string[]
38+
*/
39+
private static $validationModes = [
40+
self::VALIDATION_MODE_HTML5,
41+
self::VALIDATION_MODE_LONG,
42+
self::VALIDATION_MODE_SHORT,
43+
self::VALIDATION_MODE_NAMED_COLORS,
44+
];
45+
46+
public $message = 'This value is not a valid hexadecimal color.';
47+
public $mode;
48+
public $normalizer;
49+
50+
public function __construct(
51+
array $options = null,
52+
string $message = null,
53+
string $mode = null,
54+
callable $normalizer = null,
55+
array $groups = null,
56+
$payload = null
57+
) {
58+
if (\is_array($options) && \array_key_exists('mode', $options) && !\in_array($options['mode'], self::$validationModes, true)) {
59+
throw new InvalidArgumentException('The "mode" parameter value is not valid.');
60+
}
61+
62+
parent::__construct($options, $groups, $payload);
63+
64+
$this->message = $message ?? $this->message;
65+
$this->mode = $mode ?? $this->mode;
66+
$this->normalizer = $normalizer ?? $this->normalizer;
67+
68+
if (null !== $this->normalizer && !\is_callable($this->normalizer)) {
69+
throw new InvalidArgumentException(sprintf('The "normalizer" option must be a valid callable ("%s" given).', get_debug_type($this->normalizer)));
70+
}
71+
}
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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 Mathieu Santostefano <msantostefano@protonmail.com>
21+
*/
22+
class CssColorValidator extends ConstraintValidator
23+
{
24+
private const PATTERN_HTML5 = '/^#[0-9a-f]{6}$/i';
25+
private const PATTERN_LONG = '/^#[0-9a-f]{8}$/i';
26+
private const PATTERN_SHORT = '/^#[0-9a-f]{3,4}$/i';
27+
private const PATTERN_NAMED_COLORS = '/^(black|red|green|yellow|blue|magenta|cyan|white)/i';
28+
29+
private const COLOR_PATTERNS = [
30+
CssColor::VALIDATION_MODE_HTML5 => self::PATTERN_HTML5,
31+
CssColor::VALIDATION_MODE_LONG => self::PATTERN_LONG,
32+
CssColor::VALIDATION_MODE_SHORT => self::PATTERN_SHORT,
33+
CssColor::VALIDATION_MODE_NAMED_COLORS => self::PATTERN_NAMED_COLORS,
34+
];
35+
36+
private $defaultMode;
37+
38+
public function __construct(string $defaultMode = CssColor::VALIDATION_MODE_HTML5)
39+
{
40+
if (!isset(self::COLOR_PATTERNS[$defaultMode])) {
41+
throw new \InvalidArgumentException('The "defaultMode" parameter value is not valid.');
42+
}
43+
44+
$this->defaultMode = $defaultMode;
45+
}
46+
47+
/**
48+
* {@inheritdoc}
49+
*/
50+
public function validate($value, Constraint $constraint): void
51+
{
52+
if (!$constraint instanceof CssColor) {
53+
throw new UnexpectedTypeException($constraint, CssColor::class);
54+
}
55+
56+
if (null === $value || '' === $value) {
57+
return;
58+
}
59+
60+
if (!is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) {
61+
throw new UnexpectedValueException($value, 'string');
62+
}
63+
64+
$value = (string) $value;
65+
if ('' === $value) {
66+
return;
67+
}
68+
69+
if (null !== $constraint->normalizer) {
70+
$value = ($constraint->normalizer)($value);
71+
}
72+
73+
if (null === $constraint->mode) {
74+
$constraint->mode = $this->defaultMode;
75+
}
76+
77+
if (!isset(self::COLOR_PATTERNS[$constraint->mode])) {
78+
throw new \InvalidArgumentException(sprintf('The "%s::$mode" parameter value is not valid.', get_debug_type($constraint)));
79+
}
80+
81+
if (!preg_match(self::COLOR_PATTERNS[$constraint->mode], $value)) {
82+
$this->context->buildViolation($constraint->message)
83+
->setParameter('{{ value }}', $this->formatValue($value))
84+
->setCode(CssColor::INVALID_FORMAT_ERROR)
85+
->addViolation();
86+
}
87+
}
88+
}

src/Symfony/Component/Validator/Resources/translations/validators.en.xlf

+4
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,10 @@
390390
<source>This value should be a valid expression.</source>
391391
<target>This value should be a valid expression.</target>
392392
</trans-unit>
393+
<trans-unit id="101">
394+
<source>This value is not a valid hexadecimal color.</source>
395+
<target>This value is not a valid hexadecimal color.</target>
396+
</trans-unit>
393397
</body>
394398
</file>
395399
</xliff>

src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf

+4
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,10 @@
390390
<source>This value should be a valid expression.</source>
391391
<target>Cette valeur doit être une expression valide.</target>
392392
</trans-unit>
393+
<trans-unit id="101">
394+
<source>This value is not a valid hexadecimal color.</source>
395+
<target>Cette valeur n'est pas une couleur hexadécimale valide.</target>
396+
</trans-unit>
393397
</body>
394398
</file>
395399
</xliff>

src/Symfony/Component/Validator/Resources/translations/validators.it.xlf

+5-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
</trans-unit>
5757
<trans-unit id="14">
5858
<source>The file could not be found.</source>
59-
<target>Non è stato possibile trovare il file.</target>
59+
<target>Non è stato possible trovare il file.</target>
6060
</trans-unit>
6161
<trans-unit id="15">
6262
<source>The file is not readable.</source>
@@ -390,6 +390,10 @@
390390
<source>This value should be a valid expression.</source>
391391
<target>Questo valore dovrebbe essere un'espressione valida.</target>
392392
</trans-unit>
393+
<trans-unit id="101">
394+
<source>This value is not a valid hexadecimal color.</source>
395+
<target>Questo valore non è un colore esadecimale valido.</target>
396+
</trans-unit>
393397
</body>
394398
</file>
395399
</xliff>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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\CssColor;
16+
use Symfony\Component\Validator\Exception\InvalidArgumentException;
17+
use Symfony\Component\Validator\Mapping\ClassMetadata;
18+
use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader;
19+
20+
/**
21+
* @author Mathieu Santostefano <msantostefano@protonmail.com>
22+
*/
23+
class CssColorTest extends TestCase
24+
{
25+
public function testConstructorStrict()
26+
{
27+
$subject = new CssColor(['mode' => CssColor::VALIDATION_MODE_SHORT]);
28+
29+
$this->assertEquals(CssColor::VALIDATION_MODE_SHORT, $subject->mode);
30+
}
31+
32+
public function testUnknownModesTriggerException()
33+
{
34+
$this->expectException(InvalidArgumentException::class);
35+
$this->expectExceptionMessage('The "mode" parameter value is not valid.');
36+
new CssColor(['mode' => 'Unknown Mode']);
37+
}
38+
39+
public function testNormalizerCanBeSet()
40+
{
41+
$hexaColor = new CssColor(['normalizer' => 'trim']);
42+
43+
$this->assertEquals('trim', $hexaColor->normalizer);
44+
}
45+
46+
public function testInvalidNormalizerThrowsException()
47+
{
48+
$this->expectException(InvalidArgumentException::class);
49+
$this->expectExceptionMessage('The "normalizer" option must be a valid callable ("string" given).');
50+
new CssColor(['normalizer' => 'Unknown Callable']);
51+
}
52+
53+
public function testInvalidNormalizerObjectThrowsException()
54+
{
55+
$this->expectException(InvalidArgumentException::class);
56+
$this->expectExceptionMessage('The "normalizer" option must be a valid callable ("stdClass" given).');
57+
new CssColor(['normalizer' => new \stdClass()]);
58+
}
59+
60+
/**
61+
* @requires PHP 8
62+
*/
63+
public function testAttribute()
64+
{
65+
$metadata = new ClassMetadata(CssColorDummy::class);
66+
(new AnnotationLoader())->loadClassMetadata($metadata);
67+
68+
[$aConstraint] = $metadata->properties['a']->constraints;
69+
self::assertNull($aConstraint->mode);
70+
self::assertNull($aConstraint->normalizer);
71+
72+
[$bConstraint] = $metadata->properties['b']->constraints;
73+
self::assertSame('myMessage', $bConstraint->message);
74+
self::assertSame(CssColor::VALIDATION_MODE_HTML5, $bConstraint->mode);
75+
self::assertSame('trim', $bConstraint->normalizer);
76+
77+
[$cConstraint] = $metadata->properties['c']->getConstraints();
78+
self::assertSame(['my_group'], $cConstraint->groups);
79+
self::assertSame('some attached data', $cConstraint->payload);
80+
}
81+
}
82+
83+
class CssColorDummy
84+
{
85+
#[CssColor]
86+
private $a;
87+
88+
#[CssColor(message: 'myMessage', mode: CssColor::VALIDATION_MODE_HTML5, normalizer: 'trim')]
89+
private $b;
90+
91+
#[CssColor(groups: ['my_group'], payload: 'some attached data')]
92+
private $c;
93+
}

0 commit comments

Comments
 (0)