Skip to content

Commit

Permalink
[FrameworkBundle][Validator] Allow implementing validation groups pro…
Browse files Browse the repository at this point in the history
…vider outside DTOs
  • Loading branch information
yceruto authored and fabpot committed Oct 20, 2023
1 parent ffa004c commit 0fb9e1f
Show file tree
Hide file tree
Showing 20 changed files with 265 additions and 30 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ CHANGELOG
* Deprecate `ValidatorBuilder::enableAnnotationMapping()`, use `ValidatorBuilder::enableAttributeMapping()` instead
* Deprecate `ValidatorBuilder::disableAnnotationMapping()`, use `ValidatorBuilder::disableAttributeMapping()` instead
* Deprecate `AnnotationLoader`, use `AttributeLoader` instead
* Add `GroupProviderInterface` to implement validation group providers outside the underlying class

6.3
---
Expand Down
9 changes: 9 additions & 0 deletions Constraints/GroupSequenceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,25 @@

namespace Symfony\Component\Validator\Constraints;

use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Symfony\Component\Validator\Attribute\HasNamedArguments;

/**
* Attribute to define a group sequence provider.
*
* @Annotation
*
* @NamedArgumentConstructor
*
* @Target({"CLASS", "ANNOTATION"})
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class GroupSequenceProvider
{
#[HasNamedArguments]
public function __construct(public ?string $provider = null)
{
}
}
30 changes: 30 additions & 0 deletions GroupProviderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?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;

use Symfony\Component\Validator\Constraints\GroupSequence;

/**
* Defines the interface for a validation group provider.
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
interface GroupProviderInterface
{
/**
* Returns which validation groups should be used for a certain state
* of the object.
*
* @return string[]|string[][]|GroupSequence
*/
public function getGroups(object $object): array|GroupSequence;
}
21 changes: 20 additions & 1 deletion Mapping/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ class ClassMetadata extends GenericMetadata implements ClassMetadataInterface
*/
public bool $groupSequenceProvider = false;

/**
* @internal This property is public in order to reduce the size of the
* class' serialized representation. Do not access it. Use
* {@link getGroupProvider()} instead.
*/
public ?string $groupProvider = null;

