Skip to content

Commit c9050da

Browse files
author
symfonyaml
committed
[Validator] Add Yaml constraint for validating Yaml content
1 parent 678abb4 commit c9050da

File tree

4 files changed

+270
-0
lines changed

4 files changed

+270
-0
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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 symfonyaml
22+
*/
23+
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
24+
class Yaml extends Constraint
25+
{
26+
public const INVALID_YAML_ERROR = '63313a31-837c-42bb-99eb-542c76aacc48';
27+
28+
protected const ERROR_NAMES = [
29+
self::INVALID_YAML_ERROR => 'INVALID_YAML_ERROR',
30+
];
31+
32+
/**
33+
* @deprecated since Symfony 6.1, use const ERROR_NAMES instead
34+
*/
35+
protected static $errorNames = self::ERROR_NAMES;
36+
37+
public $message = 'This value should be valid YAML.';
38+
public $flags = 0;
39+
40+
public function __construct(
41+
?array $options = null,
42+
?string $message = null,
43+
?int $flags = null,
44+
?array $groups = null,
45+
mixed $payload = null,
46+
) {
47+
parent::__construct($options, $groups, $payload);
48+
49+
$this->message = $message ?? $this->message;
50+
$this->flags = $flags ?? $this->flags;
51+
}
52+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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+
use Symfony\Component\Yaml\Exception\ParseException;
19+
use Symfony\Component\Yaml\Parser;
20+
21+
/**
22+
* @author symfonyaml
23+
*/
24+
class YamlValidator extends ConstraintValidator
25+
{
26+
/**
27+
* @return void
28+
*/
29+
public function validate(mixed $value, Constraint $constraint)
30+
{
31+
if (!$constraint instanceof Yaml) {
32+
throw new UnexpectedTypeException($constraint, Yaml::class);
33+
}
34+
35+
if (null === $value || '' === $value) {
36+
return;
37+
}
38+
39+
if (!\is_scalar($value) && !$value instanceof \Stringable) {
40+
throw new UnexpectedValueException($value, 'string');
41+
}
42+
43+
$value = (string) $value;
44+
45+
$parser = new Parser();
46+
/** @see \Symfony\Component\Yaml\Command\LintCommand::validate() */
47+
$prevErrorHandler = set_error_handler(function ($level, $message, $file, $line) use ($parser, &$prevErrorHandler) {
48+
if (\E_USER_DEPRECATED === $level) {
49+
throw new ParseException($message, $parser->getRealCurrentLineNb() + 1);
50+
}
51+
52+
return $prevErrorHandler ? $prevErrorHandler($level, $message, $file, $line) : false;
53+
});
54+
55+
try {
56+
$parsed = $parser->parse($value, $constraint->flags);
57+
} catch (ParseException $e) {
58+
$this->context->buildViolation($constraint->message)
59+
->setParameter('{{ value }}', $this->formatValue($value))
60+
->setParameter('{{ error }}', $e->getMessage())
61+
->setParameter('{{ line }}', $e->getParsedLine())
62+
->setCode(Yaml::INVALID_YAML_ERROR)
63+
->addViolation();
64+
} finally {
65+
restore_error_handler();
66+
}
67+
}
68+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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\Yaml;
16+
use Symfony\Component\Validator\Mapping\ClassMetadata;
17+
use Symfony\Component\Validator\Mapping\Loader\AttributeLoader;
18+
use Symfony\Component\Yaml\Yaml as YamlParser;
19+
20+
class YamlTest extends TestCase
21+
{
22+
public function testAttributes()
23+
{
24+
$metadata = new ClassMetadata(YamlDummy::class);
25+
$loader = new AttributeLoader();
26+
self::assertTrue($loader->loadClassMetadata($metadata));
27+
28+
[$bConstraint] = $metadata->properties['b']->getConstraints();
29+
self::assertSame('myMessage', $bConstraint->message);
30+
self::assertSame(['Default', 'YamlDummy'], $bConstraint->groups);
31+
32+
[$cConstraint] = $metadata->properties['c']->getConstraints();
33+
self::assertSame(['my_group'], $cConstraint->groups);
34+
self::assertSame('some attached data', $cConstraint->payload);
35+
36+
[$cConstraint] = $metadata->properties['d']->getConstraints();
37+
self::assertSame(768, $cConstraint->flags);
38+
}
39+
}
40+
41+
class YamlDummy
42+
{
43+
#[Yaml]
44+
private $a;
45+
46+
#[Yaml(message: 'myMessage')]
47+
private $b;
48+
49+
#[Yaml(groups: ['my_group'], payload: 'some attached data')]
50+
private $c;
51+
52+
#[Yaml(flags: YamlParser::PARSE_CONSTANT | YamlParser::PARSE_CUSTOM_TAGS)]
53+
private $d;
54+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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\Yaml;
15+
use Symfony\Component\Validator\Constraints\YamlValidator;
16+
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
17+
use Symfony\Component\Yaml\Yaml as YamlParser;
18+
19+
class YamlValidatorTest extends ConstraintValidatorTestCase
20+
{
21+
protected function createValidator(): YamlValidator
22+
{
23+
return new YamlValidator();
24+
}
25+
26+
/**
27+
* @dataProvider getValidValues
28+
*/
29+
public function testYamlIsValid($value)
30+
{
31+
$this->validator->validate($value, new Yaml());
32+
33+
$this->assertNoViolation();
34+
}
35+
36+
public function testYamlWithFlags()
37+
{
38+
$this->validator->validate('date: 2023-01-01', new Yaml(flags: YamlParser::PARSE_DATETIME));
39+
$this->assertNoViolation();
40+
}
41+
42+
/**
43+
* @dataProvider getInvalidValues
44+
*/
45+
public function testInvalidValues($value, $message, $line)
46+
{
47+
$constraint = new Yaml([
48+
'message' => 'myMessageTest',
49+
]);
50+
51+
$this->validator->validate($value, $constraint);
52+
53+
$this->buildViolation('myMessageTest')
54+
->setParameter('{{ value }}', '"'.$value.'"')
55+
->setParameter('{{ error }}', $message)
56+
->setParameter('{{ line }}', $line)
57+
->setCode(Yaml::INVALID_YAML_ERROR)
58+
->assertRaised();
59+
}
60+
61+
public function testInvalidFlags()
62+
{
63+
$value = 'tags: [!tagged app.myclass]';
64+
$this->validator->validate($value, new Yaml());
65+
$this->buildViolation('This value should be valid YAML.')
66+
->setParameter('{{ value }}', sprintf('"%s"', $value))
67+
->setParameter('{{ error }}', 'Tags support is not enabled. Enable the "Yaml::PARSE_CUSTOM_TAGS" flag to use "!tagged" at line 1 (near "tags: [!tagged app.myclass]").')
68+
->setParameter('{{ line }}', 1)
69+
->setCode(Yaml::INVALID_YAML_ERROR)
70+
->assertRaised();
71+
}
72+
73+
public static function getValidValues()
74+
{
75+
return [
76+
['planet_diameters: {earth: 12742, mars: 6779, saturn: 116460, mercury: 4879}'],
77+
["key:\n value"],
78+
[null],
79+
[''],
80+
['"null"'],
81+
['null'],
82+
['"string"'],
83+
['1'],
84+
['true'],
85+
[1],
86+
];
87+
}
88+
89+
public static function getInvalidValues()
90+
{
91+
return [
92+
['{:INVALID]', 'Malformed unquoted YAML string at line 1 (near "{:INVALID]").', 1],
93+
["key:\nvalue", 'Unable to parse at line 2 (near "value").', 2],
94+
];
95+
}
96+
}

0 commit comments

Comments
 (0)