Skip to content

[Form] simplify the form type extension registration #24530

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion UPGRADE-4.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,38 @@ Finder
Form
----

* The `getExtendedType()` method of the `FormTypeExtensionInterface` is deprecated and will be removed in 5.0. Type
extensions must implement the static `getExtendedTypes()` method instead and return an iterable of extended types.

Before:

```php
class FooTypeExtension extends AbstractTypeExtension
{
public function getExtendedType()
{
return FormType::class;
}

// ...
}
```

After:

```php
class FooTypeExtension extends AbstractTypeExtension
{
public static function getExtendedTypes(): iterable
{
return array(FormType::class);
}

// ...
}
```
* The `scale` option of the `IntegerType` is deprecated.
* The `$scale` argument of the `IntegerToLocalizedStringTransformer` is deprecated.

* Deprecated calling `FormRenderer::searchAndRenderBlock` for fields which were already rendered.
Instead of expecting such calls to return empty strings, check if the field has already been rendered.

Expand Down
30 changes: 30 additions & 0 deletions UPGRADE-5.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,36 @@ Finder
Form
----

* The `getExtendedType()` method was removed from the `FormTypeExtensionInterface`. It is replaced by the the static
`getExtendedTypes()` method which must return an iterable of extended types.

Before:

```php
class FooTypeExtension extends AbstractTypeExtension
{
public function getExtendedType()
{
return FormType::class;
}

// ...
}
```

After:

```php
class FooTypeExtension extends AbstractTypeExtension
{
public static function getExtendedTypes(): iterable
{
return array(FormType::class);
}

// ...
}
```
* The `scale` option was removed from the `IntegerType`.
* The `$scale` argument of the `IntegerToLocalizedStringTransformer` was removed.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Form\FormTypeExtensionInterface;
use Symfony\Component\Form\FormTypeGuesserInterface;
use Symfony\Component\Form\FormTypeInterface;
use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface;
Expand Down Expand Up @@ -325,6 +326,8 @@ public function load(array $configs, ContainerBuilder $container)
->addTag('form.type');
$container->registerForAutoconfiguration(FormTypeGuesserInterface::class)
->addTag('form.type_guesser');
$container->registerForAutoconfiguration(FormTypeExtensionInterface::class)
->addTag('form.type_extension');
$container->registerForAutoconfiguration(CacheClearerInterface::class)
->addTag('kernel.cache_clearer');
$container->registerForAutoconfiguration(CacheWarmerInterface::class)
Expand Down
6 changes: 3 additions & 3 deletions src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
<!-- FormTypeHttpFoundationExtension -->
<service id="form.type_extension.form.http_foundation" class="Symfony\Component\Form\Extension\HttpFoundation\Type\FormTypeHttpFoundationExtension">
<argument type="service" id="form.type_extension.form.request_handler" />
<tag name="form.type_extension" extended-type="Symfony\Component\Form\Extension\Core\Type\FormType" />
<tag name="form.type_extension" />
</service>

<!-- HttpFoundationRequestHandler -->
Expand All @@ -92,13 +92,13 @@
<argument type="service" id="validator" />
</service>
<service id="form.type_extension.repeated.validator" class="Symfony\Component\Form\Extension\Validator\Type\RepeatedTypeValidatorExtension">
<tag name="form.type_extension" extended-type="Symfony\Component\Form\Extension\Core\Type\RepeatedType" />
<tag name="form.type_extension" />
</service>
<service id="form.type_extension.submit.validator" class="Symfony\Component\Form\Extension\Validator\Type\SubmitTypeValidatorExtension">
<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">
<tag name="form.type_extension" extended-type="Symfony\Component\Form\Extension\Core\Type\FormType" />
<tag name="form.type_extension" />
<argument type="service" id="translator"/>
<argument type="string">%validator.translation_domain%</argument>
</service>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<defaults public="false" />

<service id="form.type_extension.csrf" class="Symfony\Component\Form\Extension\Csrf\Type\FormTypeCsrfExtension">
<tag name="form.type_extension" extended-type="Symfony\Component\Form\Extension\Core\Type\FormType" />
<tag name="form.type_extension" />
<argument type="service" id="security.csrf.token_manager" />
<argument>%form.type_extension.csrf.enabled%</argument>
<argument>%form.type_extension.csrf.field_name%</argument>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

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

