Skip to content

Commit e5b271c

Browse files
committed
feature #40168 [Validator] Added CssColor constraint (welcoMattic)
This PR was merged into the 5.4 branch. Discussion ---------- [Validator] Added `CssColor` constraint | Q | A | ------------- | --- | Branch? | 5.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | | License | MIT | Doc PR | symfony/symfony-docs#14965 This PR introduces a new `CssColor` constraint. It comes with 3 validation modes: - Long, which allows all hexadecimal representation of a color, or 9 (#EEEEEEFF) characters - Short, which only allows hexadecimal colors on 4 (#EEE), 5 (#FFF00) characters - Named colors, which matches [the official list of named colors](https://www.w3.org/TR/css-color-4/#named-color) - HTML5, which allows hexadecimal colors on 7 (#EEEEEE) characters as well as the HTML5 input type color I know that such a color validation already exists in Symfony (in the `ColorType` class), but it's hardcoded in the FormType, and not usable as an assert Annotation. We could decide to remove this hardcoded validation in favor of the new added `CssColor` constraint. Let me know, if yes, I will make the change. Commits ------- b36371c06e Added new CssColor constraint
2 parents 789ff87 + 4e65f4c commit e5b271c

File tree

8 files changed

+656
-0
lines changed

8 files changed

+656
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
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 CSS colors
78
* Add support for `ConstraintViolationList::createFromMessage()`
89
* Add error's uid to `Count` and `Length` constraints with "exactly" option enabled
910

Constraints/CssColor.php

Lines changed: 106 additions & 0 deletions
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\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 HEX_LONG = 'hex_long';
27+
public const HEX_LONG_WITH_ALPHA = 'hex_long_with_alpha';
28+
public const HEX_SHORT = 'hex_short';
29+
public const HEX_SHORT_WITH_ALPHA = 'hex_short_with_alpha';
30+
public const BASIC_NAMED_COLORS = 'basic_named_colors';
31+
public const EXTENDED_NAMED_COLORS = 'extended_named_colors';
32+
public const SYSTEM_COLORS = 'system_colors';
33+
public const KEYWORDS = 'keywords';
34+
public const RGB = 'rgb';
35+
public const RGBA = 'rgba';
36+
public const HSL = 'hsl';
37+
public const HSLA = 'hsla';
38+
public const INVALID_FORMAT_ERROR = '454ab47b-aacf-4059-8f26-184b2dc9d48d';
39+
40+
protected static $errorNames = [
41+
self::INVALID_FORMAT_ERROR => 'INVALID_FORMAT_ERROR',
42+
];
43+
44+
/**
45+
* @var string[]
46+
*/
47+
private static $validationModes = [
48+
self::HEX_LONG,
49+
self::HEX_LONG_WITH_ALPHA,
50+
self::HEX_SHORT,
51+
self::HEX_SHORT_WITH_ALPHA,
52+
self::BASIC_NAMED_COLORS,
53+
self::EXTENDED_NAMED_COLORS,
54+
self::SYSTEM_COLORS,
55+
self::KEYWORDS,
56+
self::RGB,
57+
self::RGBA,
58+
self::HSL,
59+
self::HSLA,
60+
];
61+
62+
public $message = 'This value is not a valid CSS color.';
63+
public $formats;
64+
65+
/**
66+
* @param array|string $formats The types of CSS colors allowed (e.g. hexadecimal only, RGB and HSL only, etc.).
67+
*/
68+
public function __construct($formats, string $message = null, array $groups = null, $payload = null, array $options = null)
69+
{
70+
$validationModesAsString = array_reduce(self::$validationModes, function ($carry, $value) {
71+
return $carry ? $carry.', '.$value : $value;
72+
}, '');
73+
74+
if (\is_array($formats) && \is_string(key($formats))) {
75+
$options = array_merge($formats, $options);
76+
} elseif (\is_array($formats)) {
77+
if ([] === array_intersect(static::$validationModes, $formats)) {
78+
throw new InvalidArgumentException(sprintf('The "formats" parameter value is not valid. It must contain one or more of the following values: "%s".', $validationModesAsString));
79+
}
80+
81+
$options['value'] = $formats;
82+
} elseif (\is_string($formats)) {
83+
if (!\in_array($formats, static::$validationModes)) {
84+
throw new InvalidArgumentException(sprintf('The "formats" parameter value is not valid. It must contain one or more of the following values: "%s".', $validationModesAsString));
85+
}
86+
87+
$options['value'] = [$formats];
88+
} else {
89+
throw new InvalidArgumentException('The "formats" parameter type is not valid. It should be a string or an array.');
90+
}
91+
92+
parent::__construct($options, $groups, $payload);
93+
94+
$this->message = $message ?? $this->message;
95+
}
96+
97+
public function getDefaultOption(): string
98+
{
99+
return 'formats';
100+
}
101+
102+
public function getRequiredOptions(): array
103+
{
104+
return ['formats'];
105+
}
106+
}

