diff --git a/UPGRADE-2.1.md b/UPGRADE-2.1.md index 3e784c7a3b89..a919af25e8d7 100644 --- a/UPGRADE-2.1.md +++ b/UPGRADE-2.1.md @@ -539,6 +539,26 @@ $form->getConfig()->getErrorBubbling(); ``` + * The option "validation_constraint" is deprecated and will be removed + in Symfony 2.3. You should use the option "constraints" instead, + where you can pass one or more constraints for a form. + + Before: + + ``` + $builder->add('name', 'text', array( + 'validation_constraint' => new NotBlank(), + )); + ``` + + After (if the address object is an array): + + ``` + $builder->add('name', 'text', array( + 'constraints' => new NotBlank(), + )); + ``` + ### Validator * The methods `setMessage()`, `getMessageTemplate()` and diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml index 57f6f42ca8da..d3efd4674104 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml @@ -133,9 +133,12 @@ - + + + + diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index f2477a47c29e..db8efd37242a 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -83,3 +83,5 @@ CHANGELOG * `getErrorBubbling` * `getNormTransformers` * `getClientTransformers` + * deprecated the option "validation_constraint" in favor of the new + option "constraints" diff --git a/src/Symfony/Component/Form/Extension/Core/EventListener/ValidationListener.php b/src/Symfony/Component/Form/Extension/Core/EventListener/ValidationListener.php deleted file mode 100644 index 389328cfbf13..000000000000 --- a/src/Symfony/Component/Form/Extension/Core/EventListener/ValidationListener.php +++ /dev/null @@ -1,68 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Form\Extension\Core\EventListener; - -use Symfony\Component\Form\FormError; -use Symfony\Component\Form\FormEvents; -use Symfony\Component\Form\Event\DataEvent; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; - -/** - * @author Bernhard Schussek - */ -class ValidationListener implements EventSubscriberInterface -{ - /** - * {@inheritdoc} - */ - static public function getSubscribedEvents() - { - return array(FormEvents::POST_BIND => 'validateForm'); - } - - public function validateForm(DataEvent $event) - { - $form = $event->getForm(); - - if (!$form->isSynchronized()) { - $form->addError(new FormError( - $form->getAttribute('invalid_message'), - $form->getAttribute('invalid_message_parameters') - )); - } - - if (count($form->getExtraData()) > 0) { - $form->addError(new FormError('This form should not contain extra fields.')); - } - - if ($form->isRoot() && isset($_SERVER['CONTENT_LENGTH'])) { - $length = (int) $_SERVER['CONTENT_LENGTH']; - $max = trim(ini_get('post_max_size')); - - if ('' !== $max) { - switch (strtolower(substr($max, -1))) { - // The 'G' modifier is available since PHP 5.1.0 - case 'g': - $max *= 1024; - case 'm': - $max *= 1024; - case 'k': - $max *= 1024; - } - - if ($length > $max) { - $form->addError(new FormError('The uploaded file was too large. Please try to upload a smaller file')); - } - } - } - } -} diff --git a/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php b/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php index d3d37fa44280..820c6cbbccaa 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/DateTimeType.php @@ -51,8 +51,6 @@ public function buildForm(FormBuilder $builder, array $options) 'days', 'empty_value', 'required', - 'invalid_message', - 'invalid_message_parameters', 'translation_domain', ))); $timeOptions = array_intersect_key($options, array_flip(array( @@ -62,8 +60,6 @@ public function buildForm(FormBuilder $builder, array $options) 'with_seconds', 'empty_value', 'required', - 'invalid_message', - 'invalid_message_parameters', 'translation_domain', ))); diff --git a/src/Symfony/Component/Form/Extension/Core/Type/FormType.php b/src/Symfony/Component/Form/Extension/Core/Type/FormType.php index 03269184fc52..44ae6dc7fbbc 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/FormType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/FormType.php @@ -18,7 +18,6 @@ use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormView; use Symfony\Component\Form\Extension\Core\EventListener\TrimListener; -use Symfony\Component\Form\Extension\Core\EventListener\ValidationListener; use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\Form\Exception\FormException; @@ -50,19 +49,15 @@ public function buildForm(FormBuilder $builder, array $options) ->setVirtual($options['virtual']) ->setAttribute('read_only', $options['read_only']) ->setAttribute('by_reference', $options['by_reference']) - ->setAttribute('error_mapping', $options['error_mapping']) ->setAttribute('max_length', $options['max_length']) ->setAttribute('pattern', $options['pattern']) ->setAttribute('label', $options['label'] ?: $this->humanize($builder->getName())) ->setAttribute('attr', $options['attr']) ->setAttribute('label_attr', $options['label_attr']) - ->setAttribute('invalid_message', $options['invalid_message']) - ->setAttribute('invalid_message_parameters', $options['invalid_message_parameters']) ->setAttribute('translation_domain', $options['translation_domain']) ->setAttribute('single_control', $options['single_control']) ->setData($options['data']) ->setDataMapper(new PropertyPathMapper()) - ->addEventSubscriber(new ValidationListener()) ; if ($options['trim']) { @@ -197,27 +192,24 @@ public function getDefaultOptions() }; return array( - 'data' => null, - 'data_class' => $dataClass, - 'empty_data' => $emptyData, - 'trim' => true, - 'required' => true, - 'read_only' => false, - 'disabled' => false, - 'max_length' => null, - 'pattern' => null, - 'property_path' => null, - 'mapped' => $mapped, - 'by_reference' => true, - 'error_bubbling' => $errorBubbling, - 'error_mapping' => array(), - 'label' => null, - 'attr' => array(), - 'label_attr' => array(), - 'virtual' => false, - 'single_control' => false, - 'invalid_message' => 'This value is not valid.', - 'invalid_message_parameters' => array(), + 'data' => null, + 'data_class' => $dataClass, + 'empty_data' => $emptyData, + 'trim' => true, + 'required' => true, + 'read_only' => false, + 'disabled' => false, + 'max_length' => null, + 'pattern' => null, + 'property_path' => null, + 'mapped' => $mapped, + 'by_reference' => true, + 'error_bubbling' => $errorBubbling, + 'label' => null, + 'attr' => array(), + 'label_attr' => array(), + 'virtual' => false, + 'single_control' => false, 'translation_domain' => 'messages', ); } diff --git a/src/Symfony/Component/Form/Extension/Core/Type/RepeatedType.php b/src/Symfony/Component/Form/Extension/Core/Type/RepeatedType.php index 621ee713bdfe..fe48eed52c22 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/RepeatedType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/RepeatedType.php @@ -42,11 +42,6 @@ public function buildForm(FormBuilder $builder, array $options) */ public function getDefaultOptions() { - // Map errors to the first field - $errorMapping = function (Options $options) { - return array('.' => $options['first_name']); - }; - return array( 'type' => 'text', 'options' => array(), @@ -55,7 +50,6 @@ public function getDefaultOptions() 'first_name' => 'first', 'second_name' => 'second', 'error_bubbling' => false, - 'error_mapping' => $errorMapping, ); } diff --git a/src/Symfony/Component/Form/Extension/Validator/Constraints/Form.php b/src/Symfony/Component/Form/Extension/Validator/Constraints/Form.php new file mode 100644 index 000000000000..87e332976553 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Validator/Constraints/Form.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Validator\Constraints; + +use Symfony\Component\Validator\Constraint; + +/** + * @author Bernhard Schussek + */ +class Form extends Constraint +{ + /** + * Violation code marking an invalid form. + */ + const ERR_INVALID = 1; + + /** + * {@inheritdoc} + */ + public function getTargets() + { + return self::CLASS_CONSTRAINT; + } +} diff --git a/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php b/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php new file mode 100644 index 000000000000..978c76326a47 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Validator/Constraints/FormValidator.php @@ -0,0 +1,208 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Validator\Constraints; + +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\Extension\Validator\Util\ServerParams; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; + +/** + * @author Bernhard Schussek + */ +class FormValidator extends ConstraintValidator +{ + /** + * @var ServerParams + */ + private $serverParams; + + /** + * Creates a validator with the given server parameters. + * + * @param ServerParams $params The server parameters. Default + * parameters are created if null. + */ + public function __construct(ServerParams $params = null) + { + if (null === $params) { + $params = new ServerParams(); + } + + $this->serverParams = $params; + } + + /** + * {@inheritdoc} + */ + public function validate($form, Constraint $constraint) + { + if (!$form instanceof FormInterface) { + return; + } + + /* @var FormInterface $form */ + + $path = $this->context->getPropertyPath(); + $graphWalker = $this->context->getGraphWalker(); + $groups = $this->getValidationGroups($form); + + if (!empty($path)) { + $path .= '.'; + } + + if ($form->isSynchronized()) { + // Validate the form data only if transformation succeeded + + // Validate the data against its own constraints + if (self::allowDataWalking($form)) { + foreach ($groups as $group) { + $graphWalker->walkReference($form->getData(), $group, $path . 'data', true); + } + } + + // Validate the data against the constraints defined + // in the form + $constraints = $form->getAttribute('constraints'); + foreach ($constraints as $constraint) { + foreach ($groups as $group) { + $graphWalker->walkConstraint($constraint, $form->getData(), $group, $path . 'data'); + } + } + } else { + // Mark the form with an error if it is not synchronized + $this->context->addViolation( + $form->getAttribute('invalid_message'), + array('{{ value }}' => (string) $form->getClientData()), + $form->getClientData(), + null, + Form::ERR_INVALID + ); + } + + // Mark the form with an error if it contains extra fields + if (count($form->getExtraData()) > 0) { + $this->context->addViolation( + $form->getAttribute('extra_fields_message'), + array('{{ extra_fields }}' => implode('", "', array_keys($form->getExtraData()))), + $form->getExtraData() + ); + } + + // Mark the form with an error if the uploaded size was too large + $length = $this->serverParams->getContentLength(); + + if ($form->isRoot() && null !== $length) { + $max = strtoupper(trim($this->serverParams->getPostMaxSize())); + + if ('' !== $max) { + $maxLength = (int) $max; + + switch (substr($max, -1)) { + // The 'G' modifier is available since PHP 5.1.0 + case 'G': + $maxLength *= pow(1024, 3); + break; + case 'M': + $maxLength *= pow(1024, 2); + break; + case 'K': + $maxLength *= 1024; + break; + } + + if ($length > $maxLength) { + $this->context->addViolation( + $form->getAttribute('post_max_size_message'), + array('{{ max }}' => $max), + $length + ); + } + } + } + } + + /** + * Returns whether the data of a form may be walked. + * + * @param FormInterface $form The form to test. + * + * @return Boolean Whether the graph walker may walk the data. + */ + private function allowDataWalking(FormInterface $form) + { + $data = $form->getData(); + + // Scalar values cannot have mapped constraints + if (!is_object($data) && !is_array($data)) { + return false; + } + + // Root forms are always validated + if ($form->isRoot()) { + return true; + } + + // Non-root forms are validated if validation cascading + // is enabled in all ancestor forms + $parent = $form->getParent(); + + while (null !== $parent) { + if (!$parent->getAttribute('cascade_validation')) { + return false; + } + + $parent = $parent->getParent(); + } + + return true; + } + + /** + * Returns the validation groups of the given form. + * + * @param FormInterface $form The form. + * + * @return array The validation groups. + */ + private function getValidationGroups(FormInterface $form) + { + $groups = null; + + if ($form->hasAttribute('validation_groups')) { + $groups = $form->getAttribute('validation_groups'); + + if (is_callable($groups)) { + $groups = (array) call_user_func($groups, $form); + } + } + + $currentForm = $form; + while (!$groups && $currentForm->hasParent()) { + $currentForm = $currentForm->getParent(); + + if ($currentForm->hasAttribute('validation_groups')) { + $groups = $currentForm->getAttribute('validation_groups'); + + if (is_callable($groups)) { + $groups = (array) call_user_func($groups, $currentForm); + } + } + } + + if (null === $groups) { + $groups = array('Default'); + } + + return (array) $groups; + } +} diff --git a/src/Symfony/Component/Form/Extension/Validator/EventListener/DelegatingValidationListener.php b/src/Symfony/Component/Form/Extension/Validator/EventListener/DelegatingValidationListener.php deleted file mode 100644 index 362093d6f7e5..000000000000 --- a/src/Symfony/Component/Form/Extension/Validator/EventListener/DelegatingValidationListener.php +++ /dev/null @@ -1,154 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Form\Extension\Validator\EventListener; - -use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapper; -use Symfony\Component\Form\FormInterface; -use Symfony\Component\Form\FormError; -use Symfony\Component\Form\FormEvents; -use Symfony\Component\Form\Event\DataEvent; -use Symfony\Component\Form\Exception\FormException; -use Symfony\Component\Form\Util\PropertyPath; -use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\ValidatorInterface; -use Symfony\Component\Validator\ExecutionContext; - -/** - * @author Bernhard Schussek - */ -class DelegatingValidationListener implements EventSubscriberInterface -{ - private $validator; - - /** - * {@inheritdoc} - */ - static public function getSubscribedEvents() - { - return array(FormEvents::POST_BIND => 'validateForm'); - } - - /** - * Validates the data of a form - * - * This method is called automatically during the validation process. - * - * @param FormInterface $form The validated form - * @param ExecutionContext $context The current validation context - */ - static public function validateFormData(FormInterface $form, ExecutionContext $context) - { - if (is_object($form->getData()) || is_array($form->getData())) { - $propertyPath = $context->getPropertyPath(); - $graphWalker = $context->getGraphWalker(); - - // Adjust the property path accordingly - if (!empty($propertyPath)) { - $propertyPath .= '.'; - } - - $propertyPath .= 'data'; - - foreach (self::getFormValidationGroups($form) as $group) { - $graphWalker->walkReference($form->getData(), $group, $propertyPath, true); - } - } - } - - static public function validateFormChildren(FormInterface $form, ExecutionContext $context) - { - if ($form->getAttribute('cascade_validation')) { - $propertyPath = $context->getPropertyPath(); - $graphWalker = $context->getGraphWalker(); - - // Adjust the property path accordingly - if (!empty($propertyPath)) { - $propertyPath .= '.'; - } - - $propertyPath .= 'children'; - - $graphWalker->walkReference($form->getChildren(), Constraint::DEFAULT_GROUP, $propertyPath, true); - } - } - - static protected function getFormValidationGroups(FormInterface $form) - { - $groups = null; - - if ($form->hasAttribute('validation_groups')) { - $groups = $form->getAttribute('validation_groups'); - - if (is_callable($groups)) { - $groups = (array) call_user_func($groups, $form); - } - } - - $currentForm = $form; - while (!$groups && $currentForm->hasParent()) { - $currentForm = $currentForm->getParent(); - - if ($currentForm->hasAttribute('validation_groups')) { - $groups = $currentForm->getAttribute('validation_groups'); - - if (is_callable($groups)) { - $groups = (array) call_user_func($groups, $currentForm); - } - } - } - - if (null === $groups) { - $groups = array('Default'); - } - - return (array) $groups; - } - - public function __construct(ValidatorInterface $validator) - { - $this->validator = $validator; - } - - /** - * Validates the form and its domain object. - * - * @param DataEvent $event The event object - */ - public function validateForm(DataEvent $event) - { - $form = $event->getForm(); - - if ($form->isRoot()) { - // Validate the form in group "Default" - // Validation of the data in the custom group is done by validateData(), - // which is constrained by the Execute constraint - if ($form->hasAttribute('validation_constraint')) { - $violations = $this->validator->validateValue( - $form->getData(), - $form->getAttribute('validation_constraint'), - self::getFormValidationGroups($form) - ); - } else { - $violations = $this->validator->validate($form); - } - - if (count($violations) > 0) { - $mapper = new ViolationMapper(); - - foreach ($violations as $violation) { - $mapper->mapViolation($violation, $form); - } - } - } - } -} diff --git a/src/Symfony/Component/Form/Extension/Validator/EventListener/ValidationListener.php b/src/Symfony/Component/Form/Extension/Validator/EventListener/ValidationListener.php new file mode 100644 index 000000000000..89ff82902e4d --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Validator/EventListener/ValidationListener.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Validator\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Validator\ConstraintViolation; +use Symfony\Component\Form\Extension\Validator\Constraints\Form; +use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapperInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormError; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\Form\Event\DataEvent; +use Symfony\Component\Form\Exception\FormException; +use Symfony\Component\Form\Util\PropertyPath; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ValidatorInterface; +use Symfony\Component\Validator\ExecutionContext; + +/** + * @author Bernhard Schussek + */ +class ValidationListener implements EventSubscriberInterface +{ + private $validator; + + private $violationMapper; + + /** + * {@inheritdoc} + */ + static public function getSubscribedEvents() + { + return array(FormEvents::POST_BIND => 'validateForm'); + } + + public function __construct(ValidatorInterface $validator, ViolationMapperInterface $violationMapper) + { + $this->validator = $validator; + $this->violationMapper = $violationMapper; + } + + /** + * Validates the form and its domain object. + * + * @param DataEvent $event The event object + */ + public function validateForm(DataEvent $event) + { + $form = $event->getForm(); + + if ($form->isRoot()) { + // Validate the form in group "Default" + $violations = $this->validator->validate($form); + + if (count($violations) > 0) { + foreach ($violations as $violation) { + // Allow the "invalid" constraint to be put onto + // non-synchzronized forms + $allowNonSynchronized = Form::ERR_INVALID === $violation->getCode(); + + $this->violationMapper->mapViolation($violation, $form, $allowNonSynchronized); + } + } + } + } +} diff --git a/src/Symfony/Component/Form/Extension/Validator/Type/FormTypeValidatorExtension.php b/src/Symfony/Component/Form/Extension/Validator/Type/FormTypeValidatorExtension.php index 77754669f360..0135b943e802 100644 --- a/src/Symfony/Component/Form/Extension/Validator/Type/FormTypeValidatorExtension.php +++ b/src/Symfony/Component/Form/Extension/Validator/Type/FormTypeValidatorExtension.php @@ -13,21 +13,35 @@ use Symfony\Component\Form\AbstractTypeExtension; use Symfony\Component\Form\FormBuilder; -use Symfony\Component\Form\Extension\Validator\EventListener\DelegatingValidationListener; +use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapper; +use Symfony\Component\Form\Extension\Validator\EventListener\ValidationListener; use Symfony\Component\Validator\ValidatorInterface; +use Symfony\Component\OptionsResolver\Options; /** * @author Bernhard Schussek */ class FormTypeValidatorExtension extends AbstractTypeExtension { + /** + * @var ValidatorInterface + */ private $validator; + /** + * @var ViolationMapper + */ + private $violationMapper; + public function __construct(ValidatorInterface $validator) { $this->validator = $validator; + $this->violationMapper = new ViolationMapper(); } + /** + * {@inheritdoc} + */ public function buildForm(FormBuilder $builder, array $options) { if (empty($options['validation_groups'])) { @@ -38,23 +52,49 @@ public function buildForm(FormBuilder $builder, array $options) : (array) $options['validation_groups']; } + // Objects, when casted to an array, are split into their properties + $constraints = is_object($options['constraints']) + ? array($options['constraints']) + : (array) $options['constraints']; + $builder + ->setAttribute('error_mapping', $options['error_mapping']) ->setAttribute('validation_groups', $options['validation_groups']) - ->setAttribute('validation_constraint', $options['validation_constraint']) + ->setAttribute('constraints', $constraints) ->setAttribute('cascade_validation', $options['cascade_validation']) - ->addEventSubscriber(new DelegatingValidationListener($this->validator)) + ->setAttribute('invalid_message', $options['invalid_message']) + ->setAttribute('extra_fields_message', $options['extra_fields_message']) + ->setAttribute('post_max_size_message', $options['post_max_size_message']) + ->addEventSubscriber(new ValidationListener($this->validator, $this->violationMapper)) ; } + /** + * {@inheritdoc} + */ public function getDefaultOptions() { + // BC clause + $constraints = function (Options $options) { + return $options['validation_constraint']; + }; + return array( - 'validation_groups' => null, + 'error_mapping' => array(), + 'validation_groups' => null, + // "validation_constraint" is deprecated. Use "constraints". 'validation_constraint' => null, - 'cascade_validation' => false, + 'constraints' => $constraints, + 'cascade_validation' => false, + 'invalid_message' => 'This value is not valid.', + 'extra_fields_message' => 'This form should not contain extra fields.', + 'post_max_size_message' => 'The uploaded file was too large. Please try to upload a smaller file.', ); } + /** + * {@inheritdoc} + */ public function getExtendedType() { return 'form'; diff --git a/src/Symfony/Component/Form/Extension/Validator/Type/RepeatedTypeValidatorExtension.php b/src/Symfony/Component/Form/Extension/Validator/Type/RepeatedTypeValidatorExtension.php new file mode 100644 index 000000000000..3945c247b8f1 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Validator/Type/RepeatedTypeValidatorExtension.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Validator\Type; + +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\OptionsResolver\Options; + +/** + * @author Bernhard Schussek + */ +class RepeatedTypeValidatorExtension extends AbstractTypeExtension +{ + /** + * {@inheritdoc} + */ + public function getDefaultOptions() + { + // Map errors to the first field + $errorMapping = function (Options $options) { + return array('.' => $options['first_name']); + }; + + return array( + 'error_mapping' => $errorMapping, + ); + } + + /** + * {@inheritdoc} + */ + public function getExtendedType() + { + return 'repeated'; + } +} diff --git a/src/Symfony/Component/Form/Extension/Validator/Util/ServerParams.php b/src/Symfony/Component/Form/Extension/Validator/Util/ServerParams.php new file mode 100644 index 000000000000..58c8e2407e0f --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Validator/Util/ServerParams.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Validator\Util; + +/** + * @author Bernhard Schussek + */ +class ServerParams +{ + /** + * Returns the "post_max_size" ini setting. + * + * @return string The value of the ini setting. + */ + public function getPostMaxSize() + { + return ini_get('post_max_size'); + } + + /** + * Returns the content length of the request. + * + * @return mixed The request content length. + */ + public function getContentLength() + { + return isset($_SERVER['CONTENT_LENGTH']) + ? (int) $_SERVER['CONTENT_LENGTH'] + : null; + } + +} diff --git a/src/Symfony/Component/Form/Extension/Validator/ValidatorExtension.php b/src/Symfony/Component/Form/Extension/Validator/ValidatorExtension.php index 07459e2bd69d..4abd9b45f88e 100644 --- a/src/Symfony/Component/Form/Extension/Validator/ValidatorExtension.php +++ b/src/Symfony/Component/Form/Extension/Validator/ValidatorExtension.php @@ -12,9 +12,9 @@ namespace Symfony\Component\Form\Extension\Validator; use Symfony\Component\Form\Extension\Validator\Type; +use Symfony\Component\Form\Extension\Validator\Constraints\Form; use Symfony\Component\Form\AbstractExtension; use Symfony\Component\Validator\ValidatorInterface; -use Symfony\Component\Validator\Constraints\Callback; use Symfony\Component\Validator\Constraints\Valid; class ValidatorExtension extends AbstractExtension @@ -26,7 +26,7 @@ public function __construct(ValidatorInterface $validator) $this->validator = $validator; $metadata = $this->validator->getMetadataFactory()->getClassMetadata('Symfony\Component\Form\Form'); - $metadata->addConstraint(new Callback(array(array('Symfony\Component\Form\Extension\Validator\EventListener\DelegatingValidationListener', 'validateFormData')))); + $metadata->addConstraint(new Form()); $metadata->addPropertyConstraint('children', new Valid()); } @@ -39,6 +39,7 @@ protected function loadTypeExtensions() { return array( new Type\FormTypeValidatorExtension($this->validator), + new Type\RepeatedTypeValidatorExtension(), ); } } diff --git a/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php b/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php index 5e4778627ca8..736f53218433 100644 --- a/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php +++ b/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php @@ -23,7 +23,7 @@ /** * @author Bernhard Schussek */ -class ViolationMapper +class ViolationMapper implements ViolationMapperInterface { /** * @var FormInterface @@ -41,15 +41,17 @@ class ViolationMapper private $rules = array(); /** - * Maps a constraint violation to a form in the form tree under - * the given form. - * - * @param ConstraintViolation $violation The violation to map. - * @param FormInterface $form The root form of the tree - * to map it to. + * @var Boolean */ - public function mapViolation(ConstraintViolation $violation, FormInterface $form) + private $allowNonSynchronized; + + /** + * {@inheritdoc} + */ + public function mapViolation(ConstraintViolation $violation, FormInterface $form, $allowNonSynchronized = false) { + $this->allowNonSynchronized = $allowNonSynchronized; + $violationPath = new ViolationPath($violation->getPropertyPath()); $relativePath = $this->reconstructPath($violationPath, $form); $match = false; @@ -74,7 +76,7 @@ public function mapViolation(ConstraintViolation $violation, FormInterface $form $this->setScope($relativePath->getRoot()); $it = new PropertyPathIterator($relativePath); - while ($this->scope->isSynchronized() && null !== ($child = $this->matchChild($it))) { + while ($this->isValidScope() && null !== ($child = $this->matchChild($it))) { $this->setScope($child); $it->next(); $match = true; @@ -95,7 +97,7 @@ public function mapViolation(ConstraintViolation $violation, FormInterface $form // The overhead of setScope() is not needed anymore here $this->scope = $form; - while ($this->scope->isSynchronized() && $it->valid() && $it->mapsForm()) { + while ($this->isValidScope() && $it->valid() && $it->mapsForm()) { if (!$this->scope->has($it->current())) { // Break if we find a reference to a non-existing child break; @@ -109,14 +111,14 @@ public function mapViolation(ConstraintViolation $violation, FormInterface $form // Follow dot rules until we have the final target $mapping = $this->scope->getAttribute('error_mapping'); - while ($this->scope->isSynchronized() && isset($mapping['.'])) { + while ($this->isValidScope() && isset($mapping['.'])) { $dotRule = new MappingRule($this->scope, '.', $mapping['.']); $this->scope = $dotRule->getTarget(); $mapping = $this->scope->getAttribute('error_mapping'); } // Only add the error if the form is synchronized - if ($this->scope->isSynchronized()) { + if ($this->isValidScope()) { $this->scope->addError(new FormError( $violation->getMessageTemplate(), $violation->getMessageParameters(), @@ -286,4 +288,12 @@ private function setScope(FormInterface $form) } } } + + /** + * @return Boolean + */ + private function isValidScope() + { + return $this->allowNonSynchronized || $this->scope->isSynchronized(); + } } diff --git a/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapperInterface.php b/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapperInterface.php new file mode 100644 index 000000000000..49b7c61bb95b --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapperInterface.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Validator\ViolationMapper; + +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Validator\ConstraintViolation; + +/** + * @author Bernhard Schussek + */ +interface ViolationMapperInterface +{ + /** + * Maps a constraint violation to a form in the form tree under + * the given form. + * + * @param ConstraintViolation $violation The violation to map. + * @param FormInterface $form The root form of the tree + * to map it to. + * @param Boolean $allowNonSynchronized Whether to allow + * mapping to non-synchronized forms. + */ + function mapViolation(ConstraintViolation $violation, FormInterface $form, $allowNonSynchronized = false); +} diff --git a/src/Symfony/Component/Form/Resources/config/validation.xml b/src/Symfony/Component/Form/Resources/config/validation.xml index 494976c3ef20..2f2364bde489 100644 --- a/src/Symfony/Component/Form/Resources/config/validation.xml +++ b/src/Symfony/Component/Form/Resources/config/validation.xml @@ -5,15 +5,9 @@ xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> - - - Symfony\Component\Form\Extension\Validator\EventListener\DelegatingValidationListener - validateFormData - - - Symfony\Component\Form\Extension\Validator\EventListener\DelegatingValidationListener - validateFormChildren - - + + + + diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTimeTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTimeTypeTest.php index 960f272598d9..a075d55868a0 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTimeTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/DateTimeTypeTest.php @@ -226,32 +226,6 @@ public function testSubmit_differentPattern() $this->assertDateTimeEquals($dateTime, $form->getData()); } - public function testSubmit_invalidDateTime() - { - $form = $this->factory->create('datetime', null, array( - 'invalid_message' => 'Customized invalid message', - // Only possible with the "text" widget, because the "choice" - // widget automatically fields invalid values - 'widget' => 'text', - )); - - $form->bind(array( - 'date' => array( - 'day' => '31', - 'month' => '9', - 'year' => '2010', - ), - 'time' => array( - 'hour' => '25', - 'minute' => '4', - ), - )); - - $this->assertFalse($form->isValid()); - $this->assertEquals(array(new FormError('Customized invalid message', array())), $form['date']->getErrors()); - $this->assertEquals(array(new FormError('Customized invalid message', array())), $form['time']->getErrors()); - } - // Bug fix public function testInitializeWithDateTime() { diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php new file mode 100644 index 000000000000..209ebdf66d89 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php @@ -0,0 +1,694 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Extension\Validator\Constraints; + +use Symfony\Component\Form\FormBuilder; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\CallbackTransformer; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\Extension\Validator\Constraints\Form; +use Symfony\Component\Form\Extension\Validator\Constraints\FormValidator; +use Symfony\Component\Form\Util\PropertyPath; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\GlobalExecutionContext; +use Symfony\Component\Validator\ExecutionContext; + +/** + * @author Bernhard Schussek + */ +class FormValidatorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $dispatcher; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $factory; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $serverParams; + + /** + * @var FormValidator + */ + private $validator; + + protected function setUp() + { + if (!class_exists('Symfony\Component\EventDispatcher\Event')) { + $this->markTestSkipped('The "EventDispatcher" component is not available'); + } + + $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); + $this->factory = $this->getMock('Symfony\Component\Form\FormFactoryInterface'); + $this->serverParams = $this->getMock('Symfony\Component\Form\Extension\Validator\Util\ServerParams'); + $this->validator = new FormValidator($this->serverParams); + } + + public function testValidate() + { + $context = $this->getExecutionContext(); + $graphWalker = $context->getGraphWalker(); + $object = $this->getMock('\stdClass'); + $form = $this->getBuilder('name', '\stdClass') + ->setAttribute('validation_groups', array('group1', 'group2')) + ->setData($object) + ->getForm(); + + $graphWalker->expects($this->at(0)) + ->method('walkReference') + ->with($object, 'group1', 'data', true); + $graphWalker->expects($this->at(1)) + ->method('walkReference') + ->with($object, 'group2', 'data', true); + + $this->validator->initialize($context); + $this->validator->validate($form, new Form()); + } + + public function testValidateConstraints() + { + $context = $this->getExecutionContext(); + $graphWalker = $context->getGraphWalker(); + $object = $this->getMock('\stdClass'); + $constraint1 = $this->getMock('Symfony\Component\Validator\Constraint'); + $constraint2 = $this->getMock('Symfony\Component\Validator\Constraint'); + + $form = $this->getBuilder('name', '\stdClass') + ->setAttribute('validation_groups', array('group1', 'group2')) + ->setAttribute('constraints', array($constraint1, $constraint2)) + ->setData($object) + ->getForm(); + + // First default constraints + $graphWalker->expects($this->at(0)) + ->method('walkReference') + ->with($object, 'group1', 'data', true); + $graphWalker->expects($this->at(1)) + ->method('walkReference') + ->with($object, 'group2', 'data', true); + + // Then custom constraints + $graphWalker->expects($this->at(2)) + ->method('walkConstraint') + ->with($constraint1, $object, 'group1', 'data'); + $graphWalker->expects($this->at(3)) + ->method('walkConstraint') + ->with($constraint1, $object, 'group2', 'data'); + $graphWalker->expects($this->at(4)) + ->method('walkConstraint') + ->with($constraint2, $object, 'group1', 'data'); + $graphWalker->expects($this->at(5)) + ->method('walkConstraint') + ->with($constraint2, $object, 'group2', 'data'); + + $this->validator->initialize($context); + $this->validator->validate($form, new Form()); + } + + public function testDontValidateIfParentWithoutCascadeValidation() + { + $context = $this->getExecutionContext(); + $graphWalker = $context->getGraphWalker(); + $object = $this->getMock('\stdClass'); + + $parent = $this->getBuilder() + ->setAttribute('cascade_validation', false) + ->getForm(); + $form = $this->getBuilder('name', '\stdClass') + ->setAttribute('validation_groups', array('group1', 'group2')) + ->getForm(); + $parent->add($form); + + $form->setData($object); + + $graphWalker->expects($this->never()) + ->method('walkReference'); + + $this->validator->initialize($context); + $this->validator->validate($form, new Form()); + } + + public function testValidateConstraintsEvenIfNoCascadeValidation() + { + $context = $this->getExecutionContext(); + $graphWalker = $context->getGraphWalker(); + $object = $this->getMock('\stdClass'); + $constraint1 = $this->getMock('Symfony\Component\Validator\Constraint'); + $constraint2 = $this->getMock('Symfony\Component\Validator\Constraint'); + + $parent = $this->getBuilder() + ->setAttribute('cascade_validation', false) + ->getForm(); + $form = $this->getBuilder('name', '\stdClass') + ->setAttribute('validation_groups', array('group1', 'group2')) + ->setAttribute('constraints', array($constraint1, $constraint2)) + ->setData($object) + ->getForm(); + $parent->add($form); + + $graphWalker->expects($this->at(0)) + ->method('walkConstraint') + ->with($constraint1, $object, 'group1', 'data'); + $graphWalker->expects($this->at(1)) + ->method('walkConstraint') + ->with($constraint1, $object, 'group2', 'data'); + $graphWalker->expects($this->at(2)) + ->method('walkConstraint') + ->with($constraint2, $object, 'group1', 'data'); + $graphWalker->expects($this->at(3)) + ->method('walkConstraint') + ->with($constraint2, $object, 'group2', 'data'); + + $this->validator->initialize($context); + $this->validator->validate($form, new Form()); + } + + public function testDontValidateIfNotSynchronized() + { + $context = $this->getExecutionContext(); + $graphWalker = $context->getGraphWalker(); + $object = $this->getMock('\stdClass'); + + $form = $this->getBuilder('name', '\stdClass') + ->setData($object) + ->setAttribute('invalid_message', 'Invalid!') + ->appendClientTransformer(new CallbackTransformer( + function ($data) { return $data; }, + function () { throw new TransformationFailedException(); } + )) + ->getForm(); + + // Launch transformer + $form->bind(array()); + + $graphWalker->expects($this->never()) + ->method('walkReference'); + + $this->validator->initialize($context); + $this->validator->validate($form, new Form()); + + $this->assertCount(1, $context->getViolations()); + $this->assertEquals('Invalid!', $context->getViolations()->get(0)->getMessage()); + } + + public function testDontValidateConstraintsIfNotSynchronized() + { + $context = $this->getExecutionContext(); + $graphWalker = $context->getGraphWalker(); + $object = $this->getMock('\stdClass'); + $constraint1 = $this->getMock('Symfony\Component\Validator\Constraint'); + $constraint2 = $this->getMock('Symfony\Component\Validator\Constraint'); + + $form = $this->getBuilder('name', '\stdClass') + ->setData($object) + ->setAttribute('validation_groups', array('group1', 'group2')) + ->setAttribute('constraints', array($constraint1, $constraint2)) + ->appendClientTransformer(new CallbackTransformer( + function ($data) { return $data; }, + function () { throw new TransformationFailedException(); } + )) + ->getForm(); + + // Launch transformer + $form->bind(array()); + + $graphWalker->expects($this->never()) + ->method('walkReference'); + + $this->validator->initialize($context); + $this->validator->validate($form, new Form()); + } + + public function testHandleCallbackValidationGroups() + { + $context = $this->getExecutionContext(); + $graphWalker = $context->getGraphWalker(); + $object = $this->getMock('\stdClass'); + $form = $this->getBuilder('name', '\stdClass') + ->setAttribute('validation_groups', array($this, 'getValidationGroups')) + ->setData($object) + ->getForm(); + + $graphWalker->expects($this->at(0)) + ->method('walkReference') + ->with($object, 'group1', 'data', true); + $graphWalker->expects($this->at(1)) + ->method('walkReference') + ->with($object, 'group2', 'data', true); + + $this->validator->initialize($context); + $this->validator->validate($form, new Form()); + } + + public function testHandleClosureValidationGroups() + { + $context = $this->getExecutionContext(); + $graphWalker = $context->getGraphWalker(); + $object = $this->getMock('\stdClass'); + $form = $this->getBuilder('name', '\stdClass') + ->setAttribute('validation_groups', function(FormInterface $form){ + return array('group1', 'group2'); + }) + ->setData($object) + ->getForm(); + + $graphWalker->expects($this->at(0)) + ->method('walkReference') + ->with($object, 'group1', 'data', true); + $graphWalker->expects($this->at(1)) + ->method('walkReference') + ->with($object, 'group2', 'data', true); + + $this->validator->initialize($context); + $this->validator->validate($form, new Form()); + } + + public function testUseInheritedValidationGroup() + { + $context = $this->getExecutionContext('foo.bar'); + $graphWalker = $context->getGraphWalker(); + $object = $this->getMock('\stdClass'); + + $parent = $this->getBuilder() + ->setAttribute('validation_groups', 'group') + ->setAttribute('cascade_validation', true) + ->getForm(); + $form = $this->getBuilder('name', '\stdClass') + ->setAttribute('validation_groups', null) + ->getForm(); + $parent->add($form); + + $form->setData($object); + + $graphWalker->expects($this->once()) + ->method('walkReference') + ->with($object, 'group', 'foo.bar.data', true); + + $this->validator->initialize($context); + $this->validator->validate($form, new Form()); + } + + public function testUseInheritedCallbackValidationGroup() + { + $context = $this->getExecutionContext('foo.bar'); + $graphWalker = $context->getGraphWalker(); + $object = $this->getMock('\stdClass'); + + $parent = $this->getBuilder() + ->setAttribute('validation_groups', array($this, 'getValidationGroups')) + ->setAttribute('cascade_validation', true) + ->getForm(); + $form = $this->getBuilder('name', '\stdClass') + ->setAttribute('validation_groups', null) + ->getForm(); + $parent->add($form); + + $form->setData($object); + + $graphWalker->expects($this->at(0)) + ->method('walkReference') + ->with($object, 'group1', 'foo.bar.data', true); + $graphWalker->expects($this->at(1)) + ->method('walkReference') + ->with($object, 'group2', 'foo.bar.data', true); + + $this->validator->initialize($context); + $this->validator->validate($form, new Form()); + } + + public function testUseInheritedClosureValidationGroup() + { + $context = $this->getExecutionContext('foo.bar'); + $graphWalker = $context->getGraphWalker(); + $object = $this->getMock('\stdClass'); + + $parent = $this->getBuilder() + ->setAttribute('validation_groups', function(FormInterface $form){ + return array('group1', 'group2'); + }) + ->setAttribute('cascade_validation', true) + ->getForm(); + $form = $this->getBuilder('name', '\stdClass') + ->setAttribute('validation_groups', null) + ->getForm(); + $parent->add($form); + + $form->setData($object); + + $graphWalker->expects($this->at(0)) + ->method('walkReference') + ->with($object, 'group1', 'foo.bar.data', true); + $graphWalker->expects($this->at(1)) + ->method('walkReference') + ->with($object, 'group2', 'foo.bar.data', true); + + $this->validator->initialize($context); + $this->validator->validate($form, new Form()); + } + + public function testAppendPropertyPath() + { + $context = $this->getExecutionContext('foo.bar'); + $graphWalker = $context->getGraphWalker(); + $object = $this->getMock('\stdClass'); + $form = $this->getBuilder('name', '\stdClass') + ->setData($object) + ->getForm(); + + $graphWalker->expects($this->once()) + ->method('walkReference') + ->with($object, 'Default', 'foo.bar.data', true); + + $this->validator->initialize($context); + $this->validator->validate($form, new Form()); + } + + public function testDontWalkScalars() + { + $context = $this->getExecutionContext(); + $graphWalker = $context->getGraphWalker(); + + $form = $this->getBuilder() + ->setData('scalar') + ->getForm(); + + $graphWalker->expects($this->never()) + ->method('walkReference'); + + $this->validator->initialize($context); + $this->validator->validate($form, new Form()); + } + + public function testViolationIfExtraData() + { + $context = $this->getExecutionContext(); + + $form = $this->getBuilder() + ->add($this->getBuilder('child')) + ->setAttribute('extra_fields_message', 'Extra!') + ->getForm(); + + $form->bind(array('foo' => 'bar')); + + $this->validator->initialize($context); + $this->validator->validate($form, new Form()); + + $this->assertCount(1, $context->getViolations()); + $this->assertEquals('Extra!', $context->getViolations()->get(0)->getMessage()); + } + + public function testViolationIfPostMaxSizeExceeded_GigaUpper() + { + $this->serverParams->expects($this->any()) + ->method('getContentLength') + ->will($this->returnValue(pow(1024, 3) + 1)); + $this->serverParams->expects($this->any()) + ->method('getPostMaxSize') + ->will($this->returnValue('1G')); + + $context = $this->getExecutionContext(); + $form = $this->getBuilder() + ->setAttribute('post_max_size_message', 'Max {{ max }}!') + ->getForm(); + + $this->validator->initialize($context); + $this->validator->validate($form, new Form()); + + $this->assertCount(1, $context->getViolations()); + $this->assertEquals('Max 1G!', $context->getViolations()->get(0)->getMessage()); + } + + public function testViolationIfPostMaxSizeExceeded_GigaLower() + { + $this->serverParams->expects($this->any()) + ->method('getContentLength') + ->will($this->returnValue(pow(1024, 3) + 1)); + $this->serverParams->expects($this->any()) + ->method('getPostMaxSize') + ->will($this->returnValue('1g')); + + $context = $this->getExecutionContext(); + $form = $this->getBuilder() + ->setAttribute('post_max_size_message', 'Max {{ max }}!') + ->getForm(); + + $this->validator->initialize($context); + $this->validator->validate($form, new Form()); + + $this->assertCount(1, $context->getViolations()); + $this->assertEquals('Max 1G!', $context->getViolations()->get(0)->getMessage()); + } + + public function testNoViolationIfPostMaxSizeNotExceeded_Giga() + { + $this->serverParams->expects($this->any()) + ->method('getContentLength') + ->will($this->returnValue(pow(1024, 3))); + $this->serverParams->expects($this->any()) + ->method('getPostMaxSize') + ->will($this->returnValue('1G')); + + $context = $this->getExecutionContext(); + $form = $this->getForm(); + + $this->validator->initialize($context); + $this->validator->validate($form, new Form()); + + $this->assertCount(0, $context->getViolations()); + } + + public function testViolationIfPostMaxSizeExceeded_Mega() + { + $this->serverParams->expects($this->any()) + ->method('getContentLength') + ->will($this->returnValue(pow(1024, 2) + 1)); + $this->serverParams->expects($this->any()) + ->method('getPostMaxSize') + ->will($this->returnValue('1M')); + + $context = $this->getExecutionContext(); + $form = $this->getBuilder() + ->setAttribute('post_max_size_message', 'Max {{ max }}!') + ->getForm(); + + $this->validator->initialize($context); + $this->validator->validate($form, new Form()); + + $this->assertCount(1, $context->getViolations()); + $this->assertEquals('Max 1M!', $context->getViolations()->get(0)->getMessage()); + } + + public function testNoViolationIfPostMaxSizeNotExceeded_Mega() + { + $this->serverParams->expects($this->any()) + ->method('getContentLength') + ->will($this->returnValue(pow(1024, 2))); + $this->serverParams->expects($this->any()) + ->method('getPostMaxSize') + ->will($this->returnValue('1M')); + + $context = $this->getExecutionContext(); + $form = $this->getForm(); + + $this->validator->initialize($context); + $this->validator->validate($form, new Form()); + + $this->assertCount(0, $context->getViolations()); + } + + public function testViolationIfPostMaxSizeExceeded_Kilo() + { + $this->serverParams->expects($this->any()) + ->method('getContentLength') + ->will($this->returnValue(1025)); + $this->serverParams->expects($this->any()) + ->method('getPostMaxSize') + ->will($this->returnValue('1K')); + + $context = $this->getExecutionContext(); + $form = $this->getBuilder() + ->setAttribute('post_max_size_message', 'Max {{ max }}!') + ->getForm(); + + $this->validator->initialize($context); + $this->validator->validate($form, new Form()); + + $this->assertCount(1, $context->getViolations()); + $this->assertEquals('Max 1K!', $context->getViolations()->get(0)->getMessage()); + } + + public function testNoViolationIfPostMaxSizeNotExceeded_Kilo() + { + $this->serverParams->expects($this->any()) + ->method('getContentLength') + ->will($this->returnValue(1024)); + $this->serverParams->expects($this->any()) + ->method('getPostMaxSize') + ->will($this->returnValue('1K')); + + $context = $this->getExecutionContext(); + $form = $this->getForm(); + + $this->validator->initialize($context); + $this->validator->validate($form, new Form()); + + $this->assertCount(0, $context->getViolations()); + } + + public function testNoViolationIfNotRoot() + { + $this->serverParams->expects($this->any()) + ->method('getContentLength') + ->will($this->returnValue(1025)); + $this->serverParams->expects($this->any()) + ->method('getPostMaxSize') + ->will($this->returnValue('1K')); + + $context = $this->getExecutionContext(); + $parent = $this->getForm(); + $form = $this->getForm(); + $parent->add($form); + + $this->validator->initialize($context); + $this->validator->validate($form, new Form()); + + $this->assertCount(0, $context->getViolations()); + } + + public function testNoViolationIfContentLengthNull() + { + $this->serverParams->expects($this->any()) + ->method('getContentLength') + ->will($this->returnValue(null)); + $this->serverParams->expects($this->any()) + ->method('getPostMaxSize') + ->will($this->returnValue('1K')); + + $context = $this->getExecutionContext(); + $form = $this->getForm(); + + $this->validator->initialize($context); + $this->validator->validate($form, new Form()); + + $this->assertCount(0, $context->getViolations()); + } + + public function testTrimPostMaxSize() + { + $this->serverParams->expects($this->any()) + ->method('getContentLength') + ->will($this->returnValue(1025)); + $this->serverParams->expects($this->any()) + ->method('getPostMaxSize') + ->will($this->returnValue(' 1K ')); + + $context = $this->getExecutionContext(); + $form = $this->getBuilder() + ->setAttribute('post_max_size_message', 'Max {{ max }}!') + ->getForm(); + + $this->validator->initialize($context); + $this->validator->validate($form, new Form()); + + $this->assertCount(1, $context->getViolations()); + $this->assertEquals('Max 1K!', $context->getViolations()->get(0)->getMessage()); + } + + public function testNoViolationIfPostMaxSizeEmpty() + { + $this->serverParams->expects($this->any()) + ->method('getContentLength') + ->will($this->returnValue(1025)); + $this->serverParams->expects($this->any()) + ->method('getPostMaxSize') + ->will($this->returnValue(' ')); + + $context = $this->getExecutionContext(); + $form = $this->getForm(); + + $this->validator->initialize($context); + $this->validator->validate($form, new Form()); + + $this->assertCount(0, $context->getViolations()); + } + + public function testNoViolationIfPostMaxSizeNull() + { + $this->serverParams->expects($this->any()) + ->method('getContentLength') + ->will($this->returnValue(1025)); + $this->serverParams->expects($this->any()) + ->method('getPostMaxSize') + ->will($this->returnValue(null)); + + $context = $this->getExecutionContext(); + $form = $this->getForm(); + + $this->validator->initialize($context); + $this->validator->validate($form, new Form()); + + $this->assertCount(0, $context->getViolations()); + } + + /** + * Access has to be public, as this method is called via callback array + * in {@link testValidateFormDataCanHandleCallbackValidationGroups()} + * and {@link testValidateFormDataUsesInheritedCallbackValidationGroup()} + */ + public function getValidationGroups(FormInterface $form) + { + return array('group1', 'group2'); + } + + private function getMockGraphWalker() + { + return $this->getMockBuilder('Symfony\Component\Validator\GraphWalker') + ->disableOriginalConstructor() + ->getMock(); + } + + private function getMockMetadataFactory() + { + return $this->getMock('Symfony\Component\Validator\Mapping\ClassMetadataFactoryInterface'); + } + + private function getExecutionContext($propertyPath = null) + { + $graphWalker = $this->getMockGraphWalker(); + $metadataFactory = $this->getMockMetadataFactory(); + $globalContext = new GlobalExecutionContext('Root', $graphWalker, $metadataFactory); + + return new ExecutionContext($globalContext, null, $propertyPath, null, null, null); + } + + /** + * @return FormBuilder + */ + private function getBuilder($name = 'name', $dataClass = null) + { + $builder = new FormBuilder($name, $dataClass, $this->dispatcher, $this->factory); + $builder->setAttribute('constraints', array()); + + return $builder; + } + + private function getForm($name = 'name', $dataClass = null) + { + return $this->getBuilder($name, $dataClass)->getForm(); + } +} diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/EventListener/DelegatingValidationListenerTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/EventListener/DelegatingValidationListenerTest.php deleted file mode 100644 index 806eca0f41ce..000000000000 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/EventListener/DelegatingValidationListenerTest.php +++ /dev/null @@ -1,417 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Form\Tests\Extension\Validator\EventListener; - -use Symfony\Component\Form\Event\DataEvent; -use Symfony\Component\Form\FormInterface; -use Symfony\Component\Form\FormBuilder; -use Symfony\Component\Form\FormError; -use Symfony\Component\Form\Util\PropertyPath; -use Symfony\Component\Form\Extension\Validator\EventListener\DelegatingValidationListener; -use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\ConstraintViolation; -use Symfony\Component\Validator\GlobalExecutionContext; -use Symfony\Component\Validator\ExecutionContext; - -class DelegatingValidationListenerTest extends \PHPUnit_Framework_TestCase -{ - private $dispatcher; - - private $factory; - - private $builder; - - private $delegate; - - private $listener; - - private $message; - - private $params; - - protected function setUp() - { - if (!class_exists('Symfony\Component\EventDispatcher\Event')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } - - $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); - $this->factory = $this->getMock('Symfony\Component\Form\FormFactoryInterface'); - $this->delegate = $this->getMock('Symfony\Component\Validator\ValidatorInterface'); - $this->listener = new DelegatingValidationListener($this->delegate); - $this->message = 'Message'; - $this->params = array('foo' => 'bar'); - } - - protected function getMockGraphWalker() - { - return $this->getMockBuilder('Symfony\Component\Validator\GraphWalker') - ->disableOriginalConstructor() - ->getMock(); - } - - protected function getMockMetadataFactory() - { - return $this->getMock('Symfony\Component\Validator\Mapping\ClassMetadataFactoryInterface'); - } - - protected function getMockTransformer() - { - return $this->getMock('Symfony\Component\Form\DataTransformerInterface', array(), array(), '', false, false); - } - - protected function getExecutionContext($propertyPath = null) - { - $graphWalker = $this->getMockGraphWalker(); - $metadataFactory = $this->getMockMetadataFactory(); - $globalContext = new GlobalExecutionContext('Root', $graphWalker, $metadataFactory); - - return new ExecutionContext($globalContext, null, $propertyPath, null, null, null); - } - - protected function getConstraintViolation($propertyPath) - { - return new ConstraintViolation($this->message, $this->params, null, $propertyPath, null); - } - - protected function getFormError() - { - return new FormError($this->message, $this->params); - } - - protected function getBuilder($name = 'name', $propertyPath = null, $dataClass = null) - { - $builder = new FormBuilder($name, $dataClass, $this->dispatcher, $this->factory); - $builder->setPropertyPath(new PropertyPath($propertyPath ?: $name)); - $builder->setAttribute('error_mapping', array()); - $builder->setErrorBubbling(false); - $builder->setMapped(true); - - return $builder; - } - - protected function getForm($name = 'name', $propertyPath = null, $dataClass = null) - { - return $this->getBuilder($name, $propertyPath, $dataClass)->getForm(); - } - - protected function getMockForm() - { - return $this->getMock('Symfony\Component\Form\Tests\FormInterface'); - } - - /** - * Access has to be public, as this method is called via callback array - * in {@link testValidateFormDataCanHandleCallbackValidationGroups()} - * and {@link testValidateFormDataUsesInheritedCallbackValidationGroup()} - */ - public function getValidationGroups(FormInterface $form) - { - return array('group1', 'group2'); - } - - public function testUseValidateValueWhenValidationConstraintExist() - { - $constraint = $this->getMockForAbstractClass('Symfony\Component\Validator\Constraint'); - $form = $this - ->getBuilder('name') - ->setAttribute('validation_constraint', $constraint) - ->getForm(); - - $this->delegate->expects($this->once())->method('validateValue'); - - $this->listener->validateForm(new DataEvent($form, null)); - } - - // More specific mapping tests can be found in ViolationMapperTest - public function testFormErrorMapping() - { - $parent = $this->getForm(); - $child = $this->getForm('street'); - - $parent->add($child); - - $this->delegate->expects($this->once()) - ->method('validate') - ->will($this->returnValue(array( - $this->getConstraintViolation('children[street].data.constrainedProp') - ))); - - $this->listener->validateForm(new DataEvent($parent, null)); - - $this->assertFalse($parent->hasErrors()); - $this->assertEquals(array($this->getFormError()), $child->getErrors()); - } - - // More specific mapping tests can be found in ViolationMapperTest - public function testDataErrorMapping() - { - $parent = $this->getForm(); - $child = $this->getForm('firstName'); - - $parent->add($child); - - $this->delegate->expects($this->once()) - ->method('validate') - ->will($this->returnValue(array( - $this->getConstraintViolation('data.firstName.constrainedProp') - ))); - - $this->listener->validateForm(new DataEvent($parent, null)); - - $this->assertFalse($parent->hasErrors()); - $this->assertEquals(array($this->getFormError()), $child->getErrors()); - } - - public function testValidateFormData() - { - $context = $this->getExecutionContext(); - $graphWalker = $context->getGraphWalker(); - $object = $this->getMock('\stdClass'); - $form = $this->getBuilder('name', null, '\stdClass') - ->setAttribute('validation_groups', array('group1', 'group2')) - ->getForm(); - - $graphWalker->expects($this->at(0)) - ->method('walkReference') - ->with($object, 'group1', 'data', true); - $graphWalker->expects($this->at(1)) - ->method('walkReference') - ->with($object, 'group2', 'data', true); - - $form->setData($object); - - DelegatingValidationListener::validateFormData($form, $context); - } - - public function testValidateFormDataCanHandleCallbackValidationGroups() - { - $context = $this->getExecutionContext(); - $graphWalker = $context->getGraphWalker(); - $object = $this->getMock('\stdClass'); - $form = $this->getBuilder('name', null, '\stdClass') - ->setAttribute('validation_groups', array($this, 'getValidationGroups')) - ->getForm(); - - $graphWalker->expects($this->at(0)) - ->method('walkReference') - ->with($object, 'group1', 'data', true); - $graphWalker->expects($this->at(1)) - ->method('walkReference') - ->with($object, 'group2', 'data', true); - - $form->setData($object); - - DelegatingValidationListener::validateFormData($form, $context); - } - - public function testValidateFormDataCanHandleClosureValidationGroups() - { - $context = $this->getExecutionContext(); - $graphWalker = $context->getGraphWalker(); - $object = $this->getMock('\stdClass'); - $form = $this->getBuilder('name', null, '\stdClass') - ->setAttribute('validation_groups', function(FormInterface $form){ - return array('group1', 'group2'); - }) - ->getForm(); - - $graphWalker->expects($this->at(0)) - ->method('walkReference') - ->with($object, 'group1', 'data', true); - $graphWalker->expects($this->at(1)) - ->method('walkReference') - ->with($object, 'group2', 'data', true); - - $form->setData($object); - - DelegatingValidationListener::validateFormData($form, $context); - } - - public function testValidateFormDataUsesInheritedValidationGroup() - { - $context = $this->getExecutionContext('foo.bar'); - $graphWalker = $context->getGraphWalker(); - $object = $this->getMock('\stdClass'); - - $parent = $this->getBuilder() - ->setAttribute('validation_groups', 'group') - ->getForm(); - $child = $this->getBuilder('name', null, '\stdClass') - ->setAttribute('validation_groups', null) - ->getForm(); - $parent->add($child); - - $child->setData($object); - - $graphWalker->expects($this->once()) - ->method('walkReference') - ->with($object, 'group', 'foo.bar.data', true); - - DelegatingValidationListener::validateFormData($child, $context); - } - - public function testValidateFormDataUsesInheritedCallbackValidationGroup() - { - $context = $this->getExecutionContext('foo.bar'); - $graphWalker = $context->getGraphWalker(); - $object = $this->getMock('\stdClass'); - - $parent = $this->getBuilder() - ->setAttribute('validation_groups', array($this, 'getValidationGroups')) - ->getForm(); - $child = $this->getBuilder('name', null, '\stdClass') - ->setAttribute('validation_groups', null) - ->getForm(); - $parent->add($child); - - $child->setData($object); - - $graphWalker->expects($this->at(0)) - ->method('walkReference') - ->with($object, 'group1', 'foo.bar.data', true); - $graphWalker->expects($this->at(1)) - ->method('walkReference') - ->with($object, 'group2', 'foo.bar.data', true); - - DelegatingValidationListener::validateFormData($child, $context); - } - - public function testValidateFormDataUsesInheritedClosureValidationGroup() - { - $context = $this->getExecutionContext('foo.bar'); - $graphWalker = $context->getGraphWalker(); - $object = $this->getMock('\stdClass'); - - $parent = $this->getBuilder() - ->setAttribute('validation_groups', function(FormInterface $form){ - return array('group1', 'group2'); - }) - ->getForm(); - $child = $this->getBuilder('name', null, '\stdClass') - ->setAttribute('validation_groups', null) - ->getForm(); - $parent->add($child); - - $child->setData($object); - - $graphWalker->expects($this->at(0)) - ->method('walkReference') - ->with($object, 'group1', 'foo.bar.data', true); - $graphWalker->expects($this->at(1)) - ->method('walkReference') - ->with($object, 'group2', 'foo.bar.data', true); - - DelegatingValidationListener::validateFormData($child, $context); - } - - public function testValidateFormDataAppendsPropertyPath() - { - $context = $this->getExecutionContext('foo.bar'); - $graphWalker = $context->getGraphWalker(); - $object = $this->getMock('\stdClass'); - $form = $this->getForm('name', null, '\stdClass'); - - $graphWalker->expects($this->once()) - ->method('walkReference') - ->with($object, 'Default', 'foo.bar.data', true); - - $form->setData($object); - - DelegatingValidationListener::validateFormData($form, $context); - } - - public function testValidateFormDataDoesNotWalkScalars() - { - $context = $this->getExecutionContext(); - $graphWalker = $context->getGraphWalker(); - $clientTransformer = $this->getMockTransformer(); - - $form = $this->getBuilder() - ->appendClientTransformer($clientTransformer) - ->getForm(); - - $graphWalker->expects($this->never()) - ->method('walkReference'); - - $clientTransformer->expects($this->atLeastOnce()) - ->method('reverseTransform') - ->will($this->returnValue('foobar')); - - $form->bind(array('foo' => 'bar')); // reverse transformed to "foobar" - - DelegatingValidationListener::validateFormData($form, $context); - } - - public function testValidateFormChildren() - { - $context = $this->getExecutionContext(); - $graphWalker = $context->getGraphWalker(); - $form = $this->getBuilder() - ->setAttribute('cascade_validation', true) - ->setAttribute('validation_groups', array('group1', 'group2')) - ->getForm(); - $form->add($this->getForm('firstName')); - - $graphWalker->expects($this->once()) - ->method('walkReference') - // validation happens in Default group, because the Callback - // constraint is in the Default group as well - ->with($form->getChildren(), Constraint::DEFAULT_GROUP, 'children', true); - - DelegatingValidationListener::validateFormChildren($form, $context); - } - - public function testValidateFormChildrenAppendsPropertyPath() - { - $context = $this->getExecutionContext('foo.bar'); - $graphWalker = $context->getGraphWalker(); - $form = $this->getBuilder() - ->setAttribute('cascade_validation', true) - ->getForm(); - $form->add($this->getForm('firstName')); - - $graphWalker->expects($this->once()) - ->method('walkReference') - ->with($form->getChildren(), 'Default', 'foo.bar.children', true); - - DelegatingValidationListener::validateFormChildren($form, $context); - } - - public function testValidateFormChildrenDoesNothingIfDisabled() - { - $context = $this->getExecutionContext(); - $graphWalker = $context->getGraphWalker(); - $form = $this->getBuilder() - ->setAttribute('cascade_validation', false) - ->getForm(); - $form->add($this->getForm('firstName')); - - $graphWalker->expects($this->never()) - ->method('walkReference'); - - DelegatingValidationListener::validateFormChildren($form, $context); - } - - public function testValidateIgnoresNonRoot() - { - $form = $this->getMockForm(); - $form->expects($this->once()) - ->method('isRoot') - ->will($this->returnValue(false)); - - $this->delegate->expects($this->never()) - ->method('validate'); - - $this->listener->validateForm(new DataEvent($form, null)); - } -} diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/EventListener/ValidationListenerTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/EventListener/ValidationListenerTest.php new file mode 100644 index 000000000000..5bea8f45d409 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/EventListener/ValidationListenerTest.php @@ -0,0 +1,152 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Extension\Validator\EventListener; + +use Symfony\Component\Form\Event\DataEvent; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormBuilder; +use Symfony\Component\Form\FormError; +use Symfony\Component\Form\Util\PropertyPath; +use Symfony\Component\Form\Extension\Validator\Constraints\Form; +use Symfony\Component\Form\Extension\Validator\EventListener\ValidationListener; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintViolation; +use Symfony\Component\Validator\GlobalExecutionContext; +use Symfony\Component\Validator\ExecutionContext; + +class ValidationListenerTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $dispatcher; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $factory; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $validator; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $violationMapper; + + /** + * @var ValidationListener + */ + private $listener; + + private $message; + + private $params; + + protected function setUp() + { + if (!class_exists('Symfony\Component\EventDispatcher\Event')) { + $this->markTestSkipped('The "EventDispatcher" component is not available'); + } + + $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); + $this->factory = $this->getMock('Symfony\Component\Form\FormFactoryInterface'); + $this->validator = $this->getMock('Symfony\Component\Validator\ValidatorInterface'); + $this->violationMapper = $this->getMock('Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapperInterface'); + $this->listener = new ValidationListener($this->validator, $this->violationMapper); + $this->message = 'Message'; + $this->params = array('foo' => 'bar'); + } + + private function getConstraintViolation($code = null) + { + return new ConstraintViolation($this->message, $this->params, null, 'prop.path', null, null, $code); + } + + private function getFormError() + { + return new FormError($this->message, $this->params); + } + + private function getBuilder($name = 'name', $propertyPath = null, $dataClass = null) + { + $builder = new FormBuilder($name, $dataClass, $this->dispatcher, $this->factory); + $builder->setPropertyPath(new PropertyPath($propertyPath ?: $name)); + $builder->setAttribute('error_mapping', array()); + $builder->setErrorBubbling(false); + $builder->setMapped(true); + + return $builder; + } + + private function getForm($name = 'name', $propertyPath = null, $dataClass = null) + { + return $this->getBuilder($name, $propertyPath, $dataClass)->getForm(); + } + + private function getMockForm() + { + return $this->getMock('Symfony\Component\Form\Tests\FormInterface'); + } + + // More specific mapping tests can be found in ViolationMapperTest + public function testMapViolation() + { + $violation = $this->getConstraintViolation(); + $form = $this->getForm('street'); + + $this->validator->expects($this->once()) + ->method('validate') + ->will($this->returnValue(array($violation))); + + $this->violationMapper->expects($this->once()) + ->method('mapViolation') + ->with($violation, $form, false); + + $this->listener->validateForm(new DataEvent($form, null)); + } + + public function testMapViolationAllowsNonSyncIfInvalid() + { + $violation = $this->getConstraintViolation(Form::ERR_INVALID); + $form = $this->getForm('street'); + + $this->validator->expects($this->once()) + ->method('validate') + ->will($this->returnValue(array($violation))); + + $this->violationMapper->expects($this->once()) + ->method('mapViolation') + // pass true now + ->with($violation, $form, true); + + $this->listener->validateForm(new DataEvent($form, null)); + } + + public function testValidateIgnoresNonRoot() + { + $form = $this->getMockForm(); + $form->expects($this->once()) + ->method('isRoot') + ->will($this->returnValue(false)); + + $this->validator->expects($this->never()) + ->method('validate'); + + $this->violationMapper->expects($this->never()) + ->method('mapViolation'); + + $this->listener->validateForm(new DataEvent($form, null)); + } +} diff --git a/src/Symfony/Component/Validator/ConstraintViolation.php b/src/Symfony/Component/Validator/ConstraintViolation.php index a002f5b39304..101a5d72b2bc 100644 --- a/src/Symfony/Component/Validator/ConstraintViolation.php +++ b/src/Symfony/Component/Validator/ConstraintViolation.php @@ -24,8 +24,9 @@ class ConstraintViolation protected $root; protected $propertyPath; protected $invalidValue; + protected $code; - public function __construct($messageTemplate, array $messageParameters, $root, $propertyPath, $invalidValue, $messagePluralization = null) + public function __construct($messageTemplate, array $messageParameters, $root, $propertyPath, $invalidValue, $messagePluralization = null, $code = null) { $this->messageTemplate = $messageTemplate; $this->messageParameters = $messageParameters; @@ -33,6 +34,7 @@ public function __construct($messageTemplate, array $messageParameters, $root, $ $this->root = $root; $this->propertyPath = $propertyPath; $this->invalidValue = $invalidValue; + $this->code = $code; } /** @@ -42,12 +44,17 @@ public function __toString() { $class = (string) (is_object($this->root) ? get_class($this->root) : $this->root); $propertyPath = (string) $this->propertyPath; + $code = $this->code; if ('' !== $propertyPath && '[' !== $propertyPath[0] && '' !== $class) { $class .= '.'; } - return $class . $propertyPath . ":\n " . $this->getMessage(); + if (!empty($code)) { + $code = ' (code ' . $code . ')'; + } + + return $class . $propertyPath . ":\n " . $this->getMessage() . $code; } /** @@ -112,4 +119,9 @@ public function getInvalidValue() { return $this->invalidValue; } + + public function getCode() + { + return $this->code; + } } diff --git a/src/Symfony/Component/Validator/ConstraintViolationList.php b/src/Symfony/Component/Validator/ConstraintViolationList.php index 7bda2003e988..ec248ac125f5 100644 --- a/src/Symfony/Component/Validator/ConstraintViolationList.php +++ b/src/Symfony/Component/Validator/ConstraintViolationList.php @@ -79,6 +79,57 @@ public function addAll(ConstraintViolationList $otherList) } } + /** + * Returns the violation at a given offset. + * + * @param integer $offset The offset of the violation. + * + * @return ConstraintViolation The violation. + * + * @throws \OutOfBoundsException If the offset does not exist. + */ + public function get($offset) + { + if (!isset($this->violations[$offset])) { + throw new \OutOfBoundsException(sprintf('The offset "%s" does not exist.', $offset)); + } + + return $this->violations[$offset]; + } + + /** + * Returns whether the given offset exists. + * + * @param integer $offset The violation offset. + * + * @return Boolean Whether the offset exists. + */ + public function has($offset) + { + return isset($this->violations[$offset]); + } + + /** + * Sets a violation at a given offset. + * + * @param integer $offset The violation offset. + * @param ConstraintViolation $violation The violation. + */ + public function set($offset, ConstraintViolation $violation) + { + $this->violations[$offset] = $violation; + } + + /** + * Removes a violation at a given offset. + * + * @param integer $offset The offset to remove. + */ + public function remove($offset) + { + unset($this->violations[$offset]); + } + /** * @see IteratorAggregate * @@ -106,7 +157,7 @@ public function count() */ public function offsetExists($offset) { - return isset($this->violations[$offset]); + return $this->has($offset); } /** @@ -116,7 +167,7 @@ public function offsetExists($offset) */ public function offsetGet($offset) { - return isset($this->violations[$offset]) ? $this->violations[$offset] : null; + return $this->get($offset); } /** @@ -124,12 +175,12 @@ public function offsetGet($offset) * * @api */ - public function offsetSet($offset, $value) + public function offsetSet($offset, $violation) { if (null === $offset) { - $this->violations[] = $value; + $this->add($violation); } else { - $this->violations[$offset] = $value; + $this->set($offset, $violation); } } @@ -140,7 +191,7 @@ public function offsetSet($offset, $value) */ public function offsetUnset($offset) { - unset($this->violations[$offset]); + $this->remove($offset); } } diff --git a/src/Symfony/Component/Validator/ExecutionContext.php b/src/Symfony/Component/Validator/ExecutionContext.php index 65d8b97aff87..eddc9dfc28a4 100644 --- a/src/Symfony/Component/Validator/ExecutionContext.php +++ b/src/Symfony/Component/Validator/ExecutionContext.php @@ -57,10 +57,11 @@ public function __clone() * @param array $params The parameters parsed into the error message. * @param mixed $invalidValue The invalid, validated value. * @param integer|null $pluralization The number to use to pluralize of the message. + * @param integer|null $code The violation code. * * @api */ - public function addViolation($message, array $params = array(), $invalidValue = null, $pluralization = null) + public function addViolation($message, array $params = array(), $invalidValue = null, $pluralization = null, $code = null) { $this->globalContext->addViolation(new ConstraintViolation( $message, @@ -69,7 +70,8 @@ public function addViolation($message, array $params = array(), $invalidValue = $this->propertyPath, // check using func_num_args() to allow passing null values func_num_args() >= 3 ? $invalidValue : $this->value, - $pluralization + $pluralization, + $code )); } @@ -82,8 +84,9 @@ public function addViolation($message, array $params = array(), $invalidValue = * @param array $params The parameters parsed into the error message. * @param mixed $invalidValue The invalid, validated value. * @param integer|null $pluralization The number to use to pluralize of the message. + * @param integer|null $code The violation code. */ - public function addViolationAtPath($propertyPath, $message, array $params = array(), $invalidValue = null, $pluralization = null) + public function addViolationAtPath($propertyPath, $message, array $params = array(), $invalidValue = null, $pluralization = null, $code = null) { $this->globalContext->addViolation(new ConstraintViolation( $message, @@ -92,7 +95,8 @@ public function addViolationAtPath($propertyPath, $message, array $params = arra $propertyPath, // check using func_num_args() to allow passing null values func_num_args() >= 4 ? $invalidValue : $this->value, - $pluralization + $pluralization, + $code )); } @@ -105,8 +109,9 @@ public function addViolationAtPath($propertyPath, $message, array $params = arra * @param array $params The parameters parsed into the error message. * @param mixed $invalidValue The invalid, validated value. * @param integer|null $pluralization The number to use to pluralize of the message. + * @param integer|null $code The violation code. */ - public function addViolationAtSubPath($subPath, $message, array $params = array(), $invalidValue = null, $pluralization = null) + public function addViolationAtSubPath($subPath, $message, array $params = array(), $invalidValue = null, $pluralization = null, $code = null) { $this->globalContext->addViolation(new ConstraintViolation( $message, @@ -115,7 +120,8 @@ public function addViolationAtSubPath($subPath, $message, array $params = array( $this->getPropertyPath($subPath), // check using func_num_args() to allow passing null values func_num_args() >= 4 ? $invalidValue : $this->value, - $pluralization + $pluralization, + $code )); }