-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[DependencyInjection] Use current class as default class for factory declarations #20943
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
[DependencyInjection] Use current class as default class for factory declarations #20943
Conversation
(Travis and fabbot failures unrelated) |
$class = $factory->getAttribute('class') ?: $definition->getClass(); | ||
} | ||
|
||
if (null === $class) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we check if the class doesn't exist also ( i.e. ... || false === class_exists($class)
) ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure it's the loader responsibility.
Also the class might not be available at this point, in case it's generated for instance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Imo. the null === $class
check also doesnt belong to the loaders 😕
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This check is only here to provide more detailed information (file, service id, ...).
A similar exception would be thrown from the definition, but not DX friendly because those information are lost.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Point is the required information can actually be available later on, hence see #20264 (comment) :)
We gain some DX maybe, but it increases complexity. It just duplicates logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now, I don't see the benefits, nor how this method would be helpful for this PR. BTW, you still miss information about the place from where was loaded the service.
DX indeed always increases complexity, as it requires writing specific code and sometimes preventing doing wrong things ahead. But I think we're going to survive this extra if
😅
However, if you think you have a point about the Definition::resolve($id)
suggestion, I think it's worth a PR in order to show us the benefits. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If both PR's are merged, ill provide a failing test ;-) Think
services:
Namespace\Class: { factory: [~, create] }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure it's something that could be enhanced then.
Actually, I already had in mind definitions are defined and altered at different places. With the current state of this PR, if the definition class is changed in a compiler pass for instance, the factory class used won't change.
I assumed it'll be a known limitation that should not challenge this PR, because it only provides syntax sugar over declaring a factory using YAML/XML formats.
Now, if we should consider this, there are other solutions:
- [DependencyInjection] Optional class for class named services #20264 could set the class according to the service id from loaders, because it's probably not the role of the
ContainerBuilder
to alter definitions. - We can resolve the factory class in
Definition::getFactory()
instead of computing it inDefinition::setFactory()
- Or we can indeed try a more advanced solution with a new
Definition::resolve($id)
method as you suggest, but I don't think we should provide this in this PR nor in [DependencyInjection] Optional class for class named services #20264
(2 and 3 would indeed challenge these extra checks in the loaders)
} | ||
|
||
if (null === $class) { | ||
throw new InvalidArgumentException(sprintf( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we prefer a single line here
Couldn't we support this in the YAML Format too by specifying an array with only one element (same for the PHP loader)? |
Or null |
Looks like a good trade-off to me and can be handled directly in the |
@@ -363,6 +364,14 @@ private function parseCallable($callable, $parameter, $id, $file) | |||
return array($this->resolveServices($callable[0]), $callable[1]); | |||
} | |||
|
|||
if ('factory' === $parameter) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we blindly support this for configurators instead? IMHO it makes no sense for a service to configure itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
indeed, configurators should not support this
PR updated to support this directly in the Exceptions messages would need to be enhanced, but it's hard to be really succinct. |
@@ -37,3 +37,5 @@ services: | |||
new_factory1: { class: FooBarClass, factory: factory} | |||
new_factory2: { class: FooBarClass, factory: ['@baz', getClass]} | |||
new_factory3: { class: FooBarClass, factory: [BazClass, getInstance]} | |||
new_factory4: { class: BazClass, factory: [~, getInstance]} | |||
new_factory5: { class: BazClass, factory: [getInstance]} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need to support this one, it's rather confusing to have an array with just one element. Using null
seems more discoverable and easy enough to understand.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough. I'll remove it 👍
* | ||
* @throws InvalidArgumentException When errors are occuried | ||
* @throws InvalidArgumentException When errors are occurred |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This cs fix should target 3.2.
if (null === $this->class) { | ||
throw new InvalidArgumentException(sprintf('First value of the $factory array should either be a class name, a reference or the definition class should be set. "%s" given.', isset($factory[0]) ? gettype($factory[0]) : 'NULL')); | ||
} | ||
$factory[0] = $this->class; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See also #20264 (comment)
I think we should resolve properties at once, in the right order. So everythings works as expected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -64,6 +64,13 @@ public function setFactory($factory) | |||
$factory = explode('::', $factory, 2); | |||
} | |||
|
|||
if (is_array($factory) && !isset($factory[0])) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about explicitly checking for null
? Why should we support empty arrays here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't support empty arrays, but there wasn't such checks before.
I can explicitly check for null
, without the isset. Then it'll fail badly in case of an empty array.
I think the fact the array should contain 2 elements is almost part of the signature (through phpdoc), hence there is not any check about it. If we add them, it should be considered as a bug fix I guess?
@@ -184,7 +184,11 @@ private function parseDefinition(\DOMElement $service, $file) | |||
} elseif ($childService = $factory->getAttribute('service')) { | |||
$class = new Reference($childService, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, false); | |||
} else { | |||
$class = $factory->getAttribute('class'); | |||
$class = $factory->getAttribute('class') ?: $definition->getClass(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we still need that? This is now handled in the Definition
, isn't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. It's not needed, but it allows to provide a better exception message. See #20943 (comment)
Rethinking about this one, shouldn't this logic be handled by PhpDumper instead? |
@nicolas-grekas : I can give a look, but the idea was to be able to warn the user with as much context as possible if something is missing, and limit things done in his back (if the definition class is altered somewhere, the end user might not expect the factory class to change magically). services:
my_factory:
class: Bar\Baz
factory: [~, 'create'] Is only syntax sugar to avoid typing Now, if we really want to support something like: services:
my_factory:
factory: [~, 'create'] where the class can be provided later, indeed |
the PhpDumper has full context to do so |
I'll rework this PR then. Status: Needs work |
This makes the feature a compilation artifact.. not sure if that makes sense. Im fine with mutating the
|
Updated. Status: Needs Review |
@@ -174,7 +174,9 @@ private function addService($definition, $id, \DOMElement $parent) | |||
$this->addService($callable[0], null, $factory); | |||
$factory->setAttribute('method', $callable[1]); | |||
} elseif (is_array($callable)) { | |||
$factory->setAttribute($callable[0] instanceof Reference ? 'service' : 'class', $callable[0]); | |||
if (null !== $callable[0]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
extra space
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops. Good catch. Fixed :)
@ogizanagi I know that fabbot.io fail is not related with your changes, but could you fix it in this PR? |
@yceruto Those changes should be ignored ;) (see PHP-CS-Fixer/PHP-CS-Fixer#2413). |
@@ -363,6 +363,10 @@ private function parseCallable($callable, $parameter, $id, $file) | |||
return array($this->resolveServices($callable[0]), $callable[1]); | |||
} | |||
|
|||
if ('factory' === $parameter && !isset($callable[0])) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
null === $callable[0]
} | ||
|
||
if (null === $class = $definition->getClass()) { | ||
throw new RuntimeException(sprintf('The "%s" service is defined to be created by a factory but is missing the factory class. Did you forget to define the factory or service class?', $id)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
missing comma before "but"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you
@@ -363,6 +363,10 @@ private function parseCallable($callable, $parameter, $id, $file) | |||
return array($this->resolveServices($callable[0]), $callable[1]); | |||
} | |||
|
|||
if ('factory' === $parameter && null === $callable[0]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you need to validate isset($callable[1])
though, to accept it only when the second value is not null
There is a competition between this PR and #20264: both try to define the class when it's missing based on different rules. But if we're not careful, those rules can collide and we can have priority issues, with one implem taking over the other one. |
Hum, I completely messed up with my previous comment, you can forget about it :) |
Just in order to be able to use those formats without getting an exception thrown by the loaders because factory class is missing. (Happy new year 🎉 🎈🍾🥂😃) |
The compiler pass is missing recursivity handing into arguments, etc. Note that I'd be really fine with considering that PhpDumper + ContainerBuilder::createService have this responsibility of interpreting such state correctly as I proposed previously... (Happy new year ! :) ) |
As I don't have a strong opinion on the Compiler pass vs. PhpDumper + ContainerBuilder, I can update both commits and we can choose then...but that would help me not doing everything twice if someone else has a stronger opinion on this. Isn't a complete Definition better, for instance when using the |
@ogizanagi can you please amend the first commit so that it gets in what's fixed by the 2nd one (ie everything but the compiler pass). We miss a test + implementation for ContainerBuilder. @stof please confirm you'd be ok with only the first commit. Personally, I am. A recursive compiler pass vs 4 lines in the PhpDumper + 4 lines in ContainerBuilder, I prefer the 4x2 lines. |
@nicolas-grekas : amended + added implementation for the ContainerBuilder + test. Despite it's the most straightforward solution, we should be aware that it'll still allow an "incomplete" Definition. Thus, the following: foo:
class: Foo
factory: [~, create] along with the Information for Service "foo"
=============================
------------------ --------
Option Value
------------------ --------
Service ID foo
Class Foo
Tags -
Public yes
Synthetic no
Lazy no
Shared yes
Abstract no
Autowired no
Autowiring Types -
Factory Class
Factory Method create
------------------ -------- (i.e no factory class, unless we adjust the command or opt for the pass) EDIT: I've added the required changes in descriptors regarding this in the first commit. Let me know. |
@stof : Anything to declare? 😄 Otherwise I'll remove the last commit about the compiler pass implementation. Thank you. |
@ogizanagi would you mind updating the compiler pass ti make it able to handle nested inline definitions? |
@nicolas-grekas : I was going to remove the second commit and go forward. Did you change your mind, or should we simply wait for more opinions on this? BTW, regarding the PHPDumper + ContainerBuilder implementation, now that dumping a non-compiled container is deprecated and #21133 is merged, this test is failing when adding
|
@ogizanagi can you rebase and take advantage of AbstractRecursivePass now that #21327 is merged? |
Tests are failing, but that's not related AFAIK. |
👍 |
@ogizanagi Can you add a note in the component CHANGELOG? |
Sure. Done :) |
Thank you @ogizanagi. |
…ss for factory declarations (ogizanagi) This PR was merged into the 3.3-dev branch. Discussion ---------- [DependencyInjection] Use current class as default class for factory declarations | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #20888 | License | MIT | Doc PR | Should update the notice about the "class" attribute on http://symfony.com/doc/current/service_container/factories.html #20888 makes sense to me, considering the following sample extracted from the documentation: ```xml <service id="app.newsletter_manager" class="AppBundle\Email\NewsletterManager"> <factory class="AppBundle\Email\NewsletterManager" method="create" /> </service> ``` The class is used as a factory to create itself, thus it can be simplified to: ```xml <service id="app.newsletter_manager" class="AppBundle\Email\NewsletterManager"> <factory method="create" /> </service> ``` However, it's not possible to provide the same feature for the YAML format, because it doesn't allow to distinct a function from a method call if the class is not provided explicitly under the `factory` key, whereas the xml format use a dedicated `function` attribute. Would this inconsistency between those two formats be a no-go for this feature? The doc notices: > When using a factory to create services, the value chosen for the class option has no effect on the resulting service. The actual class name only depends on the object that is returned by the factory. However, the configured class name may be used by compiler passes and therefore should be set to a sensible value. If this is merged, it should be updated wisely in order to not confuse everyone with this feature when using the xml format. UPDATE: The yaml format is now supported when the class is not provided in the factory array: ```yml services: my_factory: class: Bar\Baz factory: [~, 'create'] ``` Commits ------- e6d8570 [DependencyInjection] Use current class as default class for factory declarations
…elf as factory (OskarStark) This PR was squashed before being merged into the 5.4 branch. Discussion ---------- [DependencyInjection] Explain how to use the class itself as factory Follows * symfony/symfony#20943 Commits ------- 95a06a1 [DependencyInjection] Explain how to use the class itself as factory
#20888 makes sense to me, considering the following sample extracted from the documentation:
The class is used as a factory to create itself, thus it can be simplified to:
However, it's not possible to provide the same feature for the YAML format, because it doesn't allow to distinct a function from a method call if the class is not provided explicitly under the
factory
key, whereas the xml format use a dedicatedfunction
attribute.Would this inconsistency between those two formats be a no-go for this feature?
The doc notices:
If this is merged, it should be updated wisely in order to not confuse everyone with this feature when using the xml format.
UPDATE: The yaml format is now supported when the class is not provided in the factory array: