Skip to content

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

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
Feb 28, 2017

Conversation

hhamon
Copy link
Contributor

@hhamon hhamon commented Feb 20, 2017

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.

@hhamon
Copy link
Contributor Author

hhamon commented Feb 20, 2017

Related to #21553.

@hhamon hhamon force-pushed the feat-private-form-types branch 6 times, most recently from 2ca9847 to 5fc62de Compare February 20, 2017 18:34
* @param iterable[] $typeExtensionServices
* @param iterable[] $guesserServices
*/
public function __construct(ContainerInterface $typeContainer, array $typeExtensionServices, array $guesserServices, array $guesserServiceIds = null)
Copy link
Member

Choose a reason for hiding this comment

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

I would not rename $container to reduce the diff

@@ -30,7 +30,7 @@

<!-- DependencyInjectionExtension -->
<service id="form.extension" class="Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension" public="false">
<argument type="service" id="service_container" />
<argument type="service-locator" />
Copy link
Member

Choose a reason for hiding this comment

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

According to the current deprecation the fourth argument should be removed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep good catch!

}

$definition->replaceArgument(0, new ServiceLocatorArgument($servicesMap));
Copy link
Member

Choose a reason for hiding this comment

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

I'm wondering if mixing types/guessers/extensions in the same locator is a good thing here, as we have still 3 constructor arguments that's a bit confusing to me.
Instead, couldn't guesserServices be an IteratorArgument of guessers? It seems we only iterate over them to build a chain one, no need for having them identified right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes It's on the way! This is what I'm currently doing :)

Copy link
Contributor

Choose a reason for hiding this comment

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

Why not 2 service locators, and one iterator?

So the DependencyInjectionExtension::__construct() method signature would be:

public function __construct(ContainerInterface $typesLocator, ContainerInterface $typeExtensionsLocator, iterator $guessers)

Might not be the easiest for BC though.

Copy link
Contributor

@ogizanagi ogizanagi Feb 20, 2017

Choose a reason for hiding this comment

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

Oh, actually $typeExtensionServiceIds cannot easily be a locator, as it is a map of multiple type extension services indexed by type. Symfony's ServiceLocator does not handle collection of services as values.
But couldn't it be a collection of iterables indexed by type?

Edit: actually in order to keep things simple, I think the current implementation is probably safer.

*
* @param ContainerInterface $typeContainer
* @param iterable[] $typeExtensionServices
* @param iterable[] $guesserServices
Copy link
Member

@chalasr chalasr Feb 20, 2017

Choose a reason for hiding this comment

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

iterable[] make me understand that the value should be an array of iterables.
iterable|FormTypeGuesserInterface[] instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes it can be an array or an iterator.

$service = $this->typeContainer->get($serviceId);
}

$guessers[] = $service;
Copy link
Member

@chalasr chalasr Feb 20, 2017

Choose a reason for hiding this comment

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

In fact, guessers do not need to be instantiated at this point.
I think we should register FormTypeGuesserChain as a service, pass it guessers as an IteratorArgument and deprecate both $guesserServices and $guesserServiceIds in favor of a FormTypeGuesserInterface $guesser here, even more lazy. Not sure how it would be hard to keep it BC though, maybe deprecating the whole class would be simpler.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes that's probably a good thing! I'll check this tomorrow with @nicolas-grekas.

Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure we need FormTypeGuesserChain to be a service, but simply pass it the guessers iterator (so allow FormTypeGuesserChain to accept iterable).
IMHO the extension is in charge of building the whole thing and the use of the FormTypeGuesserChain is an implementation detail. There is no need to pass the guesser chain as a dependency.

There is an issue with FormTypeGuesserChain, though... its constructor. It iterates over the guessers and this would trigger the loading too soon anyway.

I'm not sure lazy loading here is a big deal though, as FormExtensionInterface::getTypeGuesser() is called in FormRegistryInterface::getTypeGuesser(), called in Formfactory::createBuilderForProperty() and is used directly to guess types by iterating over every guessers.

Copy link
Member

@chalasr chalasr Feb 20, 2017

Choose a reason for hiding this comment

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

IMHO the extension is in charge of building the whole thing

Too many responsibilities to me.

and the use of the FormTypeGuesserChain is an implementation detail. There is no need to pass the guesser chain as a dependency.

The fact the guessers are multiple services is a detail IMO. At the end we have only one FormTypeGuesserInterface $guesser, the class could be usable with an iterable or a simple guesser, which looks better to me. Also it'd be consistent with other places in the core.

There is an issue with FormTypeGuesserChain, though... its constructor. It iterates over the guessers and this would trigger the loading too soon anyway.

Just validation/merge, can be moved at compile time I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought about deprecating the whole class and replacing it with a new one. But after discussing with @nicolas-grekas it requires too much efforts then to maintain both implementations. This is why we choose to adapt the class itself. One class to maintain and one tests suite. It's just a bit more difficult to read and understand the code at the moment because of the mixed two layers.

Copy link
Member

Choose a reason for hiding this comment

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

It's just a bit more difficult to read and understand the code at the moment because of the mixed two layers.

