[Form] Added FormTypeExtensionInterface

With implementations of this interface, existing types can be amended.
The Csrf extension, for example, now contains a class FormTypeCsrfExtension
that adds CSRF capabilities to the "form" type.

To register new type extensions in the DIC, tag them with "form.type_extension"
and the name of the extended type as alias.
This commit is contained in:
Bernhard Schussek 2011-04-22 19:22:26 +02:00
parent 54e66c518f
commit 1ce2db87e2
22 changed files with 431 additions and 81 deletions

View File

@ -42,9 +42,25 @@ class FormPass implements CompilerPassInterface
$container->getDefinition('form.extension')->replaceArgument(1, $types);
$typeExtensions = array();
foreach ($container->findTaggedServiceIds('form.type_extension') as $serviceId => $tag) {
$alias = isset($tag[0]['alias'])
? $tag[0]['alias']
: $serviceId;
if (!isset($typeExtensions[$alias])) {
$typeExtensions[$alias] = array();
}
$typeExtensions[$alias][] = $serviceId;
}
$container->getDefinition('form.extension')->replaceArgument(2, $typeExtensions);
// Find all services annotated with "form.type_guesser"
$guessers = array_keys($container->findTaggedServiceIds('form.type_guesser'));
$container->getDefinition('form.extension')->replaceArgument(2, $guessers);
$container->getDefinition('form.extension')->replaceArgument(3, $guessers);
}
}

View File

@ -42,6 +42,11 @@
-->
<argument type="collection" />
<!--
All services with tag "form.type_extension" are inserted here by
InitFormsPass
-->
<argument type="collection" />
<!--
All services with tag "form.type_guesser" are inserted here by
InitFormsPass
-->
@ -68,7 +73,7 @@
<argument>%file.temporary_storage.directory%</argument>
</service>
<!-- FieldTypes -->
<!-- CoreExtension -->
<service id="form.type.field" class="Symfony\Component\Form\Extension\Core\Type\FieldType">
<tag name="form.type" alias="field" />
<argument type="service" id="validator" />
@ -91,10 +96,6 @@
<service id="form.type.country" class="Symfony\Component\Form\Extension\Core\Type\CountryType">
<tag name="form.type" alias="country" />
</service>
<service id="form.type.csrf" class="Symfony\Component\Form\Extension\Csrf\Type\CsrfType">
<tag name="form.type" alias="csrf" />
<argument type="service" id="form.csrf_provider" />
</service>
<service id="form.type.date" class="Symfony\Component\Form\Extension\Core\Type\DateType">
<tag name="form.type" alias="date" />
</service>
@ -153,6 +154,15 @@
<service id="form.type.url" class="Symfony\Component\Form\Extension\Core\Type\UrlType">
<tag name="form.type" alias="url" />
</service>
<!-- CsrfExtension -->
<service id="form.type.csrf" class="Symfony\Component\Form\Extension\Csrf\Type\CsrfType">
<tag name="form.type" alias="csrf" />
<argument type="service" id="form.csrf_provider" />
</service>
<service id="form.type_extension.csrf" class="Symfony\Component\Form\Extension\Csrf\Type\FormTypeCsrfExtension">
<tag name="form.type_extension" alias="form" />
</service>
</services>
</container>

View File

