feature #21690 [Form] allow form types + form type extensions + form type guessers to be private services (hhamon)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[Form] allow form types + form type extensions + form type guessers to be private services

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | ~
| License       | MIT
| Doc PR        | ~

This pull request is about making internal form services (aka form types, form type extensions and form type guessers) private. They used to be public until Symfony 3.2 for one valid reason: lazyness. However, Symfony 3.3 now comes with built-in mechanism to support effective lazy loading of private services with service locators and proxies.

This PR makes the `DependencyInjectionExtension` class of the `Form` component leverage these new DI component mechanisms. Form types, form type extensions and form type guessers can now be declared private as a best practice. We decided to make these services private as of Symfony 3.3 and of course it would break BC. But this PR introduces a BC layer using a Symfony trick to keep internal form services public. The service container currently has a known issue where private services are not really private if they're referenced by at least two other services in the container. We use this trick to maintain the legacy services public even though the new API relies on private ones. This trick is done thanks to the `deprecated.form.registry` and `deprecated.form.registry.csrf` fake services that will be removed in Symfony 4.0.

Commits
-------

600e75ce88 [Form] use new service locator in DependencyInjectionExtension class, so that form types can be made private at some point.
This commit is contained in:
Fabien Potencier 2017-02-28 12:26:30 -08:00
commit d1da474f8d
9 changed files with 341 additions and 254 deletions

View File

@ -30,17 +30,13 @@
<!-- DependencyInjectionExtension -->
<service id="form.extension" class="Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension" public="false">
<argument type="service" id="service_container" />
<!-- All services with tag "form.type" are inserted here by FormPass -->
<argument type="collection" />
<!-- All services with tag "form.type_extension" are inserted here by FormPass -->
<argument type="collection" />
<!-- All services with tag "form.type_guesser" are inserted here by FormPass -->
<argument type="collection" />
<argument type="service-locator" /><!-- All services with tag "form.type" are stored in a service locator by FormPass -->
<argument type="collection" /><!-- All services with tag "form.type_extension" are stored here by FormPass -->
<argument type="iterator" /><!-- All services with tag "form.type_guesser" are stored here by FormPass -->
</service>
<!-- ValidatorTypeGuesser -->
<service id="form.type_guesser.validator" class="Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser">
<service id="form.type_guesser.validator" class="Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser" public="false">
<tag name="form.type_guesser" />
<argument type="service" id="validator.mapping.class_metadata_factory" />
</service>
@ -61,7 +57,7 @@
<service id="form.choice_list_factory" alias="form.choice_list_factory.cached" public="false"/>
<service id="form.type.form" class="Symfony\Component\Form\Extension\Core\Type\FormType">
<service id="form.type.form" class="Symfony\Component\Form\Extension\Core\Type\FormType" public="false">
<argument type="service" id="form.property_accessor" />
<tag name="form.type" />
</service>
@ -71,7 +67,7 @@
<service id="form.type.checkbox" class="Symfony\Component\Form\Extension\Core\Type\CheckboxType">
<deprecated>The "%service_id%" service is deprecated since Symfony 3.1 and will be removed in 4.0.</deprecated>
</service>
<service id="form.type.choice" class="Symfony\Component\Form\Extension\Core\Type\ChoiceType">
<service id="form.type.choice" class="Symfony\Component\Form\Extension\Core\Type\ChoiceType" public="false">
<tag name="form.type" />
<argument type="service" id="form.choice_list_factory"/>
</service>
@ -158,7 +154,7 @@
</service>
<!-- FormTypeHttpFoundationExtension -->
<service id="form.type_extension.form.http_foundation" class="Symfony\Component\Form\Extension\HttpFoundation\Type\FormTypeHttpFoundationExtension">
<service id="form.type_extension.form.http_foundation" class="Symfony\Component\Form\Extension\HttpFoundation\Type\FormTypeHttpFoundationExtension" public="false">
<argument type="service" id="form.type_extension.form.request_handler" />
<tag name="form.type_extension" extended-type="Symfony\Component\Form\Extension\Core\Type\FormType" />
</service>
@ -173,20 +169,34 @@
</service>
<!-- FormTypeValidatorExtension -->
<service id="form.type_extension.form.validator" class="Symfony\Component\Form\Extension\Validator\Type\FormTypeValidatorExtension">
<service id="form.type_extension.form.validator" class="Symfony\Component\Form\Extension\Validator\Type\FormTypeValidatorExtension" public="false">
<tag name="form.type_extension" extended-type="Symfony\Component\Form\Extension\Core\Type\FormType" />
<argument type="service" id="validator" />
</service>
<service id="form.type_extension.repeated.validator" class="Symfony\Component\Form\Extension\Validator\Type\RepeatedTypeValidatorExtension">
<service id="form.type_extension.repeated.validator" class="Symfony\Component\Form\Extension\Validator\Type\RepeatedTypeValidatorExtension" public="false">
<tag name="form.type_extension" extended-type="Symfony\Component\Form\Extension\Core\Type\RepeatedType" />
</service>
<service id="form.type_extension.submit.validator" class="Symfony\Component\Form\Extension\Validator\Type\SubmitTypeValidatorExtension">
<service id="form.type_extension.submit.validator" class="Symfony\Component\Form\Extension\Validator\Type\SubmitTypeValidatorExtension" public="false">
<tag name="form.type_extension" extended-type="Symfony\Component\Form\Extension\Core\Type\SubmitType" />
</service>
<service id="form.type_extension.upload.validator" class="Symfony\Component\Form\Extension\Validator\Type\UploadValidatorExtension">
<service id="form.type_extension.upload.validator" class="Symfony\Component\Form\Extension\Validator\Type\UploadValidatorExtension" public="false">
<tag name="form.type_extension" extended-type="Symfony\Component\Form\Extension\Core\Type\FormType" />
<argument type="service" id="translator"/>
<argument type="string">%validator.translation_domain%</argument>
</service>
<service id="deprecated.form.registry" class="stdClass">
<property name="registry" type="collection">
<property type="service" id="form.type_guesser.validator" />
<property type="service" id="form.type.choice" />
<property type="service" id="form.type.form" />
<property type="service" id="form.type_extension.form.http_foundation" />
<property type="service" id="form.type_extension.form.validator" />
<property type="service" id="form.type_extension.repeated.validator" />
<property type="service" id="form.type_extension.submit.validator" />
<property type="service" id="form.type_extension.upload.validator" />
</property>
<deprecated>The service "%service_id%" is internal and deprecated since Symfony 3.3 and will be removed in Symfony 4.0</deprecated>
</service>
</services>
</container>

