8000 feature #27735 [Validator][DoctrineBridge][FWBundle] Automatic data v… · symfony/framework-bundle@ffc4adc · GitHub
[go: up one dir, main page]

Skip to content

Commit ffc4adc

Browse files
committed
feature #27735 [Validator][DoctrineBridge][FWBundle] Automatic data validation (dunglas)
This PR was merged into the 4.3-dev branch. Discussion ---------- [Validator][DoctrineBridge][FWBundle] Automatic data validation | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes<!-- don't forget to update src/**/CHANGELOG.md files --> | BC breaks? | no <!-- see https://symfony.com/bc --> | Deprecations? | no <!-- don't forget to update UPGRADE-*.md and src/**/CHANGELOG.md files --> | Tests pass? | yes <!-- please add some, will be required by reviewers --> | Fixed tickets | n/a <!-- #-prefixed issue number(s), if any --> | License | MIT | Doc PR | symfony/symfony-docs#11132 This feature automatically adds some validation constraints by inferring existing metadata. To do so, it uses the PropertyInfo component and Doctrine metadata, but it has been designed to be easily extendable. Example: ```php use Doctrine\ORM\Mapping as ORM; /** * @Orm\Entity */ class Dummy { /** * @Orm\Id * @Orm\GeneratedValue(strategy="AUTO") * @Orm\Column(type="integer") */ public $id; /** * @Orm\Column(nullable=true) */ public $columnNullable; /** * @Orm\Column(length=20) */ public $columnLength; /** * @Orm\Column(unique=true) */ public $columnUnique; } $manager = $this->managerRegistry->getManager(); $manager->getRepository(Dummy::class); $firstOne = new Dummy(); $firstOne->columnUnique = 'unique'; $firstOne->columnLength = '0'; $manager->persist($firstOne); $manager->flush(); $dummy = new Dummy(); $dummy->columnNullable = 1; // type mistmatch $dummy->columnLength = '012345678901234567890'; // too long $dummy->columnUnique = 'unique'; // not unique $res = $this->validator->validate($dummy); dump((string) $res); /* Object(App\Entity\Dummy).columnUnique:\n This value is already used. (code 23bd9dbf-6b9b-41cd-a99e-4844bcf3077f)\n Object(App\Entity\Dummy).columnLength:\n This value is too long. It should have 20 characters or less. (code d94b19cc-114f-4f44-9cc4-4138e80a87b9)\n Object(App\Entity\Dummy).id:\n This value should not be null. (code ad32d13f-c3d4-423b-909a-857b961eb720)\n Object(App\Entity\Dummy).columnNullable:\n This value should be of type string. (code ba785a8c-82cb-4283-967c-3cf342181b40)\n */ ``` It also works for DTOs: ```php class MyDto { /** @var string */ public $name; } $dto = new MyDto(); $dto->name = 1; // type error dump($validator->validate($dto)); /* Object(MyDto).name:\n This value should be of type string. (code ba785a8c-82cb-4283-967c-3cf342181b40)\n */ ``` Supported constraints currently are: * `@NotNull` (using PropertyInfo type extractor, so supports Doctrine metadata, getters/setters and PHPDoc) * `@Type` (using PropertyInfo type extractor, so supports Doctrine metadata, getters/setters and PHPDoc) * `@UniqueEntity` (using Doctrine's `unique` metadata) * `@Length` (using Doctrine's `length` metadata) Many users don't understand that the Doctrine mapping doesn't validate anything (it's just a hint for the schema generator). It leads to usability and security issues (that are not entirely fixed by this PR!!). Even the ones who add constraints often omit important ones like `@Length`, or `@Type` (important when building web APIs). This PR aims to improve things a bit, and ease the development process in RAD and when prototyping. It provides an upgrade path to use proper validation constraints. I plan to make it opt-in, disabled by default, but enabled in the default Flex recipe. (= off by default when using components, on by default when using the full stack framework) TODO: * [x] Add configuration flags * [x] Move the Doctrine-related DI logic from the extension to DoctrineBundle: doctrine/DoctrineBundle#831 * [x] Commit the tests Commits ------- 2d64e703c2 [Validator][DoctrineBridge][FWBundle] Automatic data validation
2 parents a0b89d9 + 8c9858f commit ffc4adc

10 files changed

+125
-3
lines changed

