diff --git a/src/Symfony/Component/Form/ButtonBuilder.php b/src/Symfony/Component/Form/ButtonBuilder.php index c65fb303ddcd3..4b4f6cf774464 100644 --- a/src/Symfony/Component/Form/ButtonBuilder.php +++ b/src/Symfony/Component/Form/ButtonBuilder.php @@ -12,15 +12,16 @@ namespace Symfony\Component\Form; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\Form\Exception\InvalidArgumentException; use Symfony\Component\Form\Exception\BadMethodCallException; +use Symfony\Component\Form\Exception\InvalidArgumentException; +use Symfony\Component\Form\Exception\InvalidConfigurationException; /** * A builder for {@link Button} instances. * * @author Bernhard Schussek */ -class ButtonBuilder implements \IteratorAggregate, FormBuilderInterface +class ButtonBuilder implements \IteratorAggregate, FormBuilderInterface, OrderedFormConfigBuilderInterface { /** * @var bool @@ -47,6 +48,11 @@ class ButtonBuilder implements \IteratorAggregate, FormBuilderInterface */ private $attributes = array(); + /** + * @var null|string|array + */ + private $position; + /** * @var array */ @@ -58,7 +64,7 @@ class ButtonBuilder implements \IteratorAggregate, FormBuilderInterface * @param string $name The name of the button * @param array $options The button's options * - * @throws InvalidArgumentException If the name is empty. + * @throws InvalidArgumentException if the name is empty */ public function __construct($name, array $options = array()) { @@ -503,6 +509,28 @@ public function setAutoInitialize($initialize) return $this; } + /** + * {@inheritdoc} + */ + public function setPosition($position) + { + if ($this->locked) { + throw new BadMethodCallException('ButtonBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + if (is_string($position) && $position !== 'first' && $position !== 'last') { + throw new InvalidConfigurationException('When using position as a string, the only supported values are "first" and "last".'); + } + + if (is_array($position) && !isset($position['before']) && !isset($position['after'])) { + throw new InvalidConfigurationException('When using position as an array, the "before" or "after" option must be defined.'); + } + + $this->position = $position; + + return $this; + } + /** * Unsupported method. * @@ -752,6 +780,14 @@ public function getAutoInitialize() return false; } + /** + * {@inheritdoc} + */ + public function getPosition() + { + return $this->position; + } + /** * Unsupported method. * diff --git a/src/Symfony/Component/Form/Extension/Core/Type/BaseType.php b/src/Symfony/Component/Form/Extension/Core/Type/BaseType.php index d68337e3bc8c1..6c79a0236547a 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/BaseType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/BaseType.php @@ -32,8 +32,10 @@ abstract class BaseType extends AbstractType */ public function buildForm(FormBuilderInterface $builder, array $options) { - $builder->setDisabled($options['disabled']); - $builder->setAutoInitialize($options['auto_initialize']); + $builder + ->setDisabled($options['disabled']) + ->setAutoInitialize($options['auto_initialize']) + ->setPosition($options['position']); } /** @@ -117,6 +119,7 @@ public function configureOptions(OptionsResolver $resolver) 'attr' => array(), 'translation_domain' => null, 'auto_initialize' => true, + 'position' => null, )); $resolver->setAllowedTypes('attr', 'array'); diff --git a/src/Symfony/Component/Form/FormConfigBuilder.php b/src/Symfony/Component/Form/FormConfigBuilder.php index eee201d93a83e..eb6666b267418 100644 --- a/src/Symfony/Component/Form/FormConfigBuilder.php +++ b/src/Symfony/Component/Form/FormConfigBuilder.php @@ -13,6 +13,7 @@ use Symfony\Component\Form\Exception\BadMethodCallException; use Symfony\Component\Form\Exception\InvalidArgumentException; +use Symfony\Component\Form\Exception\InvalidConfigurationException; use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\PropertyAccess\PropertyPath; use Symfony\Component\PropertyAccess\PropertyPathInterface; @@ -25,7 +26,7 @@ * * @author Bernhard Schussek */ -class FormConfigBuilder implements FormConfigBuilderInterface +class FormConfigBuilder implements FormConfigBuilderInterface, OrderedFormConfigBuilderInterface { /** * Caches a globally unique {@link NativeRequestHandler} instance. @@ -172,6 +173,11 @@ class FormConfigBuilder implements FormConfigBuilderInterface */ private $autoInitialize = false; + /** + * @var null|string|array + */ + private $position; + /** * @var array */ @@ -185,8 +191,8 @@ class FormConfigBuilder implements FormConfigBuilderInterface * @param EventDispatcherInterface $dispatcher The event dispatcher * @param array $options The form options * - * @throws InvalidArgumentException If the data class is not a valid class or if - * the name contains invalid characters. + * @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, array $options = array()) { @@ -513,6 +519,14 @@ public function getAutoInitialize() return $this->autoInitialize; } + /** + * {@inheritdoc} + */ + public function getPosition() + { + return $this->position; + } + /** * {@inheritdoc} */ @@ -831,6 +845,28 @@ public function setAutoInitialize($initialize) return $this; } + /** + * {@inheritdoc} + */ + public function setPosition($position) + { + if ($this->locked) { + throw new BadMethodCallException('FormConfigBuilder methods cannot be accessed anymore once the builder is turned into a FormConfigInterface instance.'); + } + + if (is_string($position) && ($position !== 'first') && ($position !== 'last')) { + throw new InvalidConfigurationException('When using position as a string, the only supported values are "first" and "last".'); + } + + if (is_array($position) && !isset($position['before']) && !isset($position['after'])) { + throw new InvalidConfigurationException('When using position as an array, the "before" or "after" option must be defined.'); + } + + $this->position = $position; + + return $this; + } + /** * {@inheritdoc} */ @@ -852,8 +888,8 @@ public function getFormConfig() * * @param string|int $name The tested form name * - * @throws UnexpectedTypeException If the name is not a string or an integer. - * @throws InvalidArgumentException If the name contains invalid characters. + * @throws UnexpectedTypeException if the name is not a string or an integer + * @throws InvalidArgumentException if the name contains invalid characters */ public static function validateName($name) { diff --git a/src/Symfony/Component/Form/FormOrderer.php b/src/Symfony/Component/Form/FormOrderer.php new file mode 100644 index 0000000000000..1f7571be4079b --- /dev/null +++ b/src/Symfony/Component/Form/FormOrderer.php @@ -0,0 +1,307 @@ + + * + * 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\InvalidConfigurationException; + +/** + * Form orderer. + * + * @author GeLo + */ +class FormOrderer +{ + private $weights; + private $deferred; + private $firstWeight; + private $currentWeight; + private $lastWeight; + + /** + * Orders the form. + * + * @param FormInterface $form + * + * @return array The ordered form child names + * + * @throws InvalidConfigurationException If a position is not valid + */ + public function order(FormInterface $form) + { + $this->reset(); + + foreach ($form as $child) { + $position = $child->getConfig()->getPosition(); + + if (empty($position)) { + $this->processEmptyPosition($child); + } elseif (is_string($position)) { + $this->processStringPosition($child, $position); + } else { + $this->processArrayPosition($child, $position); + } + } + + asort($this->weights, SORT_NUMERIC); + + return array_keys($this->weights); + } + + /** + * Process the form using the current weight in order to maintain the default order. + * + * @param FormInterface $form + */ + private function processEmptyPosition(FormInterface $form) + { + $this->processWeight($form, $this->currentWeight); + } + + /** + * Process the form using the current first/last weight in order to put your form at the + * first/last position according to the default order. + * + * @param FormInterface $form + * @param string $position + */ + private function processStringPosition(FormInterface $form, $position) + { + if ($position === 'first') { + $this->processFirst($form); + } else { + $this->processLast($form); + } + } + + /** + * Process the form using the weight of the "before" or "after" form + * If the "before" or "after" form has not been processed yet, we defer it for the next forms. + * + * @param FormInterface $form + * @param array $position + */ + private function processArrayPosition(FormInterface $form, array $position) + { + if (isset($position['before'])) { + $this->processBefore($form, $position['before']); + } + + if (isset($position['after'])) { + $this->processAfter($form, $position['after']); + } + } + + /** + * Process the form using the current first weight in order to put + * your form at the first position according to the default order. + * + * @param FormInterface $form + */ + private function processFirst(FormInterface $form) + { + $this->processWeight($form, $this->firstWeight++); + } + + /** + * Processes the form using the current last weight in order to put + * your form at the last position according to the default order. + * + * @param FormInterface $form + */ + private function processLast(FormInterface $form) + { + $this->processWeight($form, $this->lastWeight + 1); + } + + /** + * Process the form using the weight of the "before" form + * If the "before" form has not been processed yet, we defer it for the next forms. + * + * @param FormInterface $form + * @param string $before + */ + private function processBefore(FormInterface $form, $before) + { + if (!isset($this->weights[$before])) { + $this->processDeferred($form, $before, 'before'); + } else { + $this->processWeight($form, $this->weights[$before]); + } + } + + /** + * Process the form using the weight of the "after" form + * If the "after" form has not been processed yet, we defer it for the next forms. + * + * @param FormInterface $form + * @param string $after + */ + private function processAfter(FormInterface $form, $after) + { + if (!isset($this->weights[$after])) { + $this->processDeferred($form, $after, 'after'); + } else { + $this->processWeight($form, $this->weights[$after] + 1); + } + } + + /** + * Process the form using the given weight. + * + * This method also updates the orderer state accordingly + * + * @param FormInterface $form + * @param int $weight + */ + private function processWeight(FormInterface $form, $weight) + { + foreach ($this->weights as &$weightRef) { + if ($weightRef >= $weight) { + ++$weightRef; + } + } + + if ($this->currentWeight >= $weight) { + ++$this->currentWeight; + } + + ++$this->lastWeight; + + $this->weights[$form->getName()] = $weight; + $this->finishWeight($form, $weight); + } + + /** + * Finishes the form weight processing by trying to process deferred forms + * which refers to the current processed form. + * + * @param FormInterface $form + * @param int $weight + * @param string $position + * + * @return int The new weight + */ + private function finishWeight(FormInterface $form, $weight, $position = null) + { + if ($position === null) { + foreach (array_keys($this->deferred) as $position) { + $weight = $this->finishWeight($form, $weight, $position); + } + } else { + $name = $form->getName(); + + if (isset($this->deferred[$position][$name])) { + $postIncrement = $position === 'before'; + + foreach ($this->deferred[$position][$name] as $deferred) { + $this->processWeight($deferred, $postIncrement ? $weight++ : ++$weight); + } + + unset($this->deferred[$position][$name]); + } + } + + return $weight; + } + + /** + * Processes a deferred form by checking if it is valid and + * if it does not become a circular or symmetric ordering. + * + * @param FormInterface $form + * @param string $deferred + * @param string $position + * + * @throws InvalidConfigurationException If the deferred form does not exist + */ + private function processDeferred(FormInterface $form, $deferred, $position) + { + if (!$form->getParent()->has($deferred)) { + throw new InvalidConfigurationException(sprintf('The "%s" form is configured to be placed just %s the form "%s" but the form "%s" does not exist.', $form->getName(), $position, $deferred, $deferred)); + } + + $this->deferred[$position][$deferred][] = $form; + + $name = $form->getName(); + + $this->detectCircularDeferred($name, $position); + $this->detectedSymmetricDeferred($name, $deferred, $position); + } + + /** + * Detects circular deferred forms for after/before position such as A => B => C => A. + * + * @param string $name + * @param string $position + * @param array $stack + * + * @throws InvalidConfigurationException If there is a circular before/after deferred + */ + private function detectCircularDeferred($name, $position, array $stack = array()) + { + if (!isset($this->deferred[$position][$name])) { + return; + } + + $stack[] = $name; + + foreach ($this->deferred[$position][$name] as $deferred) { + $deferredName = $deferred->getName(); + + if ($deferredName === $stack[0]) { + $stack[] = $stack[0]; + + throw new InvalidConfigurationException(sprintf('The form ordering cannot be resolved due to conflict in %s positions (%s).', $position, implode(' => ', $stack))); + } + + $this->detectCircularDeferred($deferredName, $position, $stack); + } + } + + /** + * Detects symmetric before/after deferred such as A after B and B after A. + * + * @param string $name + * @param string $deferred + * @param string $position + * + * @throws InvalidConfigurationException If there is a symetric before/after deferred + */ + private function detectedSymmetricDeferred($name, $deferred, $position) + { + $reversePosition = ($position === 'before') ? 'after' : 'before'; + + if (isset($this->deferred[$reversePosition][$name])) { + foreach ($this->deferred[$reversePosition][$name] as $diff) { + if ($diff->getName() === $deferred) { + throw new InvalidConfigurationException(sprintf('The form ordering does not support symmetrical before/after option (%s <=> %s).', $name, $deferred)); + } + } + } + } + + /** + * Resets the orderer. + */ + private function reset() + { + $this->weights = array(); + $this->deferred = array( + 'before' => array(), + 'after' => array(), + ); + + $this->firstWeight = 0; + $this->currentWeight = 0; + $this->lastWeight = 0; + } +} diff --git a/src/Symfony/Component/Form/OrderedFormConfigBuilderInterface.php b/src/Symfony/Component/Form/OrderedFormConfigBuilderInterface.php new file mode 100644 index 0000000000000..c09c556d1725d --- /dev/null +++ b/src/Symfony/Component/Form/OrderedFormConfigBuilderInterface.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +/** + * @author GeLo + */ +interface OrderedFormConfigBuilderInterface extends OrderedFormConfigInterface +{ + /** + * Sets the form position. + * + * * The position can be `null` to reflect the original forms order. + * + * * The position can be `first` to place this form at the first position. + * If many forms are defined as `first`, the original order between these forms is maintained. + * Warning, `first` does not mean "very first" if there are many forms which are defined as `first` + * or if you set up an other form `before` this form. + * + * * The position can be `last` to place this form at the last position. + * If many forms are defined as `last`, the original order between these forms is maintained. + * Warning, `last` does not mean "very last" if there are many forms which are defined as `last` + * or if you set up an other form `after` this form. + * + * * The position can be `array('before' => 'form_name')` to place this form before the `form_name` form. + * If many forms defines the same `before` form, the original order between these forms is maintained. + * Warning, `before` does not mean "just before" if there are many forms which defined the same `before` form. + * + * * The position can be `array('after' => 'form_name')` to place this form after the `form_name` form. + * If many forms defines the same after form, the original order between these forms is maintained. + * Warning, `after` does not mean "just after" if there are many forms which defined the same `after` form. + * + * You can combine the `after` & `before` options together or with `first` and/or `last` to achieve + * more complex use cases. + * + * @param null|string|array $position the form position + * + * @throws \Symfony\Component\Form\Exception\InvalidConfigurationException if the position is not valid + * + * @return self the configuration object + */ + public function setPosition($position); +} diff --git a/src/Symfony/Component/Form/OrderedFormConfigInterface.php b/src/Symfony/Component/Form/OrderedFormConfigInterface.php new file mode 100644 index 0000000000000..6385e19d1c8b3 --- /dev/null +++ b/src/Symfony/Component/Form/OrderedFormConfigInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +/** + * @author GeLo + */ +interface OrderedFormConfigInterface +{ + /** + * Gets the form position. + * + * @see FormConfigBuilderInterface::setPosition + * + * @return null|string|array the position + */ + public function getPosition(); +} diff --git a/src/Symfony/Component/Form/ResolvedFormType.php b/src/Symfony/Component/Form/ResolvedFormType.php index b71be3ceb0a52..fd3b7dda857d2 100644 --- a/src/Symfony/Component/Form/ResolvedFormType.php +++ b/src/Symfony/Component/Form/ResolvedFormType.php @@ -53,6 +53,7 @@ public function __construct(FormTypeInterface $innerType, array $typeExtensions $this->innerType = $innerType; $this->typeExtensions = $typeExtensions; $this->parent = $parent; + $this->orderer = new FormOrderer(); } /** @@ -173,6 +174,22 @@ public function finishView(FormView $view, FormInterface $form, array $options) /* @var FormTypeExtensionInterface $extension */ $extension->finishView($view, $form, $options); } + + $children = $view->children; + $view->children = array(); + + foreach ($this->orderer->order($form) as $name) { + if (!isset($children[$name])) { + continue; + } + + $view->children[$name] = $children[$name]; + unset($children[$name]); + } + + foreach ($children as $name => $child) { + $view->children[$name] = $child; + } } /** diff --git a/src/Symfony/Component/Form/Tests/FormOrdererTest.php b/src/Symfony/Component/Form/Tests/FormOrdererTest.php new file mode 100644 index 0000000000000..de75a4ff77e08 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/FormOrdererTest.php @@ -0,0 +1,325 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests; + +use Symfony\Component\Form\Test\FormIntegrationTestCase; + +/** + * Form orderer test. + * + * @author GeLo + */ +class FormOrdererTest extends FormIntegrationTestCase +{ + public function getValidPositions() + { + return array( + // No position + array( + array('foo', 'bar', 'baz', 'bat'), + array('foo', 'bar', 'baz', 'bat'), + ), + + // First position + array( + array('foo' => 'first', 'bar', 'baz', 'bat'), + array('foo', 'bar', 'baz', 'bat'), + ), + array( + array('bar', 'baz', 'foo' => 'first', 'bat'), + array('foo', 'bar', 'baz', 'bat'), + ), + array( + array('bar', 'baz', 'bat', 'foo' => 'first'), + array('foo', 'bar', 'baz', 'bat'), + ), + array( + array('baz', 'foo' => 'first', 'bat', 'bar' => 'first'), + array('foo', 'bar', 'baz', 'bat'), + ), + + // Last position + array( + array('foo', 'bar', 'baz', 'bat' => 'last'), + array('foo', 'bar', 'baz', 'bat'), + ), + array( + array('foo', 'bar', 'bat' => 'last', 'baz'), + array('foo', 'bar', 'baz', 'bat'), + ), + array( + array('bat' => 'last', 'foo', 'bar', 'baz'), + array('foo', 'bar', 'baz', 'bat'), + ), + array( + array('baz' => 'last', 'foo', 'bat' => 'last', 'bar'), + array('foo', 'bar', 'baz', 'bat'), + ), + + // Before position + array( + array('foo' => array('before' => 'bar'), 'bar', 'baz', 'bat'), + array('foo', 'bar', 'baz', 'bat'), + ), + array( + array('bar', 'foo' => array('before' => 'bar'), 'baz', 'bat'), + array('foo', 'bar', 'baz', 'bat'), + ), + array( + array('bar', 'baz', 'bat', 'foo' => array('before' => 'bar')), + array('foo', 'bar', 'baz', 'bat'), + ), + array( + array( + 'bar' => array('before' => 'baz'), + 'foo' => array('before' => 'bar'), + 'bat', + 'baz' => array('before' => 'bat'), + ), + array('foo', 'bar', 'baz', 'bat'), + ), + array( + array( + 'bar' => array('before' => 'bat'), + 'foo' => array('before' => 'bar'), + 'bat', + 'baz' => array('before' => 'bat'), + ), + array('foo', 'bar', 'baz', 'bat'), + ), + + // After position + array( + array('foo', 'bar' => array('after' => 'foo'), 'baz', 'bat'), + array('foo', 'bar', 'baz', 'bat'), + ), + array( + array('bar' => array('after' => 'foo'), 'foo', 'baz', 'bat'), + array('foo', 'bar', 'baz', 'bat'), + ), + array( + array('foo', 'baz', 'bat', 'bar' => array('after' => 'foo')), + array('foo', 'bar', 'baz', 'bat'), + ), + array( + array( + 'foo', + 'baz' => array('after' => 'bar'), + 'bat' => array('after' => 'baz'), + 'bar' => array('after' => 'foo'), + ), + array('foo', 'bar', 'baz', 'bat'), + ), + array( + array( + 'foo', + 'baz' => array('after' => 'bar'), + 'bat' => array('after' => 'bar'), + 'bar' => array('after' => 'foo'), + ), + array('foo', 'bar', 'baz', 'bat'), + ), + + // First & last position + array( + array('foo' => 'first', 'bar', 'baz', 'bat' => 'last'), + array('foo', 'bar', 'baz', 'bat'), + ), + array( + array('bar', 'bat' => 'last', 'foo' => 'first', 'baz'), + array('foo', 'bar', 'baz', 'bat'), + ), + array( + array('baz' => 'last', 'foo' => 'first', 'bar' => 'first', 'bat' => 'last'), + array('foo', 'bar', 'baz', 'bat'), + ), + + // Before & after position + array( + array('foo', 'bar' => array('after' => 'foo', 'before' => 'baz'), 'baz', 'bat'), + array('foo', 'bar', 'baz', 'bat'), + ), + array( + array('foo', 'bar' => array('before' => 'baz', 'after' => 'foo'), 'baz', 'bat'), + array('foo', 'bar', 'baz', 'bat'), + ), + array( + array('bar' => array('after' => 'foo', 'before' => 'baz'), 'foo', 'baz', 'bat'), + array('foo', 'bar', 'baz', 'bat'), + ), + array( + array('bar' => array('before' => 'baz', 'after' => 'foo'), 'foo', 'baz', 'bat'), + array('foo', 'bar', 'baz', 'bat'), + ), + array( + array('foo', 'baz', 'bat', 'bar' => array('after' => 'foo', 'before' => 'baz')), + array('foo', 'bar', 'baz', 'bat'), + ), + array( + array('foo', 'baz', 'bat', 'bar' => array('before' => 'baz', 'after' => 'foo')), + array('foo', 'bar', 'baz', 'bat'), + ), + array( + array('foo' => array('before' => 'bar'), 'bar', 'baz' => array('after' => 'bar'), 'bat'), + array('foo', 'bar', 'baz', 'bat'), + ), + array( + array('bar', 'foo' => array('before' => 'bar'), 'bat', 'baz' => array('after' => 'bar')), + array('foo', 'bar', 'baz', 'bat'), + ), + array( + array('bar' => array('after' => 'foo'), 'foo', 'bat', 'baz' => array('before' => 'bat')), + array('foo', 'bar', 'baz', 'bat'), + ), + array( + array( + 'bar' => array('after' => 'foo', 'before' => 'baz'), + 'foo', + 'bat', + 'baz' => array('before' => 'bat', 'after' => 'bar'), + ), + array('foo', 'bar', 'baz', 'bat'), + ), + + // First, last, before & after position + array( + array( + 'bar' => array('after' => 'foo', 'before' => 'baz'), + 'foo' => 'first', + 'bat' => 'last', + 'baz' => array('before' => 'bat', 'after' => 'bar'), + ), + array('foo', 'bar', 'baz', 'bat'), + ), + array( + array( + 'bar' => array('after' => 'foo', 'before' => 'baz'), + 'foo' => 'first', + 'bat', + 'baz' => array('before' => 'bat'), + 'nan' => 'last', + 'pop' => array('after' => 'ban'), + 'ban', + 'biz' => array('before' => 'nan'), + 'boz' => array('before' => 'biz', array('after' => 'pop')), + ), + array('foo', 'bar', 'baz', 'bat', 'ban', 'pop', 'boz', 'biz', 'nan'), + ), + ); + } + + public function getInvalidPositions() + { + return array( + // Invalid before/after + array( + array('foo' => array('before' => 'bar')), + 'The "foo" form is configured to be placed just before the form "bar" but the form "bar" does not exist.', + ), + array( + array('foo' => array('after' => 'bar')), + 'The "foo" form is configured to be placed just after the form "bar" but the form "bar" does not exist.', + ), + + // Circular before + array( + array('foo' => array('before' => 'foo')), + 'The form ordering cannot be resolved due to conflict in before positions (foo => foo)', + ), + array( + array('foo' => array('before' => 'bar'), 'bar' => array('before' => 'foo')), + 'The form ordering cannot be resolved due to conflict in before positions (bar => foo => bar).', + ), + array( + array( + 'foo' => array('before' => 'bar'), + 'bar' => array('before' => 'baz'), + 'baz' => array('before' => 'foo'), + ), + 'The form ordering cannot be resolved due to conflict in before positions (baz => bar => foo => baz).', + ), + + // Circular after + array( + array('foo' => array('after' => 'foo')), + 'The form ordering cannot be resolved due to conflict in after positions (foo => foo).', + ), + array( + array('foo' => array('after' => 'bar'), 'bar' => array('after' => 'foo')), + 'The form ordering cannot be resolved due to conflict in after positions (bar => foo => bar).', + ), + array( + array( + 'foo' => array('after' => 'bar'), + 'bar' => array('after' => 'baz'), + 'baz' => array('after' => 'foo'), + ), + 'The form ordering cannot be resolved due to conflict in after positions (baz => bar => foo => baz).', + ), + + // Symetric before/after + array( + array('foo' => array('before' => 'bar'), 'bar' => array('after' => 'foo')), + 'The form ordering does not support symmetrical before/after option (bar <=> foo).', + ), + array( + array( + 'bat' => array('before' => 'baz'), + 'baz' => array('after' => 'bar'), + 'foo' => array('before' => 'bar'), + 'bar' => array('after' => 'foo'), + ), + 'The form ordering does not support symmetrical before/after option (bar <=> foo).', + ), + ); + } + + /** + * @dataProvider getValidPositions + */ + public function testValidPosition(array $config, array $expected) + { + $view = $this->createForm($config)->createView(); + $children = array_values($view->children); + + foreach ($expected as $index => $value) { + $this->assertArrayHasKey($index, $children); + $this->assertArrayHasKey($value, $view->children); + + $this->assertSame($children[$index], $view->children[$value]); + } + } + + /** + * @dataProvider getInvalidPositions + */ + public function testInvalidPosition(array $config, $exceptionMessage = null) + { + $this->setExpectedException('Symfony\Component\Form\Exception\InvalidConfigurationException', $exceptionMessage); + $this->createForm($config)->createView(); + } + + private function createForm(array $config) + { + $builder = $this->factory->createBuilder(); + + foreach ($config as $key => $value) { + if (is_string($key) && (is_string($value) || is_array($value))) { + $builder->add($key, null, array('position' => $value)); + } else { + $builder->add($value); + } + } + + return $builder->getForm(); + } +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy