diff --git a/src/Symfony/Component/Form/Exception/CreationException.php b/src/Symfony/Component/Form/Exception/CreationException.php new file mode 100644 index 000000000000..9742103d6203 --- /dev/null +++ b/src/Symfony/Component/Form/Exception/CreationException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Exception; + +/** + * Thrown when a form could not be constructed by a FormFactory + * + * @author Bernhard Schussek + */ +class CreationException extends FormException +{ +} \ No newline at end of file diff --git a/src/Symfony/Component/Form/Exception/TypeDefinitionException.php b/src/Symfony/Component/Form/Exception/TypeDefinitionException.php new file mode 100644 index 000000000000..9c88d4a6e793 --- /dev/null +++ b/src/Symfony/Component/Form/Exception/TypeDefinitionException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Exception; + +/** + * Thrown when a form type is configured incorrectly + * + * @author Bernhard Schussek + */ +class TypeDefinitionException extends FormException +{ +} \ No newline at end of file diff --git a/src/Symfony/Component/Form/FormFactory.php b/src/Symfony/Component/Form/FormFactory.php index bd103ff5df1b..365482941d34 100644 --- a/src/Symfony/Component/Form/FormFactory.php +++ b/src/Symfony/Component/Form/FormFactory.php @@ -13,9 +13,17 @@ use Symfony\Component\Form\Exception\FormException; use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\Exception\TypeDefinitionException; +use Symfony\Component\Form\Exception\CreationException; class FormFactory implements FormFactoryInterface { + private static $requiredOptions = array( + 'data', + 'required', + 'max_length', + ); + private $extensions = array(); private $types = array(); @@ -40,41 +48,36 @@ public function __construct(array $extensions) $this->extensions = $extensions; } - public function getType($name) + public function hasType($name) { - $type = null; + if (isset($this->types[$name])) { + return true; + } - if ($name instanceof FormTypeInterface) { - $type = $name; - $name = $type->getName(); + try { + $this->loadType($name); + } catch (FormException $e) { + return false; } - if (!isset($this->types[$name])) { - if (!$type) { - 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)); - } - } + return true; + } - $typeExtensions = array(); + public function addType(FormTypeInterface $type) + { + $this->loadTypeExtensions($type); - foreach ($this->extensions as $extension) { - $typeExtensions = array_merge( - $typeExtensions, - $extension->getTypeExtensions($name) - ); - } + $this->types[$type->getName()] = $type; + } - $type->setExtensions($typeExtensions); + public function getType($name) + { + if (!is_string($name)) { + throw new UnexpectedTypeException($name, 'string'); + } - $this->types[$name] = $type; + if (!isset($this->types[$name])) { + $this->loadType($name); } return $this->types[$name]; @@ -117,7 +120,11 @@ public function createNamedBuilder($type, $name, $data = null, array $options = } while (null !== $type) { - $type = $this->getType($type); + if ($type instanceof FormTypeInterface) { + $this->addType($type); + } else { + $type = $this->getType($type); + } $defaultOptions = $type->getDefaultOptions($options); @@ -131,17 +138,30 @@ public function createNamedBuilder($type, $name, $data = null, array $options = $type = $type->getParent($options); } + $type = end($types); + $diff = array_diff(self::$requiredOptions, $knownOptions); + + if (count($diff) > 0) { + throw new TypeDefinitionException(sprintf('Type "%s" should support the option(s) "%s"', $type->getName(), implode('", "', $diff))); + } + $diff = array_diff($passedOptions, $knownOptions); + if (count($diff) > 1) { + throw new CreationException(sprintf('The options "%s" do not exist', implode('", "', $diff))); + } + if (count($diff) > 0) { - throw new FormException(sprintf('The options "%s" do not exist', implode('", "', $diff))); + throw new CreationException(sprintf('The option "%s" does not exist', $diff[0])); } for ($i = 0, $l = count($types); $i < $l && !$builder; ++$i) { $builder = $types[$i]->createBuilder($name, $this, $options); } - // TODO check if instance exists + if (!$builder) { + throw new TypeDefinitionException(sprintf('Type "%s" or any of its parents should return a FormBuilder instance from createBuilder()', $type->getName())); + } $builder->setTypes($types); @@ -198,4 +218,38 @@ private function loadGuesser() $this->guesser = new FormTypeGuesserChain($guessers); } + + 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->types[$name] = $type; + } + + private function loadTypeExtensions(FormTypeInterface $type) + { + $typeExtensions = array(); + + foreach ($this->extensions as $extension) { + $typeExtensions = array_merge( + $typeExtensions, + $extension->getTypeExtensions($type->getName()) + ); + } + + $type->setExtensions($typeExtensions); + } } diff --git a/src/Symfony/Component/Form/FormFactoryInterface.php b/src/Symfony/Component/Form/FormFactoryInterface.php index 0418de514573..6206737cc0e8 100644 --- a/src/Symfony/Component/Form/FormFactoryInterface.php +++ b/src/Symfony/Component/Form/FormFactoryInterface.php @@ -26,4 +26,8 @@ function createNamedBuilder($type, $name, $data = null, array $options = array() function createBuilderForProperty($class, $property, $data = null, array $options = array()); function getType($name); + + function hasType($name); + + function addType(FormTypeInterface $type); } diff --git a/tests/Symfony/Tests/Component/Form/AbstractExtensionTest.php b/tests/Symfony/Tests/Component/Form/AbstractExtensionTest.php index 4b7d67141ea0..6e00c734c793 100644 --- a/tests/Symfony/Tests/Component/Form/AbstractExtensionTest.php +++ b/tests/Symfony/Tests/Component/Form/AbstractExtensionTest.php @@ -17,53 +17,29 @@ use Symfony\Component\Form\FormView; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormBuilder; +use Symfony\Tests\Component\Form\Fixtures\FooType; class AbstractExtensionTest extends \PHPUnit_Framework_TestCase { public function testHasType() { - $loader = new TestExtension(); + $loader = new ConcreteExtension(); $this->assertTrue($loader->hasType('foo')); $this->assertFalse($loader->hasType('bar')); } public function testGetType() { - $loader = new TestExtension(array($type)); - $this->assertInstanceOf(__NAMESPACE__.'\TestType', $loader->getType('foo')); - $this->assertSame($loader->getType('foo'), $loader->getType('foo')); + $loader = new ConcreteExtension(); + $this->assertTrue($loader->getType('foo') instanceof FooType); } } -class TestType implements FormTypeInterface -{ - public function getName() - { - return 'foo'; - } - - function buildForm(FormBuilder $builder, array $options) {} - - function buildView(FormView $view, FormInterface $form) {} - - function buildViewBottomUp(FormView $view, FormInterface $form) {} - - function createBuilder($name, FormFactoryInterface $factory, array $options) {} - - function getDefaultOptions(array $options) {} - - function getParent(array $options) {} - - function setExtensions(array $extensions) {} - - function getExtensions() {} -} - -class TestExtension extends AbstractExtension +class ConcreteExtension extends AbstractExtension { protected function loadTypes() { - return array(new TestType()); + return array(new FooType()); } protected function loadTypeGuesser() diff --git a/tests/Symfony/Tests/Component/Form/Fixtures/FooType.php b/tests/Symfony/Tests/Component/Form/Fixtures/FooType.php new file mode 100644 index 000000000000..93f3cc8f4a92 --- /dev/null +++ b/tests/Symfony/Tests/Component/Form/Fixtures/FooType.php @@ -0,0 +1,41 @@ +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, $factory, new EventDispatcher()); + } + + public function getDefaultOptions(array $options) + { + return array( + 'data' => null, + 'required' => false, + 'max_length' => null, + ); + } + + public function getParent(array $options) + { + return null; + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/Form/Fixtures/FooTypeBarExtension.php b/tests/Symfony/Tests/Component/Form/Fixtures/FooTypeBarExtension.php new file mode 100644 index 000000000000..8da2c4406340 --- /dev/null +++ b/tests/Symfony/Tests/Component/Form/Fixtures/FooTypeBarExtension.php @@ -0,0 +1,19 @@ +setAttribute('bar', 'x'); + } + + public function getExtendedType() + { + return 'foo'; + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/Form/Fixtures/FooTypeBazExtension.php b/tests/Symfony/Tests/Component/Form/Fixtures/FooTypeBazExtension.php new file mode 100644 index 000000000000..c256c39c7f4e --- /dev/null +++ b/tests/Symfony/Tests/Component/Form/Fixtures/FooTypeBazExtension.php @@ -0,0 +1,19 @@ +setAttribute('baz', 'x'); + } + + public function getExtendedType() + { + return 'foo'; + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/Form/Fixtures/TestExtension.php b/tests/Symfony/Tests/Component/Form/Fixtures/TestExtension.php new file mode 100644 index 000000000000..e3071b834207 --- /dev/null +++ b/tests/Symfony/Tests/Component/Form/Fixtures/TestExtension.php @@ -0,0 +1,63 @@ +guesser = $guesser; + } + + public function addType(FormTypeInterface $type) + { + $this->types[$type->getName()] = $type; + } + + public function getType($name) + { + return isset($this->types[$name]) ? $this->types[$name] : null; + } + + public function hasType($name) + { + return isset($this->types[$name]); + } + + public function addTypeExtension(FormTypeExtensionInterface $extension) + { + $type = $extension->getExtendedType(); + + if (!isset($this->extensions[$type])) { + $this->extensions[$type] = array(); + } + + $this->extensions[$type][] = $extension; + } + + public function getTypeExtensions($name) + { + return isset($this->extensions[$name]) ? $this->extensions[$name] : array(); + } + + public function hasTypeExtensions($name) + { + return isset($this->extensions[$name]); + } + + public function getTypeGuesser() + { + return $this->guesser; + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/Form/FormFactoryTest.php b/tests/Symfony/Tests/Component/Form/FormFactoryTest.php index dac741f8801d..8baff1a11cb7 100644 --- a/tests/Symfony/Tests/Component/Form/FormFactoryTest.php +++ b/tests/Symfony/Tests/Component/Form/FormFactoryTest.php @@ -11,10 +11,15 @@ namespace Symfony\Tests\Component\Form; +use Symfony\Component\Form\FormBuilder; use Symfony\Component\Form\FormFactory; use Symfony\Component\Form\Guess\Guess; use Symfony\Component\Form\Guess\ValueGuess; use Symfony\Component\Form\Guess\TypeGuess; +use Symfony\Tests\Component\Form\Fixtures\TestExtension; +use Symfony\Tests\Component\Form\Fixtures\FooType; +use Symfony\Tests\Component\Form\Fixtures\FooTypeBarExtension; +use Symfony\Tests\Component\Form\Fixtures\FooTypeBazExtension; class FormFactoryTest extends \PHPUnit_Framework_TestCase { @@ -32,17 +37,253 @@ protected function setUp() { $this->guesser1 = $this->getMock('Symfony\Component\Form\FormTypeGuesserInterface'); $this->guesser2 = $this->getMock('Symfony\Component\Form\FormTypeGuesserInterface'); - $this->extension1 = $this->getMock('Symfony\Component\Form\FormExtensionInterface'); - $this->extension1->expects($this->any()) - ->method('getTypeGuesser') - ->will($this->returnValue($this->guesser1)); - $this->extension2 = $this->getMock('Symfony\Component\Form\FormExtensionInterface'); - $this->extension2->expects($this->any()) - ->method('getTypeGuesser') - ->will($this->returnValue($this->guesser2)); + $this->extension1 = new TestExtension($this->guesser1); + $this->extension2 = new TestExtension($this->guesser2); $this->factory = new FormFactory(array($this->extension1, $this->extension2)); } + public function testAddType() + { + $this->assertFalse($this->factory->hasType('foo')); + + $type = new FooType(); + $this->factory->addType($type); + + $this->assertTrue($this->factory->hasType('foo')); + $this->assertSame($type, $this->factory->getType('foo')); + } + + public function testAddTypeAddsExtensions() + { + $type = new FooType(); + $ext1 = new FooTypeBarExtension(); + $ext2 = new FooTypeBazExtension(); + + $this->extension1->addTypeExtension($ext1); + $this->extension2->addTypeExtension($ext2); + + $this->factory->addType($type); + + $this->assertEquals(array($ext1, $ext2), $type->getExtensions()); + } + + public function testGetTypeFromExtension() + { + $type = new FooType(); + $this->extension2->addType($type); + + $this->assertSame($type, $this->factory->getType('foo')); + } + + public function testGetTypeAddsExtensions() + { + $type = new FooType(); + $ext1 = new FooTypeBarExtension(); + $ext2 = new FooTypeBazExtension(); + + $this->extension1->addTypeExtension($ext1); + $this->extension2->addTypeExtension($ext2); + $this->extension2->addType($type); + + $type = $this->factory->getType('foo'); + + $this->assertEquals(array($ext1, $ext2), $type->getExtensions()); + } + + /** + * @expectedException Symfony\Component\Form\Exception\FormException + */ + public function testGetTypeExpectsExistingType() + { + $this->factory->getType('bar'); + } + + public function testCreateNamedBuilder() + { + $type = new FooType(); + $this->extension1->addType($type); + + $builder = $this->factory->createNamedBuilder('foo', 'bar'); + + $this->assertTrue($builder instanceof FormBuilder); + $this->assertEquals('bar', $builder->getName()); + } + + public function testCreateNamedBuilderCallsBuildFormMethods() + { + $type = new FooType(); + $ext1 = new FooTypeBarExtension(); + $ext2 = new FooTypeBazExtension(); + + $this->extension1->addTypeExtension($ext1); + $this->extension2->addTypeExtension($ext2); + $this->extension2->addType($type); + + $builder = $this->factory->createNamedBuilder('foo', 'bar'); + + $this->assertTrue($builder->hasAttribute('foo')); + $this->assertTrue($builder->hasAttribute('bar')); + $this->assertTrue($builder->hasAttribute('baz')); + } + + public function testCreateNamedBuilderFillsDataOption() + { + $type = new FooType(); + $this->extension1->addType($type); + + $builder = $this->factory->createNamedBuilder('foo', 'bar', 'xyz'); + + // see FooType::buildForm() + $this->assertEquals('xyz', $builder->getAttribute('data_option')); + } + + public function testCreateNamedBuilderDoesNotOverrideExistingDataOption() + { + $type = new FooType(); + $this->extension1->addType($type); + + $builder = $this->factory->createNamedBuilder('foo', 'bar', 'xyz', array( + 'data' => 'abc', + )); + + // 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())); + $type->expects($this->any()) + ->method('getDefaultOptions') + ->will($this->returnValue(array( + 'required' => false, + 'max_length' => null, + ))); + + $this->extension1->addType($type); + + $this->factory->createNamedBuilder('foo', 'bar'); + } + + /** + * @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())); + $type->expects($this->any()) + ->method('getDefaultOptions') + ->will($this->returnValue(array( + 'data' => null, + 'max_length' => null, + ))); + + $this->extension1->addType($type); + + $this->factory->createNamedBuilder('foo', 'bar'); + } + + /** + * @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())); + $type->expects($this->any()) + ->method('getDefaultOptions') + ->will($this->returnValue(array( + 'data' => null, + 'required' => false, + ))); + + $this->extension1->addType($type); + + $this->factory->createNamedBuilder('foo', 'bar'); + } + + /** + * @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()) + ->method('getDefaultOptions') + ->will($this->returnValue(array( + 'data' => null, + 'required' => false, + 'max_length' => null, + ))); + $type->expects($this->any()) + ->method('createBuilder') + ->will($this->returnValue(null)); + + $this->extension1->addType($type); + + $this->factory->createNamedBuilder('foo', 'bar'); + } + + /** + * @expectedException Symfony\Component\Form\Exception\CreationException + */ + public function testCreateNamedBuilderExpectsOptionsToExist() + { + $type = new FooType(); + $this->extension1->addType($type); + + $this->factory->createNamedBuilder('foo', 'bar', null, array( + 'invalid' => 'xyz', + )); + } + + public function testCreateNamedBuilderAddsTypeInstances() + { + $type = new FooType(); + $this->assertFalse($this->factory->hasType('foo')); + + $builder = $this->factory->createNamedBuilder($type, 'bar'); + + $this->assertTrue($builder instanceof FormBuilder); + $this->assertTrue($this->factory->hasType('foo')); + } + + public function testCreateUsesTypeNameAsName() + { + $type = new FooType(); + $this->extension1->addType($type); + + $builder = $this->factory->createBuilder('foo'); + + $this->assertEquals('foo', $builder->getName()); + } + public function testCreateBuilderForPropertyCreatesFieldWithHighestConfidence() { $this->guesser1->expects($this->once())