Skip to content

[Validator] Added StrictTypes as class constraint with not nullable typed properties #36494

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

Closed
wants to merge 2 commits into from
Closed
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
2 changes: 2 additions & 0 deletions src/Symfony/Component/Validator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ CHANGELOG
5.1.0
-----

* added a `StrictTypes` constraint to ease validating non nullable typed properties
* added a `Cascade` constraint to ease validating typed nested objects
* added the `Hostname` constraint and validator
* added the `alpha3` option to the `Country` and `Language` constraints
* allow to define a reusable set of constraints by extending the `Compound` constraint
Expand Down
41 changes: 41 additions & 0 deletions src/Symfony/Component/Validator/Constraints/Cascade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?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\ConstraintDefinitionException;

/**
* @Annotation
* @Target({"CLASS"})
*
* @author Jules Pietri <jules@heahprod.com>
*/
class Cascade extends Constraint
{
public function __construct($options = null)
{
if (\is_array($options) && \array_key_exists('groups', $options)) {
throw new ConstraintDefinitionException(sprintf('The option "groups" is not supported by the constraint "%s".', __CLASS__));
}

parent::__construct($options);
}

/**
* {@inheritdoc}
*/
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
}
41 changes: 41 additions & 0 deletions src/Symfony/Component/Validator/Constraints/StrictTypes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?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\ConstraintDefinitionException;

/**
* @Annotation
* @Target({"CLASS"})
*
* @author Jules Pietri <jules@heahprod.com>
*/
class StrictTypes extends Constraint
{
public function __construct($options = null)
{
if (\is_array($options) && \array_key_exists('groups', $options)) {
throw new ConstraintDefinitionException(sprintf('The option "groups" is not supported by the constraint "%s".', __CLASS__));
}

parent::__construct($options);
}

/**
* {@inheritdoc}
*/
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
}
51 changes: 48 additions & 3 deletions src/Symfony/Component/Validator/Mapping/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@
namespace Symfony\Component\Validator\Mapping;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Cascade;
use Symfony\Component\Validator\Constraints\GroupSequence;
use Symfony\Component\Validator\Constraints\NotNull;
use Symfony\Component\Validator\Constraints\StrictTypes;
use Symfony\Component\Validator\Constraints\Traverse;
use Symfony\Component\Validator\Constraints\Valid;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
use Symfony\Component\Validator\Exception\GroupDefinitionException;

Expand Down Expand Up @@ -170,6 +174,17 @@ public function getDefaultGroup()

/**
* {@inheritdoc}
*
* If the constraint {@link Cascade} is added, the cascading strategy will be
* changed to {@link CascadingStrategy::CASCADE}.
*
* If the constraint {@link Traverse} is added, the traversal strategy will be
* changed. Depending on the $traverse property of that constraint,
* the traversal strategy will be set to one of the following:
*
* - {@link TraversalStrategy::IMPLICIT} by default
* - {@link TraversalStrategy::NONE} if $traverse is disabled
* - {@link TraversalStrategy::TRAVERSE} if $traverse is enabled
*/
public function addConstraint(Constraint $constraint)
{
Expand All @@ -190,6 +205,38 @@ public function addConstraint(Constraint $constraint)
return $this;
}

if ($constraint instanceof Cascade) {
if (\PHP_VERSION_ID < 70400) {
throw new ConstraintDefinitionException(sprintf('The constraint "%s" requires PHP 7.4.', Cascade::class));
}

$this->cascadingStrategy = CascadingStrategy::CASCADE;

foreach ($this->getReflectionClass()->getProperties() as $property) {
if ($property->hasType() && (('array' === $type = $property->getType()->getName()) || class_exists(($type)))) {
$this->addPropertyConstraint($property->getName(), new Valid());
}
}

// The constraint is not added
return $this;
}

if ($constraint instanceof StrictTypes) {
if (\PHP_VERSION_ID < 70400) {
throw new ConstraintDefinitionException(sprintf('The constraint "%s" requires PHP 7.4.', StrictTypes::class));
}

foreach ($this->getReflectionClass()->getProperties() as $property) {
if ($property->hasType() && !$property->getType()->allowsNull()) {
$this->addPropertyConstraint($property->getName(), new NotNull());
}
}

// The constraint is not added
return $this;
}

$constraint->addImplicitGroupName($this->getDefaultGroup());

parent::addConstraint($constraint);
Expand Down Expand Up @@ -459,13 +506,11 @@ public function isGroupSequenceProvider()
}

