Skip to content

[Validator] Added CssColor constraint #40168

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Component/Validator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ CHANGELOG
5.4
---

* Add a `CssColor` constraint to validate CSS colors
* Add support for `ConstraintViolationList::createFromMessage()`

5.3
Expand Down
106 changes: 106 additions & 0 deletions src/Symfony/Component/Validator/Constraints/CssColor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Validator\Constraints;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\InvalidArgumentException;

/**
* @Annotation
* @Target({"PROPERTY", "METHOD", "ANNOTATION"})
*
* @author Mathieu Santostefano <msantostefano@protonmail.com>
*/
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class CssColor extends Constraint
{
public const HEX_LONG = 'hex_long';
public const HEX_LONG_WITH_ALPHA = 'hex_long_with_alpha';
public const HEX_SHORT = 'hex_short';
public const HEX_SHORT_WITH_ALPHA = 'hex_short_with_alpha';
public const BASIC_NAMED_COLORS = 'basic_named_colors';
public const EXTENDED_NAMED_COLORS = 'extended_named_colors';
public const SYSTEM_COLORS = 'system_colors';
public const KEYWORDS = 'keywords';
public const RGB = 'rgb';
public const RGBA = 'rgba';
public const HSL = 'hsl';
public const HSLA = 'hsla';
public const INVALID_FORMAT_ERROR = '454ab47b-aacf-4059-8f26-184b2dc9d48d';

protected static $errorNames = [
self::INVALID_FORMAT_ERROR => 'INVALID_FORMAT_ERROR',
];

/**
* @var string[]
*/
private static $validationModes = [
self::HEX_LONG,
self::HEX_LONG_WITH_ALPHA,
self::HEX_SHORT,
self::HEX_SHORT_WITH_ALPHA,
self::BASIC_NAMED_COLORS,
self::EXTENDED_NAMED_COLORS,
self::SYSTEM_COLORS,
self::KEYWORDS,
self::RGB,
self::RGBA,
self::HSL,
self::HSLA,
];

public $message = 'This value is not a valid CSS color.';
public $formats;

/**
* @param array|string $formats The types of CSS colors allowed (e.g. hexadecimal only, RGB and HSL only, etc.).
*/
public function __construct($formats, string $message = null, array $groups = null, $payload = null, array $options = null)
{
$validationModesAsString = array_reduce(self::$validationModes, function ($carry, $value) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, I'm reading this PR and wondering if it wouldn't be a good idea to type things as much as we can when doing a chance?

Since Symfony 6 has better typing support, wouldn't it be a good idea to require adding typing when submitting a PR ?

Thanks

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be great, but in this case I made a little mistake, this array_reduce is overengineered, it has to be a implode call.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at it twice now, and indeed, it could be replace with an implode :)

return $carry ? $carry.', '.$value : $value;
}, '');

if (\is_array($formats) && \is_string(key($formats))) {
$options = array_merge($formats, $options);
} elseif (\is_array($formats)) {
if ([] === array_intersect(static::$validationModes, $formats)) {
throw new InvalidArgumentException(sprintf('The "formats" parameter value is not valid. It must contain one or more of the following values: "%s".', $validationModesAsString));
}

$options['value'] = $formats;
} elseif (\is_string($formats)) {
if (!\in_array($formats, static::$validationModes)) {
throw new InvalidArgumentException(sprintf('The "formats" parameter value is not valid. It must contain one or more of the following values: "%s".', $validationModesAsString));
}

$options['value'] = [$formats];
} else {
throw new InvalidArgumentException('The "formats" parameter type is not valid. It should be a string or an array.');
}

parent::__construct($options, $groups, $payload);

$this->message = $message ?? $this->message;
}

public function getDefaultOption(): string
{
return 'formats';
}

public function getRequiredOptions(): array
{
return ['formats'];
}
}
86 changes: 86 additions & 0 deletions src/Symfony/Component/Validator/Constraints/CssColorValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Validator\Constraints;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;