DependencyInjection/Configuration.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,45 @@ private function addValidationSection(ArrayNodeDefinition $rootNode)
804804
->end()
805805
->end()
806806
->end()
807+
->arrayNode('auto_mapping')
808+
->useAttributeAsKey('namespace')
809+
->normalizeKeys(false)
810+
->beforeNormalization()
811+
->ifArray()
812+
->then(function (array $values): array {
813+
foreach ($values as $k => $v) {
814+
if (isset($v['service'])) {
815+
continue;
816+
}
817+
818+
if (isset($v['namespace'])) {
819+
$values[$k]['services'] = [];
820+
continue;
821+
}
822+
823+
if (!\is_array($v)) {
824+
$values[$v]['services'] = [];
825+
unset($values[$k]);
826+
continue;
827+
}
828+
829+
$tmp = $v;
830+
unset($values[$k]);
831+
$values[$k]['services'] = $tmp;
832+
}
833+
834+
return $values;
835+
})
836+
->end()
837+
->arrayPrototype()
838+
->fixXmlConfig('service')
839+
->children()
840+
->arrayNode('services')
841+
->prototype('scalar')->end()
842+
->end()
843+
->end()
844+
->end()
845+
->end()
807846
->end()
808847
->end()
809848
->end()

DependencyInjection/FrameworkExtension.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
use Symfony\Component\Translation\Command\XliffLintCommand as BaseXliffLintCommand;
111111
use Symfony\Component\Translation\Translator;
112112
use Symfony\Component\Validator\ConstraintValidatorInterface;
113+
use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader;
113114
use Symfony\Component\Validator\ObjectInitializerInterface;
114115
use Symfony\Component\WebLink\HttpHeaderSerializer;
115116
use Symfony\Component\Workflow;
@@ -284,7 +285,8 @@ public function load(array $configs, ContainerBuilder $container)
284285
$container->removeDefinition('console.command.messenger_setup_transports');
285286
}
286287

287-
$this->registerValidationConfiguration($config['validation'], $container, $loader);
288+
$propertyInfoEnabled = $this->isConfigEnabled($container, $config['property_info']);
289+
$this->registerValidationConfiguration($config['validation'], $container, $loader, $propertyInfoEnabled);
288290
$this->registerEsiConfiguration($config['esi'], $container, $loader);
289291
$this->registerSsiConfiguration($config['ssi'], $container, $loader);
290292
$this->registerFragmentsConfiguration($config['fragments'], $container, $loader);
@@ -305,7 +307,7 @@ public function load(array $configs, ContainerBuilder $container)
305307
$this->registerSerializerConfiguration($config['serializer'], $container, $loader);
306308
}
307309

308-
if ($this->isConfigEnabled($container, $config['property_info'])) {
310+
if ($propertyInfoEnabled) {
309311
$this->registerPropertyInfoConfiguration($container, $loader);
310312
}
311313

@@ -1166,7 +1168,7 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder
11661168
}
11671169
}
11681170