Expand Down
4 changes: 2 additions & 2 deletions src/Symfony/Bundle/FrameworkBundle/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"symfony/dom-crawler": "~3.4|~4.0",
"symfony/polyfill-intl-icu": "~1.0",
"symfony/security": "~3.4|~4.0",
"symfony/form": "^4.1",
"symfony/form": "^4.2",
"symfony/expression-language": "~3.4|~4.0",
"symfony/messenger": "^4.2",
"symfony/process": "~3.4|~4.0",
Expand All @@ -66,7 +66,7 @@
"phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0",
"symfony/asset": "<3.4",
"symfony/console": "<3.4",
"symfony/form": "<4.1",
"symfony/form": "<4.2",
"symfony/messenger": "<4.2",
"symfony/property-info": "<3.4",
"symfony/serializer": "<4.1",
Expand Down
16 changes: 14 additions & 2 deletions src/Symfony/Component/Form/AbstractExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,21 @@ private function initTypeExtensions()
throw new UnexpectedTypeException($extension, 'Symfony\Component\Form\FormTypeExtensionInterface');
}

$type = $extension->getExtendedType();
if (method_exists($extension, 'getExtendedTypes')) {
$extendedTypes = array();

$this->typeExtensions[$type][] = $extension;
foreach ($extension::getExtendedTypes() as $extendedType) {
$extendedTypes[] = $extendedType;
}
} else {
@trigger_error(sprintf('Not implementing the static getExtendedTypes() method in %s when implementing the %s is deprecated since Symfony 4.2. The method will be added to the interface in 5.0.', \get_class($extension), FormTypeExtensionInterface::class), E_USER_DEPRECATED);

$extendedTypes = array($extension->getExtendedType());
}

foreach ($extendedTypes as $extendedType) {
$this->typeExtensions[$extendedType][] = $extension;
}
}
}

Expand Down
19 changes: 19 additions & 0 deletions src/Symfony/Component/Form/AbstractTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\Form;

use Symfony\Component\Form\Exception\LogicException;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
Expand Down Expand Up @@ -45,4 +46,22 @@ public function finishView(FormView $view, FormInterface $form, array $options)
public function configureOptions(OptionsResolver $resolver)
{
}

/**
* {@inheritdoc}
*
* @deprecated since Symfony 4.2, use getExtendedTypes() instead.
*/
public function getExtendedType()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xabbuh following your comment, how do you plan to deprecate this method now?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about a deprecation message + BC layer here:

$type = $extension->getExtendedType();

here:

$this->typeExtensions[$typeExtension->getExtendedType()][] = $typeExtension;

and here:

$this->typeExtensions[$typeExtension->getExtendedType()][] = $typeExtension;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO we no longer need to deprecate the method.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To elaborate a bit more on this: The new interface extends the existing FormTypeExtensionInterface. If we do not deprecate anything, we know that the extension will have the getExtendedType() method. And if we extend the AbstractTypeExtension class, we even do not have to care about this method as it is then provided by the base class in case we implement the new interface.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And if we do not implement the new interface, nothing changes at all as we still need to implement the getExtendedType() method like we had to do before.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My concern about getExtendedType() in 5.0 is that we have two methods almost equals (both return the type to extend) but the new one is much better, it also allows us autoconfigure the type extension, great! So when will it "getExtendedType()" be used in 5.0 userland?

My point, we'll have two choice to return the type to extend for 5.0+, the first one is a bad choice if the second one is better, good condition to deprecate it IMO.

Plus, if we keep getExtendedType() in AbstractTypeExtension forever, IDEs like PhpStorm will not be able to autodetect the missing method to implement anymore, this will be seen in the exception on complication time, worst DX then.

At the end: I'd like (for 5.0 o 6.0) to see the static version in FormTypeExtensionInterface instead of the current getExtendedType(), is it possible in long term? I'm dreaming, probably a nightmare :)

Copy link
Member Author

@xabbuh xabbuh Apr 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getExtendedType() will only there for BC concerns. But I am not convinced it's really necessary to deprecate it if the code will still work fine. That way we would save maintainers some hassle with updating their code.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yceruto getExtendedType() is now deprecated in the interface

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With getExtendedType deprecated, all the calls that are still done to it need to use the new static method instead when it exists. E.g. in AbstractExtension that yceruto mentioned. And it should also trigger a deprecation message or is this done automatically?