/**
* @author Mathieu Santostefano <msantostefano@protonmail.com>
*/
class CssColorValidator extends ConstraintValidator
{
private const PATTERN_HEX_LONG = '/^#[0-9a-f]{6}$/i';
private const PATTERN_HEX_LONG_WITH_ALPHA = '/^#[0-9a-f]{8}$/i';
private const PATTERN_HEX_SHORT = '/^#[0-9a-f]{3}$/i';
private const PATTERN_HEX_SHORT_WITH_ALPHA = '/^#[0-9a-f]{4}$/i';
// List comes from https://www.w3.org/wiki/CSS/Properties/color/keywords#Basic_Colors
private const PATTERN_BASIC_NAMED_COLORS = '/^(black|silver|gray|white|maroon|red|purple|fuchsia|green|lime|olive|yellow|navy|blue|teal|aqua)$/i';
// List comes from https://www.w3.org/wiki/CSS/Properties/color/keywords#Extended_colors
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';
// List comes from https://drafts.csswg.org/css-color/#css-system-colors
private const PATTERN_SYSTEM_COLORS = '/^(Canvas|CanvasText|LinkText|VisitedText|ActiveText|ButtonFace|ButtonText|ButtonBorder|Field|FieldText|Highlight|HighlightText|SelectedItem|SelectedItemText|Mark|MarkText|GrayText)$/i';
private const PATTERN_KEYWORDS = '/^(transparent|currentColor)$/i';
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';
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';
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';
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';

private const COLOR_PATTERNS = [
CssColor::HEX_LONG => self::PATTERN_HEX_LONG,
CssColor::HEX_LONG_WITH_ALPHA => self::PATTERN_HEX_LONG_WITH_ALPHA,
CssColor::HEX_SHORT => self::PATTERN_HEX_SHORT,
CssColor::HEX_SHORT_WITH_ALPHA => self::PATTERN_HEX_SHORT_WITH_ALPHA,
CssColor::BASIC_NAMED_COLORS => self::PATTERN_BASIC_NAMED_COLORS,
CssColor::EXTENDED_NAMED_COLORS => self::PATTERN_EXTENDED_NAMED_COLORS,
CssColor::SYSTEM_COLORS => self::PATTERN_SYSTEM_COLORS,
CssColor::KEYWORDS => self::PATTERN_KEYWORDS,
CssColor::RGB => self::PATTERN_RGB,
CssColor::RGBA => self::PATTERN_RGBA,
CssColor::HSL => self::PATTERN_HSL,
CssColor::HSLA => self::PATTERN_HSLA,
];

/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint): void
{
if (!$constraint instanceof CssColor) {
throw new UnexpectedTypeException($constraint, CssColor::class);
}

if (null === $value || '' === $value) {
return;
}

if (!\is_string($value)) {
throw new UnexpectedValueException($value, 'string');
}

$formats = array_flip((array) $constraint->formats);
$formatRegexes = array_intersect_key(self::COLOR_PATTERNS, $formats);

foreach ($formatRegexes as $regex) {
if (preg_match($regex, (string) $value)) {
return;
}
}

$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $this->formatValue($value))
->setCode(CssColor::INVALID_FORMAT_ERROR)
->addViolation();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,10 @@
<source>This value should be a valid expression.</source>
<target>This value should be a valid expression.</target>
</trans-unit>
<trans-unit id="101">
<source>This value is not a valid CSS color.</source>
<target>This value is not a valid CSS color.</target>
</trans-unit>
</body>
</file>
</xliff>
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,10 @@
<source>This value should be a valid expression.</source>
<target>Cette valeur doit être une expression valide.</target>
</trans-unit>
<trans-unit id="101">
<source>This value is not a valid CSS color.</source>
<target>Cette valeur n'est pas une couleur CSS valide.</target>
</trans-unit>
</body>
</file>
</xliff>
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,10 @@
<source>This value should be a valid expression.</source>
<target>Questo valore dovrebbe essere un'espressione valida.</target>
</trans-unit>
<trans-unit id="101">
<source>This value is not a valid CSS color.</source>
<target>Questo valore non è un colore CSS valido.</target>
</trans-unit>
</body>
</file>
</xliff>
56 changes: 56 additions & 0 deletions src/Symfony/Component/Validator/Tests/Constraints/CssColorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Validator\Tests\Constraints;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Constraints\CssColor;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader;

/**
* @author Mathieu Santostefano <msantostefano@protonmail.com>
* @requires PHP 8
*/
final class CssColorTest extends TestCase
{
public function testAttributes()
{
$metadata = new ClassMetadata(CssColorDummy::class);
$loader = new AnnotationLoader();
self::assertTrue($loader->loadClassMetadata($metadata));

[$aConstraint] = $metadata->properties['a']->getConstraints();
self::assertSame([CssColor::HEX_LONG, CssColor::HEX_SHORT], $aConstraint->formats);

[$bConstraint] = $metadata->properties['b']->getConstraints();
self::assertSame([CssColor::HEX_LONG], $bConstraint->formats);
self::assertSame('myMessage', $bConstraint->message);
self::assertSame(['Default', 'CssColorDummy'], $bConstraint->groups);

[$cConstraint] = $metadata->properties['c']->getConstraints();
self::assertSame([CssColor::HEX_SHORT], $cConstraint->formats);
self::assertSame(['my_group'], $cConstraint->groups);
self::assertSame('some attached data', $cConstraint->payload);
}
}

class CssColorDummy
{
#[CssColor([CssColor::HEX_LONG, CssColor::HEX_SHORT])]
private $a;

#[CssColor(formats: CssColor::HEX_LONG, message: 'myMessage')]
private $b;

#[CssColor(formats: [CssColor::HEX_SHORT], groups: ['my_group'], payload: 'some attached data')]
private $c;
}
Loading