/**
* Class nodes are never cascaded.
*
* {@inheritdoc}
*/
public function getCascadingStrategy()
{
return CascadingStrategy::NONE;
return $this->cascadingStrategy;
}

private function addPropertyMetadata(PropertyMetadataInterface $metadata)
Expand Down
7 changes: 4 additions & 3 deletions src/Symfony/Component/Validator/Mapping/GenericMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\Validator\Mapping;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Cascade;
use Symfony\Component\Validator\Constraints\DisableAutoMapping;
use Symfony\Component\Validator\Constraints\EnableAutoMapping;
use Symfony\Component\Validator\Constraints\Traverse;
Expand Down Expand Up @@ -132,12 +133,12 @@ public function __clone()
*
* @return $this
*
* @throws ConstraintDefinitionException When trying to add the
* {@link Traverse} constraint
* @throws ConstraintDefinitionException When trying to add the {@link Cascade}
* or {@link Traverse} constraint
*/
public function addConstraint(Constraint $constraint)
{
if ($constraint instanceof Traverse) {
if ($constraint instanceof Traverse || $constraint instanceof Cascade) {
throw new ConstraintDefinitionException(sprintf('The constraint "%s" can only be put on classes. Please use "Symfony\Component\Validator\Constraints\Valid" instead.', get_debug_type($constraint)));
}

Expand Down
17 changes: 17 additions & 0 deletions src/Symfony/Component/Validator/Tests/Fixtures/CascadedChild.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?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;

class CascadedChild
{
public $name;
}
28 changes: 28 additions & 0 deletions src/Symfony/Component/Validator/Tests/Fixtures/CascadingEntity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?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;

class CascadingEntity
{
public string $scalar;

public CascadedChild $requiredChild;

public ?CascadedChild $optionalChild;

public static ?CascadedChild $staticChild;

/**
* @var CascadedChild[]
*/
public array $children;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@

use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Cascade;
use Symfony\Component\Validator\Constraints\StrictTypes;
use Symfony\Component\Validator\Constraints\Valid;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
use Symfony\Component\Validator\Mapping\CascadingStrategy;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Tests\Fixtures\CascadingEntity;
use Symfony\Component\Validator\Tests\Fixtures\ConstraintA;
use Symfony\Component\Validator\Tests\Fixtures\ConstraintB;
use Symfony\Component\Validator\Tests\Fixtures\PropertyConstraint;
Expand Down Expand Up @@ -310,4 +315,66 @@ public function testGetPropertyMetadataReturnsEmptyArrayWithoutConfiguredMetadat
{
$this->assertCount(0, $this->metadata->getPropertyMetadata('foo'), '->getPropertyMetadata() returns an empty collection if no metadata is configured for the given property');
}

/**
* @requires PHP < 7.4
*/
public function testCascadeConstraintIsNotAvailable()
{
$metadata = new ClassMetadata(CascadingEntity::class);

$this->expectException(ConstraintDefinitionException::class);
$this->expectExceptionMessage('The constraint "Symfony\Component\Validator\Constraints\Cascade" requires PHP 7.4.');

$metadata->addConstraint(new Cascade());
}

/**
* @requires PHP 7.4
*/
public function testCascadeConstraint()
{
$metadata = new ClassMetadata(CascadingEntity::class);

$metadata->addConstraint(new Cascade());

$this->assertSame(CascadingStrategy::CASCADE, $metadata->getCascadingStrategy());
$this->assertCount(4, $metadata->properties);
$this->assertSame([
'requiredChild',
'optionalChild',
'staticChild',
'children',
], $metadata->getConstrainedProperties());
}

/**
* @requires PHP < 7.4
*/
public function testStrictTypesConstraintIsNotAvailable()
{
$metadata = new ClassMetadata(CascadingEntity::class);

$this->expectException(ConstraintDefinitionException::class);
$this->expectExceptionMessage('The constraint "Symfony\Component\Validator\Constraints\StrictTypes" requires PHP 7.4.');

$metadata->addConstraint(new StrictTypes());
}

/**
* @requires PHP 7.4
*/
public function testStrictTypesConstraint()
{
$metadata = new ClassMetadata(CascadingEntity::class);

$metadata->addConstraint(new StrictTypes());

$this->assertCount(3, $metadata->properties);
$this->assertSame([
'scalar',
'requiredChild',
'children',
], $metadata->getConstrainedProperties());
}
}
Loading