From d26d931ac5cf756ad555d15227f1b3c8af2c6a33 Mon Sep 17 00:00:00 2001 From: GeLo Date: Mon, 25 Jan 2016 20:26:58 +0100 Subject: [PATCH 1/3] [Form] Add position support --- src/Symfony/Component/Form/ButtonBuilder.php | 40 ++- .../Form/Extension/Core/Type/BaseType.php | 7 +- .../Component/Form/FormConfigBuilder.php | 38 +- src/Symfony/Component/Form/FormOrderer.php | 327 ++++++++++++++++++ .../OrderedFormConfigBuilderInterface.php | 52 +++ .../Form/OrderedFormConfigInterface.php | 27 ++ .../Component/Form/ResolvedFormType.php | 17 + .../Component/Form/Tests/FormOrdererTest.php | 326 +++++++++++++++++ 8 files changed, 829 insertions(+), 5 deletions(-) create mode 100644 src/Symfony/Component/Form/FormOrderer.php create mode 100644 src/Symfony/Component/Form/OrderedFormConfigBuilderInterface.php create mode 100644 src/Symfony/Component/Form/OrderedFormConfigInterface.php create mode 100644 src/Symfony/Component/Form/Tests/FormOrdererTest.php diff --git a/src/Symfony/Component/Form/ButtonBuilder.php b/src/Symfony/Component/Form/ButtonBuilder.php index c65fb303ddcd3..bd920970646c5 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 */ @@ -503,6 +509,28 @@ public function setAutoInitialize($initialize) return $this; } + /** + * {@inheritdoc} + */ + public function setPosition($position) + { + if ($this->locked) { + throw new BadMethodCallException('The config builder cannot be modified anymore.'); + } + + if (is_string($position) && ($position !== 'first') && ($position !== 'last')) { + throw new InvalidConfigurationException('If you use position as string, you can only use "first" & "last".'); + } + + if (is_array($position) && !isset($position['before']) && !isset($position['after'])) { + throw new InvalidConfigurationException('If you use position as array, you must at least define the "before" or "after" option.'); + } + + $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..82e7bf805579c 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 */ @@ -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('The config builder cannot be modified anymore.'); + } + + if (is_string($position) && ($position !== 'first') && ($position !== 'last')) { + throw new InvalidConfigurationException('If you use position as string, you can only use "first" & "last".'); + } + + if (is_array($position) && !isset($position['before']) && !isset($position['after'])) { + throw new InvalidConfigurationException('If you use position as array, you must at least define the "before" or "after" option.'); + } + + $this->position = $position; + + return $this; + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Form/FormOrderer.php b/src/Symfony/Component/Form/FormOrderer.php new file mode 100644 index 0000000000000..33c8d510b902f --- /dev/null +++ b/src/Symfony/Component/Form/FormOrderer.php @@ -0,0 +1,327 @@ + + * + * 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 +{ + /** + * @var array + */ + private $weights; + + /** + * @var array + */ + private $deferred; + + /** + * @var int + */ + private $firstWeight; + + /** + * @var int + */ + private $currentWeight; + + /** + * @var int + */ + private $lastWeight; + + /** + * Orders the form. + * + * @param FormInterface $form The form. + * + * @return array The ordered form child names. + * + * @throws \Symfony\Component\Form\Exception\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 The 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 The form. + * @param string $position The position. + */ + private function processStringPosition(FormInterface $form, $position) + { + if ($position === 'first') { + $this->processFirst($form); + } else { + $this->processLast($form); + } + } + + /** + * Processes an array position (before/after). + * + * FIXME + * + * @param FormInterface $form The form. + * @param array $position The 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 The 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 The 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 The form. + * @param string $before The before form name. + */ + 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 The form. + * @param string $after The after form name. + */ + 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 The form. + * @param int $weight The 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 The form. + * @param int $weight The weight. + * @param string $position The position (null|before|after). + * + * @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 The form. + * @param string $deferred The deferred form name. + * @param string $position The position (before|after). + * + * @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 The form name. + * @param string $position The position (before|after) + * @param array $stack The circular 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 The form name. + * @param string $deferred The deferred form name. + * @param string $position The position (before|after). + * + * @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..29e697d0ec967 --- /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..f61ef110d4f3d --- /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..eec00130f6c6c --- /dev/null +++ b/src/Symfony/Component/Form/Tests/FormOrdererTest.php @@ -0,0 +1,326 @@ + + * + * 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(); + } +} From 31241875a0d240288bf532dd9bea794e3b05eeed Mon Sep 17 00:00:00 2001 From: GeLo Date: Tue, 17 Jan 2017 20:53:01 +0100 Subject: [PATCH 2/3] Fix error messages + phpdoc + CS --- src/Symfony/Component/Form/ButtonBuilder.php | 8 +- .../Component/Form/FormConfigBuilder.php | 6 +- src/Symfony/Component/Form/FormOrderer.php | 118 ++++++++---------- 3 files changed, 56 insertions(+), 76 deletions(-) diff --git a/src/Symfony/Component/Form/ButtonBuilder.php b/src/Symfony/Component/Form/ButtonBuilder.php index bd920970646c5..728fe43adc8b9 100644 --- a/src/Symfony/Component/Form/ButtonBuilder.php +++ b/src/Symfony/Component/Form/ButtonBuilder.php @@ -515,15 +515,15 @@ public function setAutoInitialize($initialize) public function setPosition($position) { if ($this->locked) { - throw new BadMethodCallException('The config builder cannot be modified anymore.'); + 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('If you use position as string, you can only use "first" & "last".'); + 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('If you use position as array, you must at least define the "before" or "after" option.'); + throw new InvalidConfigurationException('When using position as an array, the "before" or "after" option must be defined.'); } $this->position = $position; diff --git a/src/Symfony/Component/Form/FormConfigBuilder.php b/src/Symfony/Component/Form/FormConfigBuilder.php index 82e7bf805579c..50ab7ba282cf1 100644 --- a/src/Symfony/Component/Form/FormConfigBuilder.php +++ b/src/Symfony/Component/Form/FormConfigBuilder.php @@ -851,15 +851,15 @@ public function setAutoInitialize($initialize) public function setPosition($position) { if ($this->locked) { - throw new BadMethodCallException('The config builder cannot be modified anymore.'); + 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('If you use position as string, you can only use "first" & "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('If you use position as array, you must at least define the "before" or "after" option.'); + throw new InvalidConfigurationException('When using position as an array, the "before" or "after" option must be defined.'); } $this->position = $position; diff --git a/src/Symfony/Component/Form/FormOrderer.php b/src/Symfony/Component/Form/FormOrderer.php index 33c8d510b902f..b5168eb8360ee 100644 --- a/src/Symfony/Component/Form/FormOrderer.php +++ b/src/Symfony/Component/Form/FormOrderer.php @@ -20,39 +20,20 @@ */ class FormOrderer { - /** - * @var array - */ private $weights; - - /** - * @var array - */ private $deferred; - - /** - * @var int - */ private $firstWeight; - - /** - * @var int - */ private $currentWeight; - - /** - * @var int - */ private $lastWeight; /** * Orders the form. * - * @param FormInterface $form The form. + * @param FormInterface $form * - * @return array The ordered form child names. + * @return array The ordered form child names * - * @throws \Symfony\Component\Form\Exception\InvalidConfigurationException If a position is not valid. + * @throws InvalidConfigurationException If a position is not valid */ public function order(FormInterface $form) { @@ -76,9 +57,9 @@ public function order(FormInterface $form) } /** - * Process the form using the current weight in order to maintain the default order. + * Process the form using the current weight in order to maintain the default order * - * @param FormInterface $form The form. + * @param FormInterface $form */ private function processEmptyPosition(FormInterface $form) { @@ -87,10 +68,10 @@ private function processEmptyPosition(FormInterface $form) /** * 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. + * first/last position according to the default order * - * @param FormInterface $form The form. - * @param string $position The position. + * @param FormInterface $form + * @param string $position */ private function processStringPosition(FormInterface $form, $position) { @@ -102,12 +83,11 @@ private function processStringPosition(FormInterface $form, $position) } /** - * Processes an array position (before/after). - * - * FIXME + * 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 The form. - * @param array $position The position. + * @param FormInterface $form + * @param array $position */ private function processArrayPosition(FormInterface $form, array $position) { @@ -122,9 +102,9 @@ private function processArrayPosition(FormInterface $form, array $position) /** * Process the form using the current first weight in order to put - * your form at the first position according to the default order. + * your form at the first position according to the default order * - * @param FormInterface $form The form. + * @param FormInterface $form */ private function processFirst(FormInterface $form) { @@ -133,9 +113,9 @@ private function processFirst(FormInterface $form) /** * Processes the form using the current last weight in order to put - * your form at the last position according to the default order. + * your form at the last position according to the default order * - * @param FormInterface $form The form. + * @param FormInterface $form */ private function processLast(FormInterface $form) { @@ -143,11 +123,11 @@ private function processLast(FormInterface $form) } /** - * 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. + * 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 The form. - * @param string $before The before form name. + * @param FormInterface $form + * @param string $before */ private function processBefore(FormInterface $form, $before) { @@ -159,11 +139,11 @@ private function processBefore(FormInterface $form, $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. + * 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 The form. - * @param string $after The after form name. + * @param FormInterface $form + * @param string $after */ private function processAfter(FormInterface $form, $after) { @@ -175,12 +155,12 @@ private function processAfter(FormInterface $form, $after) } /** - * Process the form using the given weight. + * Process the form using the given weight * - * This method also updates the orderer state accordingly. + * This method also updates the orderer state accordingly * - * @param FormInterface $form The form. - * @param int $weight The weight. + * @param FormInterface $form + * @param int $weight */ private function processWeight(FormInterface $form, $weight) { @@ -202,13 +182,13 @@ private function processWeight(FormInterface $form, $weight) /** * Finishes the form weight processing by trying to process deferred forms - * which refers to the current processed form. + * which refers to the current processed form * - * @param FormInterface $form The form. - * @param int $weight The weight. - * @param string $position The position (null|before|after). + * @param FormInterface $form + * @param int $weight + * @param string $position * - * @return int The new weight. + * @return int The new weight */ private function finishWeight(FormInterface $form, $weight, $position = null) { @@ -235,13 +215,13 @@ private function finishWeight(FormInterface $form, $weight, $position = null) /** * Processes a deferred form by checking if it is valid and - * if it does not become a circular or symmetric ordering. + * if it does not become a circular or symmetric ordering * - * @param FormInterface $form The form. - * @param string $deferred The deferred form name. - * @param string $position The position (before|after). + * @param FormInterface $form + * @param string $deferred + * @param string $position * - * @throws InvalidConfigurationException If the deferred form does not exist. + * @throws InvalidConfigurationException If the deferred form does not exist */ private function processDeferred(FormInterface $form, $deferred, $position) { @@ -258,13 +238,13 @@ private function processDeferred(FormInterface $form, $deferred, $position) } /** - * Detects circular deferred forms for after/before position such as A => B => C => A. + * Detects circular deferred forms for after/before position such as A => B => C => A * - * @param string $name The form name. - * @param string $position The position (before|after) - * @param array $stack The circular stack. + * @param string $name + * @param string $position + * @param array $stack * - * @throws InvalidConfigurationException If there is a circular before/after deferred. + * @throws InvalidConfigurationException If there is a circular before/after deferred */ private function detectCircularDeferred($name, $position, array $stack = array()) { @@ -288,13 +268,13 @@ private function detectCircularDeferred($name, $position, array $stack = array() } /** - * Detects symmetric before/after deferred such as A after B and B after A. + * Detects symmetric before/after deferred such as A after B and B after A * - * @param string $name The form name. - * @param string $deferred The deferred form name. - * @param string $position The position (before|after). + * @param string $name + * @param string $deferred + * @param string $position * - * @throws InvalidConfigurationException If there is a symetric before/after deferred. + * @throws InvalidConfigurationException If there is a symetric before/after deferred */ private function detectedSymmetricDeferred($name, $deferred, $position) { @@ -310,7 +290,7 @@ private function detectedSymmetricDeferred($name, $deferred, $position) } /** - * Resets the orderer. + * Resets the orderer */ private function reset() { From a1c523868546e82facb1bd85efd663a44d0650e0 Mon Sep 17 00:00:00 2001 From: GeLo Date: Tue, 17 Jan 2017 20:58:39 +0100 Subject: [PATCH 3/3] Apply fabbot.io changes --- src/Symfony/Component/Form/ButtonBuilder.php | 2 +- .../Component/Form/FormConfigBuilder.php | 8 +++--- src/Symfony/Component/Form/FormOrderer.php | 26 +++++++++---------- .../OrderedFormConfigBuilderInterface.php | 6 ++--- .../Form/OrderedFormConfigInterface.php | 2 +- .../Component/Form/Tests/FormOrdererTest.php | 1 - 6 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/Symfony/Component/Form/ButtonBuilder.php b/src/Symfony/Component/Form/ButtonBuilder.php index 728fe43adc8b9..4b4f6cf774464 100644 --- a/src/Symfony/Component/Form/ButtonBuilder.php +++ b/src/Symfony/Component/Form/ButtonBuilder.php @@ -64,7 +64,7 @@ class ButtonBuilder implements \IteratorAggregate, FormBuilderInterface, Ordered * @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()) { diff --git a/src/Symfony/Component/Form/FormConfigBuilder.php b/src/Symfony/Component/Form/FormConfigBuilder.php index 50ab7ba282cf1..eb6666b267418 100644 --- a/src/Symfony/Component/Form/FormConfigBuilder.php +++ b/src/Symfony/Component/Form/FormConfigBuilder.php @@ -191,8 +191,8 @@ class FormConfigBuilder implements FormConfigBuilderInterface, OrderedFormConfig * @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()) { @@ -888,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 index b5168eb8360ee..1f7571be4079b 100644 --- a/src/Symfony/Component/Form/FormOrderer.php +++ b/src/Symfony/Component/Form/FormOrderer.php @@ -57,7 +57,7 @@ public function order(FormInterface $form) } /** - * Process the form using the current weight in order to maintain the default order + * Process the form using the current weight in order to maintain the default order. * * @param FormInterface $form */ @@ -68,7 +68,7 @@ private function processEmptyPosition(FormInterface $form) /** * 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 + * first/last position according to the default order. * * @param FormInterface $form * @param string $position @@ -84,7 +84,7 @@ private function processStringPosition(FormInterface $form, $position) /** * 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 + * If the "before" or "after" form has not been processed yet, we defer it for the next forms. * * @param FormInterface $form * @param array $position @@ -102,7 +102,7 @@ private function processArrayPosition(FormInterface $form, array $position) /** * Process the form using the current first weight in order to put - * your form at the first position according to the default order + * your form at the first position according to the default order. * * @param FormInterface $form */ @@ -113,7 +113,7 @@ private function processFirst(FormInterface $form) /** * Processes the form using the current last weight in order to put - * your form at the last position according to the default order + * your form at the last position according to the default order. * * @param FormInterface $form */ @@ -124,7 +124,7 @@ private function processLast(FormInterface $form) /** * 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 + * If the "before" form has not been processed yet, we defer it for the next forms. * * @param FormInterface $form * @param string $before @@ -140,7 +140,7 @@ private function processBefore(FormInterface $form, $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 + * If the "after" form has not been processed yet, we defer it for the next forms. * * @param FormInterface $form * @param string $after @@ -155,7 +155,7 @@ private function processAfter(FormInterface $form, $after) } /** - * Process the form using the given weight + * Process the form using the given weight. * * This method also updates the orderer state accordingly * @@ -182,7 +182,7 @@ private function processWeight(FormInterface $form, $weight) /** * Finishes the form weight processing by trying to process deferred forms - * which refers to the current processed form + * which refers to the current processed form. * * @param FormInterface $form * @param int $weight @@ -215,7 +215,7 @@ private function finishWeight(FormInterface $form, $weight, $position = null) /** * Processes a deferred form by checking if it is valid and - * if it does not become a circular or symmetric ordering + * if it does not become a circular or symmetric ordering. * * @param FormInterface $form * @param string $deferred @@ -238,7 +238,7 @@ private function processDeferred(FormInterface $form, $deferred, $position) } /** - * Detects circular deferred forms for after/before position such as A => B => C => A + * Detects circular deferred forms for after/before position such as A => B => C => A. * * @param string $name * @param string $position @@ -268,7 +268,7 @@ private function detectCircularDeferred($name, $position, array $stack = array() } /** - * Detects symmetric before/after deferred such as A after B and B after A + * Detects symmetric before/after deferred such as A after B and B after A. * * @param string $name * @param string $deferred @@ -290,7 +290,7 @@ private function detectedSymmetricDeferred($name, $deferred, $position) } /** - * Resets the orderer + * Resets the orderer. */ private function reset() { diff --git a/src/Symfony/Component/Form/OrderedFormConfigBuilderInterface.php b/src/Symfony/Component/Form/OrderedFormConfigBuilderInterface.php index 29e697d0ec967..c09c556d1725d 100644 --- a/src/Symfony/Component/Form/OrderedFormConfigBuilderInterface.php +++ b/src/Symfony/Component/Form/OrderedFormConfigBuilderInterface.php @@ -42,11 +42,11 @@ interface OrderedFormConfigBuilderInterface extends OrderedFormConfigInterface * 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. + * @param null|string|array $position the form position * - * @throws \Symfony\Component\Form\Exception\InvalidConfigurationException If the position is not valid. + * @throws \Symfony\Component\Form\Exception\InvalidConfigurationException if the position is not valid * - * @return self The configuration object. + * @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 index f61ef110d4f3d..6385e19d1c8b3 100644 --- a/src/Symfony/Component/Form/OrderedFormConfigInterface.php +++ b/src/Symfony/Component/Form/OrderedFormConfigInterface.php @@ -21,7 +21,7 @@ interface OrderedFormConfigInterface * * @see FormConfigBuilderInterface::setPosition * - * @return null|string|array The position. + * @return null|string|array the position */ public function getPosition(); } diff --git a/src/Symfony/Component/Form/Tests/FormOrdererTest.php b/src/Symfony/Component/Form/Tests/FormOrdererTest.php index eec00130f6c6c..de75a4ff77e08 100644 --- a/src/Symfony/Component/Form/Tests/FormOrdererTest.php +++ b/src/Symfony/Component/Form/Tests/FormOrdererTest.php @@ -211,7 +211,6 @@ public function getValidPositions() 'ban', 'biz' => array('before' => 'nan'), 'boz' => array('before' => 'biz', array('after' => 'pop')), - ), array('foo', 'bar', 'baz', 'bat', 'ban', 'pop', 'boz', 'biz', 'nan'), ),