/**
* The strategy for traversing traversable objects.
*
Expand Down Expand Up @@ -123,6 +130,7 @@ public function __sleep(): array
'getters',
'groupSequence',
'groupSequenceProvider',
'groupProvider',
'members',
'name',
'properties',
Expand Down Expand Up @@ -319,6 +327,7 @@ public function addGetterMethodConstraints(string $property, string $method, arr
public function mergeConstraints(self $source)
{
if ($source->isGroupSequenceProvider()) {
$this->setGroupProvider($source->getGroupProvider());
$this->setGroupSequenceProvider(true);
}

Expand Down Expand Up @@ -432,7 +441,7 @@ public function setGroupSequenceProvider(bool $active)
throw new GroupDefinitionException('Defining a group sequence provider is not allowed with a static group sequence.');
}

if (!$this->getReflectionClass()->implementsInterface(GroupSequenceProviderInterface::class)) {
if (null === $this->groupProvider && !$this->getReflectionClass()->implementsInterface(GroupSequenceProviderInterface::class)) {
throw new GroupDefinitionException(sprintf('Class "%s" must implement GroupSequenceProviderInterface.', $this->name));
}

Expand All @@ -444,6 +453,16 @@ public function isGroupSequenceProvider(): bool
return $this->groupSequenceProvider;
}

public function setGroupProvider(?string $provider): void
{
$this->groupProvider = $provider;
}

public function getGroupProvider(): ?string
{
return $this->groupProvider;
}

public function getCascadingStrategy(): int
{
return $this->cascadingStrategy;
Expand Down
2 changes: 2 additions & 0 deletions Mapping/ClassMetadataInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
* @see GroupSequence
* @see GroupSequenceProviderInterface
* @see TraversalStrategy
*
* @method string|null getGroupProvider()
*/
interface ClassMetadataInterface extends MetadataInterface
{
Expand Down
1 change: 1 addition & 0 deletions Mapping/Loader/AnnotationLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public function loadClassMetadata(ClassMetadata $metadata): bool
if ($constraint instanceof GroupSequence) {
$metadata->setGroupSequence($constraint->groups);
} elseif ($constraint instanceof GroupSequenceProvider) {
$metadata->setGroupProvider($constraint->provider);
$metadata->setGroupSequenceProvider(true);
} elseif ($constraint instanceof Constraint) {
$metadata->addConstraint($constraint);
Expand Down
1 change: 1 addition & 0 deletions Mapping/Loader/XmlFileLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ private function loadClassesFromXml(): void
private function loadClassMetadataFromXml(ClassMetadata $metadata, \SimpleXMLElement $classDescription): void
{
if (\count($classDescription->{'group-sequence-provider'}) > 0) {
$metadata->setGroupProvider($classDescription->{'group-sequence-provider'}[0]->value ?: null);
$metadata->setGroupSequenceProvider(true);
}

Expand Down
3 changes: 3 additions & 0 deletions Mapping/Loader/YamlFileLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ private function loadClassesFromYaml(): void
private function loadClassMetadataFromYaml(ClassMetadata $metadata, array $classDescription): void
{
if (isset($classDescription['group_sequence_provider'])) {
if (\is_string($classDescription['group_sequence_provider'])) {
$metadata->setGroupProvider($classDescription['group_sequence_provider']);
}
$metadata->setGroupSequenceProvider(
(bool) $classDescription['group_sequence_provider']
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
validation constraints.
]]></xsd:documentation>
</xsd:annotation>

<xsd:element name="constraint-mapping" type="constraint-mapping" />

<xsd:complexType name="constraint-mapping">
<xsd:annotation>
<xsd:documentation><![CDATA[
Expand All @@ -28,7 +28,7 @@
<xsd:element name="class" type="class" maxOccurs="unbounded" />
</xsd:sequence>
</xsd:complexType>

<xsd:complexType name="namespace">
<xsd:annotation>
<xsd:documentation><![CDATA[
Expand All @@ -41,13 +41,13 @@
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>

<xsd:complexType name="class">
<xsd:annotation>
<xsd:documentation><![CDATA[
Contains constraints for a single class.
Nested elements may be class constraints, property and/or getter
Nested elements may be class constraints, property and/or getter
definitions.
]]></xsd:documentation>
</xsd:annotation>
Expand All @@ -72,15 +72,18 @@
<xsd:element name="value" type="value" minOccurs="1" maxOccurs="unbounded" />
</xsd:sequence>
</xsd:complexType>

<xsd:complexType name="group-sequence-provider">
<xsd:annotation>
<xsd:documentation><![CDATA[
Defines the name of the group sequence provider for a class.
]]></xsd:documentation>
</xsd:annotation>
<xsd:sequence>
<xsd:element name="value" type="value" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
</xsd:complexType>

<xsd:complexType name="property">
<xsd:annotation>
<xsd:documentation><![CDATA[
Expand All @@ -93,7 +96,7 @@
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>

<xsd:complexType name="getter">
<xsd:annotation>
<xsd:documentation><![CDATA[
Expand All @@ -106,14 +109,14 @@
</xsd:sequence>
<xsd:attribute name="property" type="xsd:string" use="required" />
</xsd:complexType>

<xsd:complexType name="constraint" mixed="true">
<xsd:annotation>
<xsd:documentation><![CDATA[
Contains a constraint definition. The name of the constraint should be
given in the "name" option.
May contain a single value, multiple "constraint" elements,
May contain a single value, multiple "constraint" elements,
multiple "value" elements or multiple "option" elements.
]]></xsd:documentation>
</xsd:annotation>
Expand All @@ -122,15 +125,15 @@
<xsd:element name="option" type="option" minOccurs="1" maxOccurs="unbounded" />
<xsd:element name="value" type="value" minOccurs="1" maxOccurs="unbounded" />
</xsd:choice>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>

<xsd:complexType name="option" mixed="true">
<xsd:annotation>
<xsd:documentation><![CDATA[
Contains a constraint option definition. The name of the option
should be given in the "name" option.
May contain a single value, multiple "value" elements or multiple
"constraint" elements.
]]></xsd:documentation>
Expand All @@ -139,14 +142,14 @@
<xsd:element name="constraint" type="constraint" minOccurs="1" maxOccurs="unbounded" />
<xsd:element name="value" type="value" minOccurs="1" maxOccurs="unbounded" />
</xsd:choice>
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>

<xsd:complexType name="value" mixed="true">
<xsd:annotation>
<xsd:documentation><![CDATA[
A value of an element.
May contain a single value, multiple "value" elements or multiple
"constraint" elements.
]]></xsd:documentation>
Expand All @@ -155,6 +158,6 @@
<xsd:element name="constraint" type="constraint" minOccurs="1" maxOccurs="unbounded" />
<xsd:element name="value" type="value" minOccurs="1" maxOccurs="unbounded" />
</xsd:choice>
<xsd:attribute name="key" type="xsd:string" use="optional" />
<xsd:attribute name="key" type="xsd:string" use="optional" />
</xsd:complexType>
</xsd:schema>
26 changes: 26 additions & 0 deletions Tests/Constraints/GroupSequenceProviderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?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\GroupSequenceProvider;
use Symfony\Component\Validator\Tests\Dummy\DummyGroupProvider;

class GroupSequenceProviderTest extends TestCase
{
public function testCreateAttributeStyle()
{
$sequence = new GroupSequenceProvider(provider: DummyGroupProvider::class);

$this->assertSame(DummyGroupProvider::class, $sequence->provider);
}
}
23 changes: 23 additions & 0 deletions Tests/Dummy/DummyGroupProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?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\Dummy;

use Symfony\Component\Validator\Constraints\GroupSequence;
use Symfony\Component\Validator\GroupProviderInterface;

class DummyGroupProvider implements GroupProviderInterface
{
public function getGroups(object $object): array|GroupSequence
{
return ['foo', 'bar'];
}
}
22 changes: 22 additions & 0 deletions Tests/Fixtures/Attribute/GroupProviderDto.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?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\Fixtures\Attribute;

use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Tests\Dummy\DummyGroupProvider;

#[Assert\GroupSequenceProvider(provider: DummyGroupProvider::class)]
class GroupProviderDto
{
public string $firstName = '';
public string $lastName = '';
}
16 changes: 16 additions & 0 deletions Tests/Mapping/Loader/XmlFileLoaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
use Symfony\Component\Validator\Exception\MappingException;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Mapping\Loader\XmlFileLoader;
use Symfony\Component\Validator\Tests\Dummy\DummyGroupProvider;
use Symfony\Component\Validator\Tests\Fixtures\Attribute\GroupProviderDto;
use Symfony\Component\Validator\Tests\Fixtures\ConstraintA;
use Symfony\Component\Validator\Tests\Fixtures\ConstraintB;
use Symfony\Component\Validator\Tests\Fixtures\ConstraintWithRequiredArgument;
Expand Down Expand Up @@ -126,6 +128,20 @@ public function testLoadGroupSequenceProvider()
$this->assertEquals($expected, $metadata);
}

public function testLoadGroupProvider()
{
$loader = new XmlFileLoader(__DIR__.'/constraint-mapping.xml');
$metadata = new ClassMetadata(GroupProviderDto::class);

$loader->loadClassMetadata($metadata);

$expected = new ClassMetadata(GroupProviderDto::class);
$expected->setGroupProvider(DummyGroupProvider::class);
$expected->setGroupSequenceProvider(true);

$this->assertEquals($expected, $metadata);
}

public function testThrowExceptionIfDocTypeIsSet()
{
$loader = new XmlFileLoader(__DIR__.'/withdoctype.xml');
Expand Down
Loading

0 comments on commit 0fb9e1f

Please sign in to comment.