[Form] Allowed native framework errors to be mapped as well

This commit is contained in:
Bernhard Schussek 2012-05-21 17:13:00 +02:00
parent 59d6b55137
commit ac6939441f
25 changed files with 1481 additions and 745 deletions

View File

@ -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

View File

@ -133,9 +133,12 @@
</service>
<!-- FormTypeValidatorExtension -->
<service id="form.type_extension.field" class="Symfony\Component\Form\Extension\Validator\Type\FormTypeValidatorExtension">
<service id="form.type_extension.form.validator" class="Symfony\Component\Form\Extension\Validator\Type\FormTypeValidatorExtension">
<tag name="form.type_extension" alias="form" />
<argument type="service" id="validator" />
</service>
<service id="form.type_extension.repeated.validator" class="Symfony\Component\Form\Extension\Validator\Type\RepeatedTypeValidatorExtension">
<tag name="form.type_extension" alias="repeated" />
</service>
</services>
</container>

View File

@ -83,3 +83,5 @@ CHANGELOG
* `getErrorBubbling`
* `getNormTransformers`
* `getClientTransformers`
* deprecated the option "validation_constraint" in favor of the new
option "constraints"

View File

@ -1,68 +0,0 @@
<?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\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 <bschussek@gmail.com>
*/
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'));
}
}
}
}
}

View File

@ -51,8 +51,6 @@ class DateTimeType extends AbstractType
'days',
'empty_value',
'required',
'invalid_message',
'invalid_message_parameters',
'translation_domain',
)));
$timeOptions = array_intersect_key($options, array_flip(array(
@ -62,8 +60,6 @@ class DateTimeType extends AbstractType
'with_seconds',
'empty_value',
'required',
'invalid_message',
'invalid_message_parameters',
'translation_domain',
)));

View File

@ -18,7 +18,6 @@ use Symfony\Component\Form\FormInterface;
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 @@ class FormType extends AbstractType
->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 @@ class FormType extends AbstractType
};
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',
);
}

View File

@ -42,11 +42,6 @@ class RepeatedType extends AbstractType
*/
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 @@ class RepeatedType extends AbstractType
'first_name' => 'first',
'second_name' => 'second',
'error_bubbling' => false,
'error_mapping' => $errorMapping,
);
}

View File

@ -0,0 +1,33 @@
<?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\Form\Extension\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class Form extends Constraint
{
/**
* Violation code marking an invalid form.
*/
const ERR_INVALID = 1;
/**
* {@inheritdoc}
*/
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
}

View File

@ -0,0 +1,208 @@
<?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\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 <bschussek@gmail.com>
*/
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;
}
}

View File

@ -1,154 +0,0 @@
<?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\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 <bschussek@gmail.com>
*/
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);
}
}
}
}
}

View File

@ -0,0 +1,75 @@
<?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\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 <bschussek@gmail.com>
*/
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);
}
}
}
}
}

View File

@ -13,21 +13,35 @@ namespace Symfony\Component\Form\Extension\Validator\Type;
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 <bschussek@gmail.com>
*/
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 @@ class FormTypeValidatorExtension extends AbstractTypeExtension
: (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';

View File

@ -0,0 +1,44 @@
<?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\Form\Extension\Validator\Type;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\OptionsResolver\Options;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
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';
}
}

View File

@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Extension\Validator\Util;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
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;
}
}

View File

@ -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 @@ class ValidatorExtension extends AbstractExtension
$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 @@ class ValidatorExtension extends AbstractExtension
{
return array(
new Type\FormTypeValidatorExtension($this->validator),
new Type\RepeatedTypeValidatorExtension(),
);
}
}

View File

@ -23,7 +23,7 @@ use Symfony\Component\Validator\ConstraintViolation;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
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 @@ class ViolationMapper
$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 @@ class ViolationMapper
// 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 @@ class ViolationMapper
// 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 @@ class ViolationMapper
}
}
}
/**
* @return Boolean
*/
private function isValidScope()
{
return $this->allowNonSynchronized || $this->scope->isSynchronized();
}
}

View File

@ -0,0 +1,33 @@
<?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\Form\Extension\Validator\ViolationMapper;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Validator\ConstraintViolation;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
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);
}

View File

@ -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">
<class name="Symfony\Component\Form\Form">
<constraint name="Callback">
<value>
<value>Symfony\Component\Form\Extension\Validator\EventListener\DelegatingValidationListener</value>
<value>validateFormData</value>
</value>
<value>
<value>Symfony\Component\Form\Extension\Validator\EventListener\DelegatingValidationListener</value>
<value>validateFormChildren</value>
</value>
</constraint>
<constraint name="Symfony\Component\Form\Extension\Validator\Constraints\Form" />
<property name="children">
<constraint name="Valid" />
</property>
</class>
</constraint-mapping>

View File

@ -226,32 +226,6 @@ class DateTimeTypeTest extends LocalizedTestCase
$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()
{

View File

@ -0,0 +1,694 @@
<?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\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 <bschussek@gmail.com>
*/
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();
}
}

View File

@ -1,417 +0,0 @@
<?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\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));
}
}

View File

@ -0,0 +1,152 @@
<?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\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));
}
}

View File

@ -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 @@ class ConstraintViolation
$this->root = $root;
$this->propertyPath = $propertyPath;
$this->invalidValue = $invalidValue;
$this->code = $code;
}
/**
@ -42,12 +44,17 @@ class ConstraintViolation
{
$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 @@ class ConstraintViolation
{
return $this->invalidValue;
}
public function getCode()
{
return $this->code;
}
}

View File

@ -79,6 +79,57 @@ class ConstraintViolationList implements \IteratorAggregate, \Countable, \ArrayA
}
}
/**
* 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 @@ class ConstraintViolationList implements \IteratorAggregate, \Countable, \ArrayA
*/
public function offsetExists($offset)
{
return isset($this->violations[$offset]);
return $this->has($offset);
}
/**
@ -116,7 +167,7 @@ class ConstraintViolationList implements \IteratorAggregate, \Countable, \ArrayA
*/
public function offsetGet($offset)
{
return isset($this->violations[$offset]) ? $this->violations[$offset] : null;
return $this->get($offset);
}
/**
@ -124,12 +175,12 @@ class ConstraintViolationList implements \IteratorAggregate, \Countable, \ArrayA
*
* @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 @@ class ConstraintViolationList implements \IteratorAggregate, \Countable, \ArrayA
*/
public function offsetUnset($offset)
{
unset($this->violations[$offset]);
$this->remove($offset);
}
}

View File

@ -57,10 +57,11 @@ class ExecutionContext
* @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 @@ class ExecutionContext
$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 @@ class ExecutionContext
* @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 @@ class ExecutionContext
$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 @@ class ExecutionContext
* @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 @@ class ExecutionContext
$this->getPropertyPath($subPath),
// check using func_num_args() to allow passing null values
func_num_args() >= 4 ? $invalidValue : $this->value,
$pluralization
$pluralization,
$code
));
}