View File

@ -5,7 +5,7 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="form.type_extension.csrf" class="Symfony\Component\Form\Extension\Csrf\Type\FormTypeCsrfExtension">
<service id="form.type_extension.csrf" class="Symfony\Component\Form\Extension\Csrf\Type\FormTypeCsrfExtension" public="false">
<tag name="form.type_extension" extended-type="Symfony\Component\Form\Extension\Core\Type\FormType" />
<argument type="service" id="security.csrf.token_manager" />
<argument>%form.type_extension.csrf.enabled%</argument>
@ -14,5 +14,13 @@
<argument>%validator.translation_domain%</argument>
<argument type="service" id="form.server_params" />
</service>
<service id="deprecated.form.registry.csrf" class="stdClass">
<property name="registry" type="collection">
<property type="service" id="form.type_extension.csrf" />
</property>
<deprecated>The service "%service_id%" is internal and deprecated since Symfony 3.3 and will be removed in Symfony 4.0</deprecated>
</service>
</services>
</container>

View File

@ -13,7 +13,7 @@
</service>
<!-- DataCollectorTypeExtension -->
<service id="form.type_extension.form.data_collector" class="Symfony\Component\Form\Extension\DataCollector\Type\DataCollectorTypeExtension">
<service id="form.type_extension.form.data_collector" class="Symfony\Component\Form\Extension\DataCollector\Type\DataCollectorTypeExtension" public="false">
<tag name="form.type_extension" extended-type="Symfony\Component\Form\Extension\Core\Type\FormType" />
<argument type="service" id="data_collector.form" />
</service>

View File