Constraints/CssColorValidator.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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_HEX_LONG = '/^#[0-9a-f]{6}$/i';
25+
private const PATTERN_HEX_LONG_WITH_ALPHA = '/^#[0-9a-f]{8}$/i';
26+
private const PATTERN_HEX_SHORT = '/^#[0-9a-f]{3}$/i';
27+
private const PATTERN_HEX_SHORT_WITH_ALPHA = '/^#[0-9a-f]{4}$/i';
28+
// List comes from https://www.w3.org/wiki/CSS/Properties/color/keywords#Basic_Colors
29+
private const PATTERN_BASIC_NAMED_COLORS = '/^(black|silver|gray|white|maroon|red|purple|fuchsia|green|lime|olive|yellow|navy|blue|teal|aqua)$/i';
30+
// List comes from https://www.w3.org/wiki/CSS/Properties/color/keywords#Extended_colors
31+
private const PATTERN_EXTENDED_NAMED_COLORS = '/^(aliceblue|antiquewhite|aqua|aquamarine|azure|beige|bisque|black|blanchedalmond|blue|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|cyan|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkgrey|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|firebrick|floralwhite|forestgreen|fuchsia|gainsboro|ghostwhite|gold|goldenrod|gray|green|greenyellow|grey|honeydew|hotpink|indianred|indigo|ivory|khaki|lavender|lavenderblush|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgray|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|lightyellow|lime|limegreen|linen|magenta|maroon|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|navy|oldlace|olive|olivedrab|orange|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|purple|red|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|silver|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|teal|thistle|tomato|turquoise|violet|wheat|white|whitesmoke|yellow|yellowgreen)$/i';
32+
// List comes from https://drafts.csswg.org/css-color/#css-system-colors
33+
private const PATTERN_SYSTEM_COLORS = '/^(Canvas|CanvasText|LinkText|VisitedText|ActiveText|ButtonFace|ButtonText|ButtonBorder|Field|FieldText|Highlight|HighlightText|SelectedItem|SelectedItemText|Mark|MarkText|GrayText)$/i';
34+
private const PATTERN_KEYWORDS = '/^(transparent|currentColor)$/i';
35+
private const PATTERN_RGB = '/^rgb\((0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d),\s?(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d),\s?(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d)\)$/i';
36+
private const PATTERN_RGBA = '/^rgba\((0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d),\s?(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d),\s?(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d),\s?(0|0?\.\d|1(\.0)?)\)$/i';
37+
private const PATTERN_HSL = '/^hsl\((0|360|35\d|3[0-4]\d|[12]\d\d|0?\d?\d),\s?(0|100|\d{1,2})%,\s?(0|100|\d{1,2})%\)$/i';
38+
private const PATTERN_HSLA = '/^hsla\((0|360|35\d|3[0-4]\d|[12]\d\d|0?\d?\d),\s?(0|100|\d{1,2})%,\s?(0|100|\d{1,2})%,\s?(0?\.\d|1(\.0)?)\)$/i';
39+
40+
private const COLOR_PATTERNS = [
41+
CssColor::HEX_LONG => self::PATTERN_HEX_LONG,
42+
CssColor::HEX_LONG_WITH_ALPHA => self::PATTERN_HEX_LONG_WITH_ALPHA,
43+
CssColor::HEX_SHORT => self::PATTERN_HEX_SHORT,
44+
CssColor::HEX_SHORT_WITH_ALPHA => self::PATTERN_HEX_SHORT_WITH_ALPHA,
45+
CssColor::BASIC_NAMED_COLORS => self::PATTERN_BASIC_NAMED_COLORS,
46+
CssColor::EXTENDED_NAMED_COLORS => self::PATTERN_EXTENDED_NAMED_COLORS,
47+
CssColor::SYSTEM_COLORS => self::PATTERN_SYSTEM_COLORS,
48+
CssColor::KEYWORDS => self::PATTERN_KEYWORDS,
49+
CssColor::RGB => self::PATTERN_RGB,
50+
CssColor::RGBA => self::PATTERN_RGBA,
51+
CssColor::HSL => self::PATTERN_HSL,
52+
CssColor::HSLA => self::PATTERN_HSLA,
53+
];
54+
55+
/**
56+
* {@inheritdoc}
57+
*/
58+
public function validate($value, Constraint $constraint): void
59+
{
60+
if (!$constraint instanceof CssColor) {
61+
throw new UnexpectedTypeException($constraint, CssColor::class);
62+
}
63+
64+
if (null === $value || '' === $value) {
65+
return;
66+
}
67+
68+
if (!\is_string($value)) {
69+
throw new UnexpectedValueException($value, 'string');
70+
}
71+
72+
$formats = array_flip((array) $constraint->formats);
73+
$formatRegexes = array_intersect_key(self::COLOR_PATTERNS, $formats);
74+
75+
foreach ($formatRegexes as $regex) {
76+
if (preg_match($regex, (string) $value)) {
77+
return;
78+
}
79+
}
80+
81+
$this->context->buildViolation($constraint->message)
82+
->setParameter('{{ value }}', $this->formatValue($value))
83+
->setCode(CssColor::INVALID_FORMAT_ERROR)
84+
->addViolation();
85+
}
86+
}

