Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\Validator\Constraints; Chris@0: Chris@0: use Symfony\Component\Validator\Constraint; Chris@0: use Symfony\Component\Validator\Exception\ConstraintDefinitionException; Chris@0: Chris@0: /** Chris@0: * A constraint that is composed of other constraints. Chris@0: * Chris@0: * You should never use the nested constraint instances anywhere else, because Chris@0: * their groups are adapted when passed to the constructor of this class. Chris@0: * Chris@0: * If you want to create your own composite constraint, extend this class and Chris@0: * let {@link getCompositeOption()} return the name of the property which Chris@0: * contains the nested constraints. Chris@0: * Chris@0: * @author Bernhard Schussek Chris@0: */ Chris@0: abstract class Composite extends Constraint Chris@0: { Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: * Chris@0: * The groups of the composite and its nested constraints are made Chris@0: * consistent using the following strategy: Chris@0: * Chris@0: * - If groups are passed explicitly to the composite constraint, but Chris@0: * not to the nested constraints, the options of the composite Chris@0: * constraint are copied to the nested constraints; Chris@0: * Chris@0: * - If groups are passed explicitly to the nested constraints, but not Chris@0: * to the composite constraint, the groups of all nested constraints Chris@0: * are merged and used as groups for the composite constraint; Chris@0: * Chris@0: * - If groups are passed explicitly to both the composite and its nested Chris@0: * constraints, the groups of the nested constraints must be a subset Chris@0: * of the groups of the composite constraint. If not, a Chris@0: * {@link ConstraintDefinitionException} is thrown. Chris@0: * Chris@0: * All this is done in the constructor, because constraints can then be Chris@0: * cached. When constraints are loaded from the cache, no more group Chris@0: * checks need to be done. Chris@0: */ Chris@0: public function __construct($options = null) Chris@0: { Chris@0: parent::__construct($options); Chris@0: Chris@0: $this->initializeNestedConstraints(); Chris@0: Chris@0: /* @var Constraint[] $nestedConstraints */ Chris@0: $compositeOption = $this->getCompositeOption(); Chris@0: $nestedConstraints = $this->$compositeOption; Chris@0: Chris@17: if (!\is_array($nestedConstraints)) { Chris@17: $nestedConstraints = [$nestedConstraints]; Chris@0: } Chris@0: Chris@0: foreach ($nestedConstraints as $constraint) { Chris@0: if (!$constraint instanceof Constraint) { Chris@17: if (\is_object($constraint)) { Chris@17: $constraint = \get_class($constraint); Chris@0: } Chris@0: Chris@17: throw new ConstraintDefinitionException(sprintf('The value %s is not an instance of Constraint in constraint %s', $constraint, \get_class($this))); Chris@0: } Chris@0: Chris@0: if ($constraint instanceof Valid) { Chris@17: throw new ConstraintDefinitionException(sprintf('The constraint Valid cannot be nested inside constraint %s. You can only declare the Valid constraint directly on a field or method.', \get_class($this))); Chris@0: } Chris@0: } Chris@0: Chris@0: if (!property_exists($this, 'groups')) { Chris@17: $mergedGroups = []; Chris@0: Chris@0: foreach ($nestedConstraints as $constraint) { Chris@0: foreach ($constraint->groups as $group) { Chris@0: $mergedGroups[$group] = true; Chris@0: } Chris@0: } Chris@0: Chris@0: $this->groups = array_keys($mergedGroups); Chris@0: $this->$compositeOption = $nestedConstraints; Chris@0: Chris@0: return; Chris@0: } Chris@0: Chris@0: foreach ($nestedConstraints as $constraint) { Chris@0: if (property_exists($constraint, 'groups')) { Chris@0: $excessGroups = array_diff($constraint->groups, $this->groups); Chris@0: Chris@17: if (\count($excessGroups) > 0) { Chris@17: throw new ConstraintDefinitionException(sprintf('The group(s) "%s" passed to the constraint %s should also be passed to its containing constraint %s', implode('", "', $excessGroups), \get_class($constraint), \get_class($this))); Chris@0: } Chris@0: } else { Chris@0: $constraint->groups = $this->groups; Chris@0: } Chris@0: } Chris@0: Chris@0: $this->$compositeOption = $nestedConstraints; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: * Chris@0: * Implicit group names are forwarded to nested constraints. Chris@0: * Chris@0: * @param string $group Chris@0: */ Chris@0: public function addImplicitGroupName($group) Chris@0: { Chris@0: parent::addImplicitGroupName($group); Chris@0: Chris@0: /** @var Constraint[] $nestedConstraints */ Chris@0: $nestedConstraints = $this->{$this->getCompositeOption()}; Chris@0: Chris@0: foreach ($nestedConstraints as $constraint) { Chris@0: $constraint->addImplicitGroupName($group); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the name of the property that contains the nested constraints. Chris@0: * Chris@0: * @return string The property name Chris@0: */ Chris@0: abstract protected function getCompositeOption(); Chris@0: Chris@0: /** Chris@0: * Initializes the nested constraints. Chris@0: * Chris@0: * This method can be overwritten in subclasses to clean up the nested Chris@0: * constraints passed to the constructor. Chris@0: * Chris@0: * @see Collection::initializeNestedConstraints() Chris@0: */ Chris@0: protected function initializeNestedConstraints() Chris@0: { Chris@0: } Chris@0: }