diff --git a/UPGRADE-2.1.md b/UPGRADE-2.1.md index 3ba03d1ea1..0af744d7c3 100644 --- a/UPGRADE-2.1.md +++ b/UPGRADE-2.1.md @@ -249,6 +249,16 @@ public function finishView(FormViewInterface $view, FormInterface $form, array $options) ``` + * The method `createBuilder` was removed from `FormTypeInterface` for performance + reasons. It is now not possible anymore to use custom implementations of + `FormBuilderInterface` for specific form types. + + If you are in such a situation, you can subclass `FormRegistry` instead and override + `resolveType` to return a custom `ResolvedFormTypeInterface` implementation, within + which you can create your own `FormBuilderInterface` implementation. You should + register this custom registry class under the service name "form.registry" in order + to replace the default implementation. + * If you previously inherited from `FieldType`, you should now inherit from `FormType`. You should also set the option `compound` to `false` if your field is not supposed to contain child fields. @@ -1001,6 +1011,24 @@ )); ``` + * The methods `addType`, `hasType` and `getType` in `FormFactory` are deprecated + and will be removed in Symfony 2.3. You should use the methods with the same + name on the `FormRegistry` instead. + + Before: + + ``` + $this->get('form.factory')->addType(new MyFormType()); + ``` + + After: + + ``` + $registry = $this->get('form.registry'); + + $registry->addType($registry->resolveType(new MyFormType())); + ``` + ### Validator * The methods `setMessage()`, `getMessageTemplate()` and diff --git a/src/Symfony/Bridge/Doctrine/CHANGELOG.md b/src/Symfony/Bridge/Doctrine/CHANGELOG.md index 1a5334ec38..30c3fe2f6f 100644 --- a/src/Symfony/Bridge/Doctrine/CHANGELOG.md +++ b/src/Symfony/Bridge/Doctrine/CHANGELOG.md @@ -7,3 +7,4 @@ CHANGELOG * added a default implementation of the ManagerRegistry * added a session storage for Doctrine DBAL * DoctrineOrmTypeGuesser now guesses "collection" for array Doctrine type + * DoctrineType now caches its choice lists in order to improve performance diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index 6f4433f4dc..31216ea319 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -68,17 +68,40 @@ abstract class DoctrineType extends AbstractType $choiceList = function (Options $options) use ($registry, &$choiceListCache, &$time) { $manager = $registry->getManager($options['em']); - $choiceHashes = is_array($options['choices']) - ? array_map('spl_object_hash', $options['choices']) - : $options['choices']; + // Support for closures + $propertyHash = is_object($options['property']) + ? spl_object_hash($options['property']) + : $options['property']; + + $choiceHashes = $options['choices']; + + // Support for recursive arrays + if (is_array($choiceHashes)) { + // A second parameter ($key) is passed, so we cannot use + // spl_object_hash() directly (which strictly requires + // one parameter) + array_walk_recursive($choiceHashes, function ($value) { + return spl_object_hash($value); + }); + } + + // Support for custom loaders (with query builders) + $loaderHash = is_object($options['loader']) + ? spl_object_hash($options['loader']) + : $options['loader']; + + // Support for closures + $groupByHash = is_object($options['group_by']) + ? spl_object_hash($options['group_by']) + : $options['group_by']; $hash = md5(json_encode(array( spl_object_hash($manager), $options['class'], - $options['property'], - $options['loader'], + $propertyHash, + $loaderHash, $choiceHashes, - $options['group_by'] + $groupByHash ))); if (!isset($choiceListCache[$hash])) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml index d3efd46741..8bef4bc28e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml @@ -6,13 +6,14 @@ Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension + Symfony\Component\Form\FormRegistry Symfony\Component\Form\FormFactory Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser - - + + + + + + diff --git a/src/Symfony/Component/Form/AbstractType.php b/src/Symfony/Component/Form/AbstractType.php index 281ee07f4d..42b24e3650 100644 --- a/src/Symfony/Component/Form/AbstractType.php +++ b/src/Symfony/Component/Form/AbstractType.php @@ -20,8 +20,9 @@ use Symfony\Component\OptionsResolver\OptionsResolverInterface; abstract class AbstractType implements FormTypeInterface { /** - * The extensions for this type - * @var array An array of FormTypeExtensionInterface instances + * @var array + * + * @deprecated Deprecated since version 2.1, to be removed in 2.3. */ private $extensions = array(); @@ -46,14 +47,6 @@ abstract class AbstractType implements FormTypeInterface { } - /** - * {@inheritdoc} - */ - public function createBuilder($name, FormFactoryInterface $factory, array $options) - { - return null; - } - /** * {@inheritdoc} */ @@ -98,21 +91,26 @@ abstract class AbstractType implements FormTypeInterface } /** - * {@inheritdoc} + * Sets the extensions for this type. + * + * @param array $extensions An array of FormTypeExtensionInterface + * + * @throws Exception\UnexpectedTypeException if any extension does not implement FormTypeExtensionInterface + * + * @deprecated Deprecated since version 2.1, to be removed in 2.3. */ public function setExtensions(array $extensions) { - foreach ($extensions as $extension) { - if (!$extension instanceof FormTypeExtensionInterface) { - throw new UnexpectedTypeException($extension, 'Symfony\Component\Form\FormTypeExtensionInterface'); - } - } - $this->extensions = $extensions; } /** - * {@inheritdoc} + * Returns the extensions associated with this type. + * + * @return array An array of FormTypeExtensionInterface + * + * @deprecated Deprecated since version 2.1, to be removed in 2.3. Use + * {@link ResolvedFormTypeInterface::getTypeExtensions()} instead. */ public function getExtensions() { diff --git a/src/Symfony/Component/Form/CHANGELOG.md b/src/Symfony/Component/Form/CHANGELOG.md index 028bf20a8d..cbc37f0766 100644 --- a/src/Symfony/Component/Form/CHANGELOG.md +++ b/src/Symfony/Component/Form/CHANGELOG.md @@ -86,6 +86,7 @@ CHANGELOG * `hasAttribute` * `getClientData` * added FormBuilder methods + * `getTypes` * `addViewTransformer` * `getViewTransformers` * `resetViewTransformers` @@ -157,3 +158,15 @@ CHANGELOG * deprecated the options "data_timezone" and "user_timezone" in DateType, DateTimeType and TimeType and renamed them to "model_timezone" and "view_timezone" * fixed: TransformationFailedExceptions thrown in the model transformer are now caught by the form + * added FormRegistry and ResolvedFormTypeInterface + * deprecated FormFactory methods + * `addType` + * `hasType` + * `getType` + * [BC BREAK] FormFactory now expects a FormRegistryInterface as constructor argument + * [BC BREAK] The method `createBuilder` in FormTypeInterface is not supported anymore for performance reasons + * [BC BREAK] Removed `setTypes` from FormBuilder + * deprecated AbstractType methods + * `getExtensions` + * `setExtensions` + * ChoiceType now caches its created choice lists to improve performance diff --git a/src/Symfony/Component/Form/Extension/Core/Type/FormType.php b/src/Symfony/Component/Form/Extension/Core/Type/FormType.php index 12747fd175..0ef0def253 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/FormType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/FormType.php @@ -20,7 +20,6 @@ use Symfony\Component\Form\FormViewInterface; use Symfony\Component\Form\Extension\Core\EventListener\BindRequestListener; use Symfony\Component\Form\Extension\Core\EventListener\TrimListener; use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper; -use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\Form\Exception\FormException; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolverInterface; @@ -93,8 +92,8 @@ class FormType extends AbstractType } $types = array(); - foreach ($form->getConfig()->getTypes() as $type) { - $types[] = $type->getName(); + for ($type = $form->getConfig()->getType(); null !== $type; $type = $type->getParent()) { + array_unshift($types, $type->getName()); } if (!$translationDomain) { @@ -149,7 +148,7 @@ class FormType extends AbstractType { // Derive "data_class" option from passed "data" object $dataClass = function (Options $options) { - return is_object($options['data']) ? get_class($options['data']) : null; + return isset($options['data']) && is_object($options['data']) ? get_class($options['data']) : null; }; // Derive "empty_data" closure from "data_class" option @@ -211,14 +210,6 @@ class FormType extends AbstractType )); } - /** - * {@inheritdoc} - */ - public function createBuilder($name, FormFactoryInterface $factory, array $options) - { - return new FormBuilder($name, $options['data_class'], new EventDispatcher(), $factory, $options); - } - /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index f9c56e377b..e191cc9669 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -195,11 +195,17 @@ class Form implements \IteratorAggregate, FormInterface * @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. + * {@link getConfig()} and {@link FormConfigInterface::getType()} instead. */ public function getTypes() { - return $this->config->getTypes(); + $types = array(); + + for ($type = $this->config->getType(); null !== $type; $type = $type->getParent()) { + array_unshift($types, $type->getInnerType()); + } + + return $types; } /** @@ -948,34 +954,7 @@ class Form implements \IteratorAggregate, FormInterface $parent = $this->parent->createView(); } - $view = new FormView($this->config->getName()); - - $view->setParent($parent); - - $types = (array) $this->config->getTypes(); - $options = $this->config->getOptions(); - - foreach ($types as $type) { - $type->buildView($view, $this, $options); - - foreach ($type->getExtensions() as $typeExtension) { - $typeExtension->buildView($view, $this, $options); - } - } - - foreach ($this->children as $child) { - $view->add($child->createView($view)); - } - - foreach ($types as $type) { - $type->finishView($view, $this, $options); - - foreach ($type->getExtensions() as $typeExtension) { - $typeExtension->finishView($view, $this, $options); - } - } - - return $view; + return $this->config->getType()->createView($this, $parent); } /** diff --git a/src/Symfony/Component/Form/FormBuilder.php b/src/Symfony/Component/Form/FormBuilder.php index be04e66bc3..4abec1824d 100644 --- a/src/Symfony/Component/Form/FormBuilder.php +++ b/src/Symfony/Component/Form/FormBuilder.php @@ -115,10 +115,10 @@ class FormBuilder extends FormConfig implements \IteratorAggregate, FormBuilderI } if (null !== $type) { - return $this->getFormFactory()->createNamedBuilder($name, $type, null, $options, $this); + return $this->factory->createNamedBuilder($name, $type, null, $options, $this); } - return $this->getFormFactory()->createBuilderForProperty($this->getDataClass(), $name, null, $options, $this); + return $this->factory->createBuilderForProperty($this->getDataClass(), $name, null, $options, $this); } /** @@ -266,4 +266,23 @@ class FormBuilder extends FormConfig implements \IteratorAggregate, FormBuilderI { return new \ArrayIterator($this->children); } + + /** + * Returns the types used by this builder. + * + * @return array An array of FormTypeInterface + * + * @deprecated Deprecated since version 2.1, to be removed in 2.3. Use + * {@link FormConfigInterface::getType()} instead. + */ + public function getTypes() + { + $types = array(); + + for ($type = $this->getType(); null !== $type; $type = $type->getParent()) { + array_unshift($types, $type->getInnerType()); + } + + return $types; + } } diff --git a/src/Symfony/Component/Form/FormConfig.php b/src/Symfony/Component/Form/FormConfig.php index 55a7303407..af68a13fce 100644 --- a/src/Symfony/Component/Form/FormConfig.php +++ b/src/Symfony/Component/Form/FormConfig.php @@ -59,9 +59,9 @@ class FormConfig implements FormConfigEditorInterface private $compound = false; /** - * @var array + * @var ResolvedFormTypeInterface */ - private $types = array(); + private $type; /** * @var array @@ -377,9 +377,9 @@ class FormConfig implements FormConfigEditorInterface /** * {@inheritdoc} */ - public function getTypes() + public function getType() { - return $this->types; + return $this->type; } /** @@ -671,9 +671,9 @@ class FormConfig implements FormConfigEditorInterface /** * {@inheritdoc} */ - public function setTypes(array $types) + public function setType(ResolvedFormTypeInterface $type) { - $this->types = $types; + $this->type = $type; return $this; } diff --git a/src/Symfony/Component/Form/FormConfigEditorInterface.php b/src/Symfony/Component/Form/FormConfigEditorInterface.php index 8463ad017c..b84dbb0eac 100644 --- a/src/Symfony/Component/Form/FormConfigEditorInterface.php +++ b/src/Symfony/Component/Form/FormConfigEditorInterface.php @@ -213,11 +213,11 @@ interface FormConfigEditorInterface extends FormConfigInterface /** * Set the types. * - * @param array $types An array FormTypeInterface + * @param ResolvedFormTypeInterface $type The type of the form. * * @return self The configuration object. */ - public function setTypes(array $types); + public function setType(ResolvedFormTypeInterface $type); /** * Sets the initial data of the form. diff --git a/src/Symfony/Component/Form/FormConfigInterface.php b/src/Symfony/Component/Form/FormConfigInterface.php index 25064b6adb..1274e4c7a5 100644 --- a/src/Symfony/Component/Form/FormConfigInterface.php +++ b/src/Symfony/Component/Form/FormConfigInterface.php @@ -79,9 +79,9 @@ interface FormConfigInterface /** * Returns the form types used to construct the form. * - * @return array An array of {@link FormTypeInterface} instances. + * @return ResolvedFormTypeInterface The form's type. */ - public function getTypes(); + public function getType(); /** * Returns the view transformers of the form. diff --git a/src/Symfony/Component/Form/FormFactory.php b/src/Symfony/Component/Form/FormFactory.php index 046804f039..a925b1a9bc 100644 --- a/src/Symfony/Component/Form/FormFactory.php +++ b/src/Symfony/Component/Form/FormFactory.php @@ -18,92 +18,14 @@ use Symfony\Component\OptionsResolver\OptionsResolver; class FormFactory implements FormFactoryInterface { - private static $requiredOptions = array( - 'data', - 'required', - 'max_length', - ); - /** - * Extensions - * @var array An array of FormExtensionInterface + * @var FormRegistryInterface */ - private $extensions = array(); + private $registry; - /** - * All known types (cache) - * @var array An array of FormTypeInterface - */ - private $types = array(); - - /** - * The guesser chain - * @var FormTypeGuesserChain - */ - private $guesser; - - /** - * Constructor. - * - * @param array $extensions An array of FormExtensionInterface - * - * @throws UnexpectedTypeException if any extension does not implement FormExtensionInterface - */ - public function __construct(array $extensions) + public function __construct(FormRegistryInterface $registry) { - foreach ($extensions as $extension) { - if (!$extension instanceof FormExtensionInterface) { - throw new UnexpectedTypeException($extension, 'Symfony\Component\Form\FormExtensionInterface'); - } - } - - $this->extensions = $extensions; - } - - /** - * {@inheritdoc} - */ - public function hasType($name) - { - if (isset($this->types[$name])) { - return true; - } - - try { - $this->loadType($name); - } catch (FormException $e) { - return false; - } - - return true; - } - - /** - * {@inheritdoc} - */ - public function addType(FormTypeInterface $type) - { - $this->loadTypeExtensions($type); - - $this->validateFormTypeName($type); - - $this->types[$type->getName()] = $type; - } - - /** - * {@inheritdoc} - */ - public function getType($name) - { - if (!is_string($name)) { - throw new UnexpectedTypeException($name, 'string'); - } - - if (!isset($this->types[$name])) { - $this->loadType($name); - } - - return $this->types[$name]; + $this->registry = $registry; } /** @@ -135,7 +57,9 @@ class FormFactory implements FormFactoryInterface */ public function createBuilder($type, $data = null, array $options = array(), FormBuilderInterface $parent = null) { - $name = is_object($type) ? $type->getName() : $type; + $name = $type instanceof FormTypeInterface || $type instanceof ResolvedFormTypeInterface + ? $type->getName() + : $type; return $this->createNamedBuilder($name, $type, $data, $options, $parent); } @@ -145,89 +69,22 @@ class FormFactory implements FormFactoryInterface */ public function createNamedBuilder($name, $type, $data = null, array $options = array(), FormBuilderInterface $parent = null) { - if (!array_key_exists('data', $options)) { + if (null !== $data && !array_key_exists('data', $options)) { $options['data'] = $data; } - $builder = null; - $types = array(); - $optionsResolver = new OptionsResolver(); - - // Bottom-up determination of the type hierarchy - // Start with the actual type and look for the parent type - // The complete hierarchy is saved in $types, the first entry being - // the root and the last entry being the leaf (the concrete type) - while (null !== $type) { - if ($type instanceof FormTypeInterface) { - if ($type->getName() == $type->getParent($options)) { - throw new FormException(sprintf('The form type name "%s" for class "%s" cannot be the same as the parent type.', $type->getName(), get_class($type))); - } - - $this->addType($type); - } elseif (is_string($type)) { - $type = $this->getType($type); - } else { - throw new UnexpectedTypeException($type, 'string or Symfony\Component\Form\FormTypeInterface'); - } - - array_unshift($types, $type); - - $type = $type->getParent(); + if ($type instanceof ResolvedFormTypeInterface) { + $this->registry->addType($type); + } elseif ($type instanceof FormTypeInterface) { + $type = $this->registry->resolveType($type); + $this->registry->addType($type); + } elseif (is_string($type)) { + $type = $this->registry->getType($type); + } else { + throw new UnexpectedTypeException($type, 'string, Symfony\Component\Form\ResolvedFormTypeInterface or Symfony\Component\Form\FormTypeInterface'); } - // Top-down determination of the default options - foreach ($types as $type) { - // Merge the default options of all types to an array of default - // options. Default options of children override default options - // of parents. - /* @var FormTypeInterface $type */ - $type->setDefaultOptions($optionsResolver); - - foreach ($type->getExtensions() as $typeExtension) { - /* @var FormTypeExtensionInterface $typeExtension */ - $typeExtension->setDefaultOptions($optionsResolver); - } - } - - // Resolve concrete type - $type = end($types); - - // Validate options required by the factory - $diff = array(); - - foreach (self::$requiredOptions as $requiredOption) { - if (!$optionsResolver->isKnown($requiredOption)) { - $diff[] = $requiredOption; - } - } - - if (count($diff) > 0) { - throw new TypeDefinitionException(sprintf('Type "%s" should support the option(s) "%s"', $type->getName(), implode('", "', $diff))); - } - - // Resolve options - $options = $optionsResolver->resolve($options); - - for ($i = 0, $l = count($types); $i < $l && !$builder; ++$i) { - $builder = $types[$i]->createBuilder($name, $this, $options); - } - - if (!$builder) { - throw new TypeDefinitionException(sprintf('Type "%s" or any of its parents should return a FormBuilderInterface instance from createBuilder()', $type->getName())); - } - - $builder->setTypes($types); - $builder->setParent($parent); - - foreach ($types as $type) { - $type->buildForm($builder, $options); - - foreach ($type->getExtensions() as $typeExtension) { - $typeExtension->buildForm($builder, $options); - } - } - - return $builder; + return $type->createBuilder($this, $name, $options, $parent); } /** @@ -235,16 +92,13 @@ class FormFactory implements FormFactoryInterface */ public function createBuilderForProperty($class, $property, $data = null, array $options = array(), FormBuilderInterface $parent = null) { - if (!$this->guesser) { - $this->loadGuesser(); - } - - $typeGuess = $this->guesser->guessType($class, $property); - $maxLengthGuess = $this->guesser->guessMaxLength($class, $property); + $guesser = $this->registry->getTypeGuesser(); + $typeGuess = $guesser->guessType($class, $property); + $maxLengthGuess = $guesser->guessMaxLength($class, $property); // Keep $minLengthGuess for BC until Symfony 2.3 - $minLengthGuess = $this->guesser->guessMinLength($class, $property); - $requiredGuess = $this->guesser->guessRequired($class, $property); - $patternGuess = $this->guesser->guessPattern($class, $property); + $minLengthGuess = $guesser->guessMinLength($class, $property); + $requiredGuess = $guesser->guessRequired($class, $property); + $patternGuess = $guesser->guessPattern($class, $property); $type = $typeGuess ? $typeGuess->getType() : 'text'; @@ -278,75 +132,50 @@ class FormFactory implements FormFactoryInterface } /** - * Initializes the guesser chain. + * Returns whether the given type is supported. + * + * @param string $name The name of the type + * + * @return Boolean Whether the type is supported + * + * @deprecated Deprecated since version 2.1, to be removed in 2.3. Use + * {@link FormRegistryInterface::hasType()} instead. */ - private function loadGuesser() + public function hasType($name) { - $guessers = array(); - - foreach ($this->extensions as $extension) { - $guesser = $extension->getTypeGuesser(); - - if ($guesser) { - $guessers[] = $guesser; - } - } - - $this->guesser = new FormTypeGuesserChain($guessers); + return $this->registry->hasType($name); } /** - * Loads a type. - * - * @param string $name The type name - * - * @throws FormException if the type is not provided by any registered extension - */ - private function loadType($name) - { - $type = null; - - foreach ($this->extensions as $extension) { - if ($extension->hasType($name)) { - $type = $extension->getType($name); - break; - } - } - - if (!$type) { - throw new FormException(sprintf('Could not load type "%s"', $name)); - } - - $this->loadTypeExtensions($type); - - $this->validateFormTypeName($type); - - $this->types[$name] = $type; - } - - /** - * Loads the extensions for a given type. + * Adds a type. * * @param FormTypeInterface $type The type + * + * @deprecated Deprecated since version 2.1, to be removed in 2.3. Use + * {@link FormRegistryInterface::resolveType()} and + * {@link FormRegistryInterface::addType()} instead. */ - private function loadTypeExtensions(FormTypeInterface $type) + public function addType(FormTypeInterface $type) { - $typeExtensions = array(); - - foreach ($this->extensions as $extension) { - $typeExtensions = array_merge( - $typeExtensions, - $extension->getTypeExtensions($type->getName()) - ); - } - - $type->setExtensions($typeExtensions); + $this->registry->addType($this->registry->resolveType($type)); } - private function validateFormTypeName(FormTypeInterface $type) + /** + * Returns a type by name. + * + * This methods registers the type extensions from the form extensions. + * + * @param string $name The name of the type + * + * @return FormTypeInterface The type + * + * @throws Exception\FormException if the type can not be retrieved from any extension + * + * @deprecated Deprecated since version 2.1, to be removed in 2.3. Use + * {@link FormRegistryInterface::getType()} instead. + */ + public function getType($name) { - if (!preg_match('/^[a-z0-9_]*$/i', $type->getName())) { - throw new FormException(sprintf('The "%s" form type name ("%s") is not valid. Names must only contain letters, numbers, and "_".', get_class($type), $type->getName())); - } + return $this->registry->getType($name)->getInnerType(); } } diff --git a/src/Symfony/Component/Form/FormFactoryInterface.php b/src/Symfony/Component/Form/FormFactoryInterface.php index 457be5e24f..73f31aa373 100644 --- a/src/Symfony/Component/Form/FormFactoryInterface.php +++ b/src/Symfony/Component/Form/FormFactoryInterface.php @@ -112,33 +112,4 @@ interface FormFactoryInterface * @throws Exception\FormException if any given option is not applicable to the form type */ public function createBuilderForProperty($class, $property, $data = null, array $options = array(), FormBuilderInterface $parent = null); - - /** - * Returns a type by name. - * - * This methods registers the type extensions from the form extensions. - * - * @param string $name The name of the type - * - * @return FormTypeInterface The type - * - * @throws Exception\FormException if the type can not be retrieved from any extension - */ - public function getType($name); - - /** - * Returns whether the given type is supported. - * - * @param string $name The name of the type - * - * @return Boolean Whether the type is supported - */ - public function hasType($name); - - /** - * Adds a type. - * - * @param FormTypeInterface $type The type - */ - public function addType(FormTypeInterface $type); } diff --git a/src/Symfony/Component/Form/FormRegistry.php b/src/Symfony/Component/Form/FormRegistry.php new file mode 100644 index 0000000000..12c610153e --- /dev/null +++ b/src/Symfony/Component/Form/FormRegistry.php @@ -0,0 +1,164 @@ + + * + * 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\Exception\FormException; + +/** + * The central registry of the Form component. + * + * @author Bernhard Schussek + */ +class FormRegistry implements FormRegistryInterface +{ + /** + * Extensions + * @var array An array of FormExtensionInterface + */ + private $extensions = array(); + + /** + * @var array + */ + private $types = array(); + + /** + * @var FormTypeGuesserInterface + */ + private $guesser; + + /** + * Constructor. + * + * @param array $extensions An array of FormExtensionInterface + * + * @throws UnexpectedTypeException if any extension does not implement FormExtensionInterface + */ + public function __construct(array $extensions) + { + foreach ($extensions as $extension) { + if (!$extension instanceof FormExtensionInterface) { + throw new UnexpectedTypeException($extension, 'Symfony\Component\Form\FormExtensionInterface'); + } + } + + $this->extensions = $extensions; + } + + /** + * {@inheritdoc} + */ + public function resolveType(FormTypeInterface $type) + { + $typeExtensions = array(); + + foreach ($this->extensions as $extension) { + /* @var FormExtensionInterface $extension */ + $typeExtensions = array_merge( + $typeExtensions, + $extension->getTypeExtensions($type->getName()) + ); + } + + $parent = $type->getParent() ? $this->getType($type->getParent()) : null; + + return new ResolvedFormType($type, $typeExtensions, $parent); + } + + /** + * {@inheritdoc} + */ + public function addType(ResolvedFormTypeInterface $type) + { + $this->types[$type->getName()] = $type; + } + + /** + * {@inheritdoc} + */ + public function getType($name) + { + if (!is_string($name)) { + throw new UnexpectedTypeException($name, 'string'); + } + + if (!isset($this->types[$name])) { + $type = null; + + foreach ($this->extensions as $extension) { + /* @var FormExtensionInterface $extension */ + if ($extension->hasType($name)) { + $type = $extension->getType($name); + break; + } + } + + if (!$type) { + throw new FormException(sprintf('Could not load type "%s"', $name)); + } + + $this->addType($this->resolveType($type)); + } + + return $this->types[$name]; + } + + /** + * {@inheritdoc} + */ + public function hasType($name) + { + if (isset($this->types[$name])) { + return true; + } + + try { + $this->getType($name); + } catch (FormException $e) { + return false; + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function getTypeGuesser() + { + if (null === $this->guesser) { + $guessers = array(); + + foreach ($this->extensions as $extension) { + /* @var FormExtensionInterface $extension */ + $guesser = $extension->getTypeGuesser(); + + if ($guesser) { + $guessers[] = $guesser; + } + } + + $this->guesser = new FormTypeGuesserChain($guessers); + } + + return $this->guesser; + } + + /** + * {@inheritdoc} + */ + public function getExtensions() + { + return $this->extensions; + } +} diff --git a/src/Symfony/Component/Form/FormRegistryInterface.php b/src/Symfony/Component/Form/FormRegistryInterface.php new file mode 100644 index 0000000000..e4f4099dcd --- /dev/null +++ b/src/Symfony/Component/Form/FormRegistryInterface.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +/** + * The central registry of the Form component. + * + * @author Bernhard Schussek + */ +interface FormRegistryInterface +{ + /** + * Adds a form type. + * + * @param ResolvedFormTypeInterface $type The type + */ + public function addType(ResolvedFormTypeInterface $type); + + /** + * Returns a form type by name. + * + * This methods registers the type extensions from the form extensions. + * + * @param string $name The name of the type + * + * @return ResolvedFormTypeInterface The type + * + * @throws Exception\FormException if the type can not be retrieved from any extension + */ + public function getType($name); + + /** + * Returns whether the given form type is supported. + * + * @param string $name The name of the type + * + * @return Boolean Whether the type is supported + */ + public function hasType($name); + + /** + * Resolves a form type. + * + * @param FormTypeInterface $type + * + * @return ResolvedFormTypeInterface + */ + public function resolveType(FormTypeInterface $type); + + /** + * Returns the guesser responsible for guessing types. + * + * @return FormTypeGuesserInterface + */ + public function getTypeGuesser(); + + /** + * Returns the extensions loaded by the framework. + * + * @return array + */ + public function getExtensions(); +} diff --git a/src/Symfony/Component/Form/FormTypeInterface.php b/src/Symfony/Component/Form/FormTypeInterface.php index 0e5d20d723..00e96dd25b 100644 --- a/src/Symfony/Component/Form/FormTypeInterface.php +++ b/src/Symfony/Component/Form/FormTypeInterface.php @@ -68,20 +68,6 @@ interface FormTypeInterface */ public function finishView(FormViewInterface $view, FormInterface $form, array $options); - /** - * Returns a builder for the current type. - * - * The builder is retrieved by going up in the type hierarchy when a type does - * not provide one. - * - * @param string $name The name of the builder - * @param FormFactoryInterface $factory The form factory - * @param array $options The options - * - * @return FormBuilderInterface|null A form builder or null when the type does not have a builder - */ - public function createBuilder($name, FormFactoryInterface $factory, array $options); - /** * Sets the default options for this type. * @@ -102,20 +88,4 @@ interface FormTypeInterface * @return string The name of this type */ public function getName(); - - /** - * Sets the extensions for this type. - * - * @param array $extensions An array of FormTypeExtensionInterface - * - * @throws Exception\UnexpectedTypeException if any extension does not implement FormTypeExtensionInterface - */ - public function setExtensions(array $extensions); - - /** - * Returns the extensions associated with this type. - * - * @return array An array of FormTypeExtensionInterface - */ - public function getExtensions(); } diff --git a/src/Symfony/Component/Form/ResolvedFormType.php b/src/Symfony/Component/Form/ResolvedFormType.php new file mode 100644 index 0000000000..3a85679e26 --- /dev/null +++ b/src/Symfony/Component/Form/ResolvedFormType.php @@ -0,0 +1,213 @@ + + * + * 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\FormException; +use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\Exception\TypeDefinitionException; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * A wrapper for a form type and its extensions. + * + * @author Bernhard Schussek + */ +class ResolvedFormType implements ResolvedFormTypeInterface +{ + /** + * @var FormTypeInterface + */ + private $innerType; + + /** + * @var array + */ + private $typeExtensions; + + /** + * @var ResolvedFormType + */ + private $parent; + + /** + * @var OptionsResolver + */ + private $optionsResolver; + + public function __construct(FormTypeInterface $innerType, array $typeExtensions = array(), ResolvedFormType $parent = null) + { + if (!preg_match('/^[a-z0-9_]*$/i', $innerType->getName())) { + throw new FormException(sprintf( + 'The "%s" form type name ("%s") is not valid. Names must only contain letters, numbers, and "_".', + get_class($innerType), + $innerType->getName() + )); + } + + foreach ($typeExtensions as $extension) { + if (!$extension instanceof FormTypeExtensionInterface) { + throw new UnexpectedTypeException($extension, 'Symfony\Component\Form\FormTypeExtensionInterface'); + } + } + + // BC + if ($innerType instanceof AbstractType) { + /* @var AbstractType $innerType */ + $innerType->setExtensions($typeExtensions); + } + + $this->innerType = $innerType; + $this->typeExtensions = $typeExtensions; + $this->parent = $parent; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->innerType->getName(); + } + + /** + * {@inheritdoc} + */ + public function getParent() + { + return $this->parent; + } + + /** + * {@inheritdoc} + */ + public function getInnerType() + { + return $this->innerType; + } + + /** + * {@inheritdoc} + */ + public function getTypeExtensions() + { + // BC + if ($this->innerType instanceof AbstractType) { + return $this->innerType->getExtensions(); + } + + return $this->typeExtensions; + } + + /** + * {@inheritdoc} + */ + public function createBuilder(FormFactoryInterface $factory, $name, array $options = array(), FormBuilderInterface $parent = null) + { + $options = $this->getOptionsResolver()->resolve($options); + + // Should be decoupled from the specific option at some point + $dataClass = isset($options['data_class']) ? $options['data_class'] : null; + + $builder = new FormBuilder($name, $dataClass, new EventDispatcher(), $factory, $options); + $builder->setType($this); + $builder->setParent($parent); + + $this->buildForm($builder, $options); + + return $builder; + } + + /** + * {@inheritdoc} + */ + public function createView(FormInterface $form, FormViewInterface $parent = null) + { + $options = $form->getConfig()->getOptions(); + + $view = new FormView($form->getConfig()->getName()); + $view->setParent($parent); + + $this->buildView($view, $form, $options); + + foreach ($form as $child) { + /* @var FormInterface $child */ + $view->add($child->createView($view)); + } + + $this->finishView($view, $form, $options); + + return $view; + } + + private function buildForm(FormBuilderInterface $builder, array $options) + { + if (null !== $this->parent) { + $this->parent->buildForm($builder, $options); + } + + $this->innerType->buildForm($builder, $options); + + foreach ($this->typeExtensions as $extension) { + /* @var FormTypeExtensionInterface $extension */ + $extension->buildForm($builder, $options); + } + } + + private function buildView(FormViewInterface $view, FormInterface $form, array $options) + { + if (null !== $this->parent) { + $this->parent->buildView($view, $form, $options); + } + + $this->innerType->buildView($view, $form, $options); + + foreach ($this->typeExtensions as $extension) { + /* @var FormTypeExtensionInterface $extension */ + $extension->buildView($view, $form, $options); + } + } + + private function finishView(FormViewInterface $view, FormInterface $form, array $options) + { + if (null !== $this->parent) { + $this->parent->finishView($view, $form, $options); + } + + $this->innerType->finishView($view, $form, $options); + + foreach ($this->typeExtensions as $extension) { + /* @var FormTypeExtensionInterface $extension */ + $extension->finishView($view, $form, $options); + } + } + + private function getOptionsResolver() + { + if (null === $this->optionsResolver) { + if (null !== $this->parent) { + $this->optionsResolver = clone $this->parent->getOptionsResolver(); + } else { + $this->optionsResolver = new OptionsResolver(); + } + + $this->innerType->setDefaultOptions($this->optionsResolver); + + foreach ($this->typeExtensions as $extension) { + /* @var FormTypeExtensionInterface $extension */ + $extension->setDefaultOptions($this->optionsResolver); + } + } + + return $this->optionsResolver; + } +} diff --git a/src/Symfony/Component/Form/ResolvedFormTypeInterface.php b/src/Symfony/Component/Form/ResolvedFormTypeInterface.php new file mode 100644 index 0000000000..0620981544 --- /dev/null +++ b/src/Symfony/Component/Form/ResolvedFormTypeInterface.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +/** + * A wrapper for a form type and its extensions. + * + * @author Bernhard Schussek + */ +interface ResolvedFormTypeInterface +{ + /** + * Returns the name of the type. + * + * @return string The type name. + */ + public function getName(); + + /** + * Returns the parent type. + * + * @return ResolvedFormTypeInterface The parent type or null. + */ + public function getParent(); + + /** + * Returns the wrapped form type. + * + * @return FormTypeInterface The wrapped form type. + */ + public function getInnerType(); + + /** + * Returns the extensions of the wrapped form type. + * + * @return array An array of {@link FormTypeExtensionInterface} instances. + */ + public function getTypeExtensions(); + + /** + * Creates a new form builder for this type. + * + * @param FormFactoryInterface $factory The form factory. + * @param string $name The name for the builder. + * @param array $options The builder options. + * @param FormBuilderInterface $parent The parent builder object or null. + * + * @return FormBuilderInterface The created form builder. + */ + public function createBuilder(FormFactoryInterface $factory, $name, array $options = array(), FormBuilderInterface $parent = null); + + /** + * Creates a new form view for a form of this type. + * + * @param FormInterface $form The form to create a view for. + * @param FormViewInterface $parent The parent view or null. + * + * @return FormViewInterface The created form view. + */ + public function createView(FormInterface $form, FormViewInterface $parent = null); +} diff --git a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php index 4398adf70a..40195c2f3d 100644 --- a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php @@ -17,7 +17,7 @@ use Symfony\Component\Form\FormFactory; use Symfony\Component\Form\Extension\Core\CoreExtension; use Symfony\Component\Form\Extension\Csrf\CsrfExtension; -abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase +abstract class AbstractLayoutTest extends FormIntegrationTestCase { protected $csrfProvider; @@ -33,10 +33,15 @@ abstract class AbstractLayoutTest extends \PHPUnit_Framework_TestCase $this->csrfProvider = $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'); - $this->factory = new FormFactory(array( + parent::setUp(); + } + + protected function getExtensions() + { + return array( new CoreExtension(), new CsrfExtension($this->csrfProvider), - )); + ); } protected function tearDown() diff --git a/src/Symfony/Component/Form/Tests/CompoundFormPerformanceTest.php b/src/Symfony/Component/Form/Tests/CompoundFormPerformanceTest.php new file mode 100644 index 0000000000..4da8afbbe3 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/CompoundFormPerformanceTest.php @@ -0,0 +1,46 @@ + + * + * 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 + */ +class CompoundFormPerformanceTest extends FormPerformanceTestCase +{ + /** + * Create a compound form multiple times, as happens in a collection form + */ + public function testArrayBasedForm() + { + $this->setMaxRunningTime(1); + + for ($i = 0; $i < 40; ++$i) { + $form = $this->factory->createBuilder('form') + ->add('firstName', 'text') + ->add('lastName', 'text') + ->add('gender', 'choice', array( + 'choices' => array('male' => 'Male', 'female' => 'Female'), + 'required' => false, + )) + ->add('age', 'number') + ->add('birthDate', 'birthday') + ->add('city', 'choice', array( + // simulate 300 different cities + 'choices' => range(1, 300), + )) + ->getForm(); + + // load the form into a view + $form->createView(); + } + } +} diff --git a/src/Symfony/Component/Form/Tests/CompoundFormTest.php b/src/Symfony/Component/Form/Tests/CompoundFormTest.php index 8c9652d30b..1fecee9c44 100644 --- a/src/Symfony/Component/Form/Tests/CompoundFormTest.php +++ b/src/Symfony/Component/Form/Tests/CompoundFormTest.php @@ -558,103 +558,6 @@ class FormTest extends AbstractFormTest $this->assertEquals(array('extra' => 'data'), $form->getExtraData()); } - public function testCreateView() - { - $test = $this; - $type1 = $this->getMock('Symfony\Component\Form\FormTypeInterface'); - $type1Extension = $this->getMock('Symfony\Component\Form\FormTypeExtensionInterface'); - $type1->expects($this->any()) - ->method('getExtensions') - ->will($this->returnValue(array($type1Extension))); - $type2 = $this->getMock('Symfony\Component\Form\FormTypeInterface'); - $type2Extension = $this->getMock('Symfony\Component\Form\FormTypeExtensionInterface'); - $type2->expects($this->any()) - ->method('getExtensions') - ->will($this->returnValue(array($type2Extension))); - $calls = array(); - - $type1->expects($this->once()) - ->method('buildView') - ->will($this->returnCallback(function (FormView $view, Form $form) use ($test, &$calls) { - $calls[] = 'type1::buildView'; - $test->assertTrue($view->hasParent()); - $test->assertEquals(0, count($view)); - })); - - $type1Extension->expects($this->once()) - ->method('buildView') - ->will($this->returnCallback(function (FormView $view, Form $form) use ($test, &$calls) { - $calls[] = 'type1ext::buildView'; - $test->assertTrue($view->hasParent()); - $test->assertEquals(0, count($view)); - })); - - $type2->expects($this->once()) - ->method('buildView') - ->will($this->returnCallback(function (FormView $view, Form $form) use ($test, &$calls) { - $calls[] = 'type2::buildView'; - $test->assertTrue($view->hasParent()); - $test->assertEquals(0, count($view)); - })); - - $type2Extension->expects($this->once()) - ->method('buildView') - ->will($this->returnCallback(function (FormView $view, Form $form) use ($test, &$calls) { - $calls[] = 'type2ext::buildView'; - $test->assertTrue($view->hasParent()); - $test->assertEquals(0, count($view)); - })); - - $type1->expects($this->once()) - ->method('finishView') - ->will($this->returnCallback(function (FormView $view, Form $form) use ($test, &$calls) { - $calls[] = 'type1::finishView'; - $test->assertGreaterThan(0, count($view)); - })); - - $type1Extension->expects($this->once()) - ->method('finishView') - ->will($this->returnCallback(function (FormView $view, Form $form) use ($test, &$calls) { - $calls[] = 'type1ext::finishView'; - $test->assertGreaterThan(0, count($view)); - })); - - $type2->expects($this->once()) - ->method('finishView') - ->will($this->returnCallback(function (FormView $view, Form $form) use ($test, &$calls) { - $calls[] = 'type2::finishView'; - $test->assertGreaterThan(0, count($view)); - })); - - $type2Extension->expects($this->once()) - ->method('finishView') - ->will($this->returnCallback(function (FormView $view, Form $form) use ($test, &$calls) { - $calls[] = 'type2ext::finishView'; - $test->assertGreaterThan(0, count($view)); - })); - - $form = $this->getBuilder() - ->setCompound(true) - ->setDataMapper($this->getDataMapper()) - ->setTypes(array($type1, $type2)) - ->getForm(); - $form->setParent($this->getBuilder()->getForm()); - $form->add($this->getBuilder()->getForm()); - - $form->createView(); - - $this->assertEquals(array( - 0 => 'type1::buildView', - 1 => 'type1ext::buildView', - 2 => 'type2::buildView', - 3 => 'type2ext::buildView', - 4 => 'type1::finishView', - 5 => 'type1ext::finishView', - 6 => 'type2::finishView', - 7 => 'type2ext::finishView', - ), $calls); - } - public function testGetErrorsAsStringDeep() { $parent = $this->getBuilder() diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/TypeTestCase.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/TypeTestCase.php index 6acda436c4..9101e45308 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/TypeTestCase.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/TypeTestCase.php @@ -12,17 +12,11 @@ namespace Symfony\Component\Form\Tests\Extension\Core\Type; use Symfony\Component\Form\FormBuilder; -use Symfony\Component\Form\FormFactory; -use Symfony\Component\Form\Extension\Core\CoreExtension; +use Symfony\Component\Form\Tests\FormIntegrationTestCase; use Symfony\Component\EventDispatcher\EventDispatcher; -abstract class TypeTestCase extends \PHPUnit_Framework_TestCase +abstract class TypeTestCase extends FormIntegrationTestCase { - /** - * @var FormFactory - */ - protected $factory; - /** * @var FormBuilder */ @@ -35,29 +29,12 @@ abstract class TypeTestCase extends \PHPUnit_Framework_TestCase protected function setUp() { - if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { - $this->markTestSkipped('The "EventDispatcher" component is not available'); - } + parent::setUp(); $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); - $this->factory = new FormFactory($this->getExtensions()); $this->builder = new FormBuilder(null, null, $this->dispatcher, $this->factory); } - protected function tearDown() - { - $this->builder = null; - $this->dispatcher = null; - $this->factory = null; - } - - protected function getExtensions() - { - return array( - new CoreExtension(), - ); - } - public static function assertDateTimeEquals(\DateTime $expected, \DateTime $actual) { self::assertEquals($expected->format('c'), $actual->format('c')); diff --git a/src/Symfony/Component/Form/Tests/Fixtures/FooSubType.php b/src/Symfony/Component/Form/Tests/Fixtures/FooSubType.php new file mode 100644 index 0000000000..4f7ba6d4a7 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Fixtures/FooSubType.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Fixtures; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormBuilder; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; + +class FooSubType extends AbstractType +{ + public function getName() + { + return 'foo_sub_type'; + } + + public function getParent() + { + return 'foo'; + } +} diff --git a/src/Symfony/Component/Form/Tests/Fixtures/FooType.php b/src/Symfony/Component/Form/Tests/Fixtures/FooType.php index feae68900f..d26d3f7683 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/FooType.php +++ b/src/Symfony/Component/Form/Tests/Fixtures/FooType.php @@ -16,42 +16,15 @@ use Symfony\Component\Form\FormBuilder; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; class FooType extends AbstractType { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder->setAttribute('foo', 'x'); - $builder->setAttribute('data_option', $options['data']); - } - public function getName() { return 'foo'; } - public function createBuilder($name, FormFactoryInterface $factory, array $options) - { - return new FormBuilder($name, null, new EventDispatcher(), $factory); - } - - public function getDefaultOptions() - { - return array( - 'data' => null, - 'required' => false, - 'max_length' => null, - 'a_or_b' => 'a', - ); - } - - public function getAllowedOptionValues() - { - return array( - 'a_or_b' => array('a', 'b'), - ); - } - public function getParent() { return null; diff --git a/src/Symfony/Component/Form/Tests/FormFactoryTest.php b/src/Symfony/Component/Form/Tests/FormFactoryTest.php index 189cdeec8c..6165fb97b5 100644 --- a/src/Symfony/Component/Form/Tests/FormFactoryTest.php +++ b/src/Symfony/Component/Form/Tests/FormFactoryTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Form\Tests; use Symfony\Component\Form\FormBuilder; +use Symfony\Component\Form\FormTypeGuesserChain; use Symfony\Component\Form\FormFactory; use Symfony\Component\Form\Guess\Guess; use Symfony\Component\Form\Guess\ValueGuess; @@ -23,16 +24,29 @@ use Symfony\Component\Form\Tests\Fixtures\FooType; use Symfony\Component\Form\Tests\Fixtures\FooTypeBarExtension; use Symfony\Component\Form\Tests\Fixtures\FooTypeBazExtension; +/** + * @author Bernhard Schussek + */ class FormFactoryTest extends \PHPUnit_Framework_TestCase { - private $extension1; - - private $extension2; - + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ private $guesser1; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ private $guesser2; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $registry; + + /** + * @var FormFactory + */ private $factory; protected function setUp() @@ -43,270 +57,252 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase $this->guesser1 = $this->getMock('Symfony\Component\Form\FormTypeGuesserInterface'); $this->guesser2 = $this->getMock('Symfony\Component\Form\FormTypeGuesserInterface'); - $this->extension1 = new TestExtension($this->guesser1); - $this->extension2 = new TestExtension($this->guesser2); - $this->factory = new FormFactory(array($this->extension1, $this->extension2)); - } + $this->registry = $this->getMock('Symfony\Component\Form\FormRegistryInterface'); + $this->factory = new FormFactory($this->registry); - protected function tearDown() - { - $this->extension1 = null; - $this->extension2 = null; - $this->guesser1 = null; - $this->guesser2 = null; - $this->factory = null; + $this->registry->expects($this->any()) + ->method('getTypeGuesser') + ->will($this->returnValue(new FormTypeGuesserChain(array( + $this->guesser1, + $this->guesser2, + )))); } public function testAddType() { - $this->assertFalse($this->factory->hasType('foo')); - $type = new FooType(); - $this->factory->addType($type); + $resolvedType = $this->getMockResolvedType(); - $this->assertTrue($this->factory->hasType('foo')); - $this->assertSame($type, $this->factory->getType('foo')); - } + $this->registry->expects($this->once()) + ->method('resolveType') + ->with($type) + ->will($this->returnValue($resolvedType)); - public function testAddTypeAddsExtensions() - { - $type = new FooType(); - $ext1 = new FooTypeBarExtension(); - $ext2 = new FooTypeBazExtension(); - - $this->extension1->addTypeExtension($ext1); - $this->extension2->addTypeExtension($ext2); + $this->registry->expects($this->once()) + ->method('addType') + ->with($resolvedType); $this->factory->addType($type); - - $this->assertEquals(array($ext1, $ext2), $type->getExtensions()); } - public function testGetTypeFromExtension() + public function testHasType() + { + $this->registry->expects($this->once()) + ->method('hasType') + ->with('name') + ->will($this->returnValue('RESULT')); + + $this->assertSame('RESULT', $this->factory->hasType('name')); + } + + public function testGetType() { $type = new FooType(); - $this->extension2->addType($type); + $resolvedType = $this->getMockResolvedType(); - $this->assertSame($type, $this->factory->getType('foo')); + $resolvedType->expects($this->once()) + ->method('getInnerType') + ->will($this->returnValue($type)); + + $this->registry->expects($this->once()) + ->method('getType') + ->with('name') + ->will($this->returnValue($resolvedType)); + + $this->assertEquals($type, $this->factory->getType('name')); } - public function testGetTypeAddsExtensions() + public function testCreateNamedBuilderWithTypeName() { - $type = new FooType(); - $ext1 = new FooTypeBarExtension(); - $ext2 = new FooTypeBazExtension(); + $options = array('a' => '1', 'b' => '2'); + $resolvedType = $this->getMockResolvedType(); - $this->extension1->addTypeExtension($ext1); - $this->extension2->addTypeExtension($ext2); - $this->extension2->addType($type); + $this->registry->expects($this->once()) + ->method('getType') + ->with('type') + ->will($this->returnValue($resolvedType)); - $type = $this->factory->getType('foo'); + $resolvedType->expects($this->once()) + ->method('createBuilder') + ->with($this->factory, 'name', $options) + ->will($this->returnValue('BUILDER')); - $this->assertEquals(array($ext1, $ext2), $type->getExtensions()); + $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', 'type', null, $options)); } - /** - * @expectedException Symfony\Component\Form\Exception\FormException - */ - public function testGetTypeExpectsExistingType() + public function testCreateNamedBuilderWithTypeInstance() { - $this->factory->getType('bar'); + $options = array('a' => '1', 'b' => '2'); + $type = $this->getMockType(); + $resolvedType = $this->getMockResolvedType(); + + $this->registry->expects($this->once()) + ->method('resolveType') + ->with($type) + ->will($this->returnValue($resolvedType)); + + // The type is also implicitely added to the registry + $this->registry->expects($this->once()) + ->method('addType') + ->with($resolvedType); + + $resolvedType->expects($this->once()) + ->method('createBuilder') + ->with($this->factory, 'name', $options) + ->will($this->returnValue('BUILDER')); + + $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', $type, null, $options)); } - public function testCreateNamedBuilder() + public function testCreateNamedBuilderWithResolvedTypeInstance() { - $type = new FooType(); - $this->extension1->addType($type); + $options = array('a' => '1', 'b' => '2'); + $resolvedType = $this->getMockResolvedType(); - $builder = $this->factory->createNamedBuilder('bar', 'foo'); + // The type is also implicitely added to the registry + $this->registry->expects($this->once()) + ->method('addType') + ->with($resolvedType); - $this->assertTrue($builder instanceof FormBuilder); - $this->assertEquals('bar', $builder->getName()); - $this->assertNull($builder->getParent()); + $resolvedType->expects($this->once()) + ->method('createBuilder') + ->with($this->factory, 'name', $options) + ->will($this->returnValue('BUILDER')); + + $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', $resolvedType, null, $options)); } - public function testCreateNamedBuilderCallsBuildFormMethods() + public function testCreateNamedBuilderWithParentBuilder() { - $type = new FooType(); - $ext1 = new FooTypeBarExtension(); - $ext2 = new FooTypeBazExtension(); + $options = array('a' => '1', 'b' => '2'); + $parentBuilder = $this->getMockFormBuilder(); + $resolvedType = $this->getMockResolvedType(); - $this->extension1->addTypeExtension($ext1); - $this->extension2->addTypeExtension($ext2); - $this->extension2->addType($type); + $this->registry->expects($this->once()) + ->method('getType') + ->with('type') + ->will($this->returnValue($resolvedType)); - $builder = $this->factory->createNamedBuilder('bar', 'foo'); + $resolvedType->expects($this->once()) + ->method('createBuilder') + ->with($this->factory, 'name', $options, $parentBuilder) + ->will($this->returnValue('BUILDER')); - $this->assertTrue($builder->hasAttribute('foo')); - $this->assertTrue($builder->hasAttribute('bar')); - $this->assertTrue($builder->hasAttribute('baz')); + $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', 'type', null, $options, $parentBuilder)); } public function testCreateNamedBuilderFillsDataOption() { - $type = new FooType(); - $this->extension1->addType($type); + $givenOptions = array('a' => '1', 'b' => '2'); + $expectedOptions = array_merge($givenOptions, array('data' => 'DATA')); + $resolvedType = $this->getMockResolvedType(); - $builder = $this->factory->createNamedBuilder('bar', 'foo', 'xyz'); + $this->registry->expects($this->once()) + ->method('getType') + ->with('type') + ->will($this->returnValue($resolvedType)); - // see FooType::buildForm() - $this->assertEquals('xyz', $builder->getAttribute('data_option')); + $resolvedType->expects($this->once()) + ->method('createBuilder') + ->with($this->factory, 'name', $expectedOptions) + ->will($this->returnValue('BUILDER')); + + $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', 'type', 'DATA', $givenOptions)); } public function testCreateNamedBuilderDoesNotOverrideExistingDataOption() { - $type = new FooType(); - $this->extension1->addType($type); + $options = array('a' => '1', 'b' => '2', 'data' => 'CUSTOM'); + $resolvedType = $this->getMockResolvedType(); - $builder = $this->factory->createNamedBuilder('bar', 'foo', 'xyz', array( - 'data' => 'abc', - )); + $this->registry->expects($this->once()) + ->method('getType') + ->with('type') + ->will($this->returnValue($resolvedType)); - // see FooType::buildForm() - $this->assertEquals('abc', $builder->getAttribute('data_option')); - } - - /** - * @expectedException Symfony\Component\Form\Exception\TypeDefinitionException - */ - public function testCreateNamedBuilderExpectsDataOptionToBeSupported() - { - $type = $this->getMock('Symfony\Component\Form\FormTypeInterface'); - $type->expects($this->any()) - ->method('getName') - ->will($this->returnValue('foo')); - $type->expects($this->any()) - ->method('getExtensions') - ->will($this->returnValue(array())); - - $this->extension1->addType($type); - - $this->factory->createNamedBuilder('bar', 'foo'); - } - - /** - * @expectedException Symfony\Component\Form\Exception\TypeDefinitionException - */ - public function testCreateNamedBuilderExpectsRequiredOptionToBeSupported() - { - $type = $this->getMock('Symfony\Component\Form\FormTypeInterface'); - $type->expects($this->any()) - ->method('getName') - ->will($this->returnValue('foo')); - $type->expects($this->any()) - ->method('getExtensions') - ->will($this->returnValue(array())); - - $this->extension1->addType($type); - - $this->factory->createNamedBuilder('bar', 'foo'); - } - - /** - * @expectedException Symfony\Component\Form\Exception\TypeDefinitionException - */ - public function testCreateNamedBuilderExpectsMaxLengthOptionToBeSupported() - { - $type = $this->getMock('Symfony\Component\Form\FormTypeInterface'); - $type->expects($this->any()) - ->method('getName') - ->will($this->returnValue('foo')); - $type->expects($this->any()) - ->method('getExtensions') - ->will($this->returnValue(array())); - - $this->extension1->addType($type); - - $this->factory->createNamedBuilder('bar', 'foo'); - } - - /** - * @expectedException Symfony\Component\Form\Exception\TypeDefinitionException - */ - public function testCreateNamedBuilderExpectsBuilderToBeReturned() - { - $type = $this->getMock('Symfony\Component\Form\FormTypeInterface'); - $type->expects($this->any()) - ->method('getName') - ->will($this->returnValue('foo')); - $type->expects($this->any()) - ->method('getExtensions') - ->will($this->returnValue(array())); - $type->expects($this->any()) + $resolvedType->expects($this->once()) ->method('createBuilder') - ->will($this->returnValue(null)); + ->with($this->factory, 'name', $options) + ->will($this->returnValue('BUILDER')); - $this->extension1->addType($type); - - $this->factory->createNamedBuilder('bar', 'foo'); - } - - /** - * @expectedException Symfony\Component\OptionsResolver\Exception\InvalidOptionsException - */ - public function testCreateNamedBuilderExpectsOptionsToExist() - { - $type = new FooType(); - $this->extension1->addType($type); - - $this->factory->createNamedBuilder('bar', 'foo', null, array( - 'invalid' => 'xyz', - )); - } - - /** - * @expectedException Symfony\Component\OptionsResolver\Exception\InvalidOptionsException - */ - public function testCreateNamedBuilderExpectsOptionsToBeInValidRange() - { - $type = new FooType(); - $this->extension1->addType($type); - - $this->factory->createNamedBuilder('bar', 'foo', null, array( - 'a_or_b' => 'c', - )); - } - - public function testCreateNamedBuilderAllowsExtensionsToExtendAllowedOptionValues() - { - $type = new FooType(); - $this->extension1->addType($type); - $this->extension1->addTypeExtension(new FooTypeBarExtension()); - - // no exception this time - $this->factory->createNamedBuilder('bar', 'foo', null, array( - 'a_or_b' => 'c', - )); - } - - public function testCreateNamedBuilderAddsTypeInstances() - { - $type = new FooType(); - $this->assertFalse($this->factory->hasType('foo')); - - $builder = $this->factory->createNamedBuilder('bar', $type); - - $this->assertTrue($builder instanceof FormBuilder); - $this->assertTrue($this->factory->hasType('foo')); + $this->assertSame('BUILDER', $this->factory->createNamedBuilder('name', 'type', 'DATA', $options)); } /** * @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException - * @expectedExceptionMessage Expected argument of type "string or Symfony\Component\Form\FormTypeInterface", "stdClass" given + * @expectedExceptionMessage Expected argument of type "string, Symfony\Component\Form\ResolvedFormTypeInterface or Symfony\Component\Form\FormTypeInterface", "stdClass" given */ public function testCreateNamedBuilderThrowsUnderstandableException() { $this->factory->createNamedBuilder('name', new \stdClass()); } - public function testCreateUsesTypeNameAsName() + public function testCreateUsesTypeNameIfTypeGivenAsString() { - $type = new FooType(); - $this->extension1->addType($type); + $options = array('a' => '1', 'b' => '2'); + $resolvedType = $this->getMockResolvedType(); + $builder = $this->getMockFormBuilder(); - $builder = $this->factory->createBuilder('foo'); + $this->registry->expects($this->once()) + ->method('getType') + ->with('TYPE') + ->will($this->returnValue($resolvedType)); - $this->assertEquals('foo', $builder->getName()); + $resolvedType->expects($this->once()) + ->method('createBuilder') + ->with($this->factory, 'TYPE', $options) + ->will($this->returnValue($builder)); + + $builder->expects($this->once()) + ->method('getForm') + ->will($this->returnValue('FORM')); + + $this->assertSame('FORM', $this->factory->create('TYPE', null, $options)); + } + + public function testCreateUsesTypeNameIfTypeGivenAsObject() + { + $options = array('a' => '1', 'b' => '2'); + $resolvedType = $this->getMockResolvedType(); + $builder = $this->getMockFormBuilder(); + + $resolvedType->expects($this->once()) + ->method('getName') + ->will($this->returnValue('TYPE')); + + $resolvedType->expects($this->once()) + ->method('createBuilder') + ->with($this->factory, 'TYPE', $options) + ->will($this->returnValue($builder)); + + $builder->expects($this->once()) + ->method('getForm') + ->will($this->returnValue('FORM')); + + $this->assertSame('FORM', $this->factory->create($resolvedType, null, $options)); + } + + public function testCreateNamed() + { + $options = array('a' => '1', 'b' => '2'); + $resolvedType = $this->getMockResolvedType(); + $builder = $this->getMockFormBuilder(); + + $this->registry->expects($this->once()) + ->method('getType') + ->with('type') + ->will($this->returnValue($resolvedType)); + + $resolvedType->expects($this->once()) + ->method('createBuilder') + ->with($this->factory, 'name', $options) + ->will($this->returnValue($builder)); + + $builder->expects($this->once()) + ->method('getForm') + ->will($this->returnValue('FORM')); + + $this->assertSame('FORM', $this->factory->createNamed('name', 'type', null, $options)); } public function testCreateBuilderForPropertyCreatesFormWithHighestConfidence() @@ -329,7 +325,7 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase Guess::HIGH_CONFIDENCE ))); - $factory = $this->createMockFactory(array('createNamedBuilder')); + $factory = $this->getMockFactory(array('createNamedBuilder')); $factory->expects($this->once()) ->method('createNamedBuilder') @@ -348,7 +344,7 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase ->with('Application\Author', 'firstName') ->will($this->returnValue(null)); - $factory = $this->createMockFactory(array('createNamedBuilder')); + $factory = $this->getMockFactory(array('createNamedBuilder')); $factory->expects($this->once()) ->method('createNamedBuilder') @@ -371,7 +367,7 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase Guess::MEDIUM_CONFIDENCE ))); - $factory = $this->createMockFactory(array('createNamedBuilder')); + $factory = $this->getMockFactory(array('createNamedBuilder')); $factory->expects($this->once()) ->method('createNamedBuilder') @@ -406,7 +402,7 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase Guess::HIGH_CONFIDENCE ))); - $factory = $this->createMockFactory(array('createNamedBuilder')); + $factory = $this->getMockFactory(array('createNamedBuilder')); $factory->expects($this->once()) ->method('createNamedBuilder') @@ -439,7 +435,7 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase Guess::HIGH_CONFIDENCE ))); - $factory = $this->createMockFactory(array('createNamedBuilder')); + $factory = $this->getMockFactory(array('createNamedBuilder')); $factory->expects($this->once()) ->method('createNamedBuilder') @@ -474,7 +470,7 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase Guess::LOW_CONFIDENCE ))); - $factory = $this->createMockFactory(array('createNamedBuilder')); + $factory = $this->getMockFactory(array('createNamedBuilder')); $factory->expects($this->once()) ->method('createNamedBuilder') @@ -507,7 +503,7 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase Guess::HIGH_CONFIDENCE ))); - $factory = $this->createMockFactory(array('createNamedBuilder')); + $factory = $this->getMockFactory(array('createNamedBuilder')); $factory->expects($this->once()) ->method('createNamedBuilder') @@ -540,7 +536,7 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase Guess::HIGH_CONFIDENCE ))); - $factory = $this->createMockFactory(array('createNamedBuilder')); + $factory = $this->getMockFactory(array('createNamedBuilder')); $factory->expects($this->once()) ->method('createNamedBuilder') @@ -555,41 +551,26 @@ class FormFactoryTest extends \PHPUnit_Framework_TestCase $this->assertEquals('builderInstance', $builder); } - public function testCreateNamedBuilderFromParentBuilder() - { - $type = new FooType(); - $this->extension1->addType($type); - - $parentBuilder = $this->getMockBuilder('Symfony\Component\Form\FormBuilder') - ->setConstructorArgs(array('name', null, $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'), $this->factory)) - ->getMock() - ; - - $builder = $this->factory->createNamedBuilder('bar', 'foo', null, array(), $parentBuilder); - - $this->assertNotEquals($builder, $builder->getParent()); - $this->assertEquals($parentBuilder, $builder->getParent()); - } - - public function testFormTypeCreatesDefaultValueForEmptyDataOption() - { - $factory = new FormFactory(array(new \Symfony\Component\Form\Extension\Core\CoreExtension())); - - $form = $factory->createNamedBuilder('author', new AuthorType())->getForm(); - $form->bind(array('firstName' => 'John', 'lastName' => 'Smith')); - - $author = new Author(); - $author->firstName = 'John'; - $author->setLastName('Smith'); - - $this->assertEquals($author, $form->getData()); - } - - private function createMockFactory(array $methods = array()) + private function getMockFactory(array $methods = array()) { return $this->getMockBuilder('Symfony\Component\Form\FormFactory') ->setMethods($methods) - ->setConstructorArgs(array(array($this->extension1, $this->extension2))) + ->setConstructorArgs(array($this->registry)) ->getMock(); } + + private function getMockResolvedType() + { + return $this->getMock('Symfony\Component\Form\ResolvedFormTypeInterface'); + } + + private function getMockType() + { + return $this->getMock('Symfony\Component\Form\FormTypeInterface'); + } + + private function getMockFormBuilder() + { + return $this->getMock('Symfony\Component\Form\Tests\FormBuilderInterface'); + } } diff --git a/src/Symfony/Component/Form/Tests/FormIntegrationTestCase.php b/src/Symfony/Component/Form/Tests/FormIntegrationTestCase.php index 50923492ba..fa03a4002e 100644 --- a/src/Symfony/Component/Form/Tests/FormIntegrationTestCase.php +++ b/src/Symfony/Component/Form/Tests/FormIntegrationTestCase.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Form\Tests; use Symfony\Component\Form\FormFactory; +use Symfony\Component\Form\FormRegistry; use Symfony\Component\Form\Extension\Core\CoreExtension; /** @@ -20,7 +21,12 @@ use Symfony\Component\Form\Extension\Core\CoreExtension; class FormIntegrationTestCase extends \PHPUnit_Framework_TestCase { /** - * @var \Symfony\Component\Form\FormFactoryInterface + * @var FormRegistry + */ + protected $registry; + + /** + * @var FormFactory */ protected $factory; @@ -30,7 +36,8 @@ class FormIntegrationTestCase extends \PHPUnit_Framework_TestCase $this->markTestSkipped('The "EventDispatcher" component is not available'); } - $this->factory = new FormFactory($this->getExtensions()); + $this->registry = new FormRegistry($this->getExtensions()); + $this->factory = new FormFactory($this->registry); } protected function getExtensions() diff --git a/src/Symfony/Component/Form/Tests/FormPerformanceTestCase.php b/src/Symfony/Component/Form/Tests/FormPerformanceTestCase.php index be381074f1..d68d67a7b4 100644 --- a/src/Symfony/Component/Form/Tests/FormPerformanceTestCase.php +++ b/src/Symfony/Component/Form/Tests/FormPerformanceTestCase.php @@ -49,7 +49,6 @@ class FormPerformanceTestCase extends FormIntegrationTestCase /** * @param integer $maxRunningTime * @throws InvalidArgumentException - * @since Method available since Release 2.3.0 */ public function setMaxRunningTime($maxRunningTime) { diff --git a/src/Symfony/Component/Form/Tests/FormRegistryTest.php b/src/Symfony/Component/Form/Tests/FormRegistryTest.php new file mode 100644 index 0000000000..ae0946fc33 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/FormRegistryTest.php @@ -0,0 +1,177 @@ + + * + * 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\Tests\Fixtures\TestExtension; +use Symfony\Component\Form\Tests\Fixtures\FooSubType; +use Symfony\Component\Form\Tests\Fixtures\FooTypeBazExtension; +use Symfony\Component\Form\Tests\Fixtures\FooTypeBarExtension; +use Symfony\Component\Form\Tests\Fixtures\FooType; + +/** + * @author Bernhard Schussek + */ +class FormRegistryTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var FormRegistry + */ + private $registry; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $guesser1; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $guesser2; + + /** + * @var TestExtension + */ + private $extension1; + + /** + * @var TestExtension + */ + private $extension2; + + protected function setUp() + { + $this->guesser1 = $this->getMock('Symfony\Component\Form\FormTypeGuesserInterface'); + $this->guesser2 = $this->getMock('Symfony\Component\Form\FormTypeGuesserInterface'); + $this->extension1 = new TestExtension($this->guesser1); + $this->extension2 = new TestExtension($this->guesser2); + $this->registry = new FormRegistry(array( + $this->extension1, + $this->extension2, + )); + } + + public function testResolveType() + { + $type = new FooType(); + $ext1 = new FooTypeBarExtension(); + $ext2 = new FooTypeBazExtension(); + + $this->extension1->addTypeExtension($ext1); + $this->extension2->addTypeExtension($ext2); + + $resolvedType = $this->registry->resolveType($type); + + $this->assertEquals($type, $resolvedType->getInnerType()); + $this->assertEquals(array($ext1, $ext2), $resolvedType->getTypeExtensions()); + } + + public function testResolveTypeConnectsParent() + { + $parentType = new FooType(); + $type = new FooSubType(); + + $resolvedParentType = $this->registry->resolveType($parentType); + + $this->registry->addType($resolvedParentType); + + $resolvedType = $this->registry->resolveType($type); + + $this->assertSame($resolvedParentType, $resolvedType->getParent()); + } + + /** + * @expectedException Symfony\Component\Form\Exception\FormException + */ + public function testResolveTypeThrowsExceptionIfParentNotFound() + { + $type = new FooSubType(); + + $this->registry->resolveType($type); + } + + public function testGetTypeReturnsAddedType() + { + $type = new FooType(); + + $resolvedType = $this->registry->resolveType($type); + + $this->registry->addType($resolvedType); + + $this->assertSame($resolvedType, $this->registry->getType('foo')); + } + + public function testGetTypeFromExtension() + { + $type = new FooType(); + + $this->extension2->addType($type); + + $resolvedType = $this->registry->getType('foo'); + + $this->assertInstanceOf('Symfony\Component\Form\ResolvedFormTypeInterface', $resolvedType); + $this->assertSame($type, $resolvedType->getInnerType()); + } + + /** + * @expectedException Symfony\Component\Form\Exception\FormException + */ + public function testGetTypeThrowsExceptionIfTypeNotFound() + { + $this->registry->getType('bar'); + } + + /** + * @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testGetTypeThrowsExceptionIfNoString() + { + $this->registry->getType(array()); + } + + public function testHasTypeAfterAdding() + { + $type = new FooType(); + + $resolvedType = $this->registry->resolveType($type); + + $this->assertFalse($this->registry->hasType('foo')); + + $this->registry->addType($resolvedType); + + $this->assertTrue($this->registry->hasType('foo')); + } + + public function testHasTypeAfterLoadingFromExtension() + { + $type = new FooType(); + + $this->assertFalse($this->registry->hasType('foo')); + + $this->extension2->addType($type); + + $this->assertTrue($this->registry->hasType('foo')); + } + + public function testGetTypeGuesser() + { + $expectedGuesser = new FormTypeGuesserChain(array($this->guesser1, $this->guesser2)); + + $this->assertEquals($expectedGuesser, $this->registry->getTypeGuesser()); + } + + public function testGetExtensions() + { + $expectedExtensions = array($this->extension1, $this->extension2); + + $this->assertEquals($expectedExtensions, $this->registry->getExtensions()); + } +} diff --git a/src/Symfony/Component/Form/Tests/FormViewInterface.php b/src/Symfony/Component/Form/Tests/FormViewInterface.php new file mode 100644 index 0000000000..431dd3a05c --- /dev/null +++ b/src/Symfony/Component/Form/Tests/FormViewInterface.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests; + +interface FormViewInterface extends \Iterator, \Symfony\Component\Form\FormViewInterface +{ +} diff --git a/src/Symfony/Component/Form/Tests/ResolvedFormTypeTest.php b/src/Symfony/Component/Form/Tests/ResolvedFormTypeTest.php new file mode 100644 index 0000000000..e89e650bad --- /dev/null +++ b/src/Symfony/Component/Form/Tests/ResolvedFormTypeTest.php @@ -0,0 +1,285 @@ + + * + * 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\ResolvedFormType; +use Symfony\Component\Form\FormView; +use Symfony\Component\Form\FormViewInterface; +use Symfony\Component\Form\FormBuilder; +use Symfony\Component\Form\FormConfig; +use Symfony\Component\Form\Form; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; + +/** + * @author Bernhard Schussek + */ +class ResolvedFormTypeTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $dispatcher; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $factory; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $dataMapper; + + protected function setUp() + { + if (!class_exists('Symfony\Component\OptionsResolver\OptionsResolver')) { + $this->markTestSkipped('The "OptionsResolver" component is not available'); + } + + if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) { + $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->dataMapper = $this->getMock('Symfony\Component\Form\DataMapperInterface'); + } + + public function testCreateBuilder() + { + $parentType = $this->getMockFormType(); + $type = $this->getMockFormType(); + $extension1 = $this->getMockFormTypeExtension(); + $extension2 = $this->getMockFormTypeExtension(); + + $parentResolvedType = new ResolvedFormType($parentType); + $resolvedType = new ResolvedFormType($type, array($extension1, $extension2), $parentResolvedType); + + $test = $this; + $i = 0; + + $assertIndex = function ($index) use (&$i, $test) { + return function () use (&$i, $test, $index) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertEquals($index, $i, 'Executed at index ' . $index); + + ++$i; + }; + }; + + $assertIndexAndAddOption = function ($index, $option, $default) use ($assertIndex) { + $assertIndex = $assertIndex($index); + + return function (OptionsResolverInterface $resolver) use ($assertIndex, $index, $option, $default) { + $assertIndex(); + + $resolver->setDefaults(array($option => $default)); + }; + }; + + // First the default options are generated for the super type + $parentType->expects($this->once()) + ->method('setDefaultOptions') + ->will($this->returnCallback($assertIndexAndAddOption(0, 'a', 'a_default'))); + + // The form type itself + $type->expects($this->once()) + ->method('setDefaultOptions') + ->will($this->returnCallback($assertIndexAndAddOption(1, 'b', 'b_default'))); + + // And its extensions + $extension1->expects($this->once()) + ->method('setDefaultOptions') + ->will($this->returnCallback($assertIndexAndAddOption(2, 'c', 'c_default'))); + + $extension2->expects($this->once()) + ->method('setDefaultOptions') + ->will($this->returnCallback($assertIndexAndAddOption(3, 'd', 'd_default'))); + + // Can only be uncommented when the following PHPUnit "bug" is fixed: + // https://github.com/sebastianbergmann/phpunit-mock-objects/issues/47 + // $givenOptions = array('a' => 'a_custom', 'c' => 'c_custom'); + // $resolvedOptions = array('a' => 'a_custom', 'b' => 'b_default', 'c' => 'c_custom', 'd' => 'd_default'); + + $givenOptions = array(); + $resolvedOptions = array(); + + // Then the form is built for the super type + $parentType->expects($this->once()) + ->method('buildForm') + ->with($this->anything(), $resolvedOptions) + ->will($this->returnCallback($assertIndex(4))); + + // Then the type itself + $type->expects($this->once()) + ->method('buildForm') + ->with($this->anything(), $resolvedOptions) + ->will($this->returnCallback($assertIndex(5))); + + // Then its extensions + $extension1->expects($this->once()) + ->method('buildForm') + ->with($this->anything(), $resolvedOptions) + ->will($this->returnCallback($assertIndex(6))); + + $extension2->expects($this->once()) + ->method('buildForm') + ->with($this->anything(), $resolvedOptions) + ->will($this->returnCallback($assertIndex(7))); + + $factory = $this->getMockFormFactory(); + $parentBuilder = $this->getBuilder('parent'); + $builder = $resolvedType->createBuilder($factory, 'name', $givenOptions, $parentBuilder); + + $this->assertSame($parentBuilder, $builder->getParent()); + $this->assertSame($resolvedType, $builder->getType()); + } + + public function testCreateView() + { + $parentType = $this->getMockFormType(); + $type = $this->getMockFormType(); + $field1Type = $this->getMockFormType(); + $field2Type = $this->getMockFormType(); + $extension1 = $this->getMockFormTypeExtension(); + $extension2 = $this->getMockFormTypeExtension(); + + $parentResolvedType = new ResolvedFormType($parentType); + $resolvedType = new ResolvedFormType($type, array($extension1, $extension2), $parentResolvedType); + $field1ResolvedType = new ResolvedFormType($field1Type); + $field2ResolvedType = new ResolvedFormType($field2Type); + + $options = array('a' => '1', 'b' => '2'); + $form = $this->getBuilder('name', $options) + ->setCompound(true) + ->setDataMapper($this->dataMapper) + ->setType($resolvedType) + ->add($this->getBuilder('foo')->setType($field1ResolvedType)) + ->add($this->getBuilder('bar')->setType($field2ResolvedType)) + ->getForm(); + + $test = $this; + $i = 0; + + $assertIndexAndNbOfChildViews = function ($index, $nbOfChildViews) use (&$i, $test) { + return function (FormViewInterface $view) use (&$i, $test, $index, $nbOfChildViews) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertEquals($index, $i, 'Executed at index ' . $index); + $test->assertCount($nbOfChildViews, $view); + + ++$i; + }; + }; + + // First the super type + $parentType->expects($this->once()) + ->method('buildView') + ->with($this->anything(), $form, $options) + ->will($this->returnCallback($assertIndexAndNbOfChildViews(0, 0))); + + // Then the type itself + $type->expects($this->once()) + ->method('buildView') + ->with($this->anything(), $form, $options) + ->will($this->returnCallback($assertIndexAndNbOfChildViews(1, 0))); + + // Then its extensions + $extension1->expects($this->once()) + ->method('buildView') + ->with($this->anything(), $form, $options) + ->will($this->returnCallback($assertIndexAndNbOfChildViews(2, 0))); + + $extension2->expects($this->once()) + ->method('buildView') + ->with($this->anything(), $form, $options) + ->will($this->returnCallback($assertIndexAndNbOfChildViews(3, 0))); + + // Now the first child form + $field1Type->expects($this->once()) + ->method('buildView') + ->will($this->returnCallback($assertIndexAndNbOfChildViews(4, 0))); + $field1Type->expects($this->once()) + ->method('finishView') + ->will($this->returnCallback($assertIndexAndNbOfChildViews(5, 0))); + + // And the second child form + $field2Type->expects($this->once()) + ->method('buildView') + ->will($this->returnCallback($assertIndexAndNbOfChildViews(6, 0))); + $field2Type->expects($this->once()) + ->method('finishView') + ->will($this->returnCallback($assertIndexAndNbOfChildViews(7, 0))); + + // Again first the parent + $parentType->expects($this->once()) + ->method('finishView') + ->with($this->anything(), $form, $options) + ->will($this->returnCallback($assertIndexAndNbOfChildViews(8, 2))); + + // Then the type itself + $type->expects($this->once()) + ->method('finishView') + ->with($this->anything(), $form, $options) + ->will($this->returnCallback($assertIndexAndNbOfChildViews(9, 2))); + + // Then its extensions + $extension1->expects($this->once()) + ->method('finishView') + ->with($this->anything(), $form, $options) + ->will($this->returnCallback($assertIndexAndNbOfChildViews(10, 2))); + + $extension2->expects($this->once()) + ->method('finishView') + ->with($this->anything(), $form, $options) + ->will($this->returnCallback($assertIndexAndNbOfChildViews(11, 2))); + + $parentView = new FormView('parent'); + $view = $resolvedType->createView($form, $parentView); + + $this->assertSame($parentView, $view->getParent()); + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject + */ + private function getMockFormType() + { + return $this->getMock('Symfony\Component\Form\FormTypeInterface'); + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject + */ + private function getMockFormTypeExtension() + { + return $this->getMock('Symfony\Component\Form\FormTypeExtensionInterface'); + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject + */ + private function getMockFormFactory() + { + return $this->getMock('Symfony\Component\Form\FormFactoryInterface'); + } + + /** + * @param string $name + * @param array $options + * + * @return FormBuilder + */ + protected function getBuilder($name = 'name', array $options = array()) + { + return new FormBuilder($name, null, $this->dispatcher, $this->factory, $options); + } +} diff --git a/src/Symfony/Component/Form/Tests/SimpleFormTest.php b/src/Symfony/Component/Form/Tests/SimpleFormTest.php index 12a16fc28a..f9280280d0 100644 --- a/src/Symfony/Component/Form/Tests/SimpleFormTest.php +++ b/src/Symfony/Component/Form/Tests/SimpleFormTest.php @@ -577,13 +577,54 @@ class SimpleFormTest extends AbstractFormTest $this->assertSame(array(), $this->form->getErrors()); } - public function testCreateViewAcceptsParent() + public function testCreateView() { - $parent = new FormView('form'); + $type = $this->getMock('Symfony\Component\Form\ResolvedFormTypeInterface'); + $view = $this->getMock('Symfony\Component\Form\Tests\FormViewInterface'); + $form = $this->getBuilder()->setType($type)->getForm(); - $view = $this->form->createView($parent); + $type->expects($this->once()) + ->method('createView') + ->with($form) + ->will($this->returnValue($view)); - $this->assertSame($parent, $view->getParent()); + $this->assertSame($view, $form->createView()); + } + + public function testCreateViewWithParent() + { + $type = $this->getMock('Symfony\Component\Form\ResolvedFormTypeInterface'); + $view = $this->getMock('Symfony\Component\Form\Tests\FormViewInterface'); + $parentForm = $this->getMock('Symfony\Component\Form\Tests\FormInterface'); + $parentView = $this->getMock('Symfony\Component\Form\Tests\FormViewInterface'); + $form = $this->getBuilder()->setType($type)->getForm(); + $form->setParent($parentForm); + + $parentForm->expects($this->once()) + ->method('createView') + ->will($this->returnValue($parentView)); + + $type->expects($this->once()) + ->method('createView') + ->with($form, $parentView) + ->will($this->returnValue($view)); + + $this->assertSame($view, $form->createView()); + } + + public function testCreateViewWithExplicitParent() + { + $type = $this->getMock('Symfony\Component\Form\ResolvedFormTypeInterface'); + $view = $this->getMock('Symfony\Component\Form\Tests\FormViewInterface'); + $parentView = $this->getMock('Symfony\Component\Form\Tests\FormViewInterface'); + $form = $this->getBuilder()->setType($type)->getForm(); + + $type->expects($this->once()) + ->method('createView') + ->with($form, $parentView) + ->will($this->returnValue($view)); + + $this->assertSame($view, $form->createView($parentView)); } public function testGetErrorsAsString() diff --git a/src/Symfony/Component/Form/UnmodifiableFormConfig.php b/src/Symfony/Component/Form/UnmodifiableFormConfig.php index a39e351eb7..cb586a8ca0 100644 --- a/src/Symfony/Component/Form/UnmodifiableFormConfig.php +++ b/src/Symfony/Component/Form/UnmodifiableFormConfig.php @@ -57,9 +57,9 @@ class UnmodifiableFormConfig implements FormConfigInterface private $compound; /** - * @var array + * @var ResolvedFormTypeInterface */ - private $types; + private $type; /** * @var array @@ -145,7 +145,7 @@ class UnmodifiableFormConfig implements FormConfigInterface $this->byReference = $config->getByReference(); $this->virtual = $config->getVirtual(); $this->compound = $config->getCompound(); - $this->types = $config->getTypes(); + $this->type = $config->getType(); $this->viewTransformers = $config->getViewTransformers(); $this->modelTransformers = $config->getModelTransformers(); $this->dataMapper = $config->getDataMapper(); @@ -220,9 +220,9 @@ class UnmodifiableFormConfig implements FormConfigInterface /** * {@inheritdoc} */ - public function getTypes() + public function getType() { - return $this->types; + return $this->type; } /**