771 lines
30 KiB
PHP
771 lines
30 KiB
PHP
|
<?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\Validator;
|
||
|
|
||
|
use Psr\Container\ContainerInterface;
|
||
|
use Symfony\Component\Validator\Constraint;
|
||
|
use Symfony\Component\Validator\Constraints\Composite;
|
||
|
use Symfony\Component\Validator\Constraints\Existence;
|
||
|
use Symfony\Component\Validator\Constraints\GroupSequence;
|
||
|
use Symfony\Component\Validator\Constraints\Valid;
|
||
|
use Symfony\Component\Validator\ConstraintValidatorFactoryInterface;
|
||
|
use Symfony\Component\Validator\ConstraintViolationListInterface;
|
||
|
use Symfony\Component\Validator\Context\ExecutionContext;
|
||
|
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||
|
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
|
||
|
use Symfony\Component\Validator\Exception\NoSuchMetadataException;
|
||
|
use Symfony\Component\Validator\Exception\RuntimeException;
|
||
|
use Symfony\Component\Validator\Exception\UnexpectedValueException;
|
||
|
use Symfony\Component\Validator\Exception\UnsupportedMetadataException;
|
||
|
use Symfony\Component\Validator\Exception\ValidatorException;
|
||
|
use Symfony\Component\Validator\Mapping\CascadingStrategy;
|
||
|
use Symfony\Component\Validator\Mapping\ClassMetadataInterface;
|
||
|
use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;
|
||
|
use Symfony\Component\Validator\Mapping\GenericMetadata;
|
||
|
use Symfony\Component\Validator\Mapping\GetterMetadata;
|
||
|
use Symfony\Component\Validator\Mapping\MetadataInterface;
|
||
|
use Symfony\Component\Validator\Mapping\PropertyMetadataInterface;
|
||
|
use Symfony\Component\Validator\Mapping\TraversalStrategy;
|
||
|
use Symfony\Component\Validator\ObjectInitializerInterface;
|
||
|
use Symfony\Component\Validator\Util\PropertyPath;
|
||
|
|
||
|
/**
|
||
|
* Recursive implementation of {@link ContextualValidatorInterface}.
|
||
|
*
|
||
|
* @author Bernhard Schussek <bschussek@gmail.com>
|
||
|
*/
|
||
|
class RecursiveContextualValidator implements ContextualValidatorInterface
|
||
|
{
|
||
|
private string $defaultPropertyPath;
|
||
|
private array $defaultGroups;
|
||
|
|
||
|
/**
|
||
|
* Creates a validator for the given context.
|
||
|
*
|
||
|
* @param ObjectInitializerInterface[] $objectInitializers The object initializers
|
||
|
*/
|
||
|
public function __construct(
|
||
|
private ExecutionContextInterface $context,
|
||
|
private MetadataFactoryInterface $metadataFactory,
|
||
|
private ConstraintValidatorFactoryInterface $validatorFactory,
|
||
|
private array $objectInitializers = [],
|
||
|
private ?ContainerInterface $groupProviderLocator = null,
|
||
|
) {
|
||
|
$this->defaultPropertyPath = $context->getPropertyPath();
|
||
|
$this->defaultGroups = [$context->getGroup() ?: Constraint::DEFAULT_GROUP];
|
||
|
}
|
||
|
|
||
|
public function atPath(string $path): static
|
||
|
{
|
||
|
$this->defaultPropertyPath = $this->context->getPropertyPath($path);
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
public function validate(mixed $value, Constraint|array|null $constraints = null, string|GroupSequence|array|null $groups = null): static
|
||
|
{
|
||
|
$groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
|
||
|
|
||
|
$previousValue = $this->context->getValue();
|
||
|
$previousObject = $this->context->getObject();
|
||
|
$previousMetadata = $this->context->getMetadata();
|
||
|
$previousPath = $this->context->getPropertyPath();
|
||
|
$previousGroup = $this->context->getGroup();
|
||
|
$previousConstraint = null;
|
||
|
|
||
|
if ($this->context instanceof ExecutionContext || method_exists($this->context, 'getConstraint')) {
|
||
|
$previousConstraint = $this->context->getConstraint();
|
||
|
}
|
||
|
|
||
|
// If explicit constraints are passed, validate the value against
|
||
|
// those constraints
|
||
|
if (null !== $constraints) {
|
||
|
// You can pass a single constraint or an array of constraints
|
||
|
// Make sure to deal with an array in the rest of the code
|
||
|
if (!\is_array($constraints)) {
|
||
|
$constraints = [$constraints];
|
||
|
}
|
||
|
|
||
|
$metadata = new GenericMetadata();
|
||
|
$metadata->addConstraints($constraints);
|
||
|
|
||
|
$this->validateGenericNode(
|
||
|
$value,
|
||
|
$previousObject,
|
||
|
\is_object($value) ? $this->generateCacheKey($value) : null,
|
||
|
$metadata,
|
||
|
$this->defaultPropertyPath,
|
||
|
$groups,
|
||
|
null,
|
||
|
TraversalStrategy::IMPLICIT,
|
||
|
$this->context
|
||
|
);
|
||
|
|
||
|
$this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
|
||
|
$this->context->setGroup($previousGroup);
|
||
|
|
||
|
if (null !== $previousConstraint) {
|
||
|
$this->context->setConstraint($previousConstraint);
|
||
|
}
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
// If an object is passed without explicit constraints, validate that
|
||
|
// object against the constraints defined for the object's class
|
||
|
if (\is_object($value)) {
|
||
|
$this->validateObject(
|
||
|
$value,
|
||
|
$this->defaultPropertyPath,
|
||
|
$groups,
|
||
|
TraversalStrategy::IMPLICIT,
|
||
|
$this->context
|
||
|
);
|
||
|
|
||
|
$this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
|
||
|
$this->context->setGroup($previousGroup);
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
// If an array is passed without explicit constraints, validate each
|
||
|
// object in the array
|
||
|
if (\is_array($value)) {
|
||
|
$this->validateEachObjectIn(
|
||
|
$value,
|
||
|
$this->defaultPropertyPath,
|
||
|
$groups,
|
||
|
$this->context
|
||
|
);
|
||
|
|
||
|
$this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
|
||
|
$this->context->setGroup($previousGroup);
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
throw new RuntimeException(sprintf('Cannot validate values of type "%s" automatically. Please provide a constraint.', get_debug_type($value)));
|
||
|
}
|
||
|
|
||
|
public function validateProperty(object $object, string $propertyName, string|GroupSequence|array|null $groups = null): static
|
||
|
{
|
||
|
$classMetadata = $this->metadataFactory->getMetadataFor($object);
|
||
|
|
||
|
if (!$classMetadata instanceof ClassMetadataInterface) {
|
||
|
throw new ValidatorException(sprintf('The metadata factory should return instances of "\Symfony\Component\Validator\Mapping\ClassMetadataInterface", got: "%s".', get_debug_type($classMetadata)));
|
||
|
}
|
||
|
|
||
|
$propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName);
|
||
|
$groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
|
||
|
$cacheKey = $this->generateCacheKey($object);
|
||
|
$propertyPath = PropertyPath::append($this->defaultPropertyPath, $propertyName);
|
||
|
|
||
|
$previousValue = $this->context->getValue();
|
||
|
$previousObject = $this->context->getObject();
|
||
|
$previousMetadata = $this->context->getMetadata();
|
||
|
$previousPath = $this->context->getPropertyPath();
|
||
|
$previousGroup = $this->context->getGroup();
|
||
|
|
||
|
foreach ($propertyMetadatas as $propertyMetadata) {
|
||
|
$propertyValue = $propertyMetadata->getPropertyValue($object);
|
||
|
|
||
|
$this->validateGenericNode(
|
||
|
$propertyValue,
|
||
|
$object,
|
||
|
$cacheKey.':'.$object::class.':'.$propertyName,
|
||
|
$propertyMetadata,
|
||
|
$propertyPath,
|
||
|
$groups,
|
||
|
null,
|
||
|
TraversalStrategy::IMPLICIT,
|
||
|
$this->context
|
||
|
);
|
||
|
}
|
||
|
|
||
|
$this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
|
||
|
$this->context->setGroup($previousGroup);
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
public function validatePropertyValue(object|string $objectOrClass, string $propertyName, mixed $value, string|GroupSequence|array|null $groups = null): static
|
||
|
{
|
||
|
$classMetadata = $this->metadataFactory->getMetadataFor($objectOrClass);
|
||
|
|
||
|
if (!$classMetadata instanceof ClassMetadataInterface) {
|
||
|
throw new ValidatorException(sprintf('The metadata factory should return instances of "\Symfony\Component\Validator\Mapping\ClassMetadataInterface", got: "%s".', get_debug_type($classMetadata)));
|
||
|
}
|
||
|
|
||
|
$propertyMetadatas = $classMetadata->getPropertyMetadata($propertyName);
|
||
|
$groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
|
||
|
|
||
|
if (\is_object($objectOrClass)) {
|
||
|
$object = $objectOrClass;
|
||
|
$class = $object::class;
|
||
|
$cacheKey = $this->generateCacheKey($objectOrClass);
|
||
|
$propertyPath = PropertyPath::append($this->defaultPropertyPath, $propertyName);
|
||
|
} else {
|
||
|
// $objectOrClass contains a class name
|
||
|
$object = null;
|
||
|
$class = $objectOrClass;
|
||
|
$cacheKey = null;
|
||
|
$propertyPath = $this->defaultPropertyPath;
|
||
|
}
|
||
|
|
||
|
$previousValue = $this->context->getValue();
|
||
|
$previousObject = $this->context->getObject();
|
||
|
$previousMetadata = $this->context->getMetadata();
|
||
|
$previousPath = $this->context->getPropertyPath();
|
||
|
$previousGroup = $this->context->getGroup();
|
||
|
|
||
|
foreach ($propertyMetadatas as $propertyMetadata) {
|
||
|
$this->validateGenericNode(
|
||
|
$value,
|
||
|
$object,
|
||
|
$cacheKey.':'.$class.':'.$propertyName,
|
||
|
$propertyMetadata,
|
||
|
$propertyPath,
|
||
|
$groups,
|
||
|
null,
|
||
|
TraversalStrategy::IMPLICIT,
|
||
|
$this->context
|
||
|
);
|
||
|
}
|
||
|
|
||
|
$this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
|
||
|
$this->context->setGroup($previousGroup);
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
public function getViolations(): ConstraintViolationListInterface
|
||
|
{
|
||
|
return $this->context->getViolations();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Normalizes the given group or list of groups to an array.
|
||
|
*
|
||
|
* @param string|GroupSequence|array<string|GroupSequence> $groups The groups to normalize
|
||
|
*
|
||
|
* @return array<string|GroupSequence>
|
||
|
*/
|
||
|
protected function normalizeGroups(string|GroupSequence|array $groups): array
|
||
|
{
|
||
|
if (\is_array($groups)) {
|
||
|
return $groups;
|
||
|
}
|
||
|
|
||
|
return [$groups];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Validates an object against the constraints defined for its class.
|
||
|
*
|
||
|
* If no metadata is available for the class, but the class is an instance
|
||
|
* of {@link \Traversable} and the selected traversal strategy allows
|
||
|
* traversal, the object will be iterated and each nested object will be
|
||
|
* validated instead.
|
||
|
*
|
||
|
* @throws NoSuchMetadataException If the object has no associated metadata
|
||
|
* and does not implement {@link \Traversable}
|
||
|
* or if traversal is disabled via the
|
||
|
* $traversalStrategy argument
|
||
|
* @throws UnsupportedMetadataException If the metadata returned by the
|
||
|
* metadata factory does not implement
|
||
|
* {@link ClassMetadataInterface}
|
||
|
*/
|
||
|
private function validateObject(object $object, string $propertyPath, array $groups, int $traversalStrategy, ExecutionContextInterface $context): void
|
||
|
{
|
||
|
try {
|
||
|
$classMetadata = $this->metadataFactory->getMetadataFor($object);
|
||
|
|
||
|
if (!$classMetadata instanceof ClassMetadataInterface) {
|
||
|
throw new UnsupportedMetadataException(sprintf('The metadata factory should return instances of "Symfony\Component\Validator\Mapping\ClassMetadataInterface", got: "%s".', get_debug_type($classMetadata)));
|
||
|
}
|
||
|
|
||
|
$this->validateClassNode(
|
||
|
$object,
|
||
|
$this->generateCacheKey($object),
|
||
|
$classMetadata,
|
||
|
$propertyPath,
|
||
|
$groups,
|
||
|
null,
|
||
|
$traversalStrategy,
|
||
|
$context
|
||
|
);
|
||
|
} catch (NoSuchMetadataException $e) {
|
||
|
// Rethrow if not Traversable
|
||
|
if (!$object instanceof \Traversable) {
|
||
|
throw $e;
|
||
|
}
|
||
|
|
||
|
// Rethrow unless IMPLICIT or TRAVERSE
|
||
|
if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) {
|
||
|
throw $e;
|
||
|
}
|
||
|
|
||
|
$this->validateEachObjectIn(
|
||
|
$object,
|
||
|
$propertyPath,
|
||
|
$groups,
|
||
|
$context
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Validates each object in a collection against the constraints defined
|
||
|
* for their classes.
|
||
|
*
|
||
|
* Nested arrays are also iterated.
|
||
|
*/
|
||
|
private function validateEachObjectIn(iterable $collection, string $propertyPath, array $groups, ExecutionContextInterface $context): void
|
||
|
{
|
||
|
foreach ($collection as $key => $value) {
|
||
|
if (\is_array($value)) {
|
||
|
// Also traverse nested arrays
|
||
|
$this->validateEachObjectIn(
|
||
|
$value,
|
||
|
$propertyPath.'['.$key.']',
|
||
|
$groups,
|
||
|
$context
|
||
|
);
|
||
|
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Scalar and null values in the collection are ignored
|
||
|
if (\is_object($value)) {
|
||
|
$this->validateObject(
|
||
|
$value,
|
||
|
$propertyPath.'['.$key.']',
|
||
|
$groups,
|
||
|
TraversalStrategy::IMPLICIT,
|
||
|
$context
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Validates a class node.
|
||
|
*
|
||
|
* A class node is a combination of an object with a {@link ClassMetadataInterface}
|
||
|
* instance. Each class node (conceptually) has zero or more succeeding
|
||
|
* property nodes:
|
||
|
*
|
||
|
* (Article:class node)
|
||
|
* \
|
||
|
* ($title:property node)
|
||
|
*
|
||
|
* This method validates the passed objects against all constraints defined
|
||
|
* at class level. It furthermore triggers the validation of each of the
|
||
|
* class' properties against the constraints for that property.
|
||
|
*
|
||
|
* If the selected traversal strategy allows traversal, the object is
|
||
|
* iterated and each nested object is validated against its own constraints.
|
||
|
* The object is not traversed if traversal is disabled in the class
|
||
|
* metadata.
|
||
|
*
|
||
|
* If the passed groups contain the group "Default", the validator will
|
||
|
* check whether the "Default" group has been replaced by a group sequence
|
||
|
* in the class metadata. If this is the case, the group sequence is
|
||
|
* validated instead.
|
||
|
*
|
||
|
* @throws UnsupportedMetadataException If a property metadata does not
|
||
|
* implement {@link PropertyMetadataInterface}
|
||
|
* @throws ConstraintDefinitionException If traversal was enabled but the
|
||
|
* object does not implement
|
||
|
* {@link \Traversable}
|
||
|
*
|
||
|
* @see TraversalStrategy
|
||
|
*/
|
||
|
private function validateClassNode(object $object, ?string $cacheKey, ClassMetadataInterface $metadata, string $propertyPath, array $groups, ?array $cascadedGroups, int $traversalStrategy, ExecutionContextInterface $context): void
|
||
|
{
|
||
|
$context->setNode($object, $object, $metadata, $propertyPath);
|
||
|
|
||
|
if (!$context->isObjectInitialized($cacheKey)) {
|
||
|
foreach ($this->objectInitializers as $initializer) {
|
||
|
$initializer->initialize($object);
|
||
|
}
|
||
|
|
||
|
$context->markObjectAsInitialized($cacheKey);
|
||
|
}
|
||
|
|
||
|
foreach ($groups as $key => $group) {
|
||
|
// If the "Default" group is replaced by a group sequence, remember
|
||
|
// to cascade the "Default" group when traversing the group
|
||
|
// sequence
|
||
|
$defaultOverridden = false;
|
||
|
|
||
|
// Use the object hash for group sequences
|
||
|
$groupHash = \is_object($group) ? $this->generateCacheKey($group, true) : $group;
|
||
|
|
||
|
if ($context->isGroupValidated($cacheKey, $groupHash)) {
|
||
|
// Skip this group when validating the properties and when
|
||
|
// traversing the object
|
||
|
unset($groups[$key]);
|
||
|
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
$context->markGroupAsValidated($cacheKey, $groupHash);
|
||
|
|
||
|
// Replace the "Default" group by the group sequence defined
|
||
|
// for the class, if applicable.
|
||
|
// This is done after checking the cache, so that
|
||
|
// spl_object_hash() isn't called for this sequence and
|
||
|
// "Default" is used instead in the cache. This is useful
|
||
|
// if the getters below return different group sequences in
|
||
|
// every call.
|
||
|
if (Constraint::DEFAULT_GROUP === $group) {
|
||
|
if ($metadata->hasGroupSequence()) {
|
||
|
// The group sequence is statically defined for the class
|
||
|
$group = $metadata->getGroupSequence();
|
||
|
$defaultOverridden = true;
|
||
|
} elseif ($metadata->isGroupSequenceProvider()) {
|
||
|
if (null !== $provider = $metadata->getGroupProvider()) {
|
||
|
if (null === $this->groupProviderLocator) {
|
||
|
throw new \LogicException('A group provider locator is required when using group provider.');
|
||
|
}
|
||
|
|
||
|
$group = $this->groupProviderLocator->get($provider)->getGroups($object);
|
||
|
} else {
|
||
|
// The group sequence is dynamically obtained from the validated
|
||
|
// object
|
||
|
/* @var \Symfony\Component\Validator\GroupSequenceProviderInterface $object */
|
||
|
$group = $object->getGroupSequence();
|
||
|
}
|
||
|
$defaultOverridden = true;
|
||
|
|
||
|
if (!$group instanceof GroupSequence) {
|
||
|
$group = new GroupSequence($group);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If the groups (=[<G1,G2>,G3,G4]) contain a group sequence
|
||
|
// (=<G1,G2>), then call validateClassNode() with each entry of the
|
||
|
// group sequence and abort if necessary (G1, G2)
|
||
|
if ($group instanceof GroupSequence) {
|
||
|
$this->stepThroughGroupSequence(
|
||
|
$object,
|
||
|
$object,
|
||
|
$cacheKey,
|
||
|
$metadata,
|
||
|
$propertyPath,
|
||
|
$traversalStrategy,
|
||
|
$group,
|
||
|
$defaultOverridden ? Constraint::DEFAULT_GROUP : null,
|
||
|
$context
|
||
|
);
|
||
|
|
||
|
// Skip the group sequence when validating properties, because
|
||
|
// stepThroughGroupSequence() already validates the properties
|
||
|
unset($groups[$key]);
|
||
|
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
$this->validateInGroup($object, $cacheKey, $metadata, $group, $context);
|
||
|
}
|
||
|
|
||
|
// If no more groups should be validated for the property nodes,
|
||
|
// we can safely quit
|
||
|
if (0 === \count($groups)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Validate all properties against their constraints
|
||
|
foreach ($metadata->getConstrainedProperties() as $propertyName) {
|
||
|
// If constraints are defined both on the getter of a property as
|
||
|
// well as on the property itself, then getPropertyMetadata()
|
||
|
// returns two metadata objects, not just one
|
||
|
foreach ($metadata->getPropertyMetadata($propertyName) as $propertyMetadata) {
|
||
|
if (!$propertyMetadata instanceof PropertyMetadataInterface) {
|
||
|
throw new UnsupportedMetadataException(sprintf('The property metadata instances should implement "Symfony\Component\Validator\Mapping\PropertyMetadataInterface", got: "%s".', get_debug_type($propertyMetadata)));
|
||
|
}
|
||
|
|
||
|
if ($propertyMetadata instanceof GetterMetadata) {
|
||
|
$propertyValue = new LazyProperty(static fn () => $propertyMetadata->getPropertyValue($object));
|
||
|
} else {
|
||
|
$propertyValue = $propertyMetadata->getPropertyValue($object);
|
||
|
}
|
||
|
|
||
|
$this->validateGenericNode(
|
||
|
$propertyValue,
|
||
|
$object,
|
||
|
$cacheKey.':'.$object::class.':'.$propertyName,
|
||
|
$propertyMetadata,
|
||
|
PropertyPath::append($propertyPath, $propertyName),
|
||
|
$groups,
|
||
|
$cascadedGroups,
|
||
|
TraversalStrategy::IMPLICIT,
|
||
|
$context
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If no specific traversal strategy was requested when this method
|
||
|
// was called, use the traversal strategy of the class' metadata
|
||
|
if ($traversalStrategy & TraversalStrategy::IMPLICIT) {
|
||
|
$traversalStrategy = $metadata->getTraversalStrategy();
|
||
|
}
|
||
|
|
||
|
// Traverse only if IMPLICIT or TRAVERSE
|
||
|
if (!($traversalStrategy & (TraversalStrategy::IMPLICIT | TraversalStrategy::TRAVERSE))) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// If IMPLICIT, stop unless we deal with a Traversable
|
||
|
if ($traversalStrategy & TraversalStrategy::IMPLICIT && !$object instanceof \Traversable) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// If TRAVERSE, fail if we have no Traversable
|
||
|
if (!$object instanceof \Traversable) {
|
||
|
throw new ConstraintDefinitionException(sprintf('Traversal was enabled for "%s", but this class does not implement "\Traversable".', get_debug_type($object)));
|
||
|
}
|
||
|
|
||
|
$this->validateEachObjectIn(
|
||
|
$object,
|
||
|
$propertyPath,
|
||
|
$groups,
|
||
|
$context
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Validates a node that is not a class node.
|
||
|
*
|
||
|
* Currently, two such node types exist:
|
||
|
*
|
||
|
* - property nodes, which consist of the value of an object's
|
||
|
* property together with a {@link PropertyMetadataInterface} instance
|
||
|
* - generic nodes, which consist of a value and some arbitrary
|
||
|
* constraints defined in a {@link MetadataInterface} container
|
||
|
*
|
||
|
* In both cases, the value is validated against all constraints defined
|
||
|
* in the passed metadata object. Then, if the value is an instance of
|
||
|
* {@link \Traversable} and the selected traversal strategy permits it,
|
||
|
* the value is traversed and each nested object validated against its own
|
||
|
* constraints. If the value is an array, it is traversed regardless of
|
||
|
* the given strategy.
|
||
|
*
|
||
|
* @see TraversalStrategy
|
||
|
*/
|
||
|
private function validateGenericNode(mixed $value, ?object $object, ?string $cacheKey, ?MetadataInterface $metadata, string $propertyPath, array $groups, ?array $cascadedGroups, int $traversalStrategy, ExecutionContextInterface $context): void
|
||
|
{
|
||
|
$context->setNode($value, $object, $metadata, $propertyPath);
|
||
|
|
||
|
foreach ($groups as $key => $group) {
|
||
|
if ($group instanceof GroupSequence) {
|
||
|
$this->stepThroughGroupSequence(
|
||
|
$value,
|
||
|
$object,
|
||
|
$cacheKey,
|
||
|
$metadata,
|
||
|
$propertyPath,
|
||
|
$traversalStrategy,
|
||
|
$group,
|
||
|
null,
|
||
|
$context
|
||
|
);
|
||
|
|
||
|
// Skip the group sequence when cascading, as the cascading
|
||
|
// logic is already done in stepThroughGroupSequence()
|
||
|
unset($groups[$key]);
|
||
|
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
$this->validateInGroup($value, $cacheKey, $metadata, $group, $context);
|
||
|
}
|
||
|
|
||
|
if (0 === \count($groups)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (null === $value) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
$cascadingStrategy = $metadata->getCascadingStrategy();
|
||
|
|
||
|
// Quit unless we cascade
|
||
|
if (!($cascadingStrategy & CascadingStrategy::CASCADE)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// If no specific traversal strategy was requested when this method
|
||
|
// was called, use the traversal strategy of the node's metadata
|
||
|
if ($traversalStrategy & TraversalStrategy::IMPLICIT) {
|
||
|
$traversalStrategy = $metadata->getTraversalStrategy();
|
||
|
}
|
||
|
|
||
|
// The $cascadedGroups property is set, if the "Default" group is
|
||
|
// overridden by a group sequence
|
||
|
// See validateClassNode()
|
||
|
$cascadedGroups = null !== $cascadedGroups && \count($cascadedGroups) > 0 ? $cascadedGroups : $groups;
|
||
|
|
||
|
if ($value instanceof LazyProperty) {
|
||
|
$value = $value->getPropertyValue();
|
||
|
|
||
|
if (null === $value) {
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (\is_array($value)) {
|
||
|
// Arrays are always traversed, independent of the specified
|
||
|
// traversal strategy
|
||
|
$this->validateEachObjectIn(
|
||
|
$value,
|
||
|
$propertyPath,
|
||
|
$cascadedGroups,
|
||
|
$context
|
||
|
);
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!\is_object($value)) {
|
||
|
throw new NoSuchMetadataException(sprintf('Cannot create metadata for non-objects. Got: "%s".', \gettype($value)));
|
||
|
}
|
||
|
|
||
|
$this->validateObject(
|
||
|
$value,
|
||
|
$propertyPath,
|
||
|
$cascadedGroups,
|
||
|
$traversalStrategy,
|
||
|
$context
|
||
|
);
|
||
|
|
||
|
// Currently, the traversal strategy can only be TRAVERSE for a
|
||
|
// generic node if the cascading strategy is CASCADE. Thus, traversable
|
||
|
// objects will always be handled within validateObject() and there's
|
||
|
// nothing more to do here.
|
||
|
|
||
|
// see GenericMetadata::addConstraint()
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sequentially validates a node's value in each group of a group sequence.
|
||
|
*
|
||
|
* If any of the constraints generates a violation, subsequent groups in the
|
||
|
* group sequence are skipped.
|
||
|
*/
|
||
|
private function stepThroughGroupSequence(mixed $value, ?object $object, ?string $cacheKey, ?MetadataInterface $metadata, string $propertyPath, int $traversalStrategy, GroupSequence $groupSequence, ?string $cascadedGroup, ExecutionContextInterface $context): void
|
||
|
{
|
||
|
$violationCount = \count($context->getViolations());
|
||
|
$cascadedGroups = $cascadedGroup ? [$cascadedGroup] : null;
|
||
|
|
||
|
foreach ($groupSequence->groups as $groupInSequence) {
|
||
|
$groups = (array) $groupInSequence;
|
||
|
|
||
|
if ($metadata instanceof ClassMetadataInterface) {
|
||
|
$this->validateClassNode(
|
||
|
$value,
|
||
|
$cacheKey,
|
||
|
$metadata,
|
||
|
$propertyPath,
|
||
|
$groups,
|
||
|
$cascadedGroups,
|
||
|
$traversalStrategy,
|
||
|
$context
|
||
|
);
|
||
|
} else {
|
||
|
$this->validateGenericNode(
|
||
|
$value,
|
||
|
$object,
|
||
|
$cacheKey,
|
||
|
$metadata,
|
||
|
$propertyPath,
|
||
|
$groups,
|
||
|
$cascadedGroups,
|
||
|
$traversalStrategy,
|
||
|
$context
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// Abort sequence validation if a violation was generated
|
||
|
if (\count($context->getViolations()) > $violationCount) {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Validates a node's value against all constraints in the given group.
|
||
|
*/
|
||
|
private function validateInGroup(mixed $value, ?string $cacheKey, MetadataInterface $metadata, string $group, ExecutionContextInterface $context): void
|
||
|
{
|
||
|
$context->setGroup($group);
|
||
|
|
||
|
foreach ($metadata->findConstraints($group) as $constraint) {
|
||
|
if ($constraint instanceof Existence) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Prevent duplicate validation of constraints, in the case
|
||
|
// that constraints belong to multiple validated groups
|
||
|
if (null !== $cacheKey) {
|
||
|
$constraintHash = $this->generateCacheKey($constraint, true);
|
||
|
// instanceof Valid: In case of using a Valid constraint with many groups
|
||
|
// it makes a reference object get validated by each group
|
||
|
if ($constraint instanceof Composite || $constraint instanceof Valid) {
|
||
|
$constraintHash .= $group;
|
||
|
}
|
||
|
|
||
|
if ($context->isConstraintValidated($cacheKey, $constraintHash)) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
$context->markConstraintAsValidated($cacheKey, $constraintHash);
|
||
|
}
|
||
|
|
||
|
$context->setConstraint($constraint);
|
||
|
|
||
|
$validator = $this->validatorFactory->getInstance($constraint);
|
||
|
$validator->initialize($context);
|
||
|
|
||
|
if ($value instanceof LazyProperty) {
|
||
|
$value = $value->getPropertyValue();
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
$validator->validate($value, $constraint);
|
||
|
} catch (UnexpectedValueException $e) {
|
||
|
$context->buildViolation('This value should be of type {{ type }}.')
|
||
|
->setParameter('{{ type }}', $e->getExpectedType())
|
||
|
->addViolation();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private function generateCacheKey(object $object, bool $dependsOnPropertyPath = false): string
|
||
|
{
|
||
|
if ($this->context instanceof ExecutionContext) {
|
||
|
$cacheKey = $this->context->generateCacheKey($object);
|
||
|
} else {
|
||
|
$cacheKey = spl_object_hash($object);
|
||
|
}
|
||
|
|
||
|
if ($dependsOnPropertyPath) {
|
||
|
$cacheKey .= $this->context->getPropertyPath();
|
||
|
}
|
||
|
|
||
|
return $cacheKey;
|
||
|
}
|
||
|
}
|