{
if (!method_exists($this, 'getExtendedTypes')) {
throw new LogicException(sprintf('You need to implement the static getExtendedTypes() method when implementing the %s in %s.', FormTypeExtensionInterface::class, static::class));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you must throw an exception here, telling to implement the other method or to override this one, because returning null is not allowed.


@trigger_error(sprintf('The %s::getExtendedType() method is deprecated since Symfony 4.2 and will be removed in 5.0. Use getExtendedTypes() instead.', \get_class($this)), E_USER_DEPRECATED);

foreach (static::getExtendedTypes() as $extendedType) {
return $extendedType;
}
}
}
30 changes: 30 additions & 0 deletions src/Symfony/Component/Form/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,36 @@ CHANGELOG
4.2.0
-----

* The `getExtendedType()` method of the `FormTypeExtensionInterface` is deprecated and will be removed in 5.0. Type
extensions must implement the static `getExtendedTypes()` method instead and return an iterable of extended types.

Before:

```php
class FooTypeExtension extends AbstractTypeExtension
{
public function getExtendedType()
{
return FormType::class;
}

// ...
}
```

After:

```php
class FooTypeExtension extends AbstractTypeExtension
{
public static function getExtendedTypes(): iterable
{
return array(FormType::class);
}

// ...
}
```
* deprecated the `$scale` argument of the `IntegerToLocalizedStringTransformer`
* added `Symfony\Component\Form\ClearableErrorsInterface`
* deprecated calling `FormRenderer::searchAndRenderBlock` for fields which were already rendered
Expand Down
25 changes: 20 additions & 5 deletions src/Symfony/Component/Form/DependencyInjection/FormPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Form\FormTypeExtensionInterface;

