Skip to content

Commit 31aced8

Browse files
committed
Add Configuration check to Yaml constraint.
Allow to use a Configuration class that implement `Symfony\Component\Config\Definition\ConfigurationInterface` to validate the given yaml.
1 parent cb16097 commit 31aced8

File tree

7 files changed

+129
-3
lines changed

7 files changed

+129
-3
lines changed

src/Symfony/Component/Validator/Constraints/Yaml.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,26 @@
2323
class Yaml extends Constraint
2424
{
2525
public const INVALID_YAML_ERROR = '63313a31-837c-42bb-99eb-542c76aacc48';
26+
public const INVALID_YAML_CONFIG_ERROR = '977b9d73-1c99-4db0-a6f8-8fc29802f91a';
2627

2728
protected const ERROR_NAMES = [
2829
self::INVALID_YAML_ERROR => 'INVALID_YAML_ERROR',
30+
self::INVALID_YAML_CONFIG_ERROR => 'INVALID_YAML_CONFIG_ERROR',
2931
];
3032

3133
/**
32-
* @param int-mask-of<\Symfony\Component\Yaml\Yaml::PARSE_*> $flags
33-
* @param string[]|null $groups
34+
* @param int-mask-of<\Symfony\Component\Yaml\Yaml::PARSE_*> $flags
35+
* @param string[]|null $groups
36+
* @param class-string<\Symfony\Component\Config\Definition\ConfigurationInterface>|null $configClass
3437
*/
3538
#[HasNamedArguments]
3639
public function __construct(
3740
public string $message = 'This value is not valid YAML.',
3841
public int $flags = 0,
3942
?array $groups = null,
4043
mixed $payload = null,
44+
public string $configMessage = 'This value do not match the required config.',
45+
public ?string $configClass = null,
4146
) {
4247
if (!class_exists(Parser::class)) {
4348
throw new LogicException('The Yaml component is required to use the Yaml constraint. Try running "composer require symfony/yaml".');

src/Symfony/Component/Validator/Constraints/YamlValidator.php

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,13 @@
1111

1212
namespace Symfony\Component\Validator\Constraints;
1313

14+
use Symfony\Component\Config\Definition\ConfigurationInterface;
15+
use Symfony\Component\Config\Definition\Exception\Exception as DefinitionException;
16+
use Symfony\Component\Config\Definition\Processor;
1417
use Symfony\Component\Validator\Constraint;
1518
use Symfony\Component\Validator\ConstraintValidator;
19+
use Symfony\Component\Validator\Exception\InvalidOptionsException;
20+
use Symfony\Component\Validator\Exception\LogicException;
1621
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
1722
use Symfony\Component\Validator\Exception\UnexpectedValueException;
1823
use Symfony\Component\Yaml\Exception\ParseException;
@@ -49,7 +54,7 @@ public function validate(mixed $value, Constraint $constraint): void
4954
});
5055

5156
try {
52-
(new Parser())->parse($value, $constraint->flags);
57+
$data = (new Parser())->parse($value, $constraint->flags);
5358
} catch (ParseException $e) {
5459
$this->context->buildViolation($constraint->message)
5560
->setParameter('{{ error }}', $e->getMessage())
@@ -59,5 +64,28 @@ public function validate(mixed $value, Constraint $constraint): void
5964
} finally {
6065
restore_error_handler();
6166
}
67+
68+
if (isset($data) && null !== $constraint->configClass) {
69+
if (!interface_exists(ConfigurationInterface::class)) {
70+
throw new LogicException(\sprintf('The "configurationClass" option requires the "%s" interface. Try running "composer require symfony/config".', ConfigurationInterface::class));
71+
}
72+
73+
if (!class_exists($constraint->configClass)) {
74+
throw new InvalidOptionsException(\sprintf('The given class "%s" does not exist.', $constraint->configClass), ['configurationClass']);
75+
}
76+
77+
if (!is_a($constraint->configClass, ConfigurationInterface::class, true)) {
78+
throw new UnexpectedTypeException($constraint->configClass, ConfigurationInterface::class);
79+
}
80+
81+
try {
82+
(new Processor())->processConfiguration(new ($constraint->configClass), $data);
83+
} catch (DefinitionException $e) {
84+
$this->context->buildViolation($constraint->configMessage)
85+
->setParameter('{{ error }}', $e->getMessage())
86+
->setCode(Yaml::INVALID_YAML_CONFIG_ERROR)
87+
->addViolation();
88+
}
89+
}
6290
}
6391
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Symfony\Component\Validator\Tests\Constraints\Fixtures;
4+
5+
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
6+
use Symfony\Component\Config\Definition\ConfigurationInterface;
7+
8+
class YamlConfig implements ConfigurationInterface
9+
{
10+
public function getConfigTreeBuilder(): TreeBuilder
11+
{
12+
($treeBuilder = new TreeBuilder('yaml_config'))
13+
->getRootNode()
14+
->children()
15+
->scalarNode('scale')->end()
16+
->arrayNode('array_scale')
17+
->prototype('scalar')->end()
18+
->end()
19+
->booleanNode('bool')
20+
->defaultFalse()
21+
->end()
22+
->end()
23+
;
24+
25+
return $treeBuilder;
26+
}
27+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
yaml_config:
2+
scale:
3+
- 'foo'
4+
- 'bar'
5+
bool: true
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
yaml_config:
2+
scale: 'foobar'
3+
array_scale:
4+
- 'foo'
5+
- 'bar'
6+
bool: true

src/Symfony/Component/Validator/Tests/Constraints/YamlTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Component\Validator\Constraints\Yaml;
1616
use Symfony\Component\Validator\Mapping\ClassMetadata;
1717
use Symfony\Component\Validator\Mapping\Loader\AttributeLoader;
18+
use Symfony\Component\Validator\Tests\Constraints\Fixtures\YamlConfig;
1819
use Symfony\Component\Yaml\Yaml as YamlParser;
1920

2021
/**
@@ -38,6 +39,12 @@ public function testAttributes()
3839

3940
[$cConstraint] = $metadata->properties['d']->getConstraints();
4041
self::assertSame(YamlParser::PARSE_CONSTANT | YamlParser::PARSE_CUSTOM_TAGS, $cConstraint->flags);
42+
43+
[$eConstraint] = $metadata->properties['e']->getConstraints();
44+
self::assertSame('myConfigMessage', $eConstraint->configMessage);
45+
46+
[$fConstraint] = $metadata->properties['f']->getConstraints();
47+
self::assertSame('Symfony\Component\Validator\Tests\Constraints\Fixtures\YamlConfig', $fConstraint->configClass);
4148
}
4249
}
4350

@@ -54,4 +61,10 @@ class YamlDummy
5461

5562
#[Yaml(flags: YamlParser::PARSE_CONSTANT | YamlParser::PARSE_CUSTOM_TAGS)]
5663
private $d;
64+
65+
#[Yaml(configMessage: 'myConfigMessage')]
66+
private $e;
67+
68+
#[Yaml(configClass: YamlConfig::class)]
69+
private $f;
5770
}

src/Symfony/Component/Validator/Tests/Constraints/YamlValidatorTest.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\Validator\Constraints\Yaml;
1515
use Symfony\Component\Validator\Constraints\YamlValidator;
1616
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
17+
use Symfony\Component\Validator\Tests\Constraints\Fixtures\YamlConfig;
1718
use Symfony\Component\Yaml\Yaml as YamlParser;
1819

1920
/**
@@ -71,6 +72,47 @@ public function testInvalidFlags()
7172
->assertRaised();
7273
}
7374

75+
public function testConfigValidData()
76+
{
77+
$constraint = new Yaml(
78+
configClass: YamlConfig::class,
79+
);
80+
81+
$this->validator->validate(file_get_contents(__DIR__.'/Fixtures/yaml_config_valid.yaml'), $constraint);
82+
$this->assertNoViolation();
83+
}
84+
85+
public function testConfigInvalidData()
86+
{
87+
$constraint = new Yaml(
88+
configClass: YamlConfig::class,
89+
);
90+
91+
$this->validator->validate(file_get_contents(__DIR__.'/Fixtures/yaml_config_invalid.yaml'), $constraint);
92+
$this->buildViolation('This value do not match the required config.')
93+
->setParameter('{{ error }}', 'Invalid type for path "yaml_config.scale". Expected "scalar", but got "array".')
94+
->setCode(Yaml::INVALID_YAML_CONFIG_ERROR)
95+
->assertRaised();
96+
}
97+
98+
/**
99+
* @dataProvider getInvalidValues
100+
*/
101+
public function testConfigInvalidYaml($value, $message, $line)
102+
{
103+
$constraint = new Yaml(
104+
configClass: YamlConfig::class,
105+
);
106+
107+
$this->validator->validate($value, $constraint);
108+
// If Yaml itself invalid, do not trigger any Config validation error, only the basic one.
109+
$this->buildViolation('This value is not valid YAML.')
110+
->setParameter('{{ error }}', $message)
111+
->setParameter('{{ line }}', $line)
112+
->setCode(Yaml::INVALID_YAML_ERROR)
113+
->assertRaised();
114+
}
115+
74116
public static function getValidValues()
75117
{
76118
return [

0 commit comments

Comments
 (0)