Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[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

Merged
merged 1 commit into from
Jan 24, 2017
Merged

[DependencyInjection] Use current class as default class for factory declarations #20943

merged 1 commit into from
Jan 24, 2017

Conversation

ogizanagi
Copy link
Contributor

@ogizanagi ogizanagi commented Dec 15, 2016

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:

<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:

<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:

services:
    my_factory:
        class: Bar\Baz
        factory: [~, 'create']

@ogizanagi
Copy link
Contributor Author

(Travis and fabbot failures unrelated)

$class = $factory->getAttribute('class') ?: $definition->getClass();
}

if (null === $class) {
Copy link
Member

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) ) ?

Copy link
Contributor Author

@ogizanagi ogizanagi Dec 15, 2016

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.

Copy link
Contributor

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 😕

Copy link
Contributor Author

@ogizanagi ogizanagi Dec 17, 2016

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.

Copy link
Contributor

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.

Copy link
Contributor Author

@ogizanagi ogizanagi Dec 17, 2016

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. :)

Copy link
Contributor

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] }

Copy link
Contributor Author

@ogizanagi ogizanagi Dec 17, 2016

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:

  1. [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.
  2. We can resolve the factory class in Definition::getFactory() instead of computing it in Definition::setFactory()
  3. 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(
Copy link
Member

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

@xabbuh
Copy link
Member

xabbuh commented Dec 16, 2016

Couldn't we support this in the YAML Format too by specifying an array with only one element (same for the PHP loader)?

@nicolas-grekas
Copy link
Member

Or null ~ first element?

@ogizanagi
Copy link
Contributor Author

Looks like a good trade-off to me and can be handled directly in the Definition::setFactory() method.
Anything against supporting both? (i.e factory: [~, 'create'] and factory: ['create'])

@@ -363,6 +364,14 @@ private function parseCallable($callable, $parameter, $id, $file)
return array($this->resolveServices($callable[0]), $callable[1]);
}

if ('factory' === $parameter) {
Copy link
Contributor Author

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.

Copy link
Member

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

@ogizanagi
Copy link
Contributor Author

PR updated to support this directly in the Definition class and allowing it from the YamlFileLoader.

Exceptions messages would need to be enhanced, but it's hard to be really succinct.

@ogizanagi ogizanagi changed the title [DependencyInjection] Use current class as default class for factory declarations using xml format [DependencyInjection] Use current class as default class for factory declarations Dec 16, 2016
@@ -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]}
Copy link
Member

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.

Copy link
Contributor Author

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
Copy link
Contributor

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;
Copy link
Contributor

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.

Copy link
Contributor Author

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])) {
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 explicitly checking for null? Why should we support empty arrays here?

Copy link
Contributor Author

@ogizanagi ogizanagi Dec 18, 2016

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();
Copy link
Member

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?

Copy link
Contributor Author

@ogizanagi ogizanagi Dec 18, 2016

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)

@nicolas-grekas
Copy link
Member

Rethinking about this one, shouldn't this logic be handled by PhpDumper instead?
Definition should have as low logic as possible. But PhpDumper is the right place to interpret what a definition means. Don't you think?

@ogizanagi
Copy link
Contributor Author

@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 Bar\Baz twice in the definition. Not a magical thing allowing to guess the factory class according to the final Definition class if the class has been modified elsewhere, in a compiler pass for instance. Considering this, maybe I shouldn't have changed any logic in the Definition class, but only added it to dedicated loaders.

Now, if we really want to support something like:

services:
    my_factory:
        factory: [~, 'create']

where the class can be provided later, indeed PhpDumper looks like the good candidate.

@nicolas-grekas
Copy link
Member

be able to warn the user with as much context as possible if something is missing

the PhpDumper has full context to do so

@ogizanagi
Copy link
Contributor Author

I'll rework this PR then.

Status: Needs work

@ro0NL
Copy link
Contributor

ro0NL commented Dec 18, 2016

This makes the feature a compilation artifact.. not sure if that makes sense.