@ -13,6 +13,8 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\FormPass;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
@ -27,8 +29,7 @@ class FormPassTest extends TestCase
{
public function testDoNothingIfFormExtensionNotLoaded()
{
$container = new ContainerBuilder();
$container->addCompilerPass(new FormPass());
$container = $this->createContainerBuilder();
$container->compile();
@ -37,18 +38,9 @@ class FormPassTest extends TestCase
public function testAddTaggedTypes()
{
$container = new ContainerBuilder();
$container->addCompilerPass(new FormPass());
$container = $this->createContainerBuilder();
$extDefinition = new Definition('Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension');
$extDefinition->setArguments(array(
new Reference('service_container'),
array(),
array(),
array(),
));
$container->setDefinition('form.extension', $extDefinition);
$container->setDefinition('form.extension', $this->createExtensionDefinition());
$container->register('my.type1', __CLASS__.'_Type1')->addTag('form.type');
$container->register('my.type2', __CLASS__.'_Type2')->addTag('form.type');
@ -56,10 +48,13 @@ class FormPassTest extends TestCase
$extDefinition = $container->getDefinition('form.extension');
$this->assertEquals(array(
__CLASS__.'_Type1' => 'my.type1',
__CLASS__.'_Type2' => 'my.type2',
), $extDefinition->getArgument(1));
$this->assertEquals(
new ServiceLocatorArgument(array(
__CLASS__.'_Type1' => new Reference('my.type1'),
__CLASS__.'_Type2' => new Reference('my.type2'),
)),
$extDefinition->getArgument(0)
);
}
/**
@ -67,17 +62,9 @@ class FormPassTest extends TestCase
*/
public function testAddTaggedTypeExtensions(array $extensions, array $expectedRegisteredExtensions)
{
$container = new ContainerBuilder();
$container->addCompilerPass(new FormPass());
$container = $this->createContainerBuilder();
$extDefinition = new Definition('Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension', array(
new Reference('service_container'),
array(),
array(),
array(),
));
$container->setDefinition('form.extension', $extDefinition);
$container->setDefinition('form.extension', $this->createExtensionDefinition());
foreach ($extensions as $serviceId => $tag) {
$container->register($serviceId, 'stdClass')->addTag('form.type_extension', $tag);
@ -86,7 +73,7 @@ class FormPassTest extends TestCase
$container->compile();
$extDefinition = $container->getDefinition('form.extension');
$this->assertSame($expectedRegisteredExtensions, $extDefinition->getArgument(2));
$this->assertEquals($expectedRegisteredExtensions, $extDefinition->getArgument(1));
}
/**
@ -102,8 +89,11 @@ class FormPassTest extends TestCase
'my.type_extension3' => array('extended_type' => 'type2'),
),
array(
'type1' => array('my.type_extension1', 'my.type_extension2'),
'type2' => array('my.type_extension3'),
'type1' => new IteratorArgument(array(
new Reference('my.type_extension1'),
new Reference('my.type_extension2'),
)),
'type2' => new IteratorArgument(array(new Reference('my.type_extension3'))),
),
),
array(
@ -116,8 +106,16 @@ class FormPassTest extends TestCase
'my.type_extension6' => array('extended_type' => 'type2', 'priority' => 1),
),
array(
'type1' => array('my.type_extension2', 'my.type_extension1', 'my.type_extension3'),
'type2' => array('my.type_extension4', 'my.type_extension5', 'my.type_extension6'),
'type1' => new IteratorArgument(array(
new Reference('my.type_extension2'),
new Reference('my.type_extension1'),
new Reference('my.type_extension3'),
)),
'type2' => new IteratorArgument(array(
new Reference('my.type_extension4'),
new Reference('my.type_extension5'),
new Reference('my.type_extension6'),
)),
),
),
);
@ -129,17 +127,9 @@ class FormPassTest extends TestCase
*/
public function testAddTaggedFormTypeExtensionWithoutExtendedTypeAttribute()
{
$container = new ContainerBuilder();
$container->addCompilerPass(new FormPass());
$container = $this->createContainerBuilder();
$extDefinition = new Definition('Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension', array(
new Reference('service_container'),
array(),
array(),
array(),
));
$container->setDefinition('form.extension', $extDefinition);
$container->setDefinition('form.extension', $this->createExtensionDefinition());
$container->register('my.type_extension', 'stdClass')
->addTag('form.type_extension');
@ -148,23 +138,14 @@ class FormPassTest extends TestCase
public function testAddTaggedGuessers()
{
$container = new ContainerBuilder();
$container->addCompilerPass(new FormPass());
$extDefinition = new Definition('Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension');
$extDefinition->setArguments(array(
new Reference('service_container'),
array(),
array(),
array(),
));
$container = $this->createContainerBuilder();
$definition1 = new Definition('stdClass');
$definition1->addTag('form.type_guesser');
$definition2 = new Definition('stdClass');
$definition2->addTag('form.type_guesser');
$container->setDefinition('form.extension', $extDefinition);
$container->setDefinition('form.extension', $this->createExtensionDefinition());
$container->setDefinition('my.guesser1', $definition1);
$container->setDefinition('my.guesser2', $definition2);
@ -172,37 +153,24 @@ class FormPassTest extends TestCase
$extDefinition = $container->getDefinition('form.extension');
$this->assertSame(array(
'my.guesser1',
'my.guesser2',
), $extDefinition->getArgument(3));
$this->assertEquals(
new IteratorArgument(array(
new Reference('my.guesser1'),
new Reference('my.guesser2'),
)),
$extDefinition->getArgument(2)
);
}
/**
* @dataProvider privateTaggedServicesProvider
*/
public function testPrivateTaggedServices($id, $tagName, $expectedExceptionMessage)
public function testPrivateTaggedServices($id, $tagName, array $tagAttributes = array())
{
$container = new ContainerBuilder();
$container->addCompilerPass(new FormPass());
$container = $this->createContainerBuilder();
$extDefinition = new Definition('Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension');
$extDefinition->setArguments(array(
new Reference('service_container'),
array(),
array(),
array(),
));
$container->setDefinition('form.extension', $extDefinition);
$container->register($id, 'stdClass')->setPublic(false)->addTag($tagName);
if (method_exists($this, 'expectException')) {
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage($expectedExceptionMessage);
} else {
$this->setExpectedException('InvalidArgumentException', $expectedExceptionMessage);
}
$container->setDefinition('form.extension', $this->createExtensionDefinition());
$container->register($id, 'stdClass')->setPublic(false)->addTag($tagName, $tagAttributes);
$container->compile();
}
@ -210,11 +178,31 @@ class FormPassTest extends TestCase
public function privateTaggedServicesProvider()
{
return array(
array('my.type', 'form.type', 'The service "my.type" must be public as form types are lazy-loaded'),
array('my.type_extension', 'form.type_extension', 'The service "my.type_extension" must be public as form type extensions are lazy-loaded'),
array('my.guesser', 'form.type_guesser', 'The service "my.guesser" must be public as form type guessers are lazy-loaded'),
array('my.type', 'form.type'),
array('my.type_extension', 'form.type_extension', array('extended_type' => 'Symfony\Component\Form\Extension\Core\Type\FormType')),
array('my.guesser', 'form.type_guesser'),
);
}
private function createContainerBuilder()
{
$container = new ContainerBuilder();
$container->addCompilerPass(new FormPass());
return $container;
}
private function createExtensionDefinition()
{
$definition = new Definition('Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension');
$definition->setArguments(array(
new ServiceLocatorArgument(array()),
array(),
new IteratorArgument(array()),
));
return $definition;
}
}
class FormPassTest_Type1 extends AbstractType

View File

@ -11,14 +11,18 @@
namespace Symfony\Component\Form\DependencyInjection;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
/**
* Adds all services with the tags "form.type" and "form.type_guesser" as
* arguments of the "form.extension" service.
* Adds all services with the tags "form.type", "form.type_extension" and
* "form.type_guesser" as arguments of the "form.extension" service.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
@ -46,29 +50,37 @@ class FormPass implements CompilerPassInterface
}
$definition = $container->getDefinition($this->formExtensionService);
$definition->replaceArgument(0, $this->processFormTypes($container, $definition));
$definition->replaceArgument(1, $this->processFormTypeExtensions($container));
$definition->replaceArgument(2, $this->processFormTypeGuessers($container));
}
// Builds an array with fully-qualified type class names as keys and service IDs as values
$types = array();
foreach ($container->findTaggedServiceIds($this->formTypeTag) as $serviceId => $tag) {
$serviceDefinition = $container->getDefinition($serviceId);
if (!$serviceDefinition->isPublic()) {
throw new InvalidArgumentException(sprintf('The service "%s" must be public as form types are lazy-loaded.', $serviceId));
}
// Support type access by FQCN
$types[$serviceDefinition->getClass()] = $serviceId;
private function processFormTypes(ContainerBuilder $container, Definition $definition)
{
// Get service locator argument
$servicesMap = array();
$locator = $definition->getArgument(0);
if ($locator instanceof ServiceLocatorArgument) {
$servicesMap = $locator->getValues();
}
$definition->replaceArgument(1, $types);
// Builds an array with fully-qualified type class names as keys and service IDs as values
foreach ($container->findTaggedServiceIds($this->formTypeTag) as $serviceId => $tag) {
$serviceDefinition = $container->getDefinition($serviceId);
// Add form type service to the service locator
$servicesMap[$serviceDefinition->getClass()] = new Reference($serviceId);
}
return new ServiceLocatorArgument($servicesMap);
}
private function processFormTypeExtensions(ContainerBuilder $container)
{
$typeExtensions = array();
foreach ($this->findAndSortTaggedServices($this->formTypeExtensionTag, $container) as $reference) {
$serviceId = (string) $reference;
$serviceDefinition = $container->getDefinition($serviceId);
if (!$serviceDefinition->isPublic()) {
throw new InvalidArgumentException(sprintf('The service "%s" must be public as form type extensions are lazy-loaded.', $serviceId));
}
$tag = $serviceDefinition->getTag($this->formTypeExtensionTag);
if (isset($tag[0]['extended_type'])) {
@ -77,19 +89,23 @@ class FormPass implements CompilerPassInterface
throw new InvalidArgumentException(sprintf('"%s" tagged services must have the extended type configured using the extended_type/extended-type attribute, none was configured for the "%s" service.', $this->formTypeExtensionTag, $serviceId));
}
$typeExtensions[$extendedType][] = $serviceId;
$typeExtensions[$extendedType][] = new Reference($serviceId);
}
$definition->replaceArgument(2, $typeExtensions);
$guessers = array_keys($container->findTaggedServiceIds($this->formTypeGuesserTag));
foreach ($guessers as $serviceId) {
$serviceDefinition = $container->getDefinition($serviceId);
if (!$serviceDefinition->isPublic()) {
throw new InvalidArgumentException(sprintf('The service "%s" must be public as form type guessers are lazy-loaded.', $serviceId));
}
foreach ($typeExtensions as $extendedType => $extensions) {
$typeExtensions[$extendedType] = new IteratorArgument($extensions);
}
$definition->replaceArgument(3, $guessers);
return $typeExtensions;
}
private function processFormTypeGuessers(ContainerBuilder $container)
{
$guessers = array();
foreach ($container->findTaggedServiceIds($this->formTypeGuesserTag) as $serviceId => $tags) {
$guessers[] = new Reference($serviceId);
}
return new IteratorArgument($guessers);
}
}

View File

@ -11,49 +11,80 @@
namespace Symfony\Component\Form\Extension\DependencyInjection;
use Psr\Container\ContainerInterface;
use Symfony\Component\Form\FormExtensionInterface;
use Symfony\Component\Form\FormTypeGuesserChain;
use Symfony\Component\Form\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\ContainerInterface;
class DependencyInjectionExtension implements FormExtensionInterface
{
private $container;
private $typeServiceIds;
private $typeExtensionServiceIds;
private $guesserServiceIds;
private $guesser;
private $guesserLoaded = false;
private $typeContainer;
private $typeExtensionServices;
private $guesserServices;
public function __construct(ContainerInterface $container, array $typeServiceIds, array $typeExtensionServiceIds, array $guesserServiceIds)
// @deprecated to be removed in Symfony 4.0
private $typeServiceIds;
private $guesserServiceIds;
/**
* Constructor.
*
* @param ContainerInterface $typeContainer
* @param iterable[] $typeExtensionServices
* @param iterable $guesserServices
*/
public function __construct(ContainerInterface $typeContainer, array $typeExtensionServices, $guesserServices, array $guesserServiceIds = null)
{
$this->container = $container;
$this->typeServiceIds = $typeServiceIds;
$this->typeExtensionServiceIds = $typeExtensionServiceIds;
$this->guesserServiceIds = $guesserServiceIds;
if (null !== $guesserServiceIds) {
@trigger_error(sprintf('Passing four arguments to the %s::__construct() method is deprecated since Symfony 3.3 and will be disallowed in Symfony 4.0. The new constructor only accepts three arguments.', __CLASS__), E_USER_DEPRECATED);
$this->guesserServiceIds = $guesserServiceIds;
$this->typeServiceIds = $typeExtensionServices;
}
$this->typeContainer = $typeContainer;
$this->typeExtensionServices = $typeExtensionServices;
$this->guesserServices = $guesserServices;
}
public function getType($name)
{
if (!isset($this->typeServiceIds[$name])) {
throw new InvalidArgumentException(sprintf('The field type "%s" is not registered with the service container.', $name));
if (null !== $this->guesserServiceIds) {
if (!isset($this->typeServiceIds[$name])) {
throw new InvalidArgumentException(sprintf('The field type "%s" is not registered in the service container.', $name));
}
return $this->typeContainer->get($this->typeServiceIds[$name]);
}
return $this->container->get($this->typeServiceIds[$name]);
if (!$this->typeContainer->has($name)) {
throw new InvalidArgumentException(sprintf('The field type "%s" is not registered in the service container.', $name));
}
return $this->typeContainer->get($name);
}
public function hasType($name)
{
return isset($this->typeServiceIds[$name]);
if (null !== $this->guesserServiceIds) {
return isset($this->typeServiceIds[$name]);
}
return $this->typeContainer->has($name);
}
public function getTypeExtensions($name)
{
$extensions = array();
if (isset($this->typeExtensionServiceIds[$name])) {
foreach ($this->typeExtensionServiceIds[$name] as $serviceId) {
$extensions[] = $extension = $this->container->get($serviceId);
if (isset($this->typeExtensionServices[$name])) {
foreach ($this->typeExtensionServices[$name] as $serviceId => $extension) {
if (null !== $this->guesserServiceIds) {
$extension = $this->typeContainer->get($serviceId = $extension);
}
$extensions[] = $extension;
// validate result of getExtendedType() to ensure it is consistent with the service definition
if ($extension->getExtendedType() !== $name) {
@ -73,7 +104,7 @@ class DependencyInjectionExtension implements FormExtensionInterface
public function hasTypeExtensions($name)
{
return isset($this->typeExtensionServiceIds[$name]);
return isset($this->typeExtensionServices[$name]);
}
public function getTypeGuesser()
@ -82,11 +113,15 @@ class DependencyInjectionExtension implements FormExtensionInterface
$this->guesserLoaded = true;
$guessers = array();
foreach ($this->guesserServiceIds as $serviceId) {
$guessers[] = $this->container->get($serviceId);
foreach ($this->guesserServices as $serviceId => $service) {
if (null !== $this->guesserServiceIds) {
$service = $this->typeContainer->get($serviceId = $service);
}
$guessers[] = $service;
}
if (count($guessers) > 0) {
if ($guessers) {
$this->guesser = new FormTypeGuesserChain($guessers);
}
}

View File

@ -12,6 +12,8 @@
namespace Symfony\Component\Form\Tests\DependencyInjection;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\Form\DependencyInjection\FormPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
@ -25,8 +27,7 @@ class FormPassTest extends TestCase
{
public function testDoNothingIfFormExtensionNotLoaded()
{
$container = new ContainerBuilder();
$container->addCompilerPass(new FormPass());
$container = $this->createContainerBuilder();
$container->compile();
@ -35,18 +36,9 @@ class FormPassTest extends TestCase
public function testAddTaggedTypes()
{
$container = new ContainerBuilder();
$container->addCompilerPass(new FormPass());
$container = $this->createContainerBuilder();
$extDefinition = new Definition('Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension');
$extDefinition->setArguments(array(
new Reference('service_container'),
array(),
array(),
array(),
));
$container->setDefinition('form.extension', $extDefinition);
$container->setDefinition('form.extension', $this->createExtensionDefinition());
$container->register('my.type1', __CLASS__.'_Type1')->addTag('form.type');
$container->register('my.type2', __CLASS__.'_Type2')->addTag('form.type');
@ -54,10 +46,13 @@ class FormPassTest extends TestCase
$extDefinition = $container->getDefinition('form.extension');
$this->assertEquals(array(
__CLASS__.'_Type1' => 'my.type1',
__CLASS__.'_Type2' => 'my.type2',
), $extDefinition->getArgument(1));
$this->assertEquals(
new ServiceLocatorArgument(array(
__CLASS__.'_Type1' => new Reference('my.type1'),
__CLASS__.'_Type2' => new Reference('my.type2'),
)),
$extDefinition->getArgument(0)
);
}
/**
@ -65,17 +60,9 @@ class FormPassTest extends TestCase
*/
public function testAddTaggedTypeExtensions(array $extensions, array $expectedRegisteredExtensions)
{
$container = new ContainerBuilder();
$container->addCompilerPass(new FormPass());
$container = $this->createContainerBuilder();
$extDefinition = new Definition('Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension', array(
new Reference('service_container'),
array(),
array(),
array(),
));
$container->setDefinition('form.extension', $extDefinition);
$container->setDefinition('form.extension', $this->createExtensionDefinition());
foreach ($extensions as $serviceId => $tag) {
$container->register($serviceId, 'stdClass')->addTag('form.type_extension', $tag);
@ -84,7 +71,7 @@ class FormPassTest extends TestCase
$container->compile();
$extDefinition = $container->getDefinition('form.extension');
$this->assertSame($expectedRegisteredExtensions, $extDefinition->getArgument(2));
$this->assertEquals($expectedRegisteredExtensions, $extDefinition->getArgument(1));
}
/**
@ -100,8 +87,11 @@ class FormPassTest extends TestCase
'my.type_extension3' => array('extended_type' => 'type2'),
),
array(
'type1' => array('my.type_extension1', 'my.type_extension2'),
'type2' => array('my.type_extension3'),
'type1' => new IteratorArgument(array(
new Reference('my.type_extension1'),
new Reference('my.type_extension2'),
)),
'type2' => new IteratorArgument(array(new Reference('my.type_extension3'))),
),
),
array(
@ -114,8 +104,16 @@ class FormPassTest extends TestCase
'my.type_extension6' => array('extended_type' => 'type2', 'priority' => 1),
),
array(
'type1' => array('my.type_extension2', 'my.type_extension1', 'my.type_extension3'),
'type2' => array('my.type_extension4', 'my.type_extension5', 'my.type_extension6'),
'type1' => new IteratorArgument(array(
new Reference('my.type_extension2'),
new Reference('my.type_extension1'),
new Reference('my.type_extension3'),
)),
'type2' => new IteratorArgument(array(
new Reference('my.type_extension4'),
new Reference('my.type_extension5'),
new Reference('my.type_extension6'),
)),
),
),
);
@ -127,17 +125,9 @@ class FormPassTest extends TestCase
*/
public function testAddTaggedFormTypeExtensionWithoutExtendedTypeAttribute()
{
$container = new ContainerBuilder();
$container->addCompilerPass(new FormPass());
$container = $this->createContainerBuilder();
$extDefinition = new Definition('Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension', array(
new Reference('service_container'),
array(),
array(),
array(),
));
$container->setDefinition('form.extension', $extDefinition);
$container->setDefinition('form.extension', $this->createExtensionDefinition());
$container->register('my.type_extension', 'stdClass')
->addTag('form.type_extension');
@ -146,23 +136,14 @@ class FormPassTest extends TestCase
public function testAddTaggedGuessers()
{
$container = new ContainerBuilder();
$container->addCompilerPass(new FormPass());
$extDefinition = new Definition('Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension');
$extDefinition->setArguments(array(
new Reference('service_container'),
array(),
array(),
array(),
));
$container = $this->createContainerBuilder();
$definition1 = new Definition('stdClass');
$definition1->addTag('form.type_guesser');
$definition2 = new Definition('stdClass');
$definition2->addTag('form.type_guesser');
$container->setDefinition('form.extension', $extDefinition);
$container->setDefinition('form.extension', $this->createExtensionDefinition());
$container->setDefinition('my.guesser1', $definition1);
$container->setDefinition('my.guesser2', $definition2);
@ -170,49 +151,55 @@ class FormPassTest extends TestCase
$extDefinition = $container->getDefinition('form.extension');
$this->assertSame(array(
'my.guesser1',
'my.guesser2',
), $extDefinition->getArgument(3));
$this->assertEquals(
new IteratorArgument(array(
new Reference('my.guesser1'),
new Reference('my.guesser2'),
)),
$extDefinition->getArgument(2)
);
}
/**
* @dataProvider privateTaggedServicesProvider
*/
public function testPrivateTaggedServices($id, $tagName, $expectedExceptionMessage)
public function testPrivateTaggedServices($id, $tagName, array $tagAttributes = array())
{
$container = new ContainerBuilder();
$container->addCompilerPass(new FormPass());
$extDefinition = new Definition('Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension');
$extDefinition->setArguments(array(
new Reference('service_container'),
array(),
array(),
array(),
));
$container->setDefinition('form.extension', $extDefinition);
$container->register($id, 'stdClass')->setPublic(false)->addTag($tagName);
if (method_exists($this, 'expectException')) {
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage($expectedExceptionMessage);
} else {
$this->setExpectedException('InvalidArgumentException', $expectedExceptionMessage);
}
$container = $this->createContainerBuilder();
$container->setDefinition('form.extension', $this->createExtensionDefinition());
$container->register($id, 'stdClass')->setPublic(false)->addTag($tagName, $tagAttributes);
$container->compile();
}
public function privateTaggedServicesProvider()
{
return array(
array('my.type', 'form.type', 'The service "my.type" must be public as form types are lazy-loaded'),
array('my.type_extension', 'form.type_extension', 'The service "my.type_extension" must be public as form type extensions are lazy-loaded'),
array('my.guesser', 'form.type_guesser', 'The service "my.guesser" must be public as form type guessers are lazy-loaded'),
array('my.type', 'form.type'),
array('my.type_extension', 'form.type_extension', array('extended_type' => 'Symfony\Component\Form\Extension\Core\Type\FormType')),
array('my.guesser', 'form.type_guesser'),
);
}
private function createExtensionDefinition()
{
$definition = new Definition('Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension');
$definition->setArguments(array(
new ServiceLocatorArgument(array()),
array(),
new IteratorArgument(array()),
));
return $definition;
}
private function createContainerBuilder()
{
$container = new ContainerBuilder();
$container->addCompilerPass(new FormPass());
return $container;
}
}
class FormPassTest_Type1 extends AbstractType

View File

@ -19,25 +19,56 @@ class DependencyInjectionExtensionTest extends TestCase
{
public function testGetTypeExtensions()
{
$container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface')->getMock();
$container = $this->createContainerMock();
$container->expects($this->never())->method('get');
$typeExtension1 = $this->getMockBuilder('Symfony\Component\Form\FormTypeExtensionInterface')->getMock();
$typeExtension1->expects($this->any())
->method('getExtendedType')
->willReturn('test');
$typeExtension2 = $this->getMockBuilder('Symfony\Component\Form\FormTypeExtensionInterface')->getMock();
$typeExtension2->expects($this->any())
->method('getExtendedType')
->willReturn('test');
$typeExtension3 = $this->getMockBuilder('Symfony\Component\Form\FormTypeExtensionInterface')->getMock();
$typeExtension3->expects($this->any())
->method('getExtendedType')
->willReturn('other');
$typeExtension1 = $this->createFormTypeExtensionMock('test');
$typeExtension2 = $this->createFormTypeExtensionMock('test');
$typeExtension3 = $this->createFormTypeExtensionMock('other');
$extensions = array(
'test' => new \ArrayIterator(array($typeExtension1, $typeExtension2)),
'other' => new \ArrayIterator(array($typeExtension3)),
);
$extension = new DependencyInjectionExtension($container, $extensions, array());
$this->assertTrue($extension->hasTypeExtensions('test'));
$this->assertTrue($extension->hasTypeExtensions('other'));
$this->assertFalse($extension->hasTypeExtensions('unknown'));
$this->assertSame(array($typeExtension1, $typeExtension2), $extension->getTypeExtensions('test'));
$this->assertSame(array($typeExtension3), $extension->getTypeExtensions('other'));
}
/**
* @expectedException \Symfony\Component\Form\Exception\InvalidArgumentException
*/
public function testThrowExceptionForInvalidExtendedType()
{
$container = $this->getMockBuilder('Psr\Container\ContainerInterface')->getMock();
$container->expects($this->never())->method('get');
$extensions = array(
'test' => new \ArrayIterator(array($this->createFormTypeExtensionMock('unmatched'))),
);
$extension = new DependencyInjectionExtension($container, $extensions, array());
$extension->getTypeExtensions('test');
}
/**
* @group legacy
* @expectedDeprecation Passing four arguments to the Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension::__construct() method is deprecated since Symfony 3.3 and will be disallowed in Symfony 4.0. The new constructor only accepts three arguments.
*/
public function testLegacyGetTypeExtensions()
{
$container = $this->createContainerMock();
$services = array(
'extension1' => $typeExtension1,
'extension2' => $typeExtension2,
'extension3' => $typeExtension3,
'extension1' => $typeExtension1 = $this->createFormTypeExtensionMock('test'),
'extension2' => $typeExtension2 = $this->createFormTypeExtensionMock('test'),
'extension3' => $typeExtension3 = $this->createFormTypeExtensionMock('other'),
);
$container->expects($this->any())
@ -50,7 +81,7 @@ class DependencyInjectionExtensionTest extends TestCase
throw new ServiceNotFoundException($id);
});
$extension = new DependencyInjectionExtension($container, array(), array('test' => array('extension1', 'extension2'), 'other' => array('extension3')), array());
$extension = new DependencyInjectionExtension($container, array('test' => array('extension1', 'extension2'), 'other' => array('extension3')), array(), array());
$this->assertTrue($extension->hasTypeExtensions('test'));
$this->assertFalse($extension->hasTypeExtensions('unknown'));
@ -58,24 +89,36 @@ class DependencyInjectionExtensionTest extends TestCase
}
/**
* @group legacy
* @expectedException \Symfony\Component\Form\Exception\InvalidArgumentException
* @expectedDeprecation Passing four arguments to the Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension::__construct() method is deprecated since Symfony 3.3 and will be disallowed in Symfony 4.0. The new constructor only accepts three arguments.
*/
public function testThrowExceptionForInvalidExtendedType()
public function testLegacyThrowExceptionForInvalidExtendedType()
{
$container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface')->getMock();
$typeExtension = $this->getMockBuilder('Symfony\Component\Form\FormTypeExtensionInterface')->getMock();
$typeExtension->expects($this->any())
->method('getExtendedType')
->willReturn('unmatched');
$container = $this->createContainerMock();
$container->expects($this->any())
->method('get')
->with('extension')
->willReturn($typeExtension);
->willReturn($this->createFormTypeExtensionMock('unmatched'));
$extension = new DependencyInjectionExtension($container, array(), array('test' => array('extension')), array());
$extension = new DependencyInjectionExtension($container, array('test' => array('extension')), array(), array());
$extension->getTypeExtensions('test');
}
private function createContainerMock()
{
return $this->getMockBuilder('Psr\Container\ContainerInterface')
->setMethods(array('get', 'has'))
->getMock();
}
private function createFormTypeExtensionMock($extendedType)
{
$extension = $this->getMockBuilder('Symfony\Component\Form\FormTypeExtensionInterface')->getMock();
$extension->expects($this->any())->method('getExtendedType')->willReturn($extendedType);
return $extension;
}
}

View File

@ -26,7 +26,7 @@
"require-dev": {
"doctrine/collections": "~1.0",
"symfony/validator": "^2.8.18|^3.2.5",
"symfony/dependency-injection": "~3.2",
"symfony/dependency-injection": "~3.3",
"symfony/config": "~2.7|~3.0",
"symfony/http-foundation": "~2.8|~3.0",
"symfony/http-kernel": "~2.8|~3.0",
@ -36,7 +36,7 @@
},
"conflict": {
"phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0",
"symfony/dependency-injection": "<3.2",
"symfony/dependency-injection": "<3.3",
"symfony/doctrine-bridge": "<2.7",
"symfony/framework-bundle": "<2.7",
"symfony/twig-bridge": "<2.7",