Resources/translations/validators.en.xlf

Lines changed: 4 additions & 0 deletions
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 CSS color.</source>
395+
<target>This value is not a valid CSS color.</target>
396+
</trans-unit>
393397
</body>
394398
</file>
395399
</xliff>

Resources/translations/validators.fr.xlf

Lines changed: 4 additions & 0 deletions
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 CSS color.</source>
395+
<target>Cette valeur n'est pas une couleur CSS valide.</target>
396+
</trans-unit>
393397
</body>
394398
</file>
395399
</xliff>

Resources/translations/validators.it.xlf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 CSS color.</source>
395+
<target>Questo valore non è un colore CSS valido.</target>
396+
</trans-unit>
393397
</body>
394398
</file>
395399
</xliff>

Tests/Constraints/CssColorTest.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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\Mapping\ClassMetadata;
17+
use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader;
18+
19+
/**
20+
* @author Mathieu Santostefano <msantostefano@protonmail.com>
21+
* @requires PHP 8
22+
*/
23+
final class CssColorTest extends TestCase
24+
{
25+
public function testAttributes()
26+
{
27+
$metadata = new ClassMetadata(CssColorDummy::class);
28+
$loader = new AnnotationLoader();
29+
self::assertTrue($loader->loadClassMetadata($metadata));
30+
31+
[$aConstraint] = $metadata->properties['a']->getConstraints();
32+
self::assertSame([CssColor::HEX_LONG, CssColor::HEX_SHORT], $aConstraint->formats);
33+
34+
[$bConstraint] = $metadata->properties['b']->getConstraints();
35+
self::assertSame([CssColor::HEX_LONG], $bConstraint->formats);
36+
self::assertSame('myMessage', $bConstraint->message);
37+
self::assertSame(['Default', 'CssColorDummy'], $bConstraint->groups);
38+
39+
[$cConstraint] = $metadata->properties['c']->getConstraints();
40+
self::assertSame([CssColor::HEX_SHORT], $cConstraint->formats);
41+
self::assertSame(['my_group'], $cConstraint->groups);
42+
self::assertSame('some attached data', $cConstraint->payload);
43+
}
44+
}
45+
46+
class CssColorDummy
47+
{
48+
#[CssColor([CssColor::HEX_LONG, CssColor::HEX_SHORT])]
49+
private $a;
50+
51+
#[CssColor(formats: CssColor::HEX_LONG, message: 'myMessage')]
52+
private $b;
53+
54+
#[CssColor(formats: [CssColor::HEX_SHORT], groups: ['my_group'], payload: 'some attached data')]
55+
private $c;
56+
}

0 commit comments

Comments
 (0)