Im fine with mutating the Definition with sensible defaults in runtime as well... perhaps a private ContainerBuilder::finalizeDefinition($id, Def $def) will do?

  • set class from id
  • set factory from class
  • etc.

@ogizanagi
Copy link
Contributor Author

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]) {
Copy link
Member

Choose a reason for hiding this comment

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

extra space

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops. Good catch. Fixed :)

@yceruto
Copy link
Member

yceruto commented Dec 19, 2016

@ogizanagi I know that fabbot.io fail is not related with your changes, but could you fix it in this PR?

@HeahDude
Copy link
Contributor

@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])) {
Copy link
Member

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));
Copy link
Member

Choose a reason for hiding this comment

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

missing comma before "but"

Copy link
Contributor Author

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]) {
Copy link
Member

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

@nicolas-grekas nicolas-grekas added this to the 3.3 milestone Dec 26, 2016
@nicolas-grekas
Copy link
Member

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.
I think this PR should be enhanced up to replace #20264.

@nicolas-grekas
Copy link
Member

Hum, I completely messed up with my previous comment, you can forget about it :)
Why do we need to patch the xml & yaml loader at all? I'd expect the compiler pass to deal with the logic completely. Did I miss something?

@ogizanagi
Copy link
Contributor Author

ogizanagi commented Jan 1, 2017

Why do we need to patch the xml & yaml loader at all?

Just in order to be able to use those formats without getting an exception thrown by the loaders because factory class is missing.
The compiler pass does all the job.

(Happy new year 🎉 🎈🍾🥂😃)

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Jan 1, 2017

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...
PhpDumper & ContainerBuilder already have many in common and the logic we're talking about doesn't look that divergent from what they already share.

(Happy new year ! :) )

@ogizanagi
Copy link
Contributor Author

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 debug:container command?

@nicolas-grekas
Copy link
Member

@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.

@ogizanagi
Copy link
Contributor Author

ogizanagi commented Jan 7, 2017

@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 bin/console debug:container foo command, for instance, will output:

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.

@ogizanagi
Copy link
Contributor Author

@stof : Anything to declare? 😄

Otherwise I'll remove the last commit about the compiler pass implementation.

Thank you.

@nicolas-grekas
Copy link
Member

@ogizanagi would you mind updating the compiler pass ti make it able to handle nested inline definitions?

@ogizanagi
Copy link
Contributor Author

ogizanagi commented Jan 16, 2017

@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 $container->compile(), because:

  1. [DI] Optional class for named services #21133 uses the service id if class is not set in the definition, even for a service id like bar. If we want to avoid a class_exists call to support generated classes, I wonder if we shouldn't at least enforce a leading \ in case someone wants to register a service named after a class in the global namespace?
  2. The CheckDefinitionValidityPass anyway checks a definition has a class, even for definition with a factory. So maybe this test should be marked as legacy, or should be removed along with related code?

@nicolas-grekas
Copy link
Member

@ogizanagi can you rebase and take advantage of AbstractRecursivePass now that #21327 is merged?

@ogizanagi
Copy link
Contributor Author

Tests are failing, but that's not related AFAIK.

@nicolas-grekas
Copy link
Member

👍

@fabpot
Copy link
Member

fabpot commented Jan 23, 2017

@ogizanagi Can you add a note in the component CHANGELOG?

@ogizanagi
Copy link
Contributor Author

Sure. Done :)

@fabpot
Copy link
Member

fabpot commented Jan 24, 2017

Thank you @ogizanagi.

@fabpot fabpot merged commit e6d8570 into symfony:master Jan 24, 2017
fabpot added a commit that referenced this pull request Jan 24, 2017
…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
@ogizanagi ogizanagi deleted the feature/di/factory_class_from_def_class branch January 24, 2017 06:11
@fabpot fabpot mentioned this pull request May 1, 2017
OskarStark added a commit to symfony/symfony-docs that referenced this pull request Mar 10, 2023
…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
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.

9 participants