/**
* Adds all services with the tags "form.type", "form.type_extension" and
Expand Down Expand Up @@ -92,13 +93,27 @@ private function processFormTypeExtensions(ContainerBuilder $container)

$tag = $serviceDefinition->getTag($this->formTypeExtensionTag);
if (isset($tag[0]['extended_type'])) {
$extendedType = $tag[0]['extended_type'];
if (!method_exists($serviceDefinition->getClass(), 'getExtendedTypes')) {
@trigger_error(sprintf('Not implementing the static getExtendedTypes() method in %s when implementing the %s is deprecated since Symfony 4.2. The method will be added to the interface in 5.0.', $serviceDefinition->getClass(), FormTypeExtensionInterface::class), E_USER_DEPRECATED);
}

$typeExtensions[$tag[0]['extended_type']][] = new Reference($serviceId);
$typeExtensionsClasses[] = $serviceDefinition->getClass();
} elseif (method_exists($serviceDefinition->getClass(), 'getExtendedTypes')) {
$extendsTypes = false;

foreach ($serviceDefinition->getClass()::getExtendedTypes() as $extendedType) {
$typeExtensions[$extendedType][] = new Reference($serviceId);
$typeExtensionsClasses[] = $serviceDefinition->getClass();
$extendsTypes = true;
}

if (!$extendsTypes) {
throw new InvalidArgumentException(sprintf('The getExtendedTypes() method for service "%s" does not return any extended types.', $serviceId));
}
} else {
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));
throw new InvalidArgumentException(sprintf('"%s" tagged services have to implement the static getExtendedTypes() method. The class for service "%s" does not implement it.', $this->formTypeExtensionTag, $serviceId));
}

$typeExtensions[$extendedType][] = new Reference($serviceId);
$typeExtensionsClasses[] = $serviceDefinition->getClass();
}

foreach ($typeExtensions as $extendedType => $extensions) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\Form\Extension\Csrf\Type;

use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Csrf\EventListener\CsrfValidationListener;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
Expand Down Expand Up @@ -114,8 +115,8 @@ public function configureOptions(OptionsResolver $resolver)
/**
* {@inheritdoc}
*/
public function getExtendedType()
public static function getExtendedTypes(): iterable
{
return 'Symfony\Component\Form\Extension\Core\Type\FormType';
return array(FormType::class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\Form\Extension\DataCollector\Type;

use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\DataCollector\EventListener\DataCollectorListener;
use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface;
use Symfony\Component\Form\FormBuilderInterface;
Expand Down Expand Up @@ -45,8 +46,8 @@ public function buildForm(FormBuilderInterface $builder, array $options)
/**
* {@inheritdoc}
*/
public function getExtendedType()
public static function getExtendedTypes(): iterable
{
return 'Symfony\Component\Form\Extension\Core\Type\FormType';
return array(FormType::class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,19 @@ public function getTypeExtensions($name)
foreach ($this->typeExtensionServices[$name] as $serviceId => $extension) {
$extensions[] = $extension;

// validate result of getExtendedType() to ensure it is consistent with the service definition
if ($extension->getExtendedType() !== $name) {
throw new InvalidArgumentException(sprintf('The extended type specified for the service "%s" does not match the actual extended type. Expected "%s", given "%s".', $serviceId, $name, $extension->getExtendedType()));
if (method_exists($extension, 'getExtendedTypes')) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This validation prevents to use different instances of the same class to be used for different extensions. So it prevents the only advantage of extended_type attribute that I mentioned in #24530 (comment) as people will be forced to implement the method in 5.0 when its part of the interface. So they need to return an empty array and then overwrite it with the service attribute.

IMO we do not need the validation anymore as now both the registration and implementation uses getExtendedTypes. So there should not be a mismatch anymore unless you wire something wrong manually.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

class FooBarTypeExtension extends AbstractTypeExtension
{
    public function __construct(string $type)
    {
    }

    public static function getExtendedTypes(): iterable
    {
        return array(
            FooType::class,
            BarType::class,
        );
    }
}
App\Form\FooBarTypeExtension:
    arguments: ['foo']
    tags:
        - name: form.type_extension
          extended_type: FooType

App\Form\FooBarTypeExtension:
    arguments: ['bar']
    tags:
        - name: form.type_extension
          extended_type: BarType

Do I understand correctly that you have something like this in mind?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. By returning both types from getExtendedTypes, it actually works even with the validation. So I'm fine keeping it like this. But I think it would make more sense to remove the validation as it should not matter what is returned by getExtendedTypes if you set the extended_type attribute (and you might not even know that in advance if you create a dynamic form extension).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we shouldn't rather deprecate the extended_type attribute too instead I could imagine that using it adds more confusion than an implementation where getExtendedTypes() is the single source of truth.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that it potentially is complicating things for no real use-case. But I also like that it allows to configure extensions dynamically via DI (instead of hardcoding it into a class). As I said in #24530 (comment), its similar to the command attribute of the console.command tag. So it would be consistent to keep it. But I would not object to deprecate it as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep it for now. We can still consider to deprecate it later when we noticed that it adds too much confusion.

$extendedTypes = array();

foreach ($extension::getExtendedTypes() as $extendedType) {
$extendedTypes[] = $extendedType;
}
} else {
$extendedTypes = array($extension->getExtendedType());
}

// validate the result of getExtendedTypes()/getExtendedType() to ensure it is consistent with the service definition
if (!\in_array($name, $extendedTypes, true)) {
throw new InvalidArgumentException(sprintf('The extended type specified for the service "%s" does not match the actual extended type. Expected "%s", given "%s".', $serviceId, $name, implode(', ', $extendedTypes)));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\Form\Extension\HttpFoundation\Type;

use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\RequestHandlerInterface;
Expand Down Expand Up @@ -39,8 +40,8 @@ public function buildForm(FormBuilderInterface $builder, array $options)
/**
* {@inheritdoc}
*/
public function getExtendedType()
public static function getExtendedTypes(): iterable
{
return 'Symfony\Component\Form\Extension\Core\Type\FormType';
return array(FormType::class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\Form\Extension\Validator\Type;

use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Validator\EventListener\ValidationListener;
use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapper;
use Symfony\Component\Form\FormBuilderInterface;
Expand Down Expand Up @@ -67,8 +68,8 @@ public function configureOptions(OptionsResolver $resolver)
/**
* {@inheritdoc}
*/
public function getExtendedType()
public static function getExtendedTypes(): iterable
{
return 'Symfony\Component\Form\Extension\Core\Type\FormType';
return array(FormType::class);
}
}
Loading