merged branch bschussek/issue3903 (PR #4341)

Commits
-------

1422506 [Form] Clarified the usage of "constraints" in the UPGRADE file
af41a1a [Form] Fixed typos
ac69394 [Form] Allowed native framework errors to be mapped as well
59d6b55 [Form] Fixed: error mapping aborts if reaching an unsynchronized form
9eda5f5 [Form] Fixed: RepeatedType now maps all errors to the first field
215b687 [Form] Added capability to process "." rules in "error_mapping"
c9c4900 [Form] Fixed: errors are not mapped to unsynchronized forms anymore
c8b61d5 [Form] Renamed FormMapping to MappingRule and moved some logic there to make rules more extendable
d0d1fe6 [Form] Added more information to UPGRADE and CHANGELOG
0c09a0e [Form] Made $name parameters optional in PropertyPathBuilder:replaceBy(Index|Property)
081c643 [Form] Updated UPGRADE and CHANGELOG
bbffd1b [Form] Fixed index checks in PropertyPath classes
ea5ff77 [Form] Fixed issues mentioned in the PR comments
7a4ba52 [EventDispatcher] Added class UnmodifiableEventDispatcher
306324e [Form] Greatly improved the error mapping done in DelegatingValidationListener
8f7e2f6 [Validator] Fixed: @Valid does not recurse the traversal of collections anymore by default
5e87dd8 [Form] Added tests for the case when "property_path" is null or false. Instead of setting "property_path" to false, you should set "mapped" to false instead.
2301b15 [Form] Tightened PropertyPath validation to reject any empty value (such as false)
7ff2a9b Revert "[Form] removed a constraint in PropertyPath as the path can definitely be an empty string for errors attached on the main form (when using a constraint defined with the 'validation_constraint' option)"
860dd1f [Form] Adapted Form to create a deterministic property path by default
03f5058 [Form] Fixed property name in PropertyPathMapperTest
c2a243f [Form] Made PropertyPath deterministic: "[prop]" always refers to indices (array or ArrayAccess), "prop" always refers to properties
2996340 [Form] Extracted FormConfig class to simplify the Form's constructor

Discussion
----------

[Form] Improved the error mapping and made property paths deterministic

Bug fix: yes
Feature addition: no
Backwards compatibility break: **yes**
Symfony2 tests pass: yes
Fixes the following tickets: #1971, #2945, #3272, #3308, #3903, #4329
Probably fixes: #2729
Todo: -

This PR is ready for review.

The algorithm for assigning errors to forms in the form tree was improved a lot. Also the error mapping works better now. There are still a few features to be added (e.g. wildcards "*"), but these can be implemented now pretty easily.

This PR breaks PR in that a form explicitely needs to set the "data_class" option if it wants to map to an object and needs to leave that option empty if it wants to map to an array.

Furthermore, property paths must be deterministic now: `foo` now only maps to `(g|s)etFoo()`, but not the index `["foo"]` (array or ArrayAccess), while `[foo]` only maps to the latter but not the former. See #3903 for more information.

---------------------------------------------------------------------------

by travisbot at 2012-05-19T21:35:24Z

This pull request [passes](http://travis-ci.org/symfony/symfony/builds/1377086) (merged 9e346990 into 22294617).

---------------------------------------------------------------------------

by Tobion at 2012-05-20T01:47:48Z

Good stuff in general :)

---------------------------------------------------------------------------

by bschussek at 2012-05-20T09:19:18Z

Fixed everything mentioned here so far.

---------------------------------------------------------------------------

by travisbot at 2012-05-20T09:22:22Z

This pull request [passes](http://travis-ci.org/symfony/symfony/builds/1379548) (merged 49918bef into 22294617).

---------------------------------------------------------------------------

by Tobion at 2012-05-20T14:29:14Z

many occurences of two spaces after `@param` (should be only one space).

---------------------------------------------------------------------------

by Koc at 2012-05-20T14:40:18Z

Sorry, I'm cannot observe all changes for form component in 2.1, so I have a question:

```php
<?php

protected $isPrivate;

public function isPrivate() {}

public function setPrivate() {}
```

Is it possible validate this property with accessors/mutators from code above in 2.1 now?

---------------------------------------------------------------------------

by bschussek at 2012-05-20T14:41:09Z

The type after `@param` used to be aligned with the type of the `@return` tag. Let's get the PHPDoc-guidelines straight before nitpicking more on these trivialities.

---------------------------------------------------------------------------

by bschussek at 2012-05-20T14:42:34Z

@Koc Please move your question to the user mailing list, let's keep this PR on topic.

---------------------------------------------------------------------------

by bschussek at 2012-05-20T14:45:42Z

Fixed everything mentioned until now.

---------------------------------------------------------------------------

by travisbot at 2012-05-20T14:47:48Z

This pull request [passes](http://travis-ci.org/symfony/symfony/builds/1380903) (merged 03d60a03 into f433f6b0).

---------------------------------------------------------------------------

by bschussek at 2012-05-20T15:18:12Z

CHANGELOG/UPGRADE is now updated.

---------------------------------------------------------------------------

by travisbot at 2012-05-20T15:19:39Z

This pull request [passes](http://travis-ci.org/symfony/symfony/builds/1381047) (merged 48cc3eca into f433f6b0).

---------------------------------------------------------------------------

by Tobion at 2012-05-20T16:16:51Z

All the deprecated methods and changed constructor arguments should probably be mentioned in the changelog/upgrade.

---------------------------------------------------------------------------

by travisbot at 2012-05-21T07:31:47Z

This pull request [passes](http://travis-ci.org/symfony/symfony/builds/1386621) (merged c0ef69a1 into 1407f112).

---------------------------------------------------------------------------

by travisbot at 2012-05-21T08:01:46Z

This pull request [passes](http://travis-ci.org/symfony/symfony/builds/1386826) (merged 4f3fc1fe into 1407f112).

---------------------------------------------------------------------------

by travisbot at 2012-05-21T09:22:30Z

This pull request [passes](http://travis-ci.org/symfony/symfony/builds/1387263) (merged e3675050 into 1407f112).

---------------------------------------------------------------------------

by bschussek at 2012-05-21T09:43:08Z

This PR now fixes #1971.

---------------------------------------------------------------------------

by travisbot at 2012-05-21T09:45:51Z

This pull request [passes](http://travis-ci.org/symfony/symfony/builds/1387370) (merged de33f9ef into 1407f112).

---------------------------------------------------------------------------

by travisbot at 2012-05-21T11:06:53Z

This pull request [passes](http://travis-ci.org/symfony/symfony/builds/1387838) (merged da3b562e into 1407f112).

---------------------------------------------------------------------------

by bschussek at 2012-05-21T11:07:45Z

This PR now fixes #2945.

---------------------------------------------------------------------------

by bschussek at 2012-05-21T15:33:33Z

Native errors (such as "invalid", "extra_fields" etc.) are now respected by the "error_mapping" option as well. The option "validation_constraint" was deprecated, "constraints" is its replacement and a lot handier, because it allows you to work easily with arrays.

```php
<?php
$builder
    ->add('name', 'text', array(
        'constraints' => new NotBlank(),
    ))
    ->add('phoneNumber', 'text', array(
        'constraints' => array(
            new NotBlank(),
            new MinLength(7),
            new Type('numeric')
        )
    ));
```

Ready for review again.

---------------------------------------------------------------------------

by travisbot at 2012-05-21T15:33:45Z

This pull request [fails](http://travis-ci.org/symfony/symfony/builds/1390239) (merged e162f56d into ea33d4d3).

---------------------------------------------------------------------------

by travisbot at 2012-05-21T15:40:02Z

This pull request [fails](http://travis-ci.org/symfony/symfony/builds/1390367) (merged e8729a7f into ea33d4d3).

---------------------------------------------------------------------------

by travisbot at 2012-05-21T16:06:03Z

This pull request [passes](http://travis-ci.org/symfony/symfony/builds/1390663) (merged ef39aba4 into ea33d4d3).

---------------------------------------------------------------------------

by travisbot at 2012-05-22T08:54:36Z

This pull request [passes](http://travis-ci.org/symfony/symfony/builds/1398153) (merged af41a1a5 into e4e3ce6c).

---------------------------------------------------------------------------

by travisbot at 2012-05-22T09:26:12Z

This pull request [passes](http://travis-ci.org/symfony/symfony/builds/1398415) (merged 14225067 into e4e3ce6c).
This commit is contained in:
Fabien Potencier 2012-05-22 11:55:33 +02:00
commit e2be8ed87a
84 changed files with 7127 additions and 2531 deletions

View File

@ -458,6 +458,118 @@
}
}
* Setting the option "property_path" to `false` was deprecated and will be unsupported
as of Symfony 2.3.
You should use the new option "mapped" instead in order to set that you don't want
a field to be mapped to its parent's data.
Before:
```
$builder->add('termsAccepted', 'checkbox', array(
'property_path' => false,
));
```
After:
```
$builder->add('termsAccepted', 'checkbox', array(
'mapped' => false,
));
```
* The "data_class" option now *must* be set if a form maps to an object. If
you leave it empty, the form will expect an array or a scalar value and
fail with a corresponding exception.
Likewise, if a form maps to an array, the option *must* be left empty now.
* The mapping of property paths to arrays has changed.
Previously, a property path "street" mapped to both a field `$street` of
a class (or its accessors `getStreet()` and `setStreet()`) and an index
`['street']` of an array or an object implementing `\ArrayAccess`.
Now, the property path "street" only maps to a class field (or accessors),
while the property path "[street]" only maps to indices.
If you defined property paths manually in the "property_path" option, you
should revise them and adjust them if necessary.
Before:
```
$builder->add('name', 'text', array(
'property_path' => 'address.street',
));
```
After (if the address object is an array):
```
$builder->add('name', 'text', array(
'property_path' => 'address[street]',
));
```
If address is an object in this case, the code given in "Before"
works without changes.
* The following methods in `Form` are deprecated and will be removed in
Symfony 2.3:
* `getTypes`
* `getErrorBubbling`
* `getNormTransformers`
* `getClientTransformers`
You can access these methods on the `FormConfigInterface` object instead.
Before:
```
$form->getErrorBubbling()
```
After:
```
$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:
```
$builder->add('name', 'text', array(
'constraints' => new NotBlank(),
));
```
Unlike previously, you can also pass a list of constraints now:
```
$builder->add('name', 'text', array(
'constraints' => array(
new NotBlank(),
new MinLength(3),
),
));
```
### Validator
* The methods `setMessage()`, `getMessageTemplate()` and
@ -571,6 +683,28 @@
* Core translation messages are changed. Dot is added at the end of each message.
Overwritten core translations should be fixed if any. More info here.
* Collections (arrays or instances of `\Traversable`) in properties
annotated with `Valid` are not traversed recursively by default anymore.
This means that if a collection contains an entry which is again a
collection, the inner collection won't be traversed anymore as it
happened before. You can set the BC behavior by setting the new property
`deep` of `Valid` to `true`.
Before:
```
/** @Assert\Valid */
private $recursiveCollection;
```
After:
```
/** @Assert\Valid(deep = true) */
private $recursiveCollection;
```
### Session
* Flash messages now return an array based on their type. The old method is

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

@ -13,3 +13,4 @@ CHANGELOG
* added GenericEvent event class
* added the possibility for subscribers to subscribe several times for the
same event
* added UnmodifiableEventDispatcher

View File

@ -0,0 +1,106 @@
<?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\EventDispatcher\Tests;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\EventDispatcher\UnmodifiableEventDispatcher;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class UnmodifiableEventDispatcherTest extends \PHPUnit_Framework_TestCase
{
/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
private $innerDispatcher;
/**
* @var UnmodifiableEventDispatcher
*/
private $dispatcher;
protected function setUp()
{
$this->innerDispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
$this->dispatcher = new UnmodifiableEventDispatcher($this->innerDispatcher);
}
public function testDispatchDelegates()
{
$event = new Event();
$this->innerDispatcher->expects($this->once())
->method('dispatch')
->with('event', $event)
->will($this->returnValue('result'));
$this->assertSame('result', $this->dispatcher->dispatch('event', $event));
}
public function testGetListenersDelegates()
{
$this->innerDispatcher->expects($this->once())
->method('getListeners')
->with('event')
->will($this->returnValue('result'));
$this->assertSame('result', $this->dispatcher->getListeners('event'));
}
public function testHasListenersDelegates()
{
$this->innerDispatcher->expects($this->once())
->method('hasListeners')
->with('event')
->will($this->returnValue('result'));
$this->assertSame('result', $this->dispatcher->hasListeners('event'));
}
/**
* @expectedException \BadMethodCallException
*/
public function testAddListenerDisallowed()
{
$this->dispatcher->addListener('event', function () { return 'foo'; });
}
/**
* @expectedException \BadMethodCallException
*/
public function testAddSubscriberDisallowed()
{
$subscriber = $this->getMock('Symfony\Component\EventDispatcher\EventSubscriberInterface');
$this->dispatcher->addSubscriber($subscriber);
}
/**
* @expectedException \BadMethodCallException
*/
public function testRemoveListenerDisallowed()
{
$this->dispatcher->removeListener('event', function () { return 'foo'; });
}
/**
* @expectedException \BadMethodCallException
*/
public function testRemoveSubscriberDisallowed()
{
$subscriber = $this->getMock('Symfony\Component\EventDispatcher\EventSubscriberInterface');
$this->dispatcher->removeSubscriber($subscriber);
}
}

View File

@ -0,0 +1,92 @@
<?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\EventDispatcher;
/**
* A read-only proxy for an event dispatcher.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class UnmodifiableEventDispatcher implements EventDispatcherInterface
{
/**
* The proxied dispatcher.
* @var EventDispatcherInterface
*/
private $dispatcher;
/**
* Creates an unmodifiable proxy for an event dispatcher.
*
* @param EventDispatcherInterface $dispatcher The proxied event dispatcher.
*/
public function __construct(EventDispatcherInterface $dispatcher)
{
$this->dispatcher = $dispatcher;
}
/**
* {@inheritdoc}
*/
public function dispatch($eventName, Event $event = null)
{
return $this->dispatcher->dispatch($eventName, $event);
}
/**
* {@inheritdoc}
*/
public function addListener($eventName, $listener, $priority = 0)
{
throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.');
}
/**
* {@inheritdoc}
*/
public function addSubscriber(EventSubscriberInterface $subscriber)
{
throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.');
}
/**
* {@inheritdoc}
*/
public function removeListener($eventName, $listener)
{
throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.');
}
/**
* {@inheritdoc}
*/
public function removeSubscriber(EventSubscriberInterface $subscriber)
{
throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.');
}
/**
* {@inheritdoc}
*/
public function getListeners($eventName = null)
{
return $this->dispatcher->getListeners($eventName);
}
/**
* {@inheritdoc}
*/
public function hasListeners($eventName = null)
{
return $this->dispatcher->hasListeners($eventName);
}
}

View File

@ -70,3 +70,18 @@ CHANGELOG
* deprecated method `guessMinLength` in favor of `guessPattern`
* labels don't display field attributes anymore. Label attributes can be
passed in the "label_attr" option/variable
* added option "mapped" which should be used instead of setting "property_path" to false
* "data_class" now *must* be set if a form maps to an object and should be left empty otherwise
* improved error mapping on forms
* dot (".") rules are now allowed to map errors assigned to a form to
one of its children
* errors are not mapped to unsynchronized forms anymore
* changed Form constructor to accept a single `FormConfigInterface` object
* changed argument order in the FormBuilder constructor
* deprecated Form methods
* `getTypes`
* `getErrorBubbling`
* `getNormTransformers`
* `getClientTransformers`
* deprecated the option "validation_constraint" in favor of the new
option "constraints"

View File

@ -42,7 +42,7 @@ class CallbackTransformer implements DataTransformerInterface
*
* @param mixed $data The value in the original representation
*
* @return mixed The value in the transformed representation
* @return mixed The value in the transformed representation
*
* @throws UnexpectedTypeException when the argument is not a string
* @throws TransformationFailedException when the transformation fails
@ -58,7 +58,7 @@ class CallbackTransformer implements DataTransformerInterface
*
* @param mixed $data The value in the transformed representation
*
* @return mixed The value in the original representation
* @return mixed The value in the original representation
*
* @throws UnexpectedTypeException when the argument is not of the expected type
* @throws TransformationFailedException when the transformation fails

View File

@ -41,7 +41,7 @@ interface DataTransformerInterface
*
* @param mixed $value The value in the original representation
*
* @return mixed The value in the transformed representation
* @return mixed The value in the transformed representation
*
* @throws UnexpectedTypeException when the argument is not a string
* @throws TransformationFailedException when the transformation fails
@ -68,7 +68,7 @@ interface DataTransformerInterface
*
* @param mixed $value The value in the transformed representation
*
* @return mixed The value in the original representation
* @return mixed The value in the original representation
*
* @throws UnexpectedTypeException when the argument is not of the expected type
* @throws TransformationFailedException when the transformation fails

View File

@ -0,0 +1,16 @@
<?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\Exception;
class ErrorMappingException extends FormException
{
}

View File

@ -11,7 +11,7 @@
namespace Symfony\Component\Form\Extension\Core\ChoiceList;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormConfig;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Exception\InvalidConfigurationException;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
@ -335,7 +335,7 @@ class ChoiceList implements ChoiceListInterface
{
$index = $this->createIndex($choice);
if ('' === $index || null === $index || !Form::isValidName((string)$index)) {
if ('' === $index || null === $index || !FormConfig::isValidName((string)$index)) {
throw new InvalidConfigurationException('The index "' . $index . '" created by the choice list is invalid. It should be a valid, non-empty Form name.');
}

View File

@ -18,18 +18,6 @@ use Symfony\Component\Form\Exception\UnexpectedTypeException;
class PropertyPathMapper implements DataMapperInterface
{
/**
* Stores the class that the data of this form must be instances of.
*
* @var string
*/
private $dataClass;
public function __construct($dataClass = null)
{
$this->dataClass = $dataClass;
}
/**
* {@inheritdoc}
*/
@ -40,10 +28,6 @@ class PropertyPathMapper implements DataMapperInterface
}
if (!empty($data)) {
if (null !== $this->dataClass && !$data instanceof $this->dataClass) {
throw new UnexpectedTypeException($data, $this->dataClass);
}
$iterator = new VirtualFormAwareIterator($forms);
$iterator = new \RecursiveIteratorIterator($iterator);
@ -59,9 +43,10 @@ class PropertyPathMapper implements DataMapperInterface
public function mapDataToForm($data, FormInterface $form)
{
if (!empty($data)) {
$propertyPath = $form->getAttribute('property_path');
$propertyPath = $form->getPropertyPath();
$config = $form->getConfig();
if (null !== $propertyPath) {
if (null !== $propertyPath && $config->getMapped()) {
$propertyData = $propertyPath->getValue($data);
if (is_object($propertyData) && !$form->getAttribute('by_reference')) {
@ -91,9 +76,12 @@ class PropertyPathMapper implements DataMapperInterface
*/
public function mapFormToData(FormInterface $form, &$data)
{
$propertyPath = $form->getAttribute('property_path');
$propertyPath = $form->getPropertyPath();
$config = $form->getConfig();
if (null !== $propertyPath && $form->isSynchronized() && !$form->isDisabled()) {
// Write-back is disabled if the form is not synchronized (transformation failed)
// and if the form is disabled (modification not allowed)
if (null !== $propertyPath && $config->getMapped() && $form->isSynchronized() && !$form->isDisabled()) {
// If the data is identical to the value in $data, we are
// dealing with a reference
$isReference = $form->getData() === $propertyPath->getValue($data);

View File

@ -43,7 +43,7 @@ class BooleanToStringTransformer implements DataTransformerInterface
*
* @param Boolean $value Boolean value.
*
* @return string String value.
* @return string String value.
*
* @throws UnexpectedTypeException if the given value is not a Boolean
*/
@ -65,7 +65,7 @@ class BooleanToStringTransformer implements DataTransformerInterface
*
* @param string $value String value.
*
* @return Boolean Boolean value.
* @return Boolean Boolean value.
*
* @throws UnexpectedTypeException if the given value is not a string
*/

View File

@ -46,7 +46,7 @@ class DataTransformerChain implements DataTransformerInterface
*
* @param mixed $value The original value
*
* @return mixed The transformed value
* @return mixed The transformed value
*
* @throws Symfony\Component\Form\Exception\TransformationFailedException
* @throws Symfony\Component\Form\Exception\UnexpectedTypeException
@ -71,7 +71,7 @@ class DataTransformerChain implements DataTransformerInterface
*
* @param mixed $value The transformed value
*
* @return mixed The reverse-transformed value
* @return mixed The reverse-transformed value
*
* @throws Symfony\Component\Form\Exception\TransformationFailedException
* @throws Symfony\Component\Form\Exception\UnexpectedTypeException

View File

@ -53,7 +53,7 @@ class DateTimeToArrayTransformer extends BaseDateTimeTransformer
*
* @param DateTime $dateTime Normalized date.
*
* @return array Localized date.
* @return array Localized date.
*
* @throws UnexpectedTypeException if the given value is not an instance of \DateTime
* @throws TransformationFailedException if the output timezone is not supported
@ -108,7 +108,7 @@ class DateTimeToArrayTransformer extends BaseDateTimeTransformer
*
* @param array $value Localized date
*
* @return DateTime Normalized date
* @return DateTime Normalized date
*
* @throws UnexpectedTypeException if the given value is not an array
* @throws TransformationFailedException if the value could not bet transformed

View File

@ -48,7 +48,7 @@ class DateTimeToStringTransformer extends BaseDateTimeTransformer
*
* @param DateTime $value A DateTime object
*
* @return string A value as produced by PHP's date() function
* @return string A value as produced by PHP's date() function
*
* @throws UnexpectedTypeException if the given value is not a \DateTime instance
* @throws TransformationFailedException if the output timezone is not supported

View File

@ -27,7 +27,7 @@ class DateTimeToTimestampTransformer extends BaseDateTimeTransformer
*
* @param DateTime $value A DateTime object
*
* @return integer A timestamp
* @return integer A timestamp
*
* @throws UnexpectedTypeException if the given value is not an instance of \DateTime
* @throws TransformationFailedException if the output timezone is not supported

View File

@ -48,7 +48,7 @@ class MoneyToLocalizedStringTransformer extends NumberToLocalizedStringTransform
*
* @param number $value Normalized number
*
* @return string Localized money string.
* @return string Localized money string.
*
* @throws UnexpectedTypeException if the given value is not numeric
* @throws TransformationFailedException if the value can not be transformed

View File

@ -66,7 +66,7 @@ class PercentToLocalizedStringTransformer implements DataTransformerInterface
*
* @param number $value Normalized value
*
* @return number Percentage value
* @return number Percentage value
*
* @throws UnexpectedTypeException if the given value is not numeric
* @throws TransformationFailedException if the value could not be transformed
@ -101,7 +101,7 @@ class PercentToLocalizedStringTransformer implements DataTransformerInterface
*
* @param number $value Percentage value.
*
* @return number Normalized value.
* @return number Normalized value.
*
* @throws UnexpectedTypeException if the given value is not a string
* @throws TransformationFailedException if the value could not be transformed

View File

@ -26,7 +26,7 @@ class ValueToStringTransformer implements DataTransformerInterface
*
* @param mixed $value Mixed value.
*
* @return string String value.
* @return string String value.
*
* @throws UnexpectedTypeException if the given value is not a string or number
*/
@ -48,7 +48,7 @@ class ValueToStringTransformer implements DataTransformerInterface
*
* @param string $value String value.
*
* @return string String value.
* @return string String value.
*
* @throws UnexpectedTypeException if the given value is not a string
*/

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;
@ -31,16 +30,6 @@ class FormType extends AbstractType
*/
public function buildForm(FormBuilder $builder, array $options)
{
if (null === $options['property_path']) {
$options['property_path'] = $builder->getName();
}
if (false === $options['property_path'] || '' === $options['property_path']) {
$options['property_path'] = null;
} else {
$options['property_path'] = new PropertyPath($options['property_path']);
}
if (!is_array($options['attr'])) {
throw new FormException('The "attr" option must be an "array".');
}
@ -54,23 +43,21 @@ class FormType extends AbstractType
->setDisabled($options['disabled'])
->setErrorBubbling($options['error_bubbling'])
->setEmptyData($options['empty_data'])
// BC compatibility, when "property_path" could be false
->setPropertyPath(is_string($options['property_path']) ? $options['property_path'] : null)
->setMapped($options['mapped'])
->setVirtual($options['virtual'])
->setAttribute('read_only', $options['read_only'])
->setAttribute('by_reference', $options['by_reference'])
->setAttribute('property_path', $options['property_path'])
->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('virtual', $options['virtual'])
->setAttribute('single_control', $options['single_control'])
->setData($options['data'])
->setDataMapper(new PropertyPathMapper($options['data_class']))
->addEventSubscriber(new ValidationListener())
->setDataMapper(new PropertyPathMapper())
;
if ($options['trim']) {
@ -112,7 +99,7 @@ class FormType extends AbstractType
}
$types = array();
foreach ($form->getTypes() as $type) {
foreach ($form->getConfig()->getTypes() as $type) {
$types[] = $type->getName();
}
@ -199,27 +186,30 @@ class FormType extends AbstractType
return !$options['single_control'];
};
// BC clause: former property_path=false now equals mapped=false
$mapped = function (Options $options) {
return false !== $options['property_path'];
};
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,
'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',
);
}
@ -229,7 +219,7 @@ class FormType extends AbstractType
*/
public function createBuilder($name, FormFactoryInterface $factory, array $options)
{
return new FormBuilder($name, $factory, new EventDispatcher(), $options['data_class']);
return new FormBuilder($name, $options['data_class'], new EventDispatcher(), $factory);
}
/**

View File

@ -14,6 +14,7 @@ namespace Symfony\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\Extension\Core\DataTransformer\ValueToDuplicatesTransformer;
use Symfony\Component\OptionsResolver\Options;
class RepeatedType extends AbstractType
{

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,212 @@
<?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 {
$clientDataAsString = is_scalar($form->getClientData())
? (string) $form->getClientData()
: gettype($form->getClientData());
// Mark the form with an error if it is not synchronized
$this->context->addViolation(
$form->getAttribute('invalid_message'),
array('{{ value }}' => $clientDataAsString),
$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,295 +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\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\VirtualFormAwareIterator;
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()) {
$mapping = array();
$forms = array();
$this->buildFormPathMapping($form, $mapping);
$this->buildDataPathMapping($form, $mapping);
$this->buildNamePathMapping($form, $forms);
$this->resolveMappingPlaceholders($mapping, $forms);
// 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)
);
if ($violations) {
foreach ($violations as $violation) {
$propertyPath = new PropertyPath($violation->getPropertyPath());
$template = $violation->getMessageTemplate();
$parameters = $violation->getMessageParameters();
$pluralization = $violation->getMessagePluralization();
$error = new FormError($template, $parameters, $pluralization);
$child = $form;
foreach ($propertyPath->getElements() as $element) {
$children = $child->getChildren();
if (!isset($children[$element])) {
$form->addError($error);
break;
}
$child = $children[$element];
}
$child->addError($error);
}
}
} elseif (count($violations = $this->validator->validate($form))) {
foreach ($violations as $violation) {
$propertyPath = $violation->getPropertyPath();
$template = $violation->getMessageTemplate();
$parameters = $violation->getMessageParameters();
$pluralization = $violation->getMessagePluralization();
$error = new FormError($template, $parameters, $pluralization);
foreach ($mapping as $mappedPath => $child) {
if (preg_match($mappedPath, $propertyPath)) {
$child->addError($error);
continue 2;
}
}
$form->addError($error);
}
}
}
}
private function buildFormPathMapping(FormInterface $form, array &$mapping, $formPath = 'children', $namePath = '')
{
foreach ($form->getAttribute('error_mapping') as $nestedDataPath => $nestedNamePath) {
$mapping['/^'.preg_quote($formPath.'.data.'.$nestedDataPath).'(?!\w)/'] = $namePath.'.'.$nestedNamePath;
}
$iterator = new VirtualFormAwareIterator($form->getChildren());
$iterator = new \RecursiveIteratorIterator($iterator);
foreach ($iterator as $child) {
$path = (string) $child->getAttribute('property_path');
$parts = explode('.', $path, 2);
$nestedNamePath = $namePath.'.'.$child->getName();
if ($child->hasChildren() || isset($parts[1])) {
$nestedFormPath = $formPath.'['.trim($parts[0], '[]').']';
} else {
$nestedFormPath = $formPath.'.data.'.$parts[0];
}
if (isset($parts[1])) {
$nestedFormPath .= '.data.'.$parts[1];
}
if ($child->hasChildren()) {
$this->buildFormPathMapping($child, $mapping, $nestedFormPath, $nestedNamePath);
}
$mapping['/^'.preg_quote($nestedFormPath, '/').'(?!\w)/'] = $child;
}
}
private function buildDataPathMapping(FormInterface $form, array &$mapping, $dataPath = 'data', $namePath = '')
{
foreach ($form->getAttribute('error_mapping') as $nestedDataPath => $nestedNamePath) {
$mapping['/^'.preg_quote($dataPath.'.'.$nestedDataPath).'(?!\w)/'] = $namePath.'.'.$nestedNamePath;
}
$iterator = new VirtualFormAwareIterator($form->getChildren());
$iterator = new \RecursiveIteratorIterator($iterator);
foreach ($iterator as $child) {
$path = (string) $child->getAttribute('property_path');
$nestedNamePath = $namePath.'.'.$child->getName();
if (0 === strpos($path, '[')) {
$nestedDataPaths = array($dataPath.$path);
} else {
$nestedDataPaths = array($dataPath.'.'.$path);
if ($child->hasChildren()) {
$nestedDataPaths[] = $dataPath.'['.$path.']';
}
}
if ($child->hasChildren()) {
// Needs when collection implements the Iterator
// or for array used the Valid validator.
if (is_array($child->getData()) || $child->getData() instanceof \Traversable) {
$this->buildDataPathMapping($child, $mapping, $dataPath, $nestedNamePath);
}
foreach ($nestedDataPaths as $nestedDataPath) {
$this->buildDataPathMapping($child, $mapping, $nestedDataPath, $nestedNamePath);
}
}
foreach ($nestedDataPaths as $nestedDataPath) {
$mapping['/^'.preg_quote($nestedDataPath, '/').'(?!\w)/'] = $child;
}
}
}
private function buildNamePathMapping(FormInterface $form, array &$forms, $namePath = '')
{
$iterator = new VirtualFormAwareIterator($form->getChildren());
$iterator = new \RecursiveIteratorIterator($iterator);
foreach ($iterator as $child) {
$nestedNamePath = $namePath.'.'.$child->getName();
$forms[$nestedNamePath] = $child;
if ($child->hasChildren()) {
$this->buildNamePathMapping($child, $forms, $nestedNamePath);
}
}
}
private function resolveMappingPlaceholders(array &$mapping, array $forms)
{
foreach ($mapping as $pattern => $form) {
if (is_string($form)) {
if (!isset($forms[$form])) {
throw new FormException(sprintf('The child form with path "%s" does not exist', $form));
}
$mapping[$pattern] = $forms[$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-synchronized 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

@ -89,7 +89,7 @@ class ValidatorTypeGuesser implements FormTypeGuesserInterface
*
* @param Constraint $constraint The constraint to guess for
*
* @return TypeGuess The guessed field class and options
* @return TypeGuess The guessed field class and options
*/
public function guessTypeForConstraint(Constraint $constraint)
{
@ -178,7 +178,7 @@ class ValidatorTypeGuesser implements FormTypeGuesserInterface
*
* @param Constraint $constraint The constraint to guess for
*
* @return Guess The guess whether the field is required
* @return Guess The guess whether the field is required
*/
public function guessRequiredForConstraint(Constraint $constraint)
{
@ -194,7 +194,7 @@ class ValidatorTypeGuesser implements FormTypeGuesserInterface
*
* @param Constraint $constraint The constraint to guess for
*
* @return Guess The guess for the maximum length
* @return Guess The guess for the maximum length
*/
public function guessMaxLengthForConstraint(Constraint $constraint)
{
@ -282,7 +282,7 @@ class ValidatorTypeGuesser implements FormTypeGuesserInterface
* @param mixed $default The default value assumed if no other value
* can be guessed.
*
* @return Guess The guessed value with the highest confidence
* @return Guess The guessed value with the highest confidence
*/
protected function guess($class, $property, \Closure $closure, $defaultValue = null)
{

View File

@ -0,0 +1,107 @@
<?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\Form\Util\PropertyPathInterface;
use Symfony\Component\Form\Exception\ErrorMappingException;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class MappingRule
{
/**
* @var FormInterface
*/
private $origin;
/**
* @var string
*/
private $propertyPath;
/**
* @var string
*/
private $targetPath;
public function __construct(FormInterface $origin, $propertyPath, $targetPath)
{
$this->origin = $origin;
$this->propertyPath = $propertyPath;
$this->targetPath = $targetPath;
}
/**
* @return FormInterface
*/
public function getOrigin()
{
return $this->origin;
}
/**
* Matches a property path against the rule path.
*
* If the rule matches, the form mapped by the rule is returned.
* Otherwise this method returns false.
*
* @param string $propertyPath The property path to match against the rule.
*
* @return Boolean|FormInterface The mapped form or false.
*/
public function match($propertyPath)
{
if ($propertyPath === (string) $this->propertyPath) {
return $this->getTarget();
}
return false;
}
/**
* Matches a property path against a prefix of the rule path.
*
* @param string $propertyPath The property path to match against the rule.
*
* @return Boolean Whether the property path is a prefix of the rule or not.
*/
public function isPrefix($propertyPath)
{
$length = strlen($propertyPath);
$prefix = substr($this->propertyPath, 0, $length);
$next = isset($this->propertyPath[$length]) ? $this->propertyPath[$length] : null;
return $prefix === $propertyPath && ('[' === $next || '.' === $next);
}
/**
* @return FormInterface
*
* @throws ErrorMappingException
*/
public function getTarget()
{
$childNames = explode('.', $this->targetPath);
$target = $this->origin;
foreach ($childNames as $childName) {
if (!$target->has($childName)) {
throw new ErrorMappingException(sprintf('The child "%s" of "%s" mapped by the rule "%s" in "%s" does not exist.', $childName, $target->getName(), $this->targetPath, $this->origin->getName()));
}
$target = $target->get($childName);
}
return $target;
}
}

View File

@ -0,0 +1,45 @@
<?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\Form\Util\PropertyPath;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class RelativePath extends PropertyPath
{
/**
* @var FormInterface
*/
private $root;
/**
* @param FormInterface $root
* @param string $propertyPath
*/
public function __construct(FormInterface $root, $propertyPath)
{
parent::__construct($propertyPath);
$this->root = $root;
}
/**
* @return FormInterface
*/
public function getRoot()
{
return $this->root;
}
}

View File

@ -0,0 +1,299 @@
<?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\Form\Util\VirtualFormAwareIterator;
use Symfony\Component\Form\Util\PropertyPathIterator;
use Symfony\Component\Form\Util\PropertyPathBuilder;
use Symfony\Component\Form\Util\PropertyPathIteratorInterface;
use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationPathIterator;
use Symfony\Component\Form\FormError;
use Symfony\Component\Validator\ConstraintViolation;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ViolationMapper implements ViolationMapperInterface
{
/**
* @var FormInterface
*/
private $scope;
/**
* @var array
*/
private $children;
/**
* @var array
*/
private $rules = array();
/**
* @var Boolean
*/
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;
// In general, mapping happens from the root form to the leaf forms
// First, the rules of the root form are applied to determine
// the subsequent descendant. The rules of this descendant are then
// applied to find the next and so on, until we have found the
// most specific form that matches the violation.
// If any of the forms found in this process is not synchronized,
// mapping is aborted. Non-synchronized forms could not reverse
// transform the value entered by the user, thus any further violations
// caused by the (invalid) reverse transformed value should be
// ignored.
if (null !== $relativePath) {
// Set the scope to the root of the relative path
// This root will usually be $form. If the path contains
// an unmapped form though, the last unmapped form found
// will be the root of the path.
$this->setScope($relativePath->getRoot());
$it = new PropertyPathIterator($relativePath);
while ($this->isValidScope() && null !== ($child = $this->matchChild($it))) {
$this->setScope($child);
$it->next();
$match = true;
}
}
// This case happens if an error happened in the data under a
// virtual form that does not match any of the children of
// the virtual form.
if (!$match) {
// If we could not map the error to anything more specific
// than the root element, map it to the innermost directly
// mapped form of the violation path
// e.g. "children[foo].children[bar].data.baz"
// Here the innermost directly mapped child is "bar"
$it = new ViolationPathIterator($violationPath);
// The overhead of setScope() is not needed anymore here
$this->scope = $form;
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;
}
$this->scope = $this->scope->get($it->current());
$it->next();
}
}
// Follow dot rules until we have the final target
$mapping = $this->scope->getAttribute('error_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->isValidScope()) {
$this->scope->addError(new FormError(
$violation->getMessageTemplate(),
$violation->getMessageParameters(),
$violation->getMessagePluralization()
));
}
}
/**
* Tries to match the beginning of the property path at the
* current position against the children of the scope.
*
* If a matching child is found, it is returned. Otherwise
* null is returned.
*
* @param PropertyPathIteratorInterface $it The iterator at its current position.
*
* @return null|FormInterface The found match or null.
*/
private function matchChild(PropertyPathIteratorInterface $it)
{
// Remember at what property path underneath "data"
// we are looking. Check if there is a child with that
// path, otherwise increase path by one more piece
$chunk = '';
$foundChild = null;
$foundAtIndex = 0;
// Make the path longer until we find a matching child
while (true) {
if (!$it->valid()) {
return null;
}
if ($it->isIndex()) {
$chunk .= '[' . $it->current() . ']';
} else {
$chunk .= ('' === $chunk ? '' : '.') . $it->current();
}
// Test mapping rules as long as we have any
foreach ($this->rules as $key => $rule) {
/* @var MappingRule $rule */
// Mapping rule matches completely, terminate.
if (false !== ($form = $rule->match($chunk))) {
return $form;
}
// Keep only rules that have $chunk as prefix
if (!$rule->isPrefix($chunk)) {
unset($this->rules[$key]);
}
}
// Test children unless we already found one
if (null === $foundChild) {
foreach ($this->children as $child) {
/* @var FormInterface $child */
$childPath = (string) $child->getPropertyPath();
// Child found, move scope inwards
if ($chunk === $childPath) {
$foundChild = $child;
$foundAtIndex = $it->key();
}
}
}
// Add element to the chunk
$it->next();
// If we reached the end of the path or if there are no
// more matching mapping rules, return the found child
if (null !== $foundChild && (!$it->valid() || count($this->rules) === 0)) {
// Reset index in case we tried to find mapping
// rules further down the path
$it->seek($foundAtIndex);
return $foundChild;
}
}
return null;
}
/**
* Reconstructs a property path from a violation path and a form tree.
*
* @param ViolationPath $violationPath The violation path.
* @param FormInterface $origin The root form of the tree.
*
* @return RelativePath The reconstructed path.
*/
private function reconstructPath(ViolationPath $violationPath, FormInterface $origin)
{
$propertyPathBuilder = new PropertyPathBuilder($violationPath);
$it = $violationPath->getIterator();
$scope = $origin;
// Remember the current index in the builder
$i = 0;
// Expand elements that map to a form (like "children[address]")
for ($it->rewind(); $it->valid() && $it->mapsForm(); $it->next()) {
if (!$scope->has($it->current())) {
// Scope relates to a form that does not exist
// Bail out
break;
}
// Process child form
$scope = $scope->get($it->current());
if ($scope->getConfig()->getVirtual()) {
// Form is virtual
// Cut the piece out of the property path and proceed
$propertyPathBuilder->remove($i);
} elseif (!$scope->getConfig()->getMapped()) {
// Form is not mapped
// Set the form as new origin and strip everything
// we have so far in the path
$origin = $scope;
$propertyPathBuilder->remove(0, $i + 1);
$i = 0;
} else {
/* @var \Symfony\Component\Form\Util\PropertyPathInterface $propertyPath */
$propertyPath = $scope->getPropertyPath();
if (null === $propertyPath) {
// Property path of a mapped form is null
// Should not happen, bail out
break;
}
$propertyPathBuilder->replace($i, 1, $propertyPath);
$i += $propertyPath->getLength();
}
}
$finalPath = $propertyPathBuilder->getPropertyPath();
return null !== $finalPath ? new RelativePath($origin, $finalPath) : null;
}
/**
* Sets the scope of the mapper to the given form.
*
* The scope is the currently found most specific form that
* an error should be mapped to. After setting the scope, the
* mapper will try to continue to find more specific matches in
* the children of scope. If it cannot, the error will be
* mapped to this scope.
*
* @param FormInterface $form The current scope.
*/
private function setScope(FormInterface $form)
{
$this->scope = $form;
$this->children = new \RecursiveIteratorIterator(
new VirtualFormAwareIterator($form->getChildren())
);
foreach ($form->getAttribute('error_mapping') as $propertyPath => $targetPath) {
// Dot rules are considered at the very end
if ('.' !== $propertyPath) {
$this->rules[] = new MappingRule($form, $propertyPath, $targetPath);
}
}
}
/**
* @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

@ -0,0 +1,267 @@
<?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\Util\PropertyPath;
use Symfony\Component\Form\Util\PropertyPathIterator;
use Symfony\Component\Form\Util\PropertyPathInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ViolationPath implements \IteratorAggregate, PropertyPathInterface
{
/**
* @var array
*/
private $elements = array();
/**
* @var array
*/
private $positions = array();
/**
* @var array
*/
private $isIndex = array();
/**
* @var array
*/
private $mapsForm = array();
/**
* @var string
*/
private $pathAsString = '';
/**
* @var integer
*/
private $length = 0;
/**
* Creates a new violation path from a string.
*
* @param string $violationPath The property path of a {@link ConstraintViolation}
* object.
*/
public function __construct($violationPath)
{
$path = new PropertyPath($violationPath);
$elements = $path->getElements();
$positions = $path->getPositions();
$data = false;
for ($i = 0, $l = count($elements); $i < $l; ++$i) {
if (!$data) {
// The element "data" has not yet been passed
if ('children' === $elements[$i] && $path->isProperty($i)) {
// Skip element "children"
++$i;
// Next element must exist and must be an index
// Otherwise consider this the end of the path
if ($i >= $l || !$path->isIndex($i)) {
break;
}
$this->elements[] = $elements[$i];
$this->positions[] = $positions[$i];
$this->isIndex[] = true;
$this->mapsForm[] = true;
} elseif ('data' === $elements[$i] && $path->isProperty($i)) {
// Skip element "data"
++$i;
// End of path
if ($i >= $l) {
break;
}
$this->elements[] = $elements[$i];
$this->positions[] = $positions[$i];
$this->isIndex[] = $path->isIndex($i);
$this->mapsForm[] = false;
$data = true;
} else {
// Neither "children" nor "data" property found
// Consider this the end of the path
break;
}
} else {
// Already after the "data" element
// Pick everything as is
$this->elements[] = $elements[$i];
$this->positions[] = $positions[$i];
$this->isIndex[] = $path->isIndex($i);
$this->mapsForm[] = false;
}
}
$this->length = count($this->elements);
$this->pathAsString = $violationPath;
$this->resizeString();
}
/**
* {@inheritdoc}
*/
public function __toString()
{
return $this->pathAsString;
}
/**
* {@inheritdoc}
*/
public function getPositions()
{
return $this->positions;
}
/**
* {@inheritdoc}
*/
public function getLength()
{
return $this->length;
}
/**
* {@inheritdoc}
*/
public function getParent()
{
if ($this->length <= 1) {
return null;
}
$parent = clone $this;
--$parent->length;
array_pop($parent->elements);
array_pop($parent->isIndex);
array_pop($parent->mapsForm);
array_pop($parent->positions);
$parent->resizeString();
return $parent;
}
/**
* {@inheritdoc}
*/
public function getElements()
{
return $this->elements;
}
/**
* {@inheritdoc}
*/
public function getElement($index)
{
if (!isset($this->elements[$index])) {
throw new \OutOfBoundsException('The index ' . $index . ' is not within the violation path');
}
return $this->elements[$index];
}
/**
* {@inheritdoc}
*/
public function isProperty($index)
{
if (!isset($this->isIndex[$index])) {
throw new \OutOfBoundsException('The index ' . $index . ' is not within the violation path');
}
return !$this->isIndex[$index];
}
/**
* {@inheritdoc}
*/
public function isIndex($index)
{
if (!isset($this->isIndex[$index])) {
throw new \OutOfBoundsException('The index ' . $index . ' is not within the violation path');
}
return $this->isIndex[$index];
}
/**
* Returns whether an element maps directly to a form.
*
* Consider the following violation path:
*
* <code>
* children[address].children[office].data.street
* </code>
*
* In this example, "address" and "office" map to forms, while
* "street does not.
*
* @param integer $index The element index.
*
* @return Boolean Whether the element maps to a form.
*
* @throws \OutOfBoundsException If the offset is invalid.
*/
public function mapsForm($index)
{
if (!isset($this->mapsForm[$index])) {
throw new \OutOfBoundsException('The index ' . $index . ' is not within the violation path');
}
return $this->mapsForm[$index];
}
/**
* Returns a new iterator for this path
*
* @return ViolationPathIterator
*/
public function getIterator()
{
return new ViolationPathIterator($this);
}
/**
* Resizes the string representation to match the number of elements.
*/
private function resizeString()
{
$lastIndex = $this->length - 1;
if ($lastIndex < 0) {
$this->pathAsString = '';
} else {
// +1 for the dot/opening bracket
$length = $this->positions[$lastIndex] + strlen($this->elements[$lastIndex]) + 1;
if ($this->isIndex[$lastIndex]) {
// +1 for the closing bracket
++$length;
}
$this->pathAsString = substr($this->pathAsString, 0, $length);
}
}
}

View File

@ -0,0 +1,31 @@
<?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\Util\PropertyPathIterator;
use Symfony\Component\Form\Util\PropertyPath;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ViolationPathIterator extends PropertyPathIterator
{
public function __construct(ViolationPath $violationPath)
{
parent::__construct($violationPath);
}
public function mapsForm()
{
return $this->path->mapsForm($this->key());
}
}

View File

@ -17,6 +17,7 @@ use Symfony\Component\Form\Exception\FormException;
use Symfony\Component\Form\Exception\AlreadyBoundException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Util\PropertyPath;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@ -58,10 +59,10 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class Form implements \IteratorAggregate, FormInterface
{
/**
* The name of this form
* @var string
* The form's configuration
* @var FormConfigInterface
*/
private $name;
private $config;
/**
* The parent of this form
@ -75,36 +76,18 @@ class Form implements \IteratorAggregate, FormInterface
*/
private $children = array();
/**
* The mapper for mapping data to children and back
* @var DataMapperInterface
*/
private $dataMapper;
/**
* The errors of this form
* @var array An array of FormError instances
*/
private $errors = array();
/**
* Whether added errors should bubble up to the parent
* @var Boolean
*/
private $errorBubbling;
/**
* Whether this form is bound
* @var Boolean
*/
private $bound = false;
/**
* Whether this form may or may not be empty
* @var Boolean
*/
private $required;
/**
* The form data in application format
* @var mixed
@ -123,32 +106,12 @@ class Form implements \IteratorAggregate, FormInterface
*/
private $clientData;
/**
* Data used for the client data when no value is bound
* @var mixed
*/
private $emptyData = '';
/**
* The bound values that don't belong to any children
* @var array
*/
private $extraData = array();
/**
* The transformers for transforming from application to normalized format
* and back
* @var array An array of DataTransformerInterface
*/
private $normTransformers;
/**
* The transformers for transforming from normalized to client format and
* back
* @var array An array of DataTransformerInterface
*/
private $clientTransformers;
/**
* Whether the data in application, normalized and client format is
* synchronized. Data may not be synchronized if transformation errors
@ -158,78 +121,19 @@ class Form implements \IteratorAggregate, FormInterface
private $synchronized = true;
/**
* The validators attached to this form
* @var array An array of FormValidatorInterface instances
* Creates a new form based on the given configuration.
*
* @param FormConfigInterface $config The form configuration.
*/
private $validators;
/**
* Whether this form may only be read, but not bound
* @var Boolean
*/
private $disabled = false;
/**
* The dispatcher for distributing events of this form
* @var Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
private $dispatcher;
/**
* Key-value store for arbitrary attributes attached to this form
* @var array
*/
private $attributes;
/**
* The FormTypeInterface instances used to create this form
* @var array An array of FormTypeInterface
*/
private $types;
public function __construct($name, EventDispatcherInterface $dispatcher,
array $types = array(), array $clientTransformers = array(),
array $normTransformers = array(),
DataMapperInterface $dataMapper = null, array $validators = array(),
$required = false, $disabled = false, $errorBubbling = null,
$emptyData = null, array $attributes = array())
public function __construct(FormConfigInterface $config)
{
$name = (string) $name;
self::validateName($name);
foreach ($clientTransformers as $transformer) {
if (!$transformer instanceof DataTransformerInterface) {
throw new UnexpectedTypeException($transformer, 'Symfony\Component\Form\DataTransformerInterface');
}
if (!$config instanceof UnmodifiableFormConfig) {
$config = new UnmodifiableFormConfig($config);
}
foreach ($normTransformers as $transformer) {
if (!$transformer instanceof DataTransformerInterface) {
throw new UnexpectedTypeException($transformer, 'Symfony\Component\Form\DataTransformerInterface');
}
}
$this->config = $config;
foreach ($validators as $validator) {
if (!$validator instanceof FormValidatorInterface) {
throw new UnexpectedTypeException($validator, 'Symfony\Component\Form\FormValidatorInterface');
}
}
$this->name = $name;
$this->dispatcher = $dispatcher;
$this->types = $types;
$this->clientTransformers = $clientTransformers;
$this->normTransformers = $normTransformers;
$this->dataMapper = $dataMapper;
$this->validators = $validators;
$this->required = (Boolean) $required;
$this->disabled = (Boolean) $disabled;
$this->errorBubbling = (Boolean) $errorBubbling;
$this->emptyData = $emptyData;
$this->attributes = $attributes;
$this->setData(null);
$this->setData($config->getData());
}
public function __clone()
@ -239,39 +143,66 @@ class Form implements \IteratorAggregate, FormInterface
}
}
/**
* Returns the configuration of the form.
*
* @return UnmodifiableFormConfig The form's immutable configuration.
*/
public function getConfig()
{
return $this->config;
}
/**
* Returns the name by which the form is identified in forms.
*
* @return string The name of the form.
* @return string The name of the form.
*/
public function getName()
{
return $this->name;
return $this->config->getName();
}
/**
* {@inheritdoc}
*/
public function getPropertyPath()
{
if (null !== $this->config->getPropertyPath()) {
return $this->config->getPropertyPath();
}
if (null === $this->getName() || '' === $this->getName()) {
return null;
}
if ($this->hasParent() && null === $this->getParent()->getConfig()->getDataClass()) {
return new PropertyPath('[' . $this->getName() . ']');
}
return new PropertyPath($this->getName());
}
/**
* Returns the types used by this form.
*
* @return array An array of FormTypeInterface
*
* @deprecated Deprecated since version 2.1, to be removed in 2.3. Use
* {@link getConfig()} and {@link FormConfigInterface::getTypes()} instead.
*/
public function getTypes()
{
return $this->types;
return $this->config->getTypes();
}
/**
* Returns whether the form is required to be filled out.
*
* If the form has a parent and the parent is not required, this method
* will always return false. Otherwise the value set with setRequired()
* is returned.
*
* @return Boolean
* {@inheritdoc}
*/
public function isRequired()
{
if (null === $this->parent || $this->parent->isRequired()) {
return $this->required;
return $this->config->getRequired();
}
return false;
@ -283,7 +214,7 @@ class Form implements \IteratorAggregate, FormInterface
public function isDisabled()
{
if (null === $this->parent || !$this->parent->isDisabled()) {
return $this->disabled;
return $this->config->getDisabled();
}
return true;
@ -302,8 +233,8 @@ class Form implements \IteratorAggregate, FormInterface
throw new AlreadyBoundException('You cannot set the parent of a bound form');
}
if ('' === $this->getName()) {
throw new FormException('Form with empty name can not have parent form.');
if ('' === $this->config->getName()) {
throw new FormException('A form with an empty name cannot have a parent form.');
}
$this->parent = $parent;
@ -334,7 +265,7 @@ class Form implements \IteratorAggregate, FormInterface
/**
* Returns the root of the form tree.
*
* @return FormInterface The root of the tree
* @return FormInterface The root of the tree
*/
public function getRoot()
{
@ -354,23 +285,25 @@ class Form implements \IteratorAggregate, FormInterface
/**
* Returns whether the form has an attribute with the given name.
*
* @param string $name The name of the attribute
* @param string $name The name of the attribute.
*
* @return Boolean
* @return Boolean Whether the attribute exists.
*/
public function hasAttribute($name)
{
return isset($this->attributes[$name]);
return $this->config->hasAttribute($name);
}
/**
* Returns the value of the attributes with the given name.
*
* @param string $name The name of the attribute
* @param string $name The name of the attribute
*
* @return mixed The attribute value.
*/
public function getAttribute($name)
{
return $this->attributes[$name];
return $this->config->getAttribute($name);
}
/**
@ -387,15 +320,15 @@ class Form implements \IteratorAggregate, FormInterface
}
$event = new DataEvent($this, $appData);
$this->dispatcher->dispatch(FormEvents::PRE_SET_DATA, $event);
$this->config->getEventDispatcher()->dispatch(FormEvents::PRE_SET_DATA, $event);
// Hook to change content of the data
$event = new FilterDataEvent($this, $appData);
$this->dispatcher->dispatch(FormEvents::SET_DATA, $event);
$this->config->getEventDispatcher()->dispatch(FormEvents::SET_DATA, $event);
$appData = $event->getData();
// Treat data as strings unless a value transformer exists
if (!$this->clientTransformers && !$this->normTransformers && is_scalar($appData)) {
if (!$this->config->getClientTransformers() && !$this->config->getNormTransformers() && is_scalar($appData)) {
$appData = (string) $appData;
}
@ -403,18 +336,49 @@ class Form implements \IteratorAggregate, FormInterface
$normData = $this->appToNorm($appData);
$clientData = $this->normToClient($normData);
// Validate if client data matches data class (unless empty)
if (!empty($clientData)) {
$dataClass = $this->config->getDataClass();
if (null === $dataClass && is_object($clientData)) {
$expectedType = 'scalar';
if (count($this->children) > 0 && $this->config->getDataMapper()) {
$expectedType = 'array';
}
throw new FormException(
'The form\'s client data is expected to be of type ' . $expectedType . ', ' .
'but is an instance of class ' . get_class($clientData) . '. You ' .
'can avoid this error by setting the "data_class" option to ' .
'"' . get_class($clientData) . '" or by adding a client transformer ' .
'that transforms ' . get_class($clientData) . ' to ' . $expectedType . '.'
);
}
if (null !== $dataClass && !$clientData instanceof $dataClass) {
throw new FormException(
'The form\'s client data is expected to be an instance of class ' .
$dataClass . ', but has the type ' . gettype($clientData) . '. You ' .
'can avoid this error by setting the "data_class" option to ' .
'null or by adding a client transformer that transforms ' .
gettype($clientData) . ' to ' . $dataClass . '.'
);
}
}
$this->appData = $appData;
$this->normData = $normData;
$this->clientData = $clientData;
$this->synchronized = true;
if (count($this->children) > 0 && $this->dataMapper) {
if (count($this->children) > 0 && $this->config->getDataMapper()) {
// Update child forms from the data
$this->dataMapper->mapDataToForms($clientData, $this->children);
$this->config->getDataMapper()->mapDataToForms($clientData, $this->children);
}
$event = new DataEvent($this, $appData);
$this->dispatcher->dispatch(FormEvents::POST_SET_DATA, $event);
$this->config->getEventDispatcher()->dispatch(FormEvents::POST_SET_DATA, $event);
return $this;
}
@ -483,7 +447,7 @@ class Form implements \IteratorAggregate, FormInterface
$this->errors = array();
$event = new DataEvent($this, $clientData);
$this->dispatcher->dispatch(FormEvents::PRE_BIND, $event);
$this->config->getEventDispatcher()->dispatch(FormEvents::PRE_BIND, $event);
$appData = null;
$normData = null;
@ -492,7 +456,7 @@ class Form implements \IteratorAggregate, FormInterface
// Hook to change content of the data bound by the browser
$event = new FilterDataEvent($this, $clientData);
$this->dispatcher->dispatch(FormEvents::BIND_CLIENT_DATA, $event);
$this->config->getEventDispatcher()->dispatch(FormEvents::BIND_CLIENT_DATA, $event);
$clientData = $event->getData();
if (count($this->children) > 0) {
@ -520,13 +484,13 @@ class Form implements \IteratorAggregate, FormInterface
// If we have a data mapper, use old client data and merge
// data from the children into it later
if ($this->dataMapper) {
if ($this->config->getDataMapper()) {
$clientData = $this->getClientData();
}
}
if (null === $clientData || '' === $clientData) {
$emptyData = $this->emptyData;
$emptyData = $this->config->getEmptyData();
if ($emptyData instanceof \Closure) {
$emptyData = $emptyData($this, $clientData);
@ -536,8 +500,8 @@ class Form implements \IteratorAggregate, FormInterface
}
// Merge form data from children into existing client data
if (count($this->children) > 0 && $this->dataMapper && null !== $clientData) {
$this->dataMapper->mapFormsToData($this->children, $clientData);
if (count($this->children) > 0 && $this->config->getDataMapper() && null !== $clientData) {
$this->config->getDataMapper()->mapFormsToData($this->children, $clientData);
}
try {
@ -551,7 +515,7 @@ class Form implements \IteratorAggregate, FormInterface
// Hook to change content of the data into the normalized
// representation
$event = new FilterDataEvent($this, $normData);
$this->dispatcher->dispatch(FormEvents::BIND_NORM_DATA, $event);
$this->config->getEventDispatcher()->dispatch(FormEvents::BIND_NORM_DATA, $event);
$normData = $event->getData();
// Synchronize representations - must not change the content!
@ -567,9 +531,9 @@ class Form implements \IteratorAggregate, FormInterface
$this->synchronized = $synchronized;
$event = new DataEvent($this, $clientData);
$this->dispatcher->dispatch(FormEvents::POST_BIND, $event);
$this->config->getEventDispatcher()->dispatch(FormEvents::POST_BIND, $event);
foreach ($this->validators as $validator) {
foreach ($this->config->getValidators() as $validator) {
$validator->validate($this);
}
@ -590,24 +554,26 @@ class Form implements \IteratorAggregate, FormInterface
*/
public function bindRequest(Request $request)
{
$name = $this->config->getName();
// Store the bound data in case of a post request
switch ($request->getMethod()) {
case 'POST':
case 'PUT':
case 'DELETE':
case 'PATCH':
if ('' === $this->getName()) {
if ('' === $name) {
// Form bound without name
$params = $request->request->all();
$files = $request->files->all();
} elseif ($this->hasChildren()) {
// Form bound with name and children
$params = $request->request->get($this->getName(), array());
$files = $request->files->get($this->getName(), array());
$params = $request->request->get($name, array());
$files = $request->files->get($name, array());
} else {
// Form bound with name, but without children
$params = $request->request->get($this->getName(), null);
$files = $request->files->get($this->getName(), null);
$params = $request->request->get($name, null);
$files = $request->files->get($name, null);
}
if (is_array($params) && is_array($files)) {
$data = array_replace_recursive($params, $files);
@ -616,7 +582,7 @@ class Form implements \IteratorAggregate, FormInterface
}
break;
case 'GET':
$data = '' === $this->getName() ? $request->query->all() : $request->query->get($this->getName(), array());
$data = '' === $name ? $request->query->all() : $request->query->get($name, array());
break;
default:
throw new FormException(sprintf('The request method "%s" is not supported', $request->getMethod()));
@ -628,7 +594,7 @@ class Form implements \IteratorAggregate, FormInterface
/**
* Returns the normalized data of the form.
*
* @return mixed When the form is not bound, the default data is returned.
* @return mixed When the form is not bound, the default data is returned.
* When the form is bound, the normalized bound data is
* returned if the form is valid, null otherwise.
*/
@ -659,10 +625,13 @@ class Form implements \IteratorAggregate, FormInterface
* Returns whether errors bubble up to the parent.
*
* @return Boolean
*
* @deprecated Deprecated since version 2.1, to be removed in 2.3. Use
* {@link getConfig()} and {@link FormConfigInterface::getErrorBubbling()} instead.
*/
public function getErrorBubbling()
{
return $this->errorBubbling;
return $this->config->getErrorBubbling();
}
/**
@ -730,7 +699,7 @@ class Form implements \IteratorAggregate, FormInterface
/**
* Returns whether or not there are errors.
*
* @return Boolean true if form is bound and not valid
* @return Boolean true if form is bound and not valid
*/
public function hasErrors()
{
@ -743,7 +712,7 @@ class Form implements \IteratorAggregate, FormInterface
/**
* Returns all errors.
*
* @return array An array of FormError instances that occurred during binding
* @return array An array of FormError instances that occurred during binding
*/
public function getErrors()
{
@ -784,20 +753,26 @@ class Form implements \IteratorAggregate, FormInterface
* Returns the DataTransformers.
*
* @return array An array of DataTransformerInterface
*
* @deprecated Deprecated since version 2.1, to be removed in 2.3. Use
* {@link getConfig()} and {@link FormConfigInterface::getNormTransformers()} instead.
*/
public function getNormTransformers()
{
return $this->normTransformers;
return $this->config->getNormTransformers();
}
/**
* Returns the DataTransformers.
*
* @return array An array of DataTransformerInterface
*
* @deprecated Deprecated since version 2.1, to be removed in 2.3. Use
* {@link getConfig()} and {@link FormConfigInterface::getClientTransformers()} instead.
*/
public function getClientTransformers()
{
return $this->clientTransformers;
return $this->config->getClientTransformers();
}
/**
@ -821,11 +796,7 @@ class Form implements \IteratorAggregate, FormInterface
}
/**
* Adds a child to the form.
*
* @param FormInterface $child The FormInterface to add as a child
*
* @return Form the current form
* {@inheritdoc}
*/
public function add(FormInterface $child)
{
@ -837,19 +808,15 @@ class Form implements \IteratorAggregate, FormInterface
$child->setParent($this);
if ($this->dataMapper) {
$this->dataMapper->mapDataToForm($this->getClientData(), $child);
if ($this->config->getDataMapper()) {
$this->config->getDataMapper()->mapDataToForm($this->getClientData(), $child);
}
return $this;
}
/**
* Removes a child from the form.
*
* @param string $name The name of the child to remove
*
* @return Form the current form
* {@inheritdoc}
*/
public function remove($name)
{
@ -867,11 +834,7 @@ class Form implements \IteratorAggregate, FormInterface
}
/**
* Returns whether a child with the given name exists.
*
* @param string $name
*
* @return Boolean
* {@inheritdoc}
*/
public function has($name)
{
@ -879,13 +842,7 @@ class Form implements \IteratorAggregate, FormInterface
}
/**
* Returns the child with the given name.
*
* @param string $name
*
* @return FormInterface
*
* @throws \InvalidArgumentException if the child does not exist
* {@inheritdoc}
*/
public function get($name)
{
@ -913,7 +870,7 @@ class Form implements \IteratorAggregate, FormInterface
*
* @param string $name The offset of the value to get
*
* @return FormInterface A form instance
* @return FormInterface A form instance
*/
public function offsetGet($name)
{
@ -974,11 +931,11 @@ class Form implements \IteratorAggregate, FormInterface
$parent = $this->parent->createView();
}
$view = new FormView($this->name);
$view = new FormView($this->config->getName());
$view->setParent($parent);
$types = (array) $this->types;
$types = (array) $this->config->getTypes();
foreach ($types as $type) {
$type->buildView($view, $this);
@ -1012,7 +969,7 @@ class Form implements \IteratorAggregate, FormInterface
*/
private function appToNorm($value)
{
foreach ($this->normTransformers as $transformer) {
foreach ($this->config->getNormTransformers() as $transformer) {
$value = $transformer->transform($value);
}
@ -1028,8 +985,10 @@ class Form implements \IteratorAggregate, FormInterface
*/
private function normToApp($value)
{
for ($i = count($this->normTransformers) - 1; $i >= 0; --$i) {
$value = $this->normTransformers[$i]->reverseTransform($value);
$transformers = $this->config->getNormTransformers();
for ($i = count($transformers) - 1; $i >= 0; --$i) {
$value = $transformers[$i]->reverseTransform($value);
}
return $value;
@ -1044,13 +1003,13 @@ class Form implements \IteratorAggregate, FormInterface
*/
private function normToClient($value)
{
if (!$this->clientTransformers) {
if (!$this->config->getClientTransformers()) {
// Scalar values should always be converted to strings to
// facilitate differentiation between empty ("") and zero (0).
return null === $value || is_scalar($value) ? (string) $value : $value;
}
foreach ($this->clientTransformers as $transformer) {
foreach ($this->config->getClientTransformers() as $transformer) {
$value = $transformer->transform($value);
}
@ -1066,55 +1025,16 @@ class Form implements \IteratorAggregate, FormInterface
*/
private function clientToNorm($value)
{
if (!$this->clientTransformers) {
$transformers = $this->config->getClientTransformers();
if (!$transformers) {
return '' === $value ? null : $value;
}
for ($i = count($this->clientTransformers) - 1; $i >= 0; --$i) {
$value = $this->clientTransformers[$i]->reverseTransform($value);
for ($i = count($transformers) - 1; $i >= 0; --$i) {
$value = $transformers[$i]->reverseTransform($value);
}
return $value;
}
/**
* Validates whether the given variable is a valid form name.
*
* @param string $name The tested form name.
*
* @throws UnexpectedTypeException If the name is not a string.
* @throws \InvalidArgumentException If the name contains invalid characters.
*/
static public function validateName($name)
{
if (!is_string($name)) {
throw new UnexpectedTypeException($name, 'string');
}
if (!self::isValidName($name)) {
throw new \InvalidArgumentException(sprintf(
'The name "%s" contains illegal characters. Names should start with a letter, digit or underscore and only contain letters, digits, numbers, underscores ("_"), hyphens ("-") and colons (":").',
$name
));
}
}
/**
* Returns whether the given variable contains a valid form name.
*
* A name is accepted if it
*
* * is empty
* * starts with a letter, digit or underscore
* * contains only letters, digits, numbers, underscores ("_"),
* hyphens ("-") and colons (":")
*
* @param string $name The tested form name.
*
* @return Boolean Whether the name is valid.
*/
static public function isValidName($name)
{
return '' === $name || preg_match('/^[a-zA-Z0-9_][a-zA-Z0-9_\-:]*$/D', $name);
}
}

View File

@ -18,101 +18,32 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* A builder for creating {@link Form} instances.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FormBuilder
class FormBuilder extends FormConfig
{
/**
* @var string
*/
private $name;
/**
* The form data in application format
* @var mixed
*/
private $appData;
/**
* The event dispatcher
* The form factory.
*
* @var EventDispatcherInterface
*/
private $dispatcher;
/**
* The form factory
* @var FormFactoryInterface
*/
private $factory;
/**
* @var Boolean
*/
private $disabled;
/**
* @var Boolean
*/
private $required;
/**
* The transformers for transforming from normalized to client format and
* back
* @var array An array of DataTransformerInterface
*/
private $clientTransformers = array();
/**
* The transformers for transforming from application to normalized format
* and back
* @var array An array of DataTransformerInterface
*/
private $normTransformers = array();
/**
* @var array An array of FormValidatorInterface
*/
private $validators = array();
/**
* Key-value store for arbitrary attributes attached to the form
* @var array
*/
private $attributes = array();
/**
* @var array An array of FormTypeInterface
*/
private $types = array();
/**
* @var string
*/
private $dataClass;
/**
* The children of the form
* The children of the form builder.
*
* @var array
*/
private $children = array();
/**
* @var DataMapperInterface
* The data of children who haven't been converted to form builders yet.
*
* @var array
*/
private $dataMapper;
/**
* Whether added errors should bubble up to the parent
* @var Boolean
*/
private $errorBubbling;
/**
* Data used for the client data when no value is bound
* @var mixed
*/
private $emptyData = '';
private $unresolvedChildren = array();
private $currentLoadingType;
@ -123,23 +54,18 @@ class FormBuilder
private $parent;
/**
* Constructor.
* Creates a new form builder.
*
* @param string $name
* @param FormFactoryInterface $factory
* @param EventDispatcherInterface $dispatcher
* @param string $dataClass
* @param EventDispatcherInterface $dispatcher
* @param FormFactoryInterface $factory
*/
public function __construct($name, FormFactoryInterface $factory, EventDispatcherInterface $dispatcher, $dataClass = null)
public function __construct($name, $dataClass, EventDispatcherInterface $dispatcher, FormFactoryInterface $factory)
{
$name = (string) $name;
parent::__construct($name, $dataClass, $dispatcher);
Form::validateName($name);
$this->name = $name;
$this->factory = $factory;
$this->dispatcher = $dispatcher;
$this->dataClass = $dataClass;
}
/**
@ -152,383 +78,6 @@ class FormBuilder
return $this->factory;
}
/**
* Returns the name of the form.
*
* @return string The form name
*/
public function getName()
{
return $this->name;
}
/**
* Updates the field with default data.
*
* @param array $appData The data formatted as expected for the underlying object
*
* @return FormBuilder The current builder
*/
public function setData($appData)
{
$this->appData = $appData;
return $this;
}
/**
* Returns the data in the format needed for the underlying object.
*
* @return mixed
*/
public function getData()
{
return $this->appData;
}
/**
* Set whether the form is disabled.
*
* @param Boolean $disabled Whether the form is disabled
*
* @return FormBuilder The current builder
*/
public function setDisabled($disabled)
{
$this->disabled = (Boolean) $disabled;
return $this;
}
/**
* Returns whether the form is disabled.
*
* @return Boolean Whether the form is disabled
*/
public function getDisabled()
{
return $this->disabled;
}
/**
* Sets whether this field is required to be filled out when bound.
*
* @param Boolean $required
*
* @return FormBuilder The current builder
*/
public function setRequired($required)
{
$this->required = (Boolean) $required;
return $this;
}
/**
* Returns whether this field is required to be filled out when bound.
*
* @return Boolean Whether this field is required
*/
public function getRequired()
{
return $this->required;
}
/**
* Sets whether errors bubble up to the parent.
*
* @param type $errorBubbling
*
* @return FormBuilder The current builder
*/
public function setErrorBubbling($errorBubbling)
{
$this->errorBubbling = null === $errorBubbling ? null : (Boolean) $errorBubbling;
return $this;
}
/**
* Returns whether errors bubble up to the parent.
*
* @return Boolean
*/
public function getErrorBubbling()
{
return $this->errorBubbling;
}
/**
* Adds a validator to the form.
*
* @param FormValidatorInterface $validator The validator
*
* @return FormBuilder The current builder
*
* @deprecated Deprecated since version 2.1, to be removed in 2.3.
*/
public function addValidator(FormValidatorInterface $validator)
{
$this->validators[] = $validator;
return $this;
}
/**
* Returns the validators used by the form.
*
* @return array An array of FormValidatorInterface
*
* @deprecated Deprecated since version 2.1, to be removed in 2.3.
*/
public function getValidators()
{
return $this->validators;
}
/**
* Adds an event listener for events on this field
*
* @see Symfony\Component\EventDispatcher\EventDispatcherInterface::addListener
*
* @return FormBuilder The current builder
*/
public function addEventListener($eventName, $listener, $priority = 0)
{
$this->dispatcher->addListener($eventName, $listener, $priority);
return $this;
}
/**
* Adds an event subscriber for events on this field
*
* @see Symfony\Component\EventDispatcher\EventDispatcherInterface::addSubscriber
*
* @return FormBuilder The current builder
*/
public function addEventSubscriber(EventSubscriberInterface $subscriber)
{
$this->dispatcher->addSubscriber($subscriber);
return $this;
}
/**
* Appends a transformer to the normalization transformer chain
*
* @param DataTransformerInterface $normTransformer
*
* @return FormBuilder The current builder
*/
public function appendNormTransformer(DataTransformerInterface $normTransformer)
{
$this->normTransformers[] = $normTransformer;
return $this;
}
/**
* Prepends a transformer to the normalization transformer chain
*
* @param DataTransformerInterface $normTransformer
*
* @return FormBuilder The current builder
*/
public function prependNormTransformer(DataTransformerInterface $normTransformer)
{
array_unshift($this->normTransformers, $normTransformer);
return $this;
}
/**
* Clears the normalization transformers.
*
* @return FormBuilder The current builder
*/
public function resetNormTransformers()
{
$this->normTransformers = array();
return $this;
}
/**
* Returns all the normalization transformers.
*
* @return array An array of DataTransformerInterface
*/
public function getNormTransformers()
{
return $this->normTransformers;
}
/**
* Appends a transformer to the client transformer chain
*
* @param DataTransformerInterface $clientTransformer
*
* @return FormBuilder The current builder
*/
public function appendClientTransformer(DataTransformerInterface $clientTransformer)
{
$this->clientTransformers[] = $clientTransformer;
return $this;
}
/**
* Prepends a transformer to the client transformer chain
*
* @param DataTransformerInterface $clientTransformer
*
* @return FormBuilder The current builder
*/
public function prependClientTransformer(DataTransformerInterface $clientTransformer)
{
array_unshift($this->clientTransformers, $clientTransformer);
return $this;
}
/**
* Clears the client transformers.
*
* @return FormBuilder The current builder
*/
public function resetClientTransformers()
{
$this->clientTransformers = array();
return $this;
}
/**
* Returns all the client transformers.
*
* @return array An array of DataTransformerInterface
*/
public function getClientTransformers()
{
return $this->clientTransformers;
}
/**
* Sets the value for an attribute.
*
* @param string $name The name of the attribute
* @param string $value The value of the attribute
*
* @return FormBuilder The current builder
*/
public function setAttribute($name, $value)
{
$this->attributes[$name] = $value;
return $this;
}
/**
* Returns the value of the attributes with the given name.
*
* @param string $name The name of the attribute
*/
public function getAttribute($name)
{
return $this->attributes[$name];
}
/**
* Returns whether the form has an attribute with the given name.
*
* @param string $name The name of the attribute
*/
public function hasAttribute($name)
{
return isset($this->attributes[$name]);
}
/**
* Returns all the attributes.
*
* @return array An array of attributes
*/
public function getAttributes()
{
return $this->attributes;
}
/**
* Sets the data mapper used by the form.
*
* @param DataMapperInterface $dataMapper
*
* @return FormBuilder The current builder
*/
public function setDataMapper(DataMapperInterface $dataMapper = null)
{
$this->dataMapper = $dataMapper;
return $this;
}
/**
* Returns the data mapper used by the form.
*
* @return array An array of DataMapperInterface
*/
public function getDataMapper()
{
return $this->dataMapper;
}
/**
* Set the types.
*
* @param array $types An array FormTypeInterface
*
* @return FormBuilder The current builder
*/
public function setTypes(array $types)
{
$this->types = $types;
return $this;
}
/**
* Return the types.
*
* @return array An array of FormTypeInterface
*/
public function getTypes()
{
return $this->types;
}
/**
* Sets the data used for the client data when no value is bound.
*
* @param mixed $emptyData
*/
public function setEmptyData($emptyData)
{
$this->emptyData = $emptyData;
return $this;
}
/**
* Returns the data used for the client data when no value is bound.
*
* @return mixed
*/
public function getEmptyData()
{
return $this->emptyData;
}
/**
* Adds a new field to this group. A field must have a unique name within
* the group. Otherwise the existing field is overwritten.
@ -540,7 +89,7 @@ class FormBuilder
* @param string|FormTypeInterface $type
* @param array $options
*
* @return FormBuilder The current builder
* @return FormBuilder The builder object.
*/
public function add($child, $type = null, array $options = array())
{
@ -548,6 +97,9 @@ class FormBuilder
$child->setParent($this);
$this->children[$child->getName()] = $child;
// In case an unresolved child with the same name exists
unset($this->unresolvedChildren[$child->getName()]);
return $this;
}
@ -563,9 +115,9 @@ class FormBuilder
throw new CircularReferenceException(is_string($type) ? $this->getFormFactory()->getType($type) : $type);
}
$this->children[$child] = array(
'type' => $type,
'options' => $options,
$this->unresolvedChildren[$child] = array(
'type' => $type,
'options' => $options,
);
return $this;
@ -578,11 +130,11 @@ class FormBuilder
* @param string|FormTypeInterface $type The type of the form or null if name is a property
* @param array $options The options
*
* @return FormBuilder The builder
* @return FormBuilder The created builder.
*/
public function create($name, $type = null, array $options = array())
{
if (null === $type && !$this->dataClass) {
if (null === $type && null === $this->getDataClass()) {
$type = 'text';
}
@ -590,7 +142,7 @@ class FormBuilder
return $this->getFormFactory()->createNamedBuilder($type, $name, null, $options, $this);
}
return $this->getFormFactory()->createBuilderForProperty($this->dataClass, $name, null, $options, $this);
return $this->getFormFactory()->createBuilderForProperty($this->getDataClass(), $name, null, $options, $this);
}
/**
@ -604,19 +156,15 @@ class FormBuilder
*/
public function get($name)
{
if (!isset($this->children[$name])) {
throw new FormException(sprintf('The field "%s" does not exist', $name));
if (isset($this->unresolvedChildren[$name])) {
return $this->resolveChild($name);
}
if (!$this->children[$name] instanceof FormBuilder) {
$this->children[$name] = $this->create(
$name,
$this->children[$name]['type'],
$this->children[$name]['options']
);
if (isset($this->children[$name])) {
return $this->children[$name];
}
return $this->children[$name];
throw new FormException(sprintf('The child with the name "%s" does not exist.', $name));
}
/**
@ -624,10 +172,12 @@ class FormBuilder
*
* @param string $name
*
* @return FormBuilder The current builder
* @return FormBuilder The builder object.
*/
public function remove($name)
{
unset($this->unresolvedChildren[$name]);
if (isset($this->children[$name])) {
if ($this->children[$name] instanceof self) {
$this->children[$name]->setParent(null);
@ -647,7 +197,15 @@ class FormBuilder
*/
public function has($name)
{
return isset($this->children[$name]);
if (isset($this->unresolvedChildren[$name])) {
return true;
}
if (isset($this->children[$name])) {
return true;
}
return false;
}
/**
@ -657,6 +215,8 @@ class FormBuilder
*/
public function all()
{
$this->resolveChildren();
return $this->children;
}
@ -667,30 +227,15 @@ class FormBuilder
*/
public function getForm()
{
$instance = new Form(
$this->getName(),
$this->buildDispatcher(),
$this->getTypes(),
$this->getClientTransformers(),
$this->getNormTransformers(),
$this->getDataMapper(),
$this->getValidators(),
$this->getRequired(),
$this->getDisabled(),
$this->getErrorBubbling(),
$this->getEmptyData(),
$this->getAttributes()
);
$this->resolveChildren();
foreach ($this->buildChildren() as $child) {
$instance->add($child);
$form = new Form($this);
foreach ($this->children as $child) {
$form->add($child->getForm());
}
if (null !== $this->getData()) {
$instance->setData($this->getData());
}
return $instance;
return $form;
}
public function setCurrentLoadingType($type)
@ -713,7 +258,7 @@ class FormBuilder
*
* @param FormBuilder $parent The parent builder
*
* @return FormBuilder The current builder
* @return FormBuilder The builder object.
*/
public function setParent(FormBuilder $parent = null)
{
@ -723,32 +268,31 @@ class FormBuilder
}
/**
* Returns the event dispatcher.
* Converts an unresolved child into a {@link FormBuilder} instance.
*
* @return type
* @param string $name The name of the unresolved child.
*
* @return FormBuilder The created instance.
*/
protected function buildDispatcher()
private function resolveChild($name)
{
return $this->dispatcher;
$info = $this->unresolvedChildren[$name];
$child = $this->create($name, $info['type'], $info['options']);
$this->children[$name] = $child;
unset($this->unresolvedChildren[$name]);
return $child;
}
/**
* Creates the children.
*
* @return array An array of Form
* Converts all unresolved children into {@link FormBuilder} instances.
*/
protected function buildChildren()
private function resolveChildren()
{
$children = array();
foreach ($this->children as $name => $builder) {
if (!$builder instanceof FormBuilder) {
$builder = $this->create($name, $builder['type'], $builder['options']);
}
$children[$builder->getName()] = $builder->getForm();
foreach ($this->unresolvedChildren as $name => $info) {
$this->children[$name] = $this->create($name, $info['type'], $info['options']);
}
return $children;
$this->unresolvedChildren = array();
}
}

View File

@ -0,0 +1,635 @@
<?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;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Util\PropertyPath;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* A basic form configuration.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FormConfig implements FormConfigInterface
{
/**
* @var EventDispatcherInterface
*/
private $dispatcher;
/**
* @var string
*/
private $name;
/**
* @var PropertyPath
*/
private $propertyPath;
/**
* @var Boolean
*/
private $mapped;
/**
* @var Boolean
*/
private $virtual;
/**
* @var array
*/
private $types = array();
/**
* @var array
*/
private $clientTransformers = array();
/**
* @var array
*/
private $normTransformers = array();
/**
* @var DataMapperInterface
*/
private $dataMapper;
/**
* @var array
*/
private $validators = array();
/**
* @var Boolean
*/
private $required;
/**
* @var Boolean
*/
private $disabled;
/**
* @var Boolean
*/
private $errorBubbling;
/**
* @var mixed
*/
private $emptyData;
/**
* @var array
*/
private $attributes = array();
/**
* @var mixed
*/
private $data;
/**
* @var string
*/
private $dataClass;
/**
* Creates an empty form configuration.
*
* @param string $name The form name.
* @param string $dataClass The class of the form's data.
* @param EventDispatcherInterface $dispatcher The event dispatcher.
*
* @throws UnexpectedTypeException If the name is not a string.
* @throws \InvalidArgumentException If the data class is not a valid class or if
* the name contains invalid characters.
*/
public function __construct($name, $dataClass, EventDispatcherInterface $dispatcher)
{
$name = (string) $name;
self::validateName($name);
if (null !== $dataClass && !class_exists($dataClass)) {
throw new \InvalidArgumentException(sprintf('The data class "%s" is not a valid class.', $dataClass));
}
$this->name = $name;
$this->dataClass = $dataClass;
$this->dispatcher = $dispatcher;
}
/**
* Adds an event listener to an event on this form.
*
* @param string $eventName The name of the event to listen to.
* @param callable $listener The listener to execute.
* @param integer $priority The priority of the listener. Listeners
* with a higher priority are called before
* listeners with a lower priority.
*
* @return self The configuration object.
*/
public function addEventListener($eventName, $listener, $priority = 0)
{
$this->dispatcher->addListener($eventName, $listener, $priority);
return $this;
}
/**
* Adds an event subscriber for events on this form.
*
* @param EventSubscriberInterface $subscriber The subscriber to attach.
*
* @return self The configuration object.
*/
public function addEventSubscriber(EventSubscriberInterface $subscriber)
{
$this->dispatcher->addSubscriber($subscriber);
return $this;
}
/**
* Adds a validator to the form.
*
* @param FormValidatorInterface $validator The validator.
*
* @return self The configuration object.
*
* @deprecated Deprecated since version 2.1, to be removed in 2.3.
*/
public function addValidator(FormValidatorInterface $validator)
{
$this->validators[] = $validator;
return $this;
}
/**
* Appends a transformer to the client transformer chain
*
* @param DataTransformerInterface $clientTransformer
*
* @return self The configuration object.
*/
public function appendClientTransformer(DataTransformerInterface $clientTransformer)
{
$this->clientTransformers[] = $clientTransformer;
return $this;
}
/**
* Prepends a transformer to the client transformer chain.
*
* @param DataTransformerInterface $clientTransformer
*
* @return self The configuration object.
*/
public function prependClientTransformer(DataTransformerInterface $clientTransformer)
{
array_unshift($this->clientTransformers, $clientTransformer);
return $this;
}
/**
* Clears the client transformers.
*
* @return self The configuration object.
*/
public function resetClientTransformers()
{
$this->clientTransformers = array();
return $this;
}
/**
* Appends a transformer to the normalization transformer chain
*
* @param DataTransformerInterface $normTransformer
*
* @return self The configuration object.
*/
public function appendNormTransformer(DataTransformerInterface $normTransformer)
{
$this->normTransformers[] = $normTransformer;
return $this;
}
/**
* Prepends a transformer to the normalization transformer chain
*
* @param DataTransformerInterface $normTransformer
*
* @return self The configuration object.
*/
public function prependNormTransformer(DataTransformerInterface $normTransformer)
{
array_unshift($this->normTransformers, $normTransformer);
return $this;
}
/**
* Clears the normalization transformers.
*
* @return self The configuration object.
*/
public function resetNormTransformers()
{
$this->normTransformers = array();
return $this;
}
/**
* {@inheritdoc}
*/
public function getEventDispatcher()
{
return $this->dispatcher;
}
/**
* {@inheritdoc}
*/
public function getName()
{
return $this->name;
}
/**
* {@inheritdoc}
*/
public function getPropertyPath()
{
return $this->propertyPath;
}
/**
* {@inheritdoc}
*/
public function getMapped()
{
return $this->mapped;
}
/**
* {@inheritdoc}
*/
public function getVirtual()
{
return $this->virtual;
}
/**
* {@inheritdoc}
*/
public function getTypes()
{
return $this->types;
}
/**
* {@inheritdoc}
*/
public function getClientTransformers()
{
return $this->clientTransformers;
}
/**
* {@inheritdoc}
*/
public function getNormTransformers()
{
return $this->normTransformers;
}
/**
* Returns the data mapper of the form.
*
* @return DataMapperInterface The data mapper.
*/
public function getDataMapper()
{
return $this->dataMapper;
}
/**
* {@inheritdoc}
*/
public function getValidators()
{
return $this->validators;
}
/**
* {@inheritdoc}
*/
public function getRequired()
{
return $this->required;
}
/**
* {@inheritdoc}
*/
public function getDisabled()
{
return $this->disabled;
}
/**
* {@inheritdoc}
*/
public function getErrorBubbling()
{
return $this->errorBubbling;
}
/**
* {@inheritdoc}
*/
public function getEmptyData()
{
return $this->emptyData;
}
/**
* {@inheritdoc}
*/
public function getAttributes()
{
return $this->attributes;
}
/**
* {@inheritdoc}
*/
public function hasAttribute($name)
{
return isset($this->attributes[$name]);
}
/**
* {@inheritdoc}
*/
public function getAttribute($name)
{
return isset($this->attributes[$name]) ? $this->attributes[$name] : null;
}
/**
* {@inheritdoc}
*/
public function getData()
{
return $this->data;
}
/**
* {@inheritdoc}
*/
public function getDataClass()
{
return $this->dataClass;
}
/**
* Sets the value for an attribute.
*
* @param string $name The name of the attribute
* @param string $value The value of the attribute
*
* @return self The configuration object.
*/
public function setAttribute($name, $value)
{
$this->attributes[$name] = $value;
return $this;
}
/**
* Sets the attributes.
*
* @param array $attributes The attributes.
*
* @return self The configuration object.
*/
public function setAttributes(array $attributes)
{
$this->attributes = $attributes;
return $this;
}
/**
* Sets the data mapper used by the form.
*
* @param DataMapperInterface $dataMapper
*
* @return self The configuration object.
*/
public function setDataMapper(DataMapperInterface $dataMapper = null)
{
$this->dataMapper = $dataMapper;
return $this;
}
/**
* Set whether the form is disabled.
*
* @param Boolean $disabled Whether the form is disabled
*
* @return self The configuration object.
*/
public function setDisabled($disabled)
{
$this->disabled = (Boolean) $disabled;
return $this;
}
/**
* Sets the data used for the client data when no value is bound.
*
* @param mixed $emptyData The empty data.
*
* @return self The configuration object.
*/
public function setEmptyData($emptyData)
{
$this->emptyData = $emptyData;
return $this;
}
/**
* Sets whether errors bubble up to the parent.
*
* @param Boolean $errorBubbling
*
* @return self The configuration object.
*/
public function setErrorBubbling($errorBubbling)
{
$this->errorBubbling = null === $errorBubbling ? null : (Boolean) $errorBubbling;
return $this;
}
/**
* Sets whether this field is required to be filled out when bound.
*
* @param Boolean $required
*
* @return self The configuration object.
*/
public function setRequired($required)
{
$this->required = (Boolean) $required;
return $this;
}
/**
* Sets the property path that the form should be mapped to.
*
* @param string|PropertyPath $propertyPath The property path or null if the path
* should be set automatically based on
* the form's name.
*
* @return self The configuration object.
*/
public function setPropertyPath($propertyPath)
{
if (null !== $propertyPath && !$propertyPath instanceof PropertyPath) {
$propertyPath = new PropertyPath($propertyPath);
}
$this->propertyPath = $propertyPath;
return $this;
}
/**
* Sets whether the form should be mapped to an element of its
* parent's data.
*
* @param Boolean $mapped Whether the form should be mapped.
*
* @return self The configuration object.
*/
public function setMapped($mapped)
{
$this->mapped = $mapped;
return $this;
}
/**
* Sets whether the form should be virtual.
*
* @param Boolean $virtual Whether the form should be virtual.
*
* @return self The configuration object.
*/
public function setVirtual($virtual)
{
$this->virtual = $virtual;
return $this;
}
/**
* Set the types.
*
* @param array $types An array FormTypeInterface
*
* @return self The configuration object.
*/
public function setTypes(array $types)
{
$this->types = $types;
return $this;
}
/**
* Sets the initial data of the form.
*
* @param array $data The data of the form in application format.
*
* @return self The configuration object.
*/
public function setData($data)
{
$this->data = $data;
return $this;
}
/**
* Validates whether the given variable is a valid form name.
*
* @param string $name The tested form name.
*
* @throws UnexpectedTypeException If the name is not a string.
* @throws \InvalidArgumentException If the name contains invalid characters.
*/
static public function validateName($name)
{
if (!is_string($name)) {
throw new UnexpectedTypeException($name, 'string');
}
if (!self::isValidName($name)) {
throw new \InvalidArgumentException(sprintf(
'The name "%s" contains illegal characters. Names should start with a letter, digit or underscore and only contain letters, digits, numbers, underscores ("_"), hyphens ("-") and colons (":").',
$name
));
}
}
/**
* Returns whether the given variable contains a valid form name.
*
* A name is accepted if it
*
* * is empty
* * starts with a letter, digit or underscore
* * contains only letters, digits, numbers, underscores ("_"),
* hyphens ("-") and colons (":")
*
* @param string $name The tested form name.
*
* @return Boolean Whether the name is valid.
*/
static public function isValidName($name)
{
return '' === $name || preg_match('/^[a-zA-Z0-9_][a-zA-Z0-9_\-:]*$/D', $name);
}
}

View File

@ -0,0 +1,164 @@
<?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;
/**
* The configuration of a {@link Form} object.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface FormConfigInterface
{
/**
* Returns the event dispatcher used to dispatch form events.
*
* @return \Symfony\Component\EventDispatcher\EventDispatcherInterface The dispatcher.
*/
function getEventDispatcher();
/**
* Returns the name of the form used as HTTP parameter.
*
* @return string The form name.
*/
function getName();
/**
* Returns the property path that the form should be mapped to.
*
* @return Util\PropertyPath The property path.
*/
function getPropertyPath();
/**
* Returns whether the form should be mapped to an element of its
* parent's data.
*
* @return Boolean Whether the form is mapped.
*/
function getMapped();
/**
* Returns whether the form should be virtual.
*
* When mapping data to the children of a form, the data mapper
* should ignore virtual forms and map to the children of the
* virtual form instead.
*
* @return Boolean Whether the form is virtual.
*/
function getVirtual();
/**
* Returns the form types used to construct the form.
*
* @return array An array of {@link FormTypeInterface} instances.
*/
function getTypes();
/**
* Returns the client transformers of the form.
*
* @return array An array of {@link DataTransformerInterface} instances.
*/
function getClientTransformers();
/**
* Returns the view transformers of the form.
*
* @return array An array of {@link DataTransformerInterface} instances.
*/
function getNormTransformers();
/**
* Returns the data mapper of the form.
*
* @return DataMapperInterface The data mapper.
*/
function getDataMapper();
/**
* Returns the validators of the form.
*
* @return FormValidatorInterface The form validator.
*
* @deprecated Deprecated since version 2.1, to be removed in 2.3.
*/
function getValidators();
/**
* Returns whether the form is required.
*
* @return Boolean Whether the form is required.
*/
function getRequired();
/**
* Returns whether the form is disabled.
*
* @return Boolean Whether the form is disabled.
*/
function getDisabled();
/**
* Returns whether errors attached to the form will bubble to its parent.
*
* @return Boolean Whether errors will bubble up.
*/
function getErrorBubbling();
/**
* Returns the data that should be returned when the form is empty.
*
* @return mixed The data returned if the form is empty.
*/
function getEmptyData();
/**
* Returns additional attributes of the form.
*
* @return array An array of key-value combinations.
*/
function getAttributes();
/**
* Returns whether the attribute with the given name exists.
*
* @param string $name The attribute name.
*
* @return Boolean Whether the attribute exists.
*/
function hasAttribute($name);
/**
* Returns the value of the given attribute.
*
* @param string $name The attribute name.
*
* @return mixed The attribute value.
*/
function getAttribute($name);
/**
* Returns the initial data of the form.
*
* @return mixed The initial form data.
*/
function getData();
/**
* Returns the class of the form data or null if the data is scalar or an array.
*
* @return string The data class or null.
*/
function getDataClass();
}

View File

@ -151,7 +151,6 @@ class FormFactory implements FormFactoryInterface
$builder = null;
$types = array();
$optionValues = array();
$knownOptions = array();
$optionsResolver = new OptionsResolver();

View File

@ -21,7 +21,9 @@ interface FormInterface extends \ArrayAccess, \Traversable, \Countable
/**
* Sets the parent form.
*
* @param FormInterface $parent The parent form
* @param FormInterface $parent The parent form
*
* @return FormInterface The form instance
*/
function setParent(FormInterface $parent = null);
@ -42,7 +44,9 @@ interface FormInterface extends \ArrayAccess, \Traversable, \Countable
/**
* Adds a child to the form.
*
* @param FormInterface $child The FormInterface to add as a child
* @param FormInterface $child The FormInterface to add as a child
*
* @return FormInterface The form instance
*/
function add(FormInterface $child);
@ -67,7 +71,9 @@ interface FormInterface extends \ArrayAccess, \Traversable, \Countable
/**
* Removes a child from the form.
*
* @param string $name The name of the child to remove
* @param string $name The name of the child to remove
*
* @return FormInterface The form instance
*/
function remove($name);
@ -95,9 +101,9 @@ interface FormInterface extends \ArrayAccess, \Traversable, \Countable
/**
* Updates the field with default data.
*
* @param array $appData The data formatted as expected for the underlying object
* @param array $appData The data formatted as expected for the underlying object
*
* @return Form The current form
* @return FormInterface The form instance
*/
function setData($appData);
@ -111,7 +117,7 @@ interface FormInterface extends \ArrayAccess, \Traversable, \Countable
/**
* Returns the normalized data of the field.
*
* @return mixed When the field is not bound, the default data is returned.
* @return mixed When the field is not bound, the default data is returned.
* When the field is bound, the normalized bound data is
* returned if the field is valid, null otherwise.
*/
@ -131,6 +137,13 @@ interface FormInterface extends \ArrayAccess, \Traversable, \Countable
*/
function getExtraData();
/**
* Returns the form's configuration.
*
* @return FormConfigInterface The configuration.
*/
function getConfig();
/**
* Returns whether the field is bound.
*
@ -138,24 +151,26 @@ interface FormInterface extends \ArrayAccess, \Traversable, \Countable
*/
function isBound();
/**
* Returns the supported types.
*
* @return array An array of FormTypeInterface
*/
function getTypes();
/**
* Returns the name by which the form is identified in forms.
*
* @return string The name of the form.
* @return string The name of the form.
*/
function getName();
/**
* Returns the property path that the form is mapped to.
*
* @return Util\PropertyPath The property path.
*/
function getPropertyPath();
/**
* Adds an error to this form.
*
* @param FormError $error
* @param FormError $error
*
* @return FormInterface The form instance
*/
function addError(FormError $error);
@ -207,7 +222,9 @@ interface FormInterface extends \ArrayAccess, \Traversable, \Countable
/**
* Writes data into the form.
*
* @param mixed $data The data
* @param mixed $data The data
*
* @return FormInterface The form instance
*/
function bind($data);
@ -228,7 +245,7 @@ interface FormInterface extends \ArrayAccess, \Traversable, \Countable
/**
* Returns the root of the form tree.
*
* @return FormInterface The root of the tree
* @return FormInterface The root of the tree
*/
function getRoot();

View File

@ -97,7 +97,7 @@ class FormTypeGuesserChain implements FormTypeGuesserInterface
* @param \Closure $closure The closure to execute. Accepts a guesser
* as argument and should return a Guess instance
*
* @return Guess The guess with the highest confidence
* @return Guess The guess with the highest confidence
*/
private function guess(\Closure $closure)
{

View File

@ -29,7 +29,7 @@ interface FormTypeGuesserInterface
* @param string $class The fully qualified class name
* @param string $property The name of the property to guess for
*
* @return Guess A guess for the field's required setting
* @return Guess A guess for the field's required setting
*/
function guessRequired($class, $property);
@ -39,7 +39,7 @@ interface FormTypeGuesserInterface
* @param string $class The fully qualified class name
* @param string $property The name of the property to guess for
*
* @return Guess A guess for the field's maximum length
* @return Guess A guess for the field's maximum length
*/
function guessMaxLength($class, $property);
@ -49,7 +49,7 @@ interface FormTypeGuesserInterface
* @param string $class The fully qualified class name
* @param string $property The name of the property to guess for
*
* @return Guess A guess for the field's minimum length
* @return Guess A guess for the field's minimum length
*
* @deprecated Deprecated since version 2.1, to be removed in 2.3.
*/
@ -67,7 +67,7 @@ interface FormTypeGuesserInterface
* @param string $class The fully qualified class name
* @param string $property The name of the property to guess for
*
* @return Guess A guess for the field's required pattern
* @return Guess A guess for the field's required pattern
*/
function guessPattern($class, $property);
}

View File

@ -75,7 +75,7 @@ abstract class Guess
*
* @param array $guesses A list of guesses
*
* @return Guess The guess with the highest confidence
* @return Guess The guess with the highest confidence
*/
static public function getBestGuess(array $guesses)
{
@ -103,8 +103,8 @@ abstract class Guess
/**
* Returns the confidence that the guessed value is correct
*
* @return integer One of the constants VERY_HIGH_CONFIDENCE,
* HIGH_CONFIDENCE, MEDIUM_CONFIDENCE and LOW_CONFIDENCE
* @return integer One of the constants VERY_HIGH_CONFIDENCE,
* HIGH_CONFIDENCE, MEDIUM_CONFIDENCE and LOW_CONFIDENCE
*/
public function getConfidence()
{

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

@ -23,12 +23,12 @@ abstract class PropertyPathMapperTest_Form implements FormInterface
public function setAttribute($name, $value)
{
$this->attribute[$name] = $value;
$this->attributes[$name] = $value;
}
public function getAttribute($name)
{
return isset($this->attribute[$name]) ? $this->attribute[$name] : null;
return isset($this->attributes[$name]) ? $this->attributes[$name] : null;
}
public function setData($data)
@ -64,8 +64,14 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase
->getMock();
}
private function getForm(PropertyPath $propertyPath = null, $byReference, $synchronized = true)
private function getForm(PropertyPath $propertyPath = null, $byReference, $synchronized = true, $mapped = true, $disabled = false)
{
$config = $this->getMock('Symfony\Component\Form\FormConfigInterface');
$config->expects($this->any())
->method('getMapped')
->will($this->returnValue($mapped));
$form = $this->getMockBuilder(__CLASS__ . '_Form')
// PHPUnit's getMockForAbstractClass does not behave like in the docs..
// If the array is empty, all methods are mocked. If it is not
@ -74,13 +80,24 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase
->setMethods(array('foo'))
->getMockForAbstractClass();
$form->setAttribute('property_path', $propertyPath);
$form->setAttribute('by_reference', $byReference);
$form->expects($this->any())
->method('getConfig')
->will($this->returnValue($config));
$form->expects($this->any())
->method('getPropertyPath')
->will($this->returnValue($propertyPath));
$form->expects($this->any())
->method('isSynchronized')
->will($this->returnValue($synchronized));
$form->expects($this->any())
->method('isDisabled')
->will($this->returnValue($disabled));
return $form;
}
@ -129,10 +146,24 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase
$form = $this->getForm(null, true);
$form->expects($this->never())
->method('setData');
$this->mapper->mapDataToForm($car, $form);
$this->assertNull($form->getData());
}
public function testMapDataToFormIgnoresUnmapped()
{
$car = new \stdClass();
$propertyPath = $this->getPropertyPath('engine');
$propertyPath->expects($this->never())
->method('getValue');
$form = $this->getForm($propertyPath, true, true, false);
$this->mapper->mapDataToForm($car, $form);
$this->assertNull($form->getData());
}
public function testMapDataToFormIgnoresEmptyData()
@ -140,10 +171,9 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase
$propertyPath = $this->getPropertyPath('engine');
$form = $this->getForm($propertyPath, true);
$form->expects($this->never())
->method('setData');
$this->mapper->mapDataToForm(null, $form);
$this->assertNull($form->getData());
}
public function testMapFormToDataWritesBackIfNotByReference()
@ -198,4 +228,63 @@ class PropertyPathMapperTest extends \PHPUnit_Framework_TestCase
$this->mapper->mapFormToData($form, $car);
}
public function testMapFormToDataIgnoresUnmapped()
{
$car = new \stdClass();
$engine = new \stdClass();
$propertyPath = $this->getPropertyPath('engine');
$propertyPath->expects($this->never())
->method('setValue');
$form = $this->getForm($propertyPath, true, true, false);
$form->setData($engine);
$this->mapper->mapFormToData($form, $car);
}
public function testMapFormToDataIgnoresEmptyData()
{
$car = new \stdClass();
$propertyPath = $this->getPropertyPath('engine');
$propertyPath->expects($this->never())
->method('setValue');
$form = $this->getForm($propertyPath, true);
$form->setData(null);
$this->mapper->mapFormToData($form, $car);
}
public function testMapFormToDataIgnoresUnsynchronized()
{
$car = new \stdClass();
$engine = new \stdClass();
$propertyPath = $this->getPropertyPath('engine');
$propertyPath->expects($this->never())
->method('setValue');
$form = $this->getForm($propertyPath, true, false);
$form->setData($engine);
$this->mapper->mapFormToData($form, $car);
}
public function testMapFormToDataIgnoresDisabled()
{
$car = new \stdClass();
$engine = new \stdClass();
$propertyPath = $this->getPropertyPath('engine');
$propertyPath->expects($this->never())
->method('setValue');
$form = $this->getForm($propertyPath, true, true, true, true);
$form->setData($engine);
$this->mapper->mapFormToData($form, $car);
}
}

View File

@ -11,10 +11,17 @@
namespace Symfony\Component\Form\Tests\Extension\Core\EventListener;
use Symfony\Component\Form\FormBuilder;
class MergeCollectionListenerArrayObjectTest extends MergeCollectionListenerTest
{
protected function getData(array $data)
{
return new \ArrayObject($data);
}
protected function getBuilder($name = 'name')
{
return new FormBuilder($name, '\ArrayObject', $this->dispatcher, $this->factory);
}
}

View File

@ -11,10 +11,17 @@
namespace Symfony\Component\Form\Tests\Extension\Core\EventListener;
use Symfony\Component\Form\FormBuilder;
class MergeCollectionListenerArrayTest extends MergeCollectionListenerTest
{
protected function getData(array $data)
{
return $data;
}
protected function getBuilder($name = 'name')
{
return new FormBuilder($name, null, $this->dispatcher, $this->factory);
}
}

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Form\Tests\Extension\Core\EventListener;
use Symfony\Component\Form\Tests\Fixtures\CustomArrayObject;
use Symfony\Component\Form\FormBuilder;
class MergeCollectionListenerCustomArrayObjectTest extends MergeCollectionListenerTest
{
@ -19,4 +20,9 @@ class MergeCollectionListenerCustomArrayObjectTest extends MergeCollectionListen
{
return new CustomArrayObject($data);
}
protected function getBuilder($name = 'name')
{
return new FormBuilder($name, 'Symfony\Component\Form\Tests\Fixtures\CustomArrayObject', $this->dispatcher, $this->factory);
}
}

View File

@ -17,9 +17,9 @@ use Symfony\Component\Form\FormBuilder;
abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase
{
private $dispatcher;
private $factory;
private $form;
protected $dispatcher;
protected $factory;
protected $form;
protected function setUp()
{
@ -39,10 +39,7 @@ abstract class MergeCollectionListenerTest extends \PHPUnit_Framework_TestCase
$this->form = null;
}
protected function getBuilder($name = 'name')
{
return new FormBuilder($name, $this->factory, $this->dispatcher);
}
abstract protected function getBuilder($name = 'name');
protected function getForm($name = 'name', $propertyPath = null)
{

View File

@ -42,7 +42,7 @@ class ResizeFormListenerTest extends \PHPUnit_Framework_TestCase
protected function getBuilder($name = 'name')
{
return new FormBuilder($name, $this->factory, $this->dispatcher);
return new FormBuilder($name, null, $this->dispatcher, $this->factory);
}
protected function getForm($name = 'name')

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

@ -51,41 +51,6 @@ class FormTest_AuthorWithoutRefSetter
class FormTypeTest extends TypeTestCase
{
public function testGetPropertyPathDefaultPath()
{
$form = $this->factory->createNamed('form', 'title');
$this->assertEquals(new PropertyPath('title'), $form->getAttribute('property_path'));
}
public function testGetPropertyPathPathIsZero()
{
$form = $this->factory->create('form', null, array('property_path' => '0'));
$this->assertEquals(new PropertyPath('0'), $form->getAttribute('property_path'));
}
public function testGetPropertyPathPathIsEmpty()
{
$form = $this->factory->create('form', null, array('property_path' => ''));
$this->assertNull($form->getAttribute('property_path'));
}
public function testGetPropertyPathPathIsFalse()
{
$form = $this->factory->create('form', null, array('property_path' => false));
$this->assertNull($form->getAttribute('property_path'));
}
public function testGetPropertyPathPathIsNull()
{
$form = $this->factory->createNamed('form', 'title', null, array('property_path' => null));
$this->assertEquals(new PropertyPath('title'), $form->getAttribute('property_path'));
}
public function testPassRequiredAsOption()
{
$form = $this->factory->create('form', null, array('required' => false));
@ -285,10 +250,9 @@ class FormTypeTest extends TypeTestCase
$this->assertEquals($author, $form->getData());
}
public function testBindWithEmptyDataDoesNotCreateObjectIfDataClassIsNull()
public function testBindWithEmptyDataCreatesArrayIfDataClassIsNull()
{
$form = $this->factory->create('form', null, array(
'data' => new Author(),
'data_class' => null,
'required' => false,
));
@ -365,6 +329,7 @@ class FormTypeTest extends TypeTestCase
$author = new Author();
$form = $this->factory->create('form', null, array(
'data_class' => 'Symfony\Component\Form\Tests\Fixtures\Author',
'empty_data' => $author,
));
$form->add($this->factory->createNamed('form', 'firstName'));
@ -430,10 +395,11 @@ class FormTypeTest extends TypeTestCase
{
$author = new FormTest_AuthorWithoutRefSetter(new Author());
$builder = $this->factory->createBuilder('form');
$builder->add('reference', 'form');
$builder = $this->factory->createBuilder('form', $author);
$builder->add('reference', 'form', array(
'data_class' => 'Symfony\Component\Form\Tests\Fixtures\Author',
));
$builder->get('reference')->add('firstName', 'form');
$builder->setData($author);
$form = $builder->getForm();
$form->bind(array(
@ -452,10 +418,11 @@ class FormTypeTest extends TypeTestCase
$author = new FormTest_AuthorWithoutRefSetter(null);
$newReference = new Author();
$builder = $this->factory->createBuilder('form');
$builder->add('referenceCopy', 'form');
$builder = $this->factory->createBuilder('form', $author);
$builder->add('referenceCopy', 'form', array(
'data_class' => 'Symfony\Component\Form\Tests\Fixtures\Author',
));
$builder->get('referenceCopy')->add('firstName', 'form');
$builder->setData($author);
$form = $builder->getForm();
$form['referenceCopy']->setData($newReference); // new author object
@ -474,17 +441,19 @@ class FormTypeTest extends TypeTestCase
{
$author = new FormTest_AuthorWithoutRefSetter(new Author());
$builder = $this->factory->createBuilder('form');
$builder->add('referenceCopy', 'form', array('by_reference' => false));
$builder = $this->factory->createBuilder('form', $author);
$builder->add('referenceCopy', 'form', array(
'data_class' => 'Symfony\Component\Form\Tests\Fixtures\Author',
'by_reference' => false
));
$builder->get('referenceCopy')->add('firstName', 'form');
$builder->setData($author);
$form = $builder->getForm();
$form->bind(array(
// referenceCopy has a getter that returns a copy
// referenceCopy has a getter that returns a copy
'referenceCopy' => array(
'firstName' => 'Foo',
)
)
));
// firstName can only be updated if setReferenceCopy() was called
@ -495,16 +464,14 @@ class FormTypeTest extends TypeTestCase
{
$author = new FormTest_AuthorWithoutRefSetter('scalar');
$builder = $this->factory->createBuilder('form');
$builder = $this->factory->createBuilder('form', $author);
$builder->add('referenceCopy', 'form');
$builder->get('referenceCopy')->appendClientTransformer(new CallbackTransformer(
function () {},
function ($value) { // reverseTransform
return 'foobar';
}
function () {},
function ($value) { // reverseTransform
return 'foobar';
}
));
$builder->setData($author);
$form = $builder->getForm();
$form->bind(array(
@ -525,11 +492,10 @@ class FormTypeTest extends TypeTestCase
$builder->setData($author);
$builder->add('referenceCopy', 'form');
$builder->get('referenceCopy')->appendClientTransformer(new CallbackTransformer(
function () {},
function ($value) use ($ref2) { // reverseTransform
return $ref2;
}
function () {},
function ($value) use ($ref2) { // reverseTransform
return $ref2;
}
));
$form = $builder->getForm();
@ -596,4 +562,46 @@ class FormTypeTest extends TypeTestCase
$this->assertTrue($form->getErrorBubbling());
}
public function testPropertyPath()
{
$form = $this->factory->create('form', null, array(
'property_path' => 'foo',
));
$this->assertEquals(new PropertyPath('foo'), $form->getPropertyPath());
$this->assertTrue($form->getConfig()->getMapped());
}
public function testPropertyPathNullImpliesDefault()
{
$form = $this->factory->createNamed('form', 'name', null, array(
'property_path' => null,
));
$this->assertEquals(new PropertyPath('name'), $form->getPropertyPath());
$this->assertTrue($form->getConfig()->getMapped());
}
// BC
public function testPropertyPathFalseImpliesDefaultNotMapped()
{
$form = $this->factory->createNamed('form', 'name', null, array(
'property_path' => false,
));
$this->assertEquals(new PropertyPath('name'), $form->getPropertyPath());
$this->assertFalse($form->getConfig()->getMapped());
}
public function testNotMapped()
{
$form = $this->factory->create('form', null, array(
'property_path' => 'foo',
'mapped' => false,
));
$this->assertEquals(new PropertyPath('foo'), $form->getPropertyPath());
$this->assertFalse($form->getConfig()->getMapped());
}
}

View File

@ -18,10 +18,19 @@ use Symfony\Component\EventDispatcher\EventDispatcher;
abstract class TypeTestCase extends \PHPUnit_Framework_TestCase
{
/**
* @var FormFactory
*/
protected $factory;
/**
* @var FormBuilder
*/
protected $builder;
/**
* @var EventDispatcher
*/
protected $dispatcher;
protected function setUp()
@ -32,7 +41,7 @@ abstract class TypeTestCase extends \PHPUnit_Framework_TestCase
$this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
$this->factory = new FormFactory($this->getExtensions());
$this->builder = new FormBuilder(null, $this->factory, $this->dispatcher);
$this->builder = new FormBuilder(null, null, $this->dispatcher, $this->factory);
}
protected function tearDown()

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,848 +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)
{
$builder = new FormBuilder($name, $this->factory, $this->dispatcher);
$builder->setAttribute('property_path', new PropertyPath($propertyPath ?: $name));
$builder->setAttribute('error_mapping', array());
$builder->setErrorBubbling(false);
return $builder;
}
protected function getForm($name = 'name', $propertyPath = null)
{
return $this->getBuilder($name, $propertyPath)->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));
}
public function testFormErrorsOnForm()
{
$form = $this->getForm();
$this->delegate->expects($this->once())
->method('validate')
->will($this->returnValue(array(
$this->getConstraintViolation('constrainedProp')
)));
$this->listener->validateForm(new DataEvent($form, null));
$this->assertEquals(array($this->getFormError()), $form->getErrors());
}
public function testFormErrorsOnChild()
{
$parent = $this->getForm();
$child = $this->getForm('firstName');
$parent->add($child);
$this->delegate->expects($this->once())
->method('validate')
->will($this->returnValue(array(
$this->getConstraintViolation('children.data.firstName')
)));
$this->listener->validateForm(new DataEvent($parent, null));
$this->assertFalse($parent->hasErrors());
$this->assertEquals(array($this->getFormError()), $child->getErrors());
}
public function testFormErrorsOnChildLongPropertyPath()
{
$parent = $this->getForm();
$child = $this->getForm('street', 'address.street');
$parent->add($child);
$this->delegate->expects($this->once())
->method('validate')
->will($this->returnValue(array(
$this->getConstraintViolation('children[address].data.street.constrainedProp')
)));
$this->listener->validateForm(new DataEvent($parent, null));
$this->assertFalse($parent->hasErrors());
$this->assertEquals(array($this->getFormError()), $child->getErrors());
}
public function testFormErrorsOnGrandChild()
{
$parent = $this->getForm();
$child = $this->getForm('address');
$grandChild = $this->getForm('street');
$parent->add($child);
$child->add($grandChild);
$this->delegate->expects($this->once())
->method('validate')
->will($this->returnValue(array(
$this->getConstraintViolation('children[address].data.street')
)));
$this->listener->validateForm(new DataEvent($parent, null));
$this->assertFalse($parent->hasErrors());
$this->assertFalse($child->hasErrors());
$this->assertEquals(array($this->getFormError()), $grandChild->getErrors());
}
public function testFormErrorsOnChildWithChildren()
{
$parent = $this->getForm();
$child = $this->getForm('address');
$grandChild = $this->getForm('street');
$parent->add($child);
$child->add($grandChild);
$this->delegate->expects($this->once())
->method('validate')
->will($this->returnValue(array(
$this->getConstraintViolation('children[address].constrainedProp')
)));
$this->listener->validateForm(new DataEvent($parent, null));
$this->assertFalse($parent->hasErrors());
$this->assertEquals(array($this->getFormError()), $child->getErrors());
$this->assertFalse($grandChild->hasErrors());
}
public function testFormErrorsOnParentIfNoChildFound()
{
$parent = $this->getForm();
$child = $this->getForm('firstName');
$parent->add($child);
$this->delegate->expects($this->once())
->method('validate')
->will($this->returnValue(array(
$this->getConstraintViolation('children[lastName].constrainedProp')
)));
$this->listener->validateForm(new DataEvent($parent, null));
$this->assertEquals(array($this->getFormError()), $parent->getErrors());
$this->assertFalse($child->hasErrors());
}
public function testFormErrorsOnCollectionForm()
{
$parent = $this->getForm();
for ($i = 0; $i < 2; $i++) {
$child = $this->getForm((string)$i, '['.$i.']');
$child->add($this->getForm('firstName'));
$parent->add($child);
}
$this->delegate->expects($this->once())
->method('validate')
->will($this->returnValue(array(
$this->getConstraintViolation('children[0].data.firstName'),
$this->getConstraintViolation('children[1].data.firstName'),
)));
$this->listener->validateForm(new DataEvent($parent, null));
$this->assertFalse($parent->hasErrors());
foreach ($parent as $child) {
$grandChild = $child->get('firstName');
$this->assertFalse($child->hasErrors());
$this->assertTrue($grandChild->hasErrors());
$this->assertEquals(array($this->getFormError()), $grandChild->getErrors());
}
}
public function testDataErrorsOnForm()
{
$form = $this->getForm();
$this->delegate->expects($this->once())
->method('validate')
->will($this->returnValue(array(
$this->getConstraintViolation('data.constrainedProp')
)));
$this->listener->validateForm(new DataEvent($form, null));
$this->assertEquals(array($this->getFormError()), $form->getErrors());
}
public function testDataErrorsOnChild()
{
$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 testDataErrorsOnChildLongPropertyPath()
{
$parent = $this->getForm();
$child = $this->getForm('street', 'address.street');
$parent->add($child);
$this->delegate->expects($this->once())
->method('validate')
->will($this->returnValue(array(
$this->getConstraintViolation('data.address.street.constrainedProp')
)));
$this->listener->validateForm(new DataEvent($parent, null));
$this->assertFalse($parent->hasErrors());
$this->assertEquals(array($this->getFormError()), $child->getErrors());
}
public function testDataErrorsOnChildWithChildren()
{
$parent = $this->getForm();
$child = $this->getForm('address');
$grandChild = $this->getForm('street');
$parent->add($child);
$child->add($grandChild);
$this->delegate->expects($this->once())
->method('validate')
->will($this->returnValue(array(
$this->getConstraintViolation('data.address.constrainedProp')
)));
$this->listener->validateForm(new DataEvent($parent, null));
$this->assertFalse($parent->hasErrors());
$this->assertEquals(array($this->getFormError()), $child->getErrors());
$this->assertFalse($grandChild->hasErrors());
}
public function testDataErrorsOnGrandChild()
{
$parent = $this->getForm();
$child = $this->getForm('address');
$grandChild = $this->getForm('street');
$parent->add($child);
$child->add($grandChild);
$this->delegate->expects($this->once())
->method('validate')
->will($this->returnValue(array(
$this->getConstraintViolation('data.address.street.constrainedProp')
)));
$this->listener->validateForm(new DataEvent($parent, null));
$this->assertFalse($parent->hasErrors());
$this->assertFalse($child->hasErrors());
$this->assertEquals(array($this->getFormError()), $grandChild->getErrors());
}
public function testDataErrorsOnGrandChild2()
{
$parent = $this->getForm();
$child = $this->getForm('address');
$grandChild = $this->getForm('street');
$parent->add($child);
$child->add($grandChild);
$this->delegate->expects($this->once())
->method('validate')
->will($this->returnValue(array(
$this->getConstraintViolation('children[address].data.street.constrainedProp')
)));
$this->listener->validateForm(new DataEvent($parent, null));
$this->assertFalse($parent->hasErrors());
$this->assertFalse($child->hasErrors());
$this->assertEquals(array($this->getFormError()), $grandChild->getErrors());
}
public function testDataErrorsOnGrandChild3()
{
$parent = $this->getForm();
$child = $this->getForm('address');
$grandChild = $this->getForm('street');
$parent->add($child);
$child->add($grandChild);
$this->delegate->expects($this->once())
->method('validate')
->will($this->returnValue(array(
$this->getConstraintViolation('data[address].street.constrainedProp')
)));
$this->listener->validateForm(new DataEvent($parent, null));
$this->assertFalse($parent->hasErrors());
$this->assertFalse($child->hasErrors());
$this->assertEquals(array($this->getFormError()), $grandChild->getErrors());
}
public function testDataErrorsOnParentIfNoChildFound()
{
$parent = $this->getForm();
$child = $this->getForm('firstName');
$parent->add($child);
$this->delegate->expects($this->once())
->method('validate')
->will($this->returnValue(array(
$this->getConstraintViolation('data.lastName.constrainedProp')
)));
$this->listener->validateForm(new DataEvent($parent, null));
$this->assertEquals(array($this->getFormError()), $parent->getErrors());
$this->assertFalse($child->hasErrors());
}
public function testDataErrorsOnCollectionForm()
{
$parent = $this->getForm();
$child = $this->getForm('addresses');
$parent->add($child);
for ($i = 0; $i < 2; $i++) {
$collection = $this->getForm((string)$i, '['.$i.']');
$collection->add($this->getForm('street'));
$child->add($collection);
}
$this->delegate->expects($this->once())
->method('validate')
->will($this->returnValue(array(
$this->getConstraintViolation('data[0].street'),
$this->getConstraintViolation('data.addresses[1].street')
)));
$child->setData(array());
$this->listener->validateForm(new DataEvent($parent, null));
$this->assertFalse($parent->hasErrors(), '->hasErrors() returns false for parent form');
$this->assertFalse($child->hasErrors(), '->hasErrors() returns false for child form');
foreach ($child as $collection) {
$grandChild = $collection->get('street');
$this->assertFalse($collection->hasErrors());
$this->assertTrue($grandChild->hasErrors());
$this->assertEquals(array($this->getFormError()), $grandChild->getErrors());
}
}
public function testMappedError()
{
$parent = $this->getBuilder()
->setAttribute('error_mapping', array(
'passwordPlain' => 'password',
))
->getForm();
$child = $this->getForm('password');
$parent->add($child);
$this->delegate->expects($this->once())
->method('validate')
->will($this->returnValue(array(
$this->getConstraintViolation('data.passwordPlain.constrainedProp')
)));
$this->listener->validateForm(new DataEvent($parent, null));
$this->assertFalse($parent->hasErrors());
$this->assertEquals(array($this->getFormError()), $child->getErrors());
}
public function testMappedNestedError()
{
$parent = $this->getBuilder()
->setAttribute('error_mapping', array(
'address.streetName' => 'address.street',
))
->getForm();
$child = $this->getForm('address');
$grandChild = $this->getForm('street');
$parent->add($child);
$child->add($grandChild);
$this->delegate->expects($this->once())
->method('validate')
->will($this->returnValue(array(
$this->getConstraintViolation('data.address.streetName.constrainedProp')
)));
$this->listener->validateForm(new DataEvent($parent, null));
$this->assertFalse($parent->hasErrors());
$this->assertFalse($child->hasErrors());
$this->assertEquals(array($this->getFormError()), $grandChild->getErrors());
}
public function testNestedMappingUsingForm()
{
$parent = $this->getForm();
$child = $this->getBuilder('address')
->setAttribute('error_mapping', array(
'streetName' => 'street',
))
->getForm();
$grandChild = $this->getForm('street');
$parent->add($child);
$child->add($grandChild);
$this->delegate->expects($this->once())
->method('validate')
->will($this->returnValue(array(
$this->getConstraintViolation('children[address].data.streetName.constrainedProp')
)));
$this->listener->validateForm(new DataEvent($parent, null));
$this->assertFalse($parent->hasErrors());
$this->assertFalse($child->hasErrors());
$this->assertEquals(array($this->getFormError()), $grandChild->getErrors());
}
public function testNestedMappingUsingData()
{
$parent = $this->getForm();
$child = $this->getBuilder('address')
->setAttribute('error_mapping', array(
'streetName' => 'street',
))
->getForm();
$grandChild = $this->getForm('street');
$parent->add($child);
$child->add($grandChild);
$this->delegate->expects($this->once())
->method('validate')
->will($this->returnValue(array(
$this->getConstraintViolation('data.address.streetName.constrainedProp')
)));
$this->listener->validateForm(new DataEvent($parent, null));
$this->assertFalse($parent->hasErrors());
$this->assertFalse($child->hasErrors());
$this->assertEquals(array($this->getFormError()), $grandChild->getErrors());
}
public function testNestedMappingVirtualForm()
{
$parent = $this->getBuilder()
->setAttribute('error_mapping', array(
'streetName' => 'street',
))
->getForm();
$child = $this->getBuilder('address')
->setAttribute('virtual', true)
->getForm();
$grandChild = $this->getForm('street');
$parent->add($child);
$child->add($grandChild);
$this->delegate->expects($this->once())
->method('validate')
->will($this->returnValue(array(
$this->getConstraintViolation('data.streetName.constrainedProp')
)));
$this->listener->validateForm(new DataEvent($parent, null));
$this->assertFalse($parent->hasErrors());
$this->assertFalse($child->hasErrors());
$this->assertEquals(array($this->getFormError()), $grandChild->getErrors());
}
public function testValidateFormData()
{
$context = $this->getExecutionContext();
$graphWalker = $context->getGraphWalker();
$object = $this->getMock('\stdClass');
$form = $this->getBuilder()
->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()
->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()
->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()
->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()
->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()
->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();
$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

@ -0,0 +1,246 @@
<?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\ViolationMapper;
use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationPath;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ViolationPathTest extends \PHPUnit_Framework_TestCase
{
public function providePaths()
{
return array(
array('children[address]', array(
array('address', true, true),
)),
array('children[address].children[street]', array(
array('address', true, true),
array('street', true, true),
)),
array('children[address][street]', array(
array('address', true, true),
), 'children[address]'),
array('children[address].data', array(
array('address', true, true),
), 'children[address]'),
array('children[address].data.street', array(
array('address', true, true),
array('street', false, false),
)),
array('children[address].data[street]', array(
array('address', true, true),
array('street', false, true),
)),
array('children[address].children[street].data.name', array(
array('address', true, true),
array('street', true, true),
array('name', false, false),
)),
array('children[address].children[street].data[name]', array(
array('address', true, true),
array('street', true, true),
array('name', false, true),
)),
array('data.address', array(
array('address', false, false),
)),
array('data[address]', array(
array('address', false, true),
)),
array('data.address.street', array(
array('address', false, false),
array('street', false, false),
)),
array('data[address].street', array(
array('address', false, true),
array('street', false, false),
)),
array('data.address[street]', array(
array('address', false, false),
array('street', false, true),
)),
array('data[address][street]', array(
array('address', false, true),
array('street', false, true),
)),
// A few invalid examples
array('data', array(), ''),
array('children', array(), ''),
array('children.address', array(), ''),
array('children.address[street]', array(), ''),
);
}
/**
* @dataProvider providePaths
*/
public function testCreatePath($string, $entries, $slicedPath = null)
{
if (null === $slicedPath) {
$slicedPath = $string;
}
$path = new ViolationPath($string);
$this->assertSame($slicedPath, $path->__toString());
$this->assertSame(count($entries), count($path->getElements()));
$this->assertSame(count($entries), $path->getLength());
foreach ($entries as $index => $entry) {
$this->assertEquals($entry[0], $path->getElement($index));
$this->assertSame($entry[1], $path->mapsForm($index));
$this->assertSame($entry[2], $path->isIndex($index));
$this->assertSame(!$entry[2], $path->isProperty($index));
}
}
public function provideParents()
{
return array(
array('children[address]', null),
array('children[address].children[street]', 'children[address]'),
array('children[address].data.street', 'children[address]'),
array('children[address].data[street]', 'children[address]'),
array('data.address', null),
array('data.address.street', 'data.address'),
array('data.address[street]', 'data.address'),
array('data[address].street', 'data[address]'),
array('data[address][street]', 'data[address]'),
);
}
/**
* @dataProvider provideParents
*/
public function testGetParent($violationPath, $parentPath)
{
$path = new ViolationPath($violationPath);
$parent = $parentPath === null ? null : new ViolationPath($parentPath);
$this->assertEquals($parent, $path->getParent());
}
public function testGetElement()
{
$path = new ViolationPath('children[address].data[street].name');
$this->assertEquals('street', $path->getElement(1));
}
/**
* @expectedException \OutOfBoundsException
*/
public function testGetElementDoesNotAcceptInvalidIndices()
{
$path = new ViolationPath('children[address].data[street].name');
$path->getElement(3);
}
/**
* @expectedException \OutOfBoundsException
*/
public function testGetElementDoesNotAcceptNegativeIndices()
{
$path = new ViolationPath('children[address].data[street].name');
$path->getElement(-1);
}
public function testIsProperty()
{
$path = new ViolationPath('children[address].data[street].name');
$this->assertFalse($path->isProperty(1));
$this->assertTrue($path->isProperty(2));
}
/**
* @expectedException \OutOfBoundsException
*/
public function testIsPropertyDoesNotAcceptInvalidIndices()
{
$path = new ViolationPath('children[address].data[street].name');
$path->isProperty(3);
}
/**
* @expectedException \OutOfBoundsException
*/
public function testIsPropertyDoesNotAcceptNegativeIndices()
{
$path = new ViolationPath('children[address].data[street].name');
$path->isProperty(-1);
}
public function testIsIndex()
{
$path = new ViolationPath('children[address].data[street].name');
$this->assertTrue($path->isIndex(1));
$this->assertFalse($path->isIndex(2));
}
/**
* @expectedException \OutOfBoundsException
*/
public function testIsIndexDoesNotAcceptInvalidIndices()
{
$path = new ViolationPath('children[address].data[street].name');
$path->isIndex(3);
}
/**
* @expectedException \OutOfBoundsException
*/
public function testIsIndexDoesNotAcceptNegativeIndices()
{
$path = new ViolationPath('children[address].data[street].name');
$path->isIndex(-1);
}
public function testMapsForm()
{
$path = new ViolationPath('children[address].data[street].name');
$this->assertTrue($path->mapsForm(0));
$this->assertFalse($path->mapsForm(1));
$this->assertFalse($path->mapsForm(2));
}
/**
* @expectedException \OutOfBoundsException
*/
public function testMapsFormDoesNotAcceptInvalidIndices()
{
$path = new ViolationPath('children[address].data[street].name');
$path->mapsForm(3);
}
/**
* @expectedException \OutOfBoundsException
*/
public function testMapsFormDoesNotAcceptNegativeIndices()
{
$path = new ViolationPath('children[address].data[street].name');
$path->mapsForm(-1);
}
}

View File

@ -31,7 +31,7 @@ class FooType extends AbstractType
public function createBuilder($name, FormFactoryInterface $factory, array $options)
{
return new FormBuilder($name, $factory, new EventDispatcher());
return new FormBuilder($name, null, new EventDispatcher(), $factory);
}
public function getDefaultOptions()

View File

@ -25,7 +25,7 @@ class FormBuilderTest extends \PHPUnit_Framework_TestCase
{
$this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
$this->factory = $this->getMock('Symfony\Component\Form\FormFactoryInterface');
$this->builder = new FormBuilder('name', $this->factory, $this->dispatcher);
$this->builder = new FormBuilder('name', null, $this->dispatcher, $this->factory);
}
protected function tearDown()
@ -35,37 +35,6 @@ class FormBuilderTest extends \PHPUnit_Framework_TestCase
$this->builder = null;
}
public function getHtml4Ids()
{
// The full list is tested in FormTest, since both Form and FormBuilder
// use the same implementation internally
return array(
array('#', false),
array('a ', false),
array("a\t", false),
array("a\n", false),
array('a.', false),
);
}
/**
* @dataProvider getHtml4Ids
*/
public function testConstructAcceptsOnlyNamesValidAsIdsInHtml4($name, $accepted)
{
try {
new FormBuilder($name, $this->factory, $this->dispatcher);
if (!$accepted) {
$this->fail(sprintf('The value "%s" should not be accepted', $name));
}
} catch (\InvalidArgumentException $e) {
// if the value was not accepted, but should be, rethrow exception
if ($accepted) {
throw $e;
}
}
}
/**
* Changing the name is not allowed, otherwise the name and property path
* are not synchronized anymore
@ -91,7 +60,7 @@ class FormBuilderTest extends \PHPUnit_Framework_TestCase
public function testAddWithGuessFluent()
{
$this->builder = new FormBuilder('name', $this->factory, $this->dispatcher, 'stdClass');
$this->builder = new FormBuilder('name', 'stdClass', $this->dispatcher, $this->factory);
$builder = $this->builder->add('foo');
$this->assertSame($builder, $this->builder);
}
@ -111,6 +80,11 @@ class FormBuilderTest extends \PHPUnit_Framework_TestCase
public function testAll()
{
$this->factory->expects($this->once())
->method('createNamedBuilder')
->with('text', 'foo')
->will($this->returnValue(new FormBuilder('foo', null, $this->dispatcher, $this->factory)));
$this->assertCount(0, $this->builder->all());
$this->assertFalse($this->builder->has('foo'));
@ -120,9 +94,6 @@ class FormBuilderTest extends \PHPUnit_Framework_TestCase
$this->assertTrue($this->builder->has('foo'));
$this->assertCount(1, $children);
$this->assertArrayHasKey('foo', $children);
$foo = $children['foo'];
$this->assertEquals('text', $foo['type']);
}
public function testAddFormType()
@ -157,7 +128,7 @@ class FormBuilderTest extends \PHPUnit_Framework_TestCase
public function testGetUnknown()
{
$this->setExpectedException('Symfony\Component\Form\Exception\FormException', 'The field "foo" does not exist');
$this->setExpectedException('Symfony\Component\Form\Exception\FormException', 'The child with the name "foo" does not exist.');
$this->builder->get('foo');
}
@ -188,7 +159,7 @@ class FormBuilderTest extends \PHPUnit_Framework_TestCase
->with('stdClass', $expectedName, null, $expectedOptions)
->will($this->returnValue($this->getFormBuilder()));
$this->builder = new FormBuilder('name', $this->factory, $this->dispatcher, 'stdClass');
$this->builder = new FormBuilder('name', 'stdClass', $this->dispatcher, $this->factory);
$this->builder->add($expectedName, null, $expectedOptions);
$builder = $this->builder->get($expectedName);
@ -202,14 +173,14 @@ class FormBuilderTest extends \PHPUnit_Framework_TestCase
public function testGetParentForAddedBuilder()
{
$builder = new FormBuilder('name', $this->factory, $this->dispatcher);
$builder = new FormBuilder('name', null, $this->dispatcher, $this->factory);
$this->builder->add($builder);
$this->assertSame($this->builder, $builder->getParent());
}
public function testGetParentForRemovedBuilder()
{
$builder = new FormBuilder('name', $this->factory, $this->dispatcher);
$builder = new FormBuilder('name', null, $this->dispatcher, $this->factory);
$this->builder->add($builder);
$this->builder->remove('name');
$this->assertNull($builder->getParent());
@ -217,7 +188,7 @@ class FormBuilderTest extends \PHPUnit_Framework_TestCase
public function testGetParentForCreatedBuilder()
{
$this->builder = new FormBuilder('name', $this->factory, $this->dispatcher, 'stdClass');
$this->builder = new FormBuilder('name', 'stdClass', $this->dispatcher, $this->factory);
$this->factory
->expects($this->once())
->method('createNamedBuilder')

View File

@ -0,0 +1,78 @@
<?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;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
use Symfony\Component\Form\FormConfig;
class FormConfigTest extends \PHPUnit_Framework_TestCase
{
public function getHtml4Ids()
{
return array(
array('a0', true),
array('a9', true),
array('z0', true),
array('A0', true),
array('A9', true),
array('Z0', true),
array('#', false),
array('a#', false),
array('a$', false),
array('a%', false),
array('a ', false),
array("a\t", false),
array("a\n", false),
array('a-', true),
array('a_', true),
array('a:', true),
// Periods are allowed by the HTML4 spec, but disallowed by us
// because they break the generated property paths
array('a.', false),
// Contrary to the HTML4 spec, we allow names starting with a
// number, otherwise naming fields by collection indices is not
// possible.
// For root forms, leading digits will be stripped from the
// "id" attribute to produce valid HTML4.
array('0', true),
array('9', true),
// Contrary to the HTML4 spec, we allow names starting with an
// underscore, since this is already a widely used practice in
// Symfony2.
// For root forms, leading underscores will be stripped from the
// "id" attribute to produce valid HTML4.
array('_', true),
);
}
/**
* @dataProvider getHtml4Ids
*/
public function testNameAcceptsOnlyNamesValidAsIdsInHtml4($name, $accepted)
{
$dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
try {
new FormConfig($name, null, $dispatcher);
if (!$accepted) {
$this->fail(sprintf('The value "%s" should not be accepted', $name));
}
} catch (\InvalidArgumentException $e) {
// if the value was not accepted, but should be, rethrow exception
if ($accepted) {
throw $e;
}
}
}
}

View File

@ -598,7 +598,7 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase
$this->extension1->addType($type);
$parentBuilder = $this->getMockBuilder('Symfony\Component\Form\FormBuilder')
->setConstructorArgs(array('name', $this->factory, $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface')))
->setConstructorArgs(array('name', null, $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'), $this->factory))
->getMock()
;

View File

@ -12,6 +12,8 @@
namespace Symfony\Component\Form\Tests;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\Util\PropertyPath;
use Symfony\Component\Form\FormConfig;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormError;
@ -51,72 +53,6 @@ class FormTest extends \PHPUnit_Framework_TestCase
$this->form = null;
}
/**
* @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testConstructExpectsValidValidators()
{
$validators = array(new \stdClass());
new Form('name', $this->dispatcher, array(), array(), array(), null, $validators);
}
public function getHtml4Ids()
{
return array(
array('a0', true),
array('a9', true),
array('z0', true),
array('A0', true),
array('A9', true),
array('Z0', true),
array('#', false),
array('a#', false),
array('a$', false),
array('a%', false),
array('a ', false),
array("a\t", false),
array("a\n", false),
array('a-', true),
array('a_', true),
array('a:', true),
// Periods are allowed by the HTML4 spec, but disallowed by us
// because they break the generated property paths
array('a.', false),
// Contrary to the HTML4 spec, we allow names starting with a
// number, otherwise naming fields by collection indices is not
// possible.
// For root forms, leading digits will be stripped from the
// "id" attribute to produce valid HTML4.
array('0', true),
array('9', true),
// Contrary to the HTML4 spec, we allow names starting with an
// underscore, since this is already a widely used practice in
// Symfony2.
// For root forms, leading underscores will be stripped from the
// "id" attribute to produce valid HTML4.
array('_', true),
);
}
/**
* @dataProvider getHtml4Ids
*/
public function testConstructAcceptsOnlyNamesValidAsIdsInHtml4($name, $accepted)
{
try {
new Form($name, $this->dispatcher);
if (!$accepted) {
$this->fail(sprintf('The value "%s" should not be accepted', $name));
}
} catch (\InvalidArgumentException $e) {
// if the value was not accepted, but should be, rethrow exception
if ($accepted) {
throw $e;
}
}
}
public function testDataIsInitializedEmpty()
{
$norm = new FixedDataTransformer(array(
@ -126,7 +62,10 @@ class FormTest extends \PHPUnit_Framework_TestCase
'foo' => 'bar',
));
$form = new Form('name', $this->dispatcher, array(), array($client), array($norm));
$config = new FormConfig('name', null, $this->dispatcher);
$config->appendClientTransformer($client);
$config->appendNormTransformer($norm);
$form = new Form($config);
$this->assertNull($form->getData());
$this->assertSame('foo', $form->getNormData());
@ -1259,18 +1198,75 @@ class FormTest extends \PHPUnit_Framework_TestCase
/**
* @expectedException Symfony\Component\Form\Exception\FormException
* @expectedExceptionMessage Form with empty name can not have parent form.
* @expectedExceptionMessage A form with an empty name cannot have a parent form.
*/
public function testFormCannotHaveEmptyNameNotInRootLevel()
{
$parent = $this->getBuilder()
$this->getBuilder()
->add($this->getBuilder(''))
->getForm();
}
protected function getBuilder($name = 'name', EventDispatcherInterface $dispatcher = null)
public function testGetPropertyPathReturnsConfiguredPath()
{
return new FormBuilder($name, $this->factory, $dispatcher ?: $this->dispatcher);
$form = $this->getBuilder()->setPropertyPath('address.street')->getForm();
$this->assertEquals(new PropertyPath('address.street'), $form->getPropertyPath());
}
// see https://github.com/symfony/symfony/issues/3903
public function testGetPropertyPathDefaultsToNameIfParentHasDataClass()
{
$parent = $this->getBuilder(null, null, 'stdClass')->getForm();
$form = $this->getBuilder('name')->getForm();
$parent->add($form);
$this->assertEquals(new PropertyPath('name'), $form->getPropertyPath());
}
// see https://github.com/symfony/symfony/issues/3903
public function testGetPropertyPathDefaultsToIndexedNameIfParentDataClassIsNull()
{
$parent = $this->getBuilder()->getForm();
$form = $this->getBuilder('name')->getForm();
$parent->add($form);
$this->assertEquals(new PropertyPath('[name]'), $form->getPropertyPath());
}
/**
* @expectedException Symfony\Component\Form\Exception\FormException
*/
public function testClientDataMustNotBeObjectIfDataClassIsNull()
{
$config = new FormConfig('name', null, $this->dispatcher);
$config->appendClientTransformer(new FixedDataTransformer(array(
'' => '',
'foo' => new \stdClass(),
)));
$form = new Form($config);
$form->setData('foo');
}
/**
* @expectedException Symfony\Component\Form\Exception\FormException
*/
public function testClientDataMustBeObjectIfDataClassIsSet()
{
$config = new FormConfig('name', 'stdClass', $this->dispatcher);
$config->appendClientTransformer(new FixedDataTransformer(array(
'' => '',
'foo' => array('bar' => 'baz'),
)));
$form = new Form($config);
$form->setData('foo');
}
protected function getBuilder($name = 'name', EventDispatcherInterface $dispatcher = null, $dataClass = null)
{
return new FormBuilder($name, $dataClass, $dispatcher ?: $this->dispatcher, $this->factory);
}
protected function getMockForm($name = 'name')

View File

@ -0,0 +1,238 @@
<?php
/*
* This file is new3 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\Util;
use Symfony\Component\Form\Util\PropertyPath;
use Symfony\Component\Form\Util\PropertyPathBuilder;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class PropertyPathBuilderTest extends \PHPUnit_Framework_TestCase
{
/**
* @var string
*/
const PREFIX = 'old1[old2].old3[old4][old5].old6';
/**
* @var PropertyPathBuilder
*/
private $builder;
protected function setUp()
{
$this->builder = new PropertyPathBuilder(new PropertyPath(self::PREFIX));
}
public function testCreateEmpty()
{
$builder = new PropertyPathBuilder();
$this->assertNull($builder->getPropertyPath());
}
public function testCreateCopyPath()
{
$this->assertEquals(new PropertyPath(self::PREFIX), $this->builder->getPropertyPath());
}
public function testAppendIndex()
{
$this->builder->appendIndex('new1');
$path = new PropertyPath(self::PREFIX . '[new1]');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testAppendProperty()
{
$this->builder->appendProperty('new1');
$path = new PropertyPath(self::PREFIX . '.new1');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testAppend()
{
$this->builder->append(new PropertyPath('new1[new2]'));
$path = new PropertyPath(self::PREFIX . '.new1[new2]');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testAppendWithOffset()
{
$this->builder->append(new PropertyPath('new1[new2].new3'), 1);
$path = new PropertyPath(self::PREFIX . '[new2].new3');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testAppendWithOffsetAndLength()
{
$this->builder->append(new PropertyPath('new1[new2].new3'), 1, 1);
$path = new PropertyPath(self::PREFIX . '[new2]');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testReplaceByIndex()
{
$this->builder->replaceByIndex(1, 'new1');
$path = new PropertyPath('old1[new1].old3[old4][old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testReplaceByIndexWithoutName()
{
$this->builder->replaceByIndex(0);
$path = new PropertyPath('[old1][old2].old3[old4][old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
/**
* @expectedException \OutOfBoundsException
*/
public function testReplaceByIndexDoesNotAllowInvalidOffsets()
{
$this->builder->replaceByIndex(6, 'new1');
}
/**
* @expectedException \OutOfBoundsException
*/
public function testReplaceByIndexDoesNotAllowNegativeOffsets()
{
$this->builder->replaceByIndex(-1, 'new1');
}
public function testReplaceByProperty()
{
$this->builder->replaceByProperty(1, 'new1');
$path = new PropertyPath('old1.new1.old3[old4][old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testReplaceByPropertyWithoutName()
{
$this->builder->replaceByProperty(1);
$path = new PropertyPath('old1.old2.old3[old4][old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
/**
* @expectedException \OutOfBoundsException
*/
public function testReplaceByPropertyDoesNotAllowInvalidOffsets()
{
$this->builder->replaceByProperty(6, 'new1');
}
/**
* @expectedException \OutOfBoundsException
*/
public function testReplaceByPropertyDoesNotAllowNegativeOffsets()
{
$this->builder->replaceByProperty(-1, 'new1');
}
public function testReplace()
{
$this->builder->replace(1, 1, new PropertyPath('new1[new2].new3'));
$path = new PropertyPath('old1.new1[new2].new3.old3[old4][old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
/**
* @expectedException \OutOfBoundsException
*/
public function testReplaceDoesNotAllowInvalidOffsets()
{
$this->builder->replace(6, 1, new PropertyPath('new1[new2].new3'));
}
/**
* @expectedException \OutOfBoundsException
*/
public function testReplaceDoesNotAllowNegativeOffsets()
{
$this->builder->replace(-1, 1, new PropertyPath('new1[new2].new3'));
}
public function testReplaceWithLengthGreaterOne()
{
$this->builder->replace(0, 2, new PropertyPath('new1[new2].new3'));
$path = new PropertyPath('new1[new2].new3.old3[old4][old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testReplaceSubstring()
{
$this->builder->replace(1, 1, new PropertyPath('new1[new2].new3.new4[new5]'), 1, 3);
$path = new PropertyPath('old1[new2].new3.new4.old3[old4][old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testReplaceSubstringWithLengthGreaterOne()
{
$this->builder->replace(1, 2, new PropertyPath('new1[new2].new3.new4[new5]'), 1, 3);
$path = new PropertyPath('old1[new2].new3.new4[old4][old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
public function testRemove()
{
$this->builder->remove(3);
$path = new PropertyPath('old1[old2].old3[old5].old6');
$this->assertEquals($path, $this->builder->getPropertyPath());
}
/**
* @expectedException \OutOfBoundsException
*/
public function testRemoveDoesNotAllowInvalidOffsets()
{
$this->builder->remove(6);
}
/**
* @expectedException \OutOfBoundsException
*/
public function testRemoveDoesNotAllowNegativeOffsets()
{
$this->builder->remove(-1);
}
}

View File

@ -21,25 +21,28 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
{
$array = array('firstName' => 'Bernhard');
$path = new PropertyPath('firstName');
$path = new PropertyPath('[firstName]');
$this->assertEquals('Bernhard', $path->getValue($array));
}
public function testGetValueIgnoresSingular()
/**
* @expectedException Symfony\Component\Form\Exception\InvalidPropertyException
*/
public function testGetValueThrowsExceptionIfIndexNotationExpected()
{
$array = array('children' => 'Many');
$array = array('firstName' => 'Bernhard');
$path = new PropertyPath('children|child');
$path = new PropertyPath('firstName');
$this->assertEquals('Many', $path->getValue($array));
$path->getValue($array);
}
public function testGetValueReadsZeroIndex()
{
$array = array('Bernhard');
$path = new PropertyPath('0');
$path = new PropertyPath('[0]');
$this->assertEquals('Bernhard', $path->getValue($array));
}
@ -53,20 +56,11 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('Bernhard', $path->getValue($array));
}
public function testGetValueReadsElementWithSpecialCharsExceptDot()
{
$array = array('%!@$§' => 'Bernhard');
$path = new PropertyPath('%!@$§');
$this->assertEquals('Bernhard', $path->getValue($array));
}
public function testGetValueReadsNestedIndexWithSpecialChars()
{
$array = array('root' => array('%!@$§.' => 'Bernhard'));
$path = new PropertyPath('root[%!@$§.]');
$path = new PropertyPath('[root][%!@$§.]');
$this->assertEquals('Bernhard', $path->getValue($array));
}
@ -75,7 +69,7 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
{
$array = array('child' => array('index' => array('firstName' => 'Bernhard')));
$path = new PropertyPath('child[index].firstName');
$path = new PropertyPath('[child][index][firstName]');
$this->assertEquals('Bernhard', $path->getValue($array));
}
@ -84,7 +78,7 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
{
$array = array('child' => array('index' => array()));
$path = new PropertyPath('child[index].firstName');
$path = new PropertyPath('[child][index][firstName]');
$this->assertNull($path->getValue($array));
}
@ -99,6 +93,24 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('Bernhard', $path->getValue($object));
}
public function testGetValueIgnoresSingular()
{
$object = (object) array('children' => 'Many');
$path = new PropertyPath('children|child');
$this->assertEquals('Many', $path->getValue($object));
}
public function testGetValueReadsPropertyWithSpecialCharsExceptDot()
{
$array = (object) array('%!@$§' => 'Bernhard');
$path = new PropertyPath('%!@$§');
$this->assertEquals('Bernhard', $path->getValue($array));
}
public function testGetValueReadsPropertyWithCustomPropertyPath()
{
$object = new Author();
@ -121,21 +133,23 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('Bernhard', $path->getValue($object));
}
/**
* @expectedException Symfony\Component\Form\Exception\InvalidPropertyException
*/
public function testGetValueThrowsExceptionIfArrayAccessExpected()
{
$path = new PropertyPath('[firstName]');
$this->setExpectedException('Symfony\Component\Form\Exception\InvalidPropertyException');
$path->getValue(new Author());
}
/**
* @expectedException Symfony\Component\Form\Exception\PropertyAccessDeniedException
*/
public function testGetValueThrowsExceptionIfPropertyIsNotPublic()
{
$path = new PropertyPath('privateProperty');
$this->setExpectedException('Symfony\Component\Form\Exception\PropertyAccessDeniedException');
$path->getValue(new Author());
}
@ -159,12 +173,13 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('Schussek', $path->getValue($object));
}
/**
* @expectedException Symfony\Component\Form\Exception\PropertyAccessDeniedException
*/
public function testGetValueThrowsExceptionIfGetterIsNotPublic()
{
$path = new PropertyPath('privateGetter');
$this->setExpectedException('Symfony\Component\Form\Exception\PropertyAccessDeniedException');
$path->getValue(new Author());
}
@ -198,48 +213,53 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
$this->assertSame('foobar', $path->getValue($object));
}
/**
* @expectedException Symfony\Component\Form\Exception\PropertyAccessDeniedException
*/
public function testGetValueThrowsExceptionIfIsserIsNotPublic()
{
$path = new PropertyPath('privateIsser');
$this->setExpectedException('Symfony\Component\Form\Exception\PropertyAccessDeniedException');
$path->getValue(new Author());
}
/**
* @expectedException Symfony\Component\Form\Exception\InvalidPropertyException
*/
public function testGetValueThrowsExceptionIfPropertyDoesNotExist()
{
$path = new PropertyPath('foobar');
$this->setExpectedException('Symfony\Component\Form\Exception\InvalidPropertyException');
$path->getValue(new Author());
}
/**
* @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testGetValueThrowsExceptionIfNotObjectOrArray()
{
$path = new PropertyPath('foobar');
$this->setExpectedException('Symfony\Component\Form\Exception\UnexpectedTypeException');
$path->getValue('baz');
}
/**
* @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testGetValueThrowsExceptionIfNull()
{
$path = new PropertyPath('foobar');
$this->setExpectedException('Symfony\Component\Form\Exception\UnexpectedTypeException');
$path->getValue(null);
}
/**
* @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testGetValueThrowsExceptionIfEmpty()
{
$path = new PropertyPath('foobar');
$this->setExpectedException('Symfony\Component\Form\Exception\UnexpectedTypeException');
$path->getValue('');
}
@ -247,17 +267,28 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
{
$array = array();
$path = new PropertyPath('firstName');
$path = new PropertyPath('[firstName]');
$path->setValue($array, 'Bernhard');
$this->assertEquals(array('firstName' => 'Bernhard'), $array);
}
/**
* @expectedException Symfony\Component\Form\Exception\InvalidPropertyException
*/
public function testSetValueThrowsExceptionIfIndexNotationExpected()
{
$array = array();
$path = new PropertyPath('firstName');
$path->setValue($array, 'Bernhard');
}
public function testSetValueUpdatesArraysWithCustomPropertyPath()
{
$array = array();
$path = new PropertyPath('child[index].firstName');
$path = new PropertyPath('[child][index][firstName]');
$path->setValue($array, 'Bernhard');
$this->assertEquals(array('child' => array('index' => array('firstName' => 'Bernhard'))), $array);
@ -305,12 +336,13 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('foobar', $object->__get('magicProperty'));
}
/**
* @expectedException Symfony\Component\Form\Exception\InvalidPropertyException
*/
public function testSetValueThrowsExceptionIfArrayAccessExpected()
{
$path = new PropertyPath('[firstName]');
$this->setExpectedException('Symfony\Component\Form\Exception\InvalidPropertyException');
$path->setValue(new Author(), 'Bernhard');
}
@ -334,42 +366,46 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('Schussek', $object->getLastName());
}
/**
* @expectedException Symfony\Component\Form\Exception\PropertyAccessDeniedException
*/
public function testSetValueThrowsExceptionIfGetterIsNotPublic()
{
$path = new PropertyPath('privateSetter');
$this->setExpectedException('Symfony\Component\Form\Exception\PropertyAccessDeniedException');
$path->setValue(new Author(), 'foobar');
}
/**
* @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testSetValueThrowsExceptionIfNotObjectOrArray()
{
$path = new PropertyPath('foobar');
$value = 'baz';
$this->setExpectedException('Symfony\Component\Form\Exception\UnexpectedTypeException');
$path->setValue($value, 'bam');
}
/**
* @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testSetValueThrowsExceptionIfNull()
{
$path = new PropertyPath('foobar');
$value = null;
$this->setExpectedException('Symfony\Component\Form\Exception\UnexpectedTypeException');
$path->setValue($value, 'bam');
}
/**
* @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testSetValueThrowsExceptionIfEmpty()
{
$path = new PropertyPath('foobar');
$value = '';
$this->setExpectedException('Symfony\Component\Form\Exception\UnexpectedTypeException');
$path->setValue($value, 'bam');
}
@ -380,34 +416,59 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('reference.traversable[index].property', $path->__toString());
}
/**
* @expectedException Symfony\Component\Form\Exception\InvalidPropertyPathException
*/
public function testInvalidPropertyPath_noDotBeforeProperty()
{
$this->setExpectedException('Symfony\Component\Form\Exception\InvalidPropertyPathException');
new PropertyPath('[index]property');
}
/**
* @expectedException Symfony\Component\Form\Exception\InvalidPropertyPathException
*/
public function testInvalidPropertyPath_dotAtTheBeginning()
{
$this->setExpectedException('Symfony\Component\Form\Exception\InvalidPropertyPathException');
new PropertyPath('.property');
}
/**
* @expectedException Symfony\Component\Form\Exception\InvalidPropertyPathException
*/
public function testInvalidPropertyPath_unexpectedCharacters()
{
$this->setExpectedException('Symfony\Component\Form\Exception\InvalidPropertyPathException');
new PropertyPath('property.$form');
}
/**
* @expectedException Symfony\Component\Form\Exception\InvalidPropertyPathException
*/
public function testInvalidPropertyPath_empty()
{
new PropertyPath('');
}
/**
* @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testInvalidPropertyPath_null()
{
$this->setExpectedException('Symfony\Component\Form\Exception\InvalidPropertyPathException');
new PropertyPath(null);
}
/**
* @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
*/
public function testInvalidPropertyPath_false()
{
new PropertyPath(false);
}
public function testValidPropertyPath_zero()
{
new PropertyPath('0');
}
public function testGetParent_dot()
{
$propertyPath = new PropertyPath('grandpa.parent.child');
@ -428,4 +489,95 @@ class PropertyPathTest extends \PHPUnit_Framework_TestCase
$this->assertNull($propertyPath->getParent());
}
public function testCopyConstructor()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$copy = new PropertyPath($propertyPath);
$this->assertEquals($propertyPath, $copy);
}
public function testGetElement()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$this->assertEquals('child', $propertyPath->getElement(2));
}
/**
* @expectedException \OutOfBoundsException
*/
public function testGetElementDoesNotAcceptInvalidIndices()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$propertyPath->getElement(3);
}
/**
* @expectedException \OutOfBoundsException
*/
public function testGetElementDoesNotAcceptNegativeIndices()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$propertyPath->getElement(-1);
}
public function testIsProperty()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$this->assertTrue($propertyPath->isProperty(1));
$this->assertFalse($propertyPath->isProperty(2));
}
/**
* @expectedException \OutOfBoundsException
*/
public function testIsPropertyDoesNotAcceptInvalidIndices()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$propertyPath->isProperty(3);
}
/**
* @expectedException \OutOfBoundsException
*/
public function testIsPropertyDoesNotAcceptNegativeIndices()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$propertyPath->isProperty(-1);
}
public function testIsIndex()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$this->assertFalse($propertyPath->isIndex(1));
$this->assertTrue($propertyPath->isIndex(2));
}
/**
* @expectedException \OutOfBoundsException
*/
public function testIsIndexDoesNotAcceptInvalidIndices()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$propertyPath->isIndex(3);
}
/**
* @expectedException \OutOfBoundsException
*/
public function testIsIndexDoesNotAcceptNegativeIndices()
{
$propertyPath = new PropertyPath('grandpa.parent[child]');
$propertyPath->isIndex(-1);
}
}

View File

@ -0,0 +1,294 @@
<?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;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Util\PropertyPath;
use Symfony\Component\EventDispatcher\UnmodifiableEventDispatcher;
/**
* A read-only form configuration.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class UnmodifiableFormConfig implements FormConfigInterface
{
/**
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
private $dispatcher;
/**
* @var string
*/
private $name;
/**
* @var PropertyPath
*/
private $propertyPath;
/**
* @var Boolean
*/
private $mapped;
/**
* @var Boolean
*/
private $virtual;
/**
* @var array
*/
private $types;
/**
* @var array
*/
private $clientTransformers;
/**
* @var array
*/
private $normTransformers;
/**
* @var DataMapperInterface
*/
private $dataMapper;
/**
* @var FormValidatorInterface
*/
private $validators;
/**
* @var Boolean
*/
private $required;
/**
* @var Boolean
*/
private $disabled;
/**
* @var Boolean
*/
private $errorBubbling;
/**
* @var mixed
*/
private $emptyData;
/**
* @var array
*/
private $attributes;
/**
* @var mixed
*/
private $data;
/**
* @var string
*/
private $dataClass;
/**
* Creates an unmodifiable copy of a given configuration.
*
* @param FormConfigInterface $config The configuration to copy.
*/
public function __construct(FormConfigInterface $config)
{
$dispatcher = $config->getEventDispatcher();
if (!$dispatcher instanceof UnmodifiableEventDispatcher) {
$dispatcher = new UnmodifiableEventDispatcher($dispatcher);
}
$this->dispatcher = $dispatcher;
$this->name = $config->getName();
$this->propertyPath = $config->getPropertyPath();
$this->mapped = $config->getMapped();
$this->virtual = $config->getVirtual();
$this->types = $config->getTypes();
$this->clientTransformers = $config->getClientTransformers();
$this->normTransformers = $config->getNormTransformers();
$this->dataMapper = $config->getDataMapper();
$this->validators = $config->getValidators();
$this->required = $config->getRequired();
$this->disabled = $config->getDisabled();
$this->errorBubbling = $config->getErrorBubbling();
$this->emptyData = $config->getEmptyData();
$this->attributes = $config->getAttributes();
$this->data = $config->getData();
$this->dataClass = $config->getDataClass();
}
/**
* {@inheritdoc}
*/
public function getEventDispatcher()
{
return $this->dispatcher;
}
/**
* {@inheritdoc}
*/
public function getName()
{
return $this->name;
}
/**
* {@inheritdoc}
*/
public function getPropertyPath()
{
return $this->propertyPath;
}
/**
* {@inheritdoc}
*/
public function getMapped()
{
return $this->mapped;
}
/**
* {@inheritdoc}
*/
public function getVirtual()
{
return $this->virtual;
}
/**
* {@inheritdoc}
*/
public function getTypes()
{
return $this->types;
}
/**
* {@inheritdoc}
*/
public function getClientTransformers()
{
return $this->clientTransformers;
}
/**
* {@inheritdoc}
*/
public function getNormTransformers()
{
return $this->normTransformers;
}
/**
* Returns the data mapper of the form.
*
* @return DataMapperInterface The data mapper.
*/
public function getDataMapper()
{
return $this->dataMapper;
}
/**
* {@inheritdoc}
*/
public function getValidators()
{
return $this->validators;
}
/**
* {@inheritdoc}
*/
public function getRequired()
{
return $this->required;
}
/**
* {@inheritdoc}
*/
public function getDisabled()
{
return $this->disabled;
}
/**
* {@inheritdoc}
*/
public function getErrorBubbling()
{
return $this->errorBubbling;
}
/**
* {@inheritdoc}
*/
public function getEmptyData()
{
return $this->emptyData;
}
/**
* {@inheritdoc}
*/
public function getAttributes()
{
return $this->attributes;
}
/**
* {@inheritdoc}
*/
public function hasAttribute($name)
{
return isset($this->attributes[$name]);
}
/**
* {@inheritdoc}
*/
public function getAttribute($name)
{
return isset($this->attributes[$name]) ? $this->attributes[$name] : null;
}
/**
* {@inheritdoc}
*/
public function getData()
{
return $this->data;
}
/**
* {@inheritdoc}
*/
public function getDataClass()
{
return $this->dataClass;
}
}

View File

@ -23,7 +23,7 @@ use Symfony\Component\Form\Exception\UnexpectedTypeException;
*
* @author Bernhard Schussek <bernhard.schussek@symfony.com>
*/
class PropertyPath implements \IteratorAggregate
class PropertyPath implements \IteratorAggregate, PropertyPathInterface
{
/**
* Character used for separating between plural and singular of an element.
@ -60,7 +60,7 @@ class PropertyPath implements \IteratorAggregate
* String representation of the path
* @var string
*/
private $string;
private $pathAsString;
/**
* Positions where the individual elements start in the string representation
@ -69,17 +69,36 @@ class PropertyPath implements \IteratorAggregate
private $positions;
/**
* Parses the given property path
* Constructs a property path from a string.
*
* @param string $propertyPath
* @param PropertyPath|string $propertyPath The property path as string or instance.
*
* @throws UnexpectedTypeException If the given path is not a string.
* @throws InvalidPropertyPathException If the syntax of the property path is not valid.
*/
public function __construct($propertyPath)
{
if (null === $propertyPath) {
throw new InvalidPropertyPathException('The property path must not be empty');
// Can be used as copy constructor
if ($propertyPath instanceof PropertyPath) {
/* @var PropertyPath $propertyPath */
$this->elements = $propertyPath->elements;
$this->singulars = $propertyPath->singulars;
$this->length = $propertyPath->length;
$this->isIndex = $propertyPath->isIndex;
$this->pathAsString = $propertyPath->pathAsString;
$this->positions = $propertyPath->positions;
return;
}
if (!is_string($propertyPath)) {
throw new UnexpectedTypeException($propertyPath, 'string or Symfony\Component\Form\Util\PropertyPath');
}
$this->string = (string) $propertyPath;
if ('' === $propertyPath) {
throw new InvalidPropertyPathException('The property path should not be empty.');
}
$this->pathAsString = $propertyPath;
$position = 0;
$remaining = $propertyPath;
@ -126,19 +145,23 @@ class PropertyPath implements \IteratorAggregate
}
/**
* Returns the string representation of the property path
*
* @return string
* {@inheritdoc}
*/
public function __toString()
{
return $this->string;
return $this->pathAsString;
}
/**
* Returns the length of the property path.
*
* @return integer
* {@inheritdoc}
*/
public function getPositions()
{
return $this->positions;
}
/**
* {@inheritdoc}
*/
public function getLength()
{
@ -146,14 +169,7 @@ class PropertyPath implements \IteratorAggregate
}
/**
* Returns the parent property path.
*
* The parent property path is the one that contains the same items as
* this one except for the last one.
*
* If this property path only contains one item, null is returned.
*
* @return PropertyPath The parent path or null.
* {@inheritdoc}
*/
public function getParent()
{
@ -164,7 +180,7 @@ class PropertyPath implements \IteratorAggregate
$parent = clone $this;
--$parent->length;
$parent->string = substr($parent->string, 0, $parent->positions[$parent->length]);
$parent->pathAsString = substr($parent->pathAsString, 0, $parent->positions[$parent->length]);
array_pop($parent->elements);
array_pop($parent->singulars);
array_pop($parent->isIndex);
@ -176,7 +192,7 @@ class PropertyPath implements \IteratorAggregate
/**
* Returns a new iterator for this path
*
* @return PropertyPathIterator
* @return PropertyPathIteratorInterface
*/
public function getIterator()
{
@ -184,9 +200,7 @@ class PropertyPath implements \IteratorAggregate
}
/**
* Returns the elements of the property path as array
*
* @return array An array of property/index names
* {@inheritdoc}
*/
public function getElements()
{
@ -194,38 +208,38 @@ class PropertyPath implements \IteratorAggregate
}
/**
* Returns the element at the given index in the property path
*
* @param integer $index The index key
*
* @return string A property or index name
* {@inheritdoc}
*/
public function getElement($index)
{
if (!isset($this->elements[$index])) {
throw new \OutOfBoundsException('The index ' . $index . ' is not within the property path');
}
return $this->elements[$index];
}
/**
* Returns whether the element at the given index is a property
*
* @param integer $index The index in the property path
*
* @return Boolean Whether the element at this index is a property
* {@inheritdoc}
*/
public function isProperty($index)
{
if (!isset($this->isIndex[$index])) {
throw new \OutOfBoundsException('The index ' . $index . ' is not within the property path');
}
return !$this->isIndex[$index];
}
/**
* Returns whether the element at the given index is an array index
*
* @param integer $index The index in the property path
*
* @return Boolean Whether the element at this index is an array index
* {@inheritdoc}
*/
public function isIndex($index)
{
if (!isset($this->isIndex[$index])) {
throw new \OutOfBoundsException('The index ' . $index . ' is not within the property path');
}
return $this->isIndex[$index];
}
@ -251,32 +265,14 @@ class PropertyPath implements \IteratorAggregate
*
* @param object|array $objectOrArray The object or array to traverse
*
* @return mixed The value at the end of the property path
* @return mixed The value at the end of the property path
*
* @throws InvalidPropertyException If the property/getter does not exist
* @throws PropertyAccessDeniedException If the property/getter exists but is not public
*/
public function getValue($objectOrArray)
{
for ($i = 0; $i < $this->length; ++$i) {
if (is_object($objectOrArray)) {
$value = $this->readProperty($objectOrArray, $i);
// arrays need to be treated separately (due to PHP bug?)
// http://bugs.php.net/bug.php?id=52133
} elseif (is_array($objectOrArray)) {
$property = $this->elements[$i];
if (!array_key_exists($property, $objectOrArray)) {
$objectOrArray[$property] = $i + 1 < $this->length ? array() : null;
}
$value =& $objectOrArray[$property];
} else {
throw new UnexpectedTypeException($objectOrArray, 'object or array');
}
$objectOrArray =& $value;
}
return $value;
return $this->readPropertyAt($objectOrArray, $this->length - 1);
}
/**
@ -299,63 +295,89 @@ class PropertyPath implements \IteratorAggregate
*
* If neither is found, an exception is thrown.
*
* @param object|array $objectOrArray The object or array to traverse
* @param mixed $value The value at the end of the property path
* @param object|array $objectOrArray The object or array to modify.
* @param mixed $value The value to set at the end of the property path.
*
* @throws InvalidPropertyException If the property/setter does not exist
* @throws PropertyAccessDeniedException If the property/setter exists but is not public
* @throws InvalidPropertyException If a property does not exist.
* @throws PropertyAccessDeniedException If a property cannot be accessed due to
* access restrictions (private or protected).
* @throws UnexpectedTypeException If a value within the path is neither object
* nor array.
*/
public function setValue(&$objectOrArray, $value)
{
for ($i = 0, $l = $this->length - 1; $i < $l; ++$i) {
if (is_object($objectOrArray)) {
$nestedObject = $this->readProperty($objectOrArray, $i);
// arrays need to be treated separately (due to PHP bug?)
// http://bugs.php.net/bug.php?id=52133
} elseif (is_array($objectOrArray)) {
$property = $this->elements[$i];
if (!array_key_exists($property, $objectOrArray)) {
$objectOrArray[$property] = array();
}
$nestedObject =& $objectOrArray[$property];
} else {
throw new UnexpectedTypeException($objectOrArray, 'object or array');
}
$objectOrArray =& $nestedObject;
}
$objectOrArray =& $this->readPropertyAt($objectOrArray, $this->length - 2);
if (!is_object($objectOrArray) && !is_array($objectOrArray)) {
throw new UnexpectedTypeException($objectOrArray, 'object or array');
}
$this->writeProperty($objectOrArray, $i, $value);
$property = $this->elements[$this->length - 1];
$singular = $this->singulars[$this->length - 1];
$isIndex = $this->isIndex[$this->length - 1];
$this->writeProperty($objectOrArray, $property, $singular, $isIndex, $value);
}
/**
* Reads the value of the property at the given index in the path
* Reads the path from an object up to a given path index.
*
* @param object $object The object to read from
* @param integer $currentIndex The index of the read property in the path
* @param object|array $objectOrArray The object or array to read from.
* @param integer $index The integer up to which should be read.
*
* @return mixed The value of the property
* @return mixed The value read at the end of the path.
*
* @throws UnexpectedTypeException If a value within the path is neither object nor array.
*/
protected function readProperty($object, $currentIndex)
protected function &readPropertyAt(&$objectOrArray, $index)
{
$property = $this->elements[$currentIndex];
if ($this->isIndex[$currentIndex]) {
if (!$object instanceof \ArrayAccess) {
throw new InvalidPropertyException(sprintf('Index "%s" cannot be read from object of type "%s" because it doesn\'t implement \ArrayAccess', $property, get_class($object)));
for ($i = 0; $i <= $index; ++$i) {
if (!is_object($objectOrArray) && !is_array($objectOrArray)) {
throw new UnexpectedTypeException($objectOrArray, 'object or array');
}
if (isset($object[$property])) {
return $object[$property];
// Create missing nested arrays on demand
if (is_array($objectOrArray) && !array_key_exists($this->elements[$i], $objectOrArray)) {
$objectOrArray[$this->elements[$i]] = $i + 1 < $this->length ? array() : null;
}
} else {
$property = $this->elements[$i];
$isIndex = $this->isIndex[$i];
$objectOrArray =& $this->readProperty($objectOrArray, $property, $isIndex);
}
return $objectOrArray;
}
/**
* Reads the a property from an object or array.
*
* @param object|array $objectOrArray The object or array to read from.
* @param string $property The property to read.
* @param integer $isIndex Whether to interpret the property as index.
*
* @return mixed The value of the read property
*
* @throws InvalidPropertyException If the property does not exist.
* @throws PropertyAccessDeniedException If the property cannot be accessed due to
* access restrictions (private or protected).
*/
protected function &readProperty(&$objectOrArray, $property, $isIndex)
{
$result = null;
if ($isIndex) {
if (!$objectOrArray instanceof \ArrayAccess && !is_array($objectOrArray)) {
throw new InvalidPropertyException(sprintf('Index "%s" cannot be read from object of type "%s" because it doesn\'t implement \ArrayAccess', $property, get_class($objectOrArray)));
}
if (isset($objectOrArray[$property])) {
$result =& $objectOrArray[$property];
}
} elseif (is_object($objectOrArray)) {
$camelProp = $this->camelize($property);
$reflClass = new ReflectionClass($object);
$reflClass = new ReflectionClass($objectOrArray);
$getter = 'get'.$camelProp;
$isser = 'is'.$camelProp;
$hasser = 'has'.$camelProp;
@ -365,50 +387,58 @@ class PropertyPath implements \IteratorAggregate
throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $getter, $reflClass->getName()));
}
return $object->$getter();
$result = $objectOrArray->$getter();
} elseif ($reflClass->hasMethod($isser)) {
if (!$reflClass->getMethod($isser)->isPublic()) {
throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $isser, $reflClass->getName()));
}
return $object->$isser();
$result = $objectOrArray->$isser();
} elseif ($reflClass->hasMethod($hasser)) {
if (!$reflClass->getMethod($hasser)->isPublic()) {
throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $hasser, $reflClass->getName()));
}
return $object->$hasser();
$result = $objectOrArray->$hasser();
} elseif ($reflClass->hasMethod('__get')) {
// needed to support magic method __get
return $object->$property;
$result =& $objectOrArray->$property;
} elseif ($reflClass->hasProperty($property)) {
if (!$reflClass->getProperty($property)->isPublic()) {
throw new PropertyAccessDeniedException(sprintf('Property "%s" is not public in class "%s". Maybe you should create the method "%s()" or "%s()"?', $property, $reflClass->getName(), $getter, $isser));
}
return $object->$property;
} elseif (property_exists($object, $property)) {
$result =& $objectOrArray->$property;
} elseif (property_exists($objectOrArray, $property)) {
// needed to support \stdClass instances
return $object->$property;
$result =& $objectOrArray->$property;
} else {
throw new InvalidPropertyException(sprintf('Neither property "%s" nor method "%s()" nor method "%s()" exists in class "%s"', $property, $getter, $isser, $reflClass->getName()));
}
} else {
throw new InvalidPropertyException(sprintf('Cannot read property "%s" from an array. Maybe you should write the property path as "[%s]" instead?', $property, $property));
}
return $result;
}
/**
* Sets the value of the property at the given index in the path
*
* @param object $objectOrArray The object or array to traverse
* @param integer $currentIndex The index of the modified property in the path
* @param mixed $value The value to set
* @param object|array $objectOrArray The object or array to write to.
* @param string $property The property to write.
* @param string $singular The singular form of the property name or null.
* @param integer $isIndex Whether to interpret the property as index.
* @param mixed $value The value to write.
*
* @throws InvalidPropertyException If the property does not exist.
* @throws PropertyAccessDeniedException If the property cannot be accessed due to
* access restrictions (private or protected).
*/
protected function writeProperty(&$objectOrArray, $currentIndex, $value)
protected function writeProperty(&$objectOrArray, $property, $singular, $isIndex, $value)
{
$property = $this->elements[$currentIndex];
if (is_object($objectOrArray) && $this->isIndex[$currentIndex]) {
if (!$objectOrArray instanceof \ArrayAccess) {
if ($isIndex) {
if (!$objectOrArray instanceof \ArrayAccess && !is_array($objectOrArray)) {
throw new InvalidPropertyException(sprintf('Index "%s" cannot be modified in object of type "%s" because it doesn\'t implement \ArrayAccess', $property, get_class($objectOrArray)));
}
@ -422,7 +452,6 @@ class PropertyPath implements \IteratorAggregate
// Check if the parent has matching methods to add/remove items
if (is_array($value) || $value instanceof Traversable) {
$singular = $this->singulars[$currentIndex];
if (null !== $singular) {
$addMethod = 'add' . ucfirst($singular);
$removeMethod = 'remove' . ucfirst($singular);
@ -490,7 +519,7 @@ class PropertyPath implements \IteratorAggregate
if ($addMethod && $removeMethod) {
$itemsToAdd = is_object($value) ? clone $value : $value;
$itemToRemove = array();
$previousValue = $this->readProperty($objectOrArray, $currentIndex);
$previousValue = $this->readProperty($objectOrArray, $property, $isIndex);
if (is_array($previousValue) || $previousValue instanceof Traversable) {
foreach ($previousValue as $previousItem) {
@ -538,21 +567,38 @@ class PropertyPath implements \IteratorAggregate
throw new InvalidPropertyException(sprintf('Neither element "%s" nor method "%s()" exists in class "%s"', $property, $setter, $reflClass->getName()));
}
} else {
$objectOrArray[$property] = $value;
throw new InvalidPropertyException(sprintf('Cannot write property "%s" in an array. Maybe you should write the property path as "[%s]" instead?', $property, $property));
}
}
protected function camelize($property)
/**
* Camelizes a given string.
*
* @param string $string Some string.
*
* @return string The camelized version of the string.
*/
protected function camelize($string)
{
return preg_replace_callback('/(^|_|\.)+(.)/', function ($match) { return ('.' === $match[1] ? '_' : '').strtoupper($match[2]); }, $property);
return preg_replace_callback('/(^|_|\.)+(.)/', function ($match) { return ('.' === $match[1] ? '_' : '').strtoupper($match[2]); }, $string);
}
private function isAccessible(ReflectionClass $reflClass, $methodName, $numberOfRequiredParameters)
/**
* Returns whether a method is public and has a specific number of required parameters.
*
* @param \ReflectionClass $class The class of the method.
* @param string $methodName The method name.
* @param integer $parameters The number of parameters.
*
* @return Boolean Whether the method is public and has $parameters
* required parameters.
*/
private function isAccessible(ReflectionClass $class, $methodName, $parameters)
{
if ($reflClass->hasMethod($methodName)) {
$method = $reflClass->getMethod($methodName);
if ($class->hasMethod($methodName)) {
$method = $class->getMethod($methodName);
if ($method->isPublic() && $method->getNumberOfRequiredParameters() === $numberOfRequiredParameters) {
if ($method->isPublic() && $method->getNumberOfRequiredParameters() === $parameters) {
return true;
}
}

View File

@ -0,0 +1,280 @@
<?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\Util;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class PropertyPathBuilder
{
/**
* @var array
*/
private $elements = array();
/**
* @var array
*/
private $isIndex = array();
/**
* Creates a new property path builder.
*
* @param null|PropertyPathInterface $path The path to initially store
* in the builder. Optional.
*/
public function __construct(PropertyPathInterface $path = null)
{
if (null !== $path) {
$this->append($path);
}
}
/**
* Appends a (sub-) path to the current path.
*
* @param PropertyPathInterface $path The path to append.
* @param integer $offset The offset where the appended piece
* starts in $path.
* @param integer $length The length of the appended piece.
* If 0, the full path is appended.
*/
public function append(PropertyPathInterface $path, $offset = 0, $length = 0)
{
if (0 === $length) {
$end = $path->getLength();
} else {
$end = $offset + $length;
}
for (; $offset < $end; ++$offset) {
$this->elements[] = $path->getElement($offset);
$this->isIndex[] = $path->isIndex($offset);
}
}
/**
* Appends an index element to the current path.
*
* @param string $name The name of the appended index.
*/
public function appendIndex($name)
{
$this->elements[] = $name;
$this->isIndex[] = true;
}
/**
* Appends a property element to the current path.
*
* @param string $name The name of the appended property.
*/
public function appendProperty($name)
{
$this->elements[] = $name;
$this->isIndex[] = false;
}
/**
* Removes elements from the current path.
*
* @param integer $offset The offset at which to remove.
* @param integer $length The length of the removed piece.
*/
public function remove($offset, $length = 1)
{
if (!isset($this->elements[$offset])) {
throw new \OutOfBoundsException('The offset ' . $offset . ' is not within the property path');
}
$this->resize($offset, $length, 0);
}
/**
* Replaces a sub-path by a different (sub-) path.
*
* @param integer $offset The offset at which to replace.
* @param integer $length The length of the piece to replace.
* @param PropertyPathInterface $path The path to insert.
* @param integer $pathOffset The offset where the inserted piece
* starts in $path.
* @param integer $pathLength The length of the inserted piece.
* If 0, the full path is inserted.
*
* @throws \OutOfBoundsException If the offset is invalid.
*/
public function replace($offset, $length, PropertyPathInterface $path, $pathOffset = 0, $pathLength = 0)
{
if (!isset($this->elements[$offset])) {
throw new \OutOfBoundsException('The offset ' . $offset . ' is not within the property path');
}
if (0 === $pathLength) {
$pathLength = $path->getLength() - $pathOffset;
}
$this->resize($offset, $length, $pathLength);
for ($i = 0; $i < $pathLength; ++$i) {
$this->elements[$offset + $i] = $path->getElement($pathOffset + $i);
$this->isIndex[$offset + $i] = $path->isIndex($pathOffset + $i);
}
}
/**
* Replaces a property element by an index element.
*
* @param integer $offset The offset at which to replace.
* @param string $name The new name of the element. Optional.
*
* @throws \OutOfBoundsException If the offset is invalid.
*/
public function replaceByIndex($offset, $name = null)
{
if (!isset($this->elements[$offset])) {
throw new \OutOfBoundsException('The offset ' . $offset . ' is not within the property path');
}
if (null !== $name) {
$this->elements[$offset] = $name;
}
$this->isIndex[$offset] = true;
}
/**
* Replaces an index element by a property element.
*
* @param integer $offset The offset at which to replace.
* @param string $name The new name of the element. Optional.
*
* @throws \OutOfBoundsException If the offset is invalid.
*/
public function replaceByProperty($offset, $name = null)
{
if (!isset($this->elements[$offset])) {
throw new \OutOfBoundsException('The offset ' . $offset . ' is not within the property path');
}
if (null !== $name) {
$this->elements[$offset] = $name;
}
$this->isIndex[$offset] = false;
}
/**
* Returns the length of the current path.
*
* @return integer The path length.
*/
public function getLength()
{
return count($this->elements);
}
/**
* Returns the current property path.
*
* @return PropertyPathInterface The constructed property path.
*/
public function getPropertyPath()
{
$pathAsString = $this->__toString();
return '' !== $pathAsString ? new PropertyPath($pathAsString) : null;
}
/**
* Returns the current property path as string.
*
* @return string The property path as string.
*/
public function __toString()
{
$string = '';
foreach ($this->elements as $offset => $element) {
if ($this->isIndex[$offset]) {
$element = '[' . $element . ']';
} elseif ('' !== $string) {
$string .= '.';
}
$string .= $element;
}
return $string;
}
/**
* Resizes the path so that a chunk of length $cutLength is
* removed at $offset and another chunk of length $insertionLength
* can be inserted.
*
* @param integer $offset The offset where the removed chunk starts.
* @param integer $cutLength The length of the removed chunk.
* @param integer $insertionLength The length of the inserted chunk.
*/
private function resize($offset, $cutLength, $insertionLength)
{
// Nothing else to do in this case
if ($insertionLength === $cutLength) {
return;
}
$length = count($this->elements);
if ($cutLength > $insertionLength) {
// More elements should be removed than inserted
$diff = $cutLength - $insertionLength;
$newLength = $length - $diff;
// Shift elements to the left (left-to-right until the new end)
// Max allowed offset to be shifted is such that
// $offset + $diff < $length (otherwise invalid index access)
// i.e. $offset < $length - $diff = $newLength
for ($i = $offset; $i < $newLength; ++$i) {
$this->elements[$i] = $this->elements[$i + $diff];
$this->isIndex[$i] = $this->isIndex[$i + $diff];
}
// All remaining elements should be removed
for (; $i < $length; ++$i) {
unset($this->elements[$i]);
unset($this->isIndex[$i]);
}
} else {
$diff = $insertionLength - $cutLength;
$newLength = $length + $diff;
$indexAfterInsertion = $offset + $insertionLength;
// Shift old elements to the right to make up space for the
// inserted elements. This needs to be done left-to-right in
// order to preserve an ascending array index order
for ($i = $length; $i < $newLength; ++$i) {
$this->elements[$i] = $this->elements[$i - $diff];
$this->isIndex[$i] = $this->isIndex[$i - $diff];
}
// Shift remaining elements to the right. Do this right-to-left
// so we don't overwrite elements before copying them
// The last written index is the immediate index after the inserted
// string, because the indices before that will be overwritten
// anyway.
for ($i = $length - 1; $i >= $indexAfterInsertion; --$i) {
$this->elements[$i] = $this->elements[$i - $diff];
$this->isIndex[$i] = $this->isIndex[$i - $diff];
}
}
}
}

View File

@ -0,0 +1,92 @@
<?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\Util;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface PropertyPathInterface extends \Traversable
{
/**
* Returns the string representation of the property path
*
* @return string The path as string.
*/
function __toString();
/**
* Returns the positions at which the elements of the path
* start in the string.
*
* @return array The string offsets of the elements.
*/
function getPositions();
/**
* Returns the length of the property path.
*
* @return integer The path length.
*/
function getLength();
/**
* Returns the parent property path.
*
* The parent property path is the one that contains the same items as
* this one except for the last one.
*
* If this property path only contains one item, null is returned.
*
* @return PropertyPath The parent path or null.
*/
function getParent();
/**
* Returns the elements of the property path as array
*
* @return array An array of property/index names
*/
function getElements();
/**
* Returns the element at the given index in the property path
*
* @param integer $index The index key
*
* @return string A property or index name
*
* @throws \OutOfBoundsException If the offset is invalid.
*/
function getElement($index);
/**
* Returns whether the element at the given index is a property
*
* @param integer $index The index in the property path
*
* @return Boolean Whether the element at this index is a property
*
* @throws \OutOfBoundsException If the offset is invalid.
*/
function isProperty($index);
/**
* Returns whether the element at the given index is an array index
*
* @param integer $index The index in the property path
*
* @return Boolean Whether the element at this index is an array index
*
* @throws \OutOfBoundsException If the offset is invalid.
*/
function isIndex($index);
}

View File

@ -17,7 +17,7 @@ namespace Symfony\Component\Form\Util;
*
* @author Bernhard Schussek <bernhard.schussek@symfony.com>
*/
class PropertyPathIterator extends \ArrayIterator
class PropertyPathIterator extends \ArrayIterator implements PropertyPathIteratorInterface
{
/**
* The traversed property path
@ -28,9 +28,9 @@ class PropertyPathIterator extends \ArrayIterator
/**
* Constructor.
*
* @param PropertyPath $path The property path to traverse
* @param PropertyPathInterface $path The property path to traverse
*/
public function __construct(PropertyPath $path)
public function __construct(PropertyPathInterface $path)
{
parent::__construct($path->getElements());
@ -38,20 +38,7 @@ class PropertyPathIterator extends \ArrayIterator
}
/**
* Returns whether next() can be called without making the iterator invalid
*
* @return Boolean
*/
public function hasNext()
{
return $this->offsetExists($this->key() + 1);
}
/**
* Returns whether the current element in the property path is an array
* index
*
* @return Boolean
* {@inheritdoc}
*/
public function isIndex()
{
@ -59,10 +46,7 @@ class PropertyPathIterator extends \ArrayIterator
}
/**
* Returns whether the current element in the property path is a property
* names
*
* @return Boolean
* {@inheritdoc}
*/
public function isProperty()
{

View File

@ -0,0 +1,34 @@
<?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\Util;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface PropertyPathIteratorInterface extends \Iterator, \SeekableIterator
{
/**
* Returns whether the current element in the property path is an array
* index.
*
* @return Boolean
*/
function isIndex();
/**
* Returns whether the current element in the property path is a property
* name.
*
* @return Boolean
*/
function isProperty();
}

View File

@ -28,7 +28,6 @@ class VirtualFormAwareIterator extends \ArrayIterator implements \RecursiveItera
public function hasChildren()
{
return $this->current()->hasAttribute('virtual')
&& $this->current()->getAttribute('virtual');
return $this->current()->getConfig()->getVirtual();
}
}

View File

@ -20,3 +20,6 @@ CHANGELOG
* [BC BREAK] ConstraintValidatorInterface method `isValid` has been renamed to
`validate`, its return value was dropped. ConstraintValidator still contains
`isValid` for BC
* [BC BREAK] collections in fields annotated with `Valid` are not traversed
recursively anymore by default. `Valid` contains a new property `deep`
which enables the BC behavior.

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

@ -21,4 +21,6 @@ use Symfony\Component\Validator\Constraint;
class Valid extends Constraint
{
public $traverse = true;
public $deep = false;
}

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
));
}

View File

@ -137,11 +137,11 @@ class GraphWalker
}
if ($metadata->isCascaded()) {
$this->walkReference($value, $propagatedGroup ?: $group, $propertyPath, $metadata->isCollectionCascaded());
$this->walkReference($value, $propagatedGroup ?: $group, $propertyPath, $metadata->isCollectionCascaded(), $metadata->isCollectionCascadedDeeply());
}
}
public function walkReference($value, $group, $propertyPath, $traverse)
public function walkReference($value, $group, $propertyPath, $traverse, $deep = false)
{
if (null !== $value) {
if (!is_object($value) && !is_array($value)) {
@ -152,7 +152,8 @@ class GraphWalker
foreach ($value as $key => $element) {
// Ignore any scalar values in the collection
if (is_object($element) || is_array($element)) {
$this->walkReference($element, $group, $propertyPath.'['.$key.']', $traverse);
// Only repeat the traversal if $deep is set
$this->walkReference($element, $group, $propertyPath.'['.$key.']', $deep, $deep);
}
}
}

View File

@ -22,6 +22,7 @@ abstract class MemberMetadata extends ElementMetadata
public $property;
public $cascaded = false;
public $collectionCascaded = false;
public $collectionCascadedDeeply = false;
private $reflMember;
/**
@ -52,7 +53,9 @@ abstract class MemberMetadata extends ElementMetadata
if ($constraint instanceof Valid) {
$this->cascaded = true;
/* @var Valid $constraint */
$this->collectionCascaded = $constraint->traverse;
$this->collectionCascadedDeeply = $constraint->deep;
} else {
parent::addConstraint($constraint);
}
@ -156,6 +159,17 @@ abstract class MemberMetadata extends ElementMetadata
return $this->collectionCascaded;
}
/**
* Returns whether arrays or traversable objects stored in this member
* should be traversed recursively for inner arrays/traversable objects
*
* @return Boolean
*/
public function isCollectionCascadedDeeply()
{
return $this->collectionCascadedDeeply;
}
/**
* Returns the value of this property in the given object
*

View File

@ -357,6 +357,77 @@ class GraphWalkerTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($violations, $this->walker->getViolations());
}
public function testWalkCascadedPropertyDoesNotRecurseByDefault()
{
$entity = new Entity();
$entityMetadata = new ClassMetadata(get_class($entity));
$this->factory->addClassMetadata($entityMetadata);
$this->factory->addClassMetadata(new ClassMetadata('ArrayIterator'));
// add a constraint for the entity that always fails
$entityMetadata->addConstraint(new FailingConstraint());
// validate iterator when validating the property "reference"
$this->metadata->addPropertyConstraint('reference', new Valid());
$this->walker->walkPropertyValue(
$this->metadata,
'reference',
new \ArrayIterator(array(
// The inner iterator should not be traversed by default
'key' => new \ArrayIterator(array(
'nested' => $entity,
)),
)),
'Default',
'path'
);
$violations = new ConstraintViolationList();
$this->assertEquals($violations, $this->walker->getViolations());
}
public function testWalkCascadedPropertyRecursesIfDeepIsSet()
{
$entity = new Entity();
$entityMetadata = new ClassMetadata(get_class($entity));
$this->factory->addClassMetadata($entityMetadata);
$this->factory->addClassMetadata(new ClassMetadata('ArrayIterator'));
// add a constraint for the entity that always fails
$entityMetadata->addConstraint(new FailingConstraint());
// validate iterator when validating the property "reference"
$this->metadata->addPropertyConstraint('reference', new Valid(array(
'deep' => true,
)));
$this->walker->walkPropertyValue(
$this->metadata,
'reference',
new \ArrayIterator(array(
// The inner iterator should now be traversed
'key' => new \ArrayIterator(array(
'nested' => $entity,
)),
)),
'Default',
'path'
);
$violations = new ConstraintViolationList();
$violations->add(new ConstraintViolation(
'Failed',
array(),
'Root',
'path[key][nested]',
$entity
));
$this->assertEquals($violations, $this->walker->getViolations());
}
public function testWalkCascadedPropertyDoesNotValidateNestedScalarValues()
{
// validate array when validating the property "reference"