1169-
private function registerValidationConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
1171+
private function registerValidationConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader, bool $propertyInfoEnabled)
11701172
{
11711173
if (!$this->validatorConfigEnabled = $this->isConfigEnabled($container, $config)) {
11721174
return;
@@ -1217,6 +1219,11 @@ private function registerValidationConfiguration(array $config, ContainerBuilder
12171219
if (!$container->getParameter('kernel.debug')) {
12181220
$validatorBuilder->addMethodCall('setMetadataCache', [new Reference('validator.mapping.cache.symfony')]);
12191221
}
1222+
1223+
$container->setParameter('validator.auto_mapping', $config['auto_mapping']);
1224+
if (!$propertyInfoEnabled || !$config['auto_mapping'] || !class_exists(PropertyInfoLoader::class)) {
1225+
$container->removeDefinition('validator.property_info_loader');
1226+
}
12201227
}
12211228

12221229
private function registerValidatorMapping(ContainerBuilder $container, array $config, array &$files)

FrameworkBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
use Symfony\Component\Translation\DependencyInjection\TranslationExtractorPass;
5454
use Symfony\Component\Translation\DependencyInjection\TranslatorPass;
5555
use Symfony\Component\Translation\DependencyInjection\TranslatorPathsPass;
56+
use Symfony\Component\Validator\DependencyInjection\AddAutoMappingConfigurationPass;
5657
use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass;
5758
use Symfony\Component\Validator\DependencyInjection\AddValidatorInitializersPass;
5859

@@ -124,6 +125,7 @@ public function build(ContainerBuilder $container)
124125
$container->addCompilerPass(new TestServiceContainerRealRefPass(), PassConfig::TYPE_AFTER_REMOVING);
125126
$this->addCompilerPassIfExists($container, AddMimeTypeGuesserPass::class);
126127
$this->addCompilerPassIfExists($container, MessengerPass::class);
128+
$this->addCompilerPassIfExists($container, AddAutoMappingConfigurationPass::class);
127129
$container->addCompilerPass(new RegisterReverseContainerPass(true));
128130
$container->addCompilerPass(new RegisterReverseContainerPass(false), PassConfig::TYPE_AFTER_REMOVING);
129131

Resources/config/schema/symfony-1.0.xsd

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@
190190
<xsd:choice minOccurs="0" maxOccurs="unbounded">
191191
<xsd:element name="static-method" type="xsd:string" />
192192
<xsd:element name="mapping" type="file_mapping" />
193+
<xsd:element name="auto-mapping" type="auto_mapping" />
193194
</xsd:choice>
194195

195196
<xsd:attribute name="enabled" type="xsd:boolean" />
@@ -207,6 +208,13 @@
207208
</xsd:sequence>
208209
</xsd:complexType>
209210

211+
<xsd:complexType name="auto_mapping">
212+
<xsd:sequence>
213+
<xsd:element name="service" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
214+
</xsd:sequence>
215+
<xsd:attribute name="namespace" type="xsd:string" use="required" />
216+
</xsd:complexType>
217+
210218
<xsd:simpleType name="email-validation-mode">
211219
<xsd:restriction base="xsd:string">
212220
<xsd:enumeration value="html5" />

Resources/config/validator.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,12 @@
6060
<argument></argument>
6161
<tag name="validator.constraint_validator" alias="Symfony\Component\Validator\Constraints\EmailValidator" />
6262
</service>
63+
64+
<service id="validator.property_info_loader" class="Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader">
65+
<argument type="service" id="property_info" />
66+
<argument type="service" id="property_info" />
67+
68+
<tag name="validator.auto_mapper" />
69+
</service>
6370
</services>
6471
</container>

Tests/DependencyInjection/ConfigurationTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ protected static function getBundleDefaultConfig()
233233
'mapping' => [
234234
'paths' => [],
235235
],
236+
'auto_mapping' => [],
236237
],
237238
'annotations' => [
238239
'cache' => 'php_array',
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
$container->loadFromExtension('framework', [
4+
'property_info' => ['enabled' => true],
5+
'validation' => [
6+
'auto_mapping' => [
7+
'App\\' => ['foo', 'bar'],
8+
'Symfony\\' => ['a', 'b'],
9+
'Foo\\',
10+
],
11+
],
12+
]);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" ?>
2+
3+
<container xmlns="http://symfony.com/schema/dic/services"
4+
xmlns:framework="http://symfony.com/schema/dic/symfony">
5+
6+
<framework:config>
7+
<framework:property-info enabled="true" />
8+
<framework:validation>
9+
<framework:auto-mapping namespace="App\">
10+
<framework:service>foo</framework:service>
11+
<framework:service>bar</framework:service>
12+
</framework:auto-mapping>
13+
<framework:auto-mapping namespace="Symfony\">
14+
<framework:service>a</framework:service>
15+
<framework:service>b</framework:service>
16+
</framework:auto-mapping>
17+
<framework:auto-mapping namespace="Foo\" />
18+
</framework:validation>
19+
</framework:config>
20+
</container>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
framework:
2+
property_info: { enabled: true }
3+
validation:
4+
auto_mapping:
5+
'App\': ['foo', 'bar']
6+
'Symfony\': ['a', 'b']
7+
'Foo\': []

Tests/DependencyInjection/FrameworkExtensionTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@
5050
use Symfony\Component\Serializer\Serializer;
5151
use Symfony\Component\Translation\DependencyInjection\TranslatorPass;
5252
use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass;
53+
use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader;
54+
use Symfony\Component\Validator\Validation;
5355
use Symfony\Component\Workflow;
5456
use Symfony\Contracts\HttpClient\HttpClientInterface;
5557

@@ -1057,6 +1059,23 @@ public function testValidationMapping()
10571059
$this->assertContains('validation.yaml', $calls[4][1][0][2]);
10581060
}
10591061

1062+
public function testValidationAutoMapping()
1063+
{
1064+
if (!class_exists(PropertyInfoLoader::class)) {
1065+
$this->markTestSkipped('Auto-mapping requires symfony/validation 4.2+');
1066+
}
1067+
1068+
$container = $this->createContainerFromFile('validation_auto_mapping');
1069+
$parameter = [
1070+
'App\\' => ['services' => ['foo', 'bar']],
1071+
'Symfony\\' => ['services' => ['a', 'b']],
1072+
'Foo\\' => ['services' => []],
1073+
];
1074+
1075+
$this->assertSame($parameter, $container->getParameter('validator.auto_mapping'));
1076+
$this->assertTrue($container->hasDefinition('validator.property_info_loader'));
1077+
}
1078+
10601079
public function testFormsCanBeEnabledWithoutCsrfProtection()
10611080
{
10621081
$container = $this->createContainerFromFile('form_no_csrf');

0 commit comments

Comments
 (0)
0