Indeed :)
I'm unable to say if the BC layer is correct right now, but the approach looks better!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unit tests prove the BC layer is guaranteed ;)

__CLASS__.'_Type1' => 'my.type1',
__CLASS__.'_Type2' => 'my.type2',
), $extDefinition->getArgument(1));
$this->assertSame(
Copy link
Contributor

Choose a reason for hiding this comment

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

This change should be reverted I guess.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why?

Copy link
Contributor

Choose a reason for hiding this comment

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

The code style and the assertSame change isn't related to this PR, right?

'my.guesser1',
'my.guesser2',
), $extDefinition->getArgument(3));
$this->assertSame(
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here.

}

$definition->replaceArgument(0, new ServiceLocatorArgument($servicesMap));
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not 2 service locators, and one iterator?

So the DependencyInjectionExtension::__construct() method signature would be:

public function __construct(ContainerInterface $typesLocator, ContainerInterface $typeExtensionsLocator, iterator $guessers)

Might not be the easiest for BC though.

$service = $this->typeContainer->get($serviceId);
}

$guessers[] = $service;
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure we need FormTypeGuesserChain to be a service, but simply pass it the guessers iterator (so allow FormTypeGuesserChain to accept iterable).
IMHO the extension is in charge of building the whole thing and the use of the FormTypeGuesserChain is an implementation detail. There is no need to pass the guesser chain as a dependency.

There is an issue with FormTypeGuesserChain, though... its constructor. It iterates over the guessers and this would trigger the loading too soon anyway.

I'm not sure lazy loading here is a big deal though, as FormExtensionInterface::getTypeGuesser() is called in FormRegistryInterface::getTypeGuesser(), called in Formfactory::createBuilderForProperty() and is used directly to guess types by iterating over every guessers.

__CLASS__.'_Type1' => 'my.type1',
__CLASS__.'_Type2' => 'my.type2',
), $extDefinition->getArgument(1));
$this->assertSame(
Copy link
Contributor

Choose a reason for hiding this comment

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

Should be reverted too I think.

'my.guesser2',
), $extDefinition->getArgument(3));
$this->assertSame(
array(
Copy link
Contributor

Choose a reason for hiding this comment

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

same

@hhamon hhamon force-pushed the feat-private-form-types branch 5 times, most recently from deb23a2 to 27e2a1a Compare February 21, 2017 08:35
), $extDefinition->getArgument(1));
$locator = $extDefinition->getArgument(0);

$this->assertInstanceOf(ServiceLocatorArgument::class, $locator);
Copy link
Contributor

Choose a reason for hiding this comment

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

No need for assertInstanceOf as assertEquals fails on an type-class mismatch.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed. thanks.

@hhamon hhamon force-pushed the feat-private-form-types branch 4 times, most recently from ab23d37 to e4beebf Compare February 21, 2017 10:04
// Support type access by FQCN
$types[$serviceDefinition->getClass()] = $serviceId;
// Add form type service to the service locator
$servicesMap[$serviceId] = new Reference($serviceId);
Copy link
Member

Choose a reason for hiding this comment

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

this is wrong. Form types are accessible only by class, not by service id

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

@hhamon hhamon force-pushed the feat-private-form-types branch 4 times, most recently from 0702a5b to 5c0a576 Compare February 21, 2017 10:38
@hhamon
Copy link
Contributor Author

hhamon commented Feb 27, 2017

@nicolas-grekas @HeahDude comments were addressed. I'll squash after your new view.

@hhamon hhamon force-pushed the feat-private-form-types branch from 0753fcd to ec76718 Compare February 27, 2017 09:44
Copy link
Contributor

@HeahDude HeahDude left a comment

Choose a reason for hiding this comment

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

👍

Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

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

👍

* Constructor.
*
* @param ContainerInterface $typeContainer
* @param iterable[] $typeExtensionServices
Copy link
Member

@nicolas-grekas nicolas-grekas Feb 27, 2017

Choose a reason for hiding this comment

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

iterable[] is more precise: array of iterables

@@ -25,8 +25,9 @@
},
"require-dev": {
"doctrine/collections": "~1.0",
"psr/container": "^1.0",
Copy link
Member

Choose a reason for hiding this comment

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

not required (as symfony/dependency-injection already requires it)

@hhamon hhamon force-pushed the feat-private-form-types branch 13 times, most recently from 16cab61 to a354cac Compare February 27, 2017 13:50
@hhamon
Copy link
Contributor Author

hhamon commented Feb 27, 2017

@stof @webmozart can I have your review please?

</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">
Copy link
Member

Choose a reason for hiding this comment

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

see PR description for why this is not a BC break (same for all added public="false")

Copy link
Member

@chalasr chalasr left a comment

Choose a reason for hiding this comment

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

👍 (needs rebase)

… so that form types can be made private at some point.
@fabpot
Copy link
Member

fabpot commented Feb 28, 2017

Thank you @hhamon.

@fabpot fabpot merged commit 600e75c into symfony:master Feb 28, 2017
fabpot added a commit that referenced this pull request Feb 28, 2017
…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
-------

600e75c [Form] use new service locator in DependencyInjectionExtension class, so that form types can be made private at some point.
@fabpot fabpot mentioned this pull request May 1, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.