@ -25,9 +25,9 @@ abstract class AbstractExtension implements FormExtensionInterface
private $types;
/**
* @var Boolean
* @var array
*/
private $typesLoaded = false;
private $typeExtensions;
/**
* @var FormTypeGuesserInterface
@ -39,14 +39,23 @@ abstract class AbstractExtension implements FormExtensionInterface
*/
private $typeGuesserLoaded = false;
abstract protected function loadTypes();
protected function loadTypes()
{
return array();
}
abstract protected function loadTypeGuesser();
protected function loadTypeExtensions()
{
return array();
}
protected function loadTypeGuesser()
{
return null;
}
private function initTypes()
{
$this->typesLoaded = true;
$types = $this->loadTypes();
$typesByName = array();
@ -61,6 +70,28 @@ abstract class AbstractExtension implements FormExtensionInterface
$this->types = $typesByName;
}
private function initTypeExtensions()
{
$extensions = $this->loadTypeExtensions();
$extensionsByType = array();
foreach ($extensions as $extension) {
if (!$extension instanceof FormTypeExtensionInterface) {
throw new UnexpectedTypeException($extension, 'Symfony\Component\Form\FormTypeExtensionInterface');
}
$type = $extension->getExtendedType();
if (!isset($extensionsByType[$type])) {
$extensionsByType[$type] = array();
}
$extensionsByType[$type][] = $extension;
}
$this->typeExtensions = $extensionsByType;
}
private function initTypeGuesser()
{
$this->typeGuesserLoaded = true;
@ -76,12 +107,12 @@ abstract class AbstractExtension implements FormExtensionInterface
public function getType($name)
{
if (!$this->typesLoaded) {
if (null === $this->types) {
$this->initTypes();
}
if (!isset($this->types[$name])) {
throw new FormException(sprintf('The type "%s" can not be typesLoaded by this extension', $name));
throw new FormException(sprintf('The type "%s" can not be loaded by this extension', $name));
}
return $this->types[$name];
@ -89,13 +120,33 @@ abstract class AbstractExtension implements FormExtensionInterface
public function hasType($name)
{
if (!$this->typesLoaded) {
if (null === $this->types) {
$this->initTypes();
}
return isset($this->types[$name]);
}
function getTypeExtensions($name)
{
if (null === $this->typeExtensions) {
$this->initTypeExtensions();
}
return isset($this->typeExtensions[$name])
? $this->typeExtensions[$name]
: array();
}
function hasTypeExtensions($name)
{
if (null === $this->typeExtensions) {
$this->initTypeExtensions();
}
return isset($this->typeExtensions[$name]) && count($this->typeExtensions[$name]) > 0;
}
public function getTypeGuesser()
{
if (!$this->typeGuesserLoaded) {

View File

@ -13,6 +13,8 @@ namespace Symfony\Component\Form;
abstract class AbstractType implements FormTypeInterface
{
private $extensions = array();
public function buildForm(FormBuilder $builder, array $options)
{
}
@ -46,4 +48,20 @@ abstract class AbstractType implements FormTypeInterface
return strtolower($matches[1]);
}
public function setExtensions(array $extensions)
{
foreach ($extensions as $extension) {
if (!$extension instanceof FormTypeExtensionInterface) {
throw new UnexpectedTypeException($extension, 'Symfony\Component\Form\FormTypeExtensionInterface');
}
}
$this->extensions = $extensions;
}
public function getExtensions()
{
return $this->extensions;
}
}

View File

@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form;
abstract class AbstractTypeExtension implements FormTypeExtensionInterface
{
public function buildForm(FormBuilder $builder, array $options)
{
}
public function buildView(FormView $view, FormInterface $form)
{
}
public function buildViewBottomUp(FormView $view, FormInterface $form)
{
}
public function getDefaultOptions(array $options)
{
return array();
}
}

View File

@ -25,16 +25,6 @@ class FormType extends AbstractType
{
$builder->setAttribute('virtual', $options['virtual'])
->setDataMapper(new PropertyPathMapper($options['data_class']));
if ($options['csrf_protection']) {
$csrfOptions = array('page_id' => $options['csrf_page_id']);
if ($options['csrf_provider']) {
$csrfOptions['csrf_provider'] = $options['csrf_provider'];
}
$builder->add($options['csrf_field_name'], 'csrf', $csrfOptions);
}
}
public function buildViewBottomUp(FormView $view, FormInterface $form)
@ -54,10 +44,6 @@ class FormType extends AbstractType
public function getDefaultOptions(array $options)
{
$defaultOptions = array(
'csrf_protection' => true,
'csrf_field_name' => '_token',
'csrf_provider' => null,
'csrf_page_id' => get_class($this),
'virtual' => false,
// Errors in forms bubble by default, so that form errors will
// end up as global errors in the root form

View File

@ -31,7 +31,10 @@ class CsrfExtension extends AbstractExtension
);
}
protected function loadTypeGuesser()
protected function loadTypeExtensions()
{
return array(
new Type\FormTypeCsrfExtension(),
);
}
}

View File

@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Extension\Csrf\Type;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormBuilder;
class FormTypeCsrfExtension extends AbstractTypeExtension
{
public function buildForm(FormBuilder $builder, array $options)
{
if ($options['csrf_protection']) {
$csrfOptions = array('page_id' => $options['csrf_page_id']);
if ($options['csrf_provider']) {
$csrfOptions['csrf_provider'] = $options['csrf_provider'];
}
$builder->add($options['csrf_field_name'], 'csrf', $csrfOptions);
}
}
public function getDefaultOptions(array $options)
{
return array(
'csrf_protection' => true,
'csrf_field_name' => '_token',
'csrf_provider' => null,
'csrf_page_id' => get_class($this),
);
}
public function getExtendedType()
{
return 'form';
}
}

View File

@ -28,25 +28,45 @@ class DependencyInjectionExtension implements FormExtensionInterface
private $guesserLoaded = false;
public function __construct(ContainerInterface $container,
array $typeServiceIds, array $guesserServiceIds)
array $typeServiceIds, array $typeExtensionServiceIds,
array $guesserServiceIds)
{
$this->container = $container;
$this->typeServiceIds = $typeServiceIds;
$this->typeExtensionServiceIds = $typeExtensionServiceIds;
$this->guesserServiceIds = $guesserServiceIds;
}
public function getType($identifier)
public function getType($name)
{
if (!isset($this->typeServiceIds[$identifier])) {
throw new \InvalidArgumentException(sprintf('The field type "%s" is not registered with the service container.', $identifier));
if (!isset($this->typeServiceIds[$name])) {
throw new \InvalidArgumentException(sprintf('The field type "%s" is not registered with the service container.', $name));
}
return $this->container->get($this->typeServiceIds[$identifier]);
return $this->container->get($this->typeServiceIds[$name]);
}
public function hasType($identifier)
public function hasType($name)
{
return isset($this->typeServiceIds[$identifier]);
return isset($this->typeServiceIds[$name]);
}
public function getTypeExtensions($name)
{
$extensions = array();
if (isset($this->typeExtensionServiceIds[$name])) {
foreach ($this->typeExtensionServiceIds[$name] as $serviceId) {
$extensions[] = $this->container->get($serviceId);
}
}
return $extensions;
}
public function hasTypeExtensions($name)
{
return isset($this->typeExtensionServiceIds[$name]);
}
public function getTypeGuesser()

View File

@ -844,6 +844,10 @@ class Form implements \IteratorAggregate, FormInterface
foreach ($types as $type) {
$type->buildView($view, $this);
foreach ($type->getExtensions() as $typeExtension) {
$typeExtension->buildView($view, $this);
}
}
foreach ($this->children as $key => $child) {
@ -854,6 +858,10 @@ class Form implements \IteratorAggregate, FormInterface
foreach ($types as $type) {
$type->buildViewBottomUp($view, $this);
foreach ($type->getExtensions() as $typeExtension) {
$typeExtension->buildViewBottomUp($view, $this);
}
}
return $view;

View File

@ -17,5 +17,9 @@ interface FormExtensionInterface
function hasType($name);
function getTypeExtensions($name);
function hasTypeExtensions($name);
function getTypeGuesser();
}

View File

@ -20,6 +20,8 @@ class FormFactory implements FormFactoryInterface
{
private $extensions = array();
private $types = array();
private $guesser;
public function __construct(array $extensions)
@ -48,6 +50,46 @@ class FormFactory implements FormFactoryInterface
$this->guesser = new FormTypeGuesserChain($guessers);
}
public function getType($name)
{
$type = null;
if ($name instanceof FormTypeInterface) {
$type = $name;
$name = $type->getName();
}
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));
}
}
$typeExtensions = array();
foreach ($this->extensions as $extension) {
$typeExtensions = array_merge(
$typeExtensions,
$extension->getTypeExtensions($name)
);
}
$type->setExtensions($typeExtensions);
$this->types[$name] = $type;
}
return $this->types[$name];
}
public function create($type, $data = null, array $options = array())
{
return $this->createBuilder($type, $data, $options)->getForm();
@ -77,27 +119,22 @@ class FormFactory implements FormFactoryInterface
{
$builder = null;
$types = array();
$typeExtensions = array();
$knownOptions = array();
$passedOptions = array_keys($options);
while (null !== $type) {
if (!$type instanceof FormTypeInterface) {
foreach ($this->extensions as $extension) {
if ($extension->hasType($type)) {
$type = $extension->getType($type);
break;
}
}
$type = $this->getType($type);
if (!$type) {
throw new FormException(sprintf('Could not load type "%s"', $type));
}
$defaultOptions = $type->getDefaultOptions($options);
foreach ($type->getExtensions() as $typeExtension) {
$defaultOptions = array_merge($defaultOptions, $typeExtension->getDefaultOptions($options));
}
array_unshift($types, $type);
$defaultOptions = $type->getDefaultOptions($options);
$options = array_merge($defaultOptions, $options);
$knownOptions = array_merge($knownOptions, array_keys($defaultOptions));
array_unshift($types, $type);
$type = $type->getParent($options);
}
@ -117,6 +154,10 @@ class FormFactory implements FormFactoryInterface
foreach ($types as $type) {
$type->buildForm($builder, $options);
foreach ($type->getExtensions() as $typeExtension) {
$typeExtension->buildForm($builder, $options);
}
}
if (null !== $data) {

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form;
interface FormTypeExtensionInterface
{
function buildForm(FormBuilder $builder, array $options);
function buildView(FormView $view, FormInterface $form);
function buildViewBottomUp(FormView $view, FormInterface $form);
function getDefaultOptions(array $options);
function getExtendedType();
}

View File

@ -26,4 +26,8 @@ interface FormTypeInterface
function getParent(array $options);
function getName();
function setExtensions(array $extensions);
function getExtensions();
}

View File

@ -53,6 +53,10 @@ class TestType implements FormTypeInterface
function getDefaultOptions(array $options) {}
function getParent(array $options) {}
function setExtensions(array $extensions) {}
function getExtensions() {}
}
class TestExtension extends AbstractExtension

View File

@ -16,15 +16,13 @@ use Symfony\Component\Form\Form;
class CollectionFormTest extends TypeTestCase
{
public function testContainsOnlyCsrfTokenByDefault()
public function testContainsNoFieldByDefault()
{
$form = $this->factory->create('collection', null, array(
'type' => 'field',
'csrf_field_name' => 'abc',
));
$this->assertTrue($form->has('abc'));
$this->assertEquals(1, count($form));
$this->assertEquals(0, count($form));
}
public function testSetDataAdjustsSize()

View File

@ -63,24 +63,6 @@ class FormTest_AuthorWithoutRefSetter
class FormTypeTest extends TypeTestCase
{
public function testCsrfProtectionByDefault()
{
$form = $this->factory->create('form', null, array(
'csrf_field_name' => 'csrf',
));
$this->assertTrue($form->has('csrf'));
}
public function testCsrfProtectionCanBeDisabled()
{
$form = $this->factory->create('form', null, array(
'csrf_protection' => false,
));
$this->assertEquals(0, count($form));
}
public function testValidationGroupNullByDefault()
{
$form = $this->factory->create('form');

View File

@ -14,13 +14,10 @@ namespace Symfony\Tests\Component\Form\Extension\Core\Type;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormFactory;
use Symfony\Component\Form\Extension\Core\CoreExtension;
use Symfony\Component\Form\Extension\Csrf\CsrfExtension;
use Symfony\Component\EventDispatcher\EventDispatcher;
abstract class TypeTestCase extends \PHPUnit_Framework_TestCase
{
protected $csrfProvider;
protected $validator;
protected $storage;
@ -35,7 +32,6 @@ abstract class TypeTestCase extends \PHPUnit_Framework_TestCase
protected function setUp()
{
$this->csrfProvider = $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface');
$this->validator = $this->getMock('Symfony\Component\Validator\ValidatorInterface');
$this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
$this->storage = $this->getMockBuilder('Symfony\Component\HttpFoundation\File\TemporaryStorage')
@ -49,7 +45,6 @@ abstract class TypeTestCase extends \PHPUnit_Framework_TestCase
{
return array(
new CoreExtension($this->validator, $this->storage),
new CsrfExtension($this->csrfProvider),
);
}

View File

@ -11,8 +11,6 @@
namespace Symfony\Tests\Component\Form\Extension\Csrf\Type;
use Symfony\Tests\Component\Form\Extension\Core\Type\TypeTestCase;
class CsrfTypeTest extends TypeTestCase
{
protected $provider;

View File

@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Tests\Component\Form\Extension\Csrf\Type;
class FormTypeCsrfExtensionTest extends TypeTestCase
{
public function testCsrfProtectionByDefault()
{
$form = $this->factory->create('form', null, array(
'csrf_field_name' => 'csrf',
));
$this->assertTrue($form->has('csrf'));
}
public function testCsrfProtectionCanBeDisabled()
{
$form = $this->factory->create('form', null, array(
'csrf_protection' => false,
));
$this->assertEquals(0, count($form));
}
}

View File

@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Tests\Component\Form\Extension\Csrf\Type;
use Symfony\Tests\Component\Form\Extension\Core\Type\TypeTestCase as BaseTestCase;
use Symfony\Component\Form\Extension\Csrf\CsrfExtension;
abstract class TypeTestCase extends BaseTestCase
{
protected $csrfProvider;
protected function setUp()
{
$this->csrfProvider = $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface');
parent::setUp();
}
protected function getExtensions()
{
return array_merge(parent::getExtensions(), array(
new CsrfExtension($this->csrfProvider),
));
}
}

View File

@ -869,7 +869,15 @@ class FormTest extends \PHPUnit_Framework_TestCase
{
$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())
@ -880,6 +888,14 @@ class FormTest extends \PHPUnit_Framework_TestCase
$test->assertFalse($view->hasChildren());
}));
$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->assertFalse($view->hasChildren());
}));
$type2->expects($this->once())
->method('buildView')
->will($this->returnCallback(function (FormView $view, Form $form) use ($test, &$calls) {
@ -888,6 +904,14 @@ class FormTest extends \PHPUnit_Framework_TestCase
$test->assertFalse($view->hasChildren());
}));
$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->assertFalse($view->hasChildren());
}));
$type1->expects($this->once())
->method('buildViewBottomUp')
->will($this->returnCallback(function (FormView $view, Form $form) use ($test, &$calls) {
@ -895,6 +919,13 @@ class FormTest extends \PHPUnit_Framework_TestCase
$test->assertTrue($view->hasChildren());
}));
$type1Extension->expects($this->once())
->method('buildViewBottomUp')
->will($this->returnCallback(function (FormView $view, Form $form) use ($test, &$calls) {
$calls[] = 'type1ext::buildViewBottomUp';
$test->assertTrue($view->hasChildren());
}));
$type2->expects($this->once())
->method('buildViewBottomUp')
->will($this->returnCallback(function (FormView $view, Form $form) use ($test, &$calls) {
@ -902,6 +933,13 @@ class FormTest extends \PHPUnit_Framework_TestCase
$test->assertTrue($view->hasChildren());
}));
$type2Extension->expects($this->once())
->method('buildViewBottomUp')
->will($this->returnCallback(function (FormView $view, Form $form) use ($test, &$calls) {
$calls[] = 'type2ext::buildViewBottomUp';
$test->assertTrue($view->hasChildren());
}));
$form = $this->getBuilder()->setTypes(array($type1, $type2))->getForm();
$form->setParent($this->getBuilder()->getForm());
$form->add($this->getBuilder()->getForm());
@ -910,9 +948,13 @@ class FormTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(array(
0 => 'type1::buildView',
1 => 'type2::buildView',
2 => 'type1::buildViewBottomUp',
3 => 'type2::buildViewBottomUp',
1 => 'type1ext::buildView',
2 => 'type2::buildView',
3 => 'type2ext::buildView',
4 => 'type1::buildViewBottomUp',
5 => 'type1ext::buildViewBottomUp',
6 => 'type2::buildViewBottomUp',
7 => 'type2ext::buildViewBottomUp',
), $calls);
}