Skip to content

[WIP] Annotation support for Services #21376

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

Closed
wants to merge 6 commits into from

Conversation

weaverryan
Copy link
Member

@weaverryan weaverryan commented Jan 23, 2017

Q A
Branch? master
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets #21103
License MIT
Doc PR TODO

Hi guys!

This is a WIP solution for adding annotations to services, and is similar to some proposals in #21103. Notice, this does NOT include a "discovery" mechanism for services: you must still register your service like normal (e.g. in YML or XML file), but then the annotations in the service are used to augment the service Definition. But, if #21289 is merged, then that'll cover automatic "discovery" for these services :).

For me, the BIG motivation is to augment autowiring, when you have just one argument that can't be autowired. Annotations read much better than, for example, setting the index "2" argument in YAML.

_defaults:
    autowire: true
    tags:
        # activates the feature
        - { name: annotated }

parameters:
    mandrill_api_key: XXXXXXXXX

services:
    App\NewsletterManager: ~
        # you could add tags, arguments or anything else here
        # these would override the annotations, if they overlap
use Doctrine\ORM\EntityManager;
use Psr\Log\LoggerInterface;

/**
 * You could optionally configure any service definition options here:
 * @DI\Definition(public=false)
 */
class NewsletterManager
{
    private $logger;
    private $em;
    private $mandrillApiKey;
    private $serializer;

    /**
     * @DI\Argument(name="mandrillApiKey", value="%mandrill_api_key%")
     */
    public function __construct(LoggerInterface $logger, EntityManager $em, $mandrillApiKey)
    {
        $this->logger = $logger;
        $this->em = $em;
        $this->mandrillApiKey = $mandrillApiKey;
    }
}

Pretty much anything that you can set in YAML would be supported (most is still a TODO). The feature is currently activated by a tag... which is really ugly (though _defaults at least helps this). We could add this to the Definition object (like we did with autowire) if we want.

In #22103, there was some talk of following a Spring standard. While I think it IS important to look and learn from them, this keeps very close to our existing standard: effectively re-using all our existing terminology, but in YAML. The @Argument annotation re-uses all the options from the <argument> tag in XML service config.

Cheers!

TODOS

  • Add support for calls
  • Add support for factory, deprecated, configurator, decoratedService, autowiringTypes and autowiring/autowiringMethods
  • Add support for injection on properties
  • Add an @Tag annotation
  • Finish collection and iterator argument types
  • Handle parent class / interface annotations
  • Do not throw an exception when you inherit an annotation from a base class, but that arg doesn't exist on the overridden constructor (would currently throw an error)
  • Add a new "Resource" class so that the container cache is rebuilt (and rebuilt intelligently)
  • Add a lot more tests for many edge cases

*/
public function process(ContainerBuilder $container)
{
if (class_exists(AnnotationReader::class)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

if (!class_exists(AnnotationReader::class)) ?

Copy link
Member Author

Choose a reason for hiding this comment

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

thanks :)

*/
class Service
{
private $shared;
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 add autowired too?

Copy link
Member Author

Choose a reason for hiding this comment

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

Definitely - it's one of my todos :)

}
}

private function augmentServiceDefinition(Definition $definition)
Copy link
Member

Choose a reason for hiding this comment

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

It should read recursively annotations from parent classes and implemented interfaces.


$onInvalid = $arg->getOnInvalid();
$invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
if ('ignore' == $onInvalid) {
Copy link
Member

Choose a reason for hiding this comment

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

===

$invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
if ('ignore' == $onInvalid) {
$invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
} elseif ('null' == $onInvalid) {
Copy link
Member

Choose a reason for hiding this comment

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

===

{
$arguments = array();
$i = 0;
foreach ($method->getParameters() as $parameter) {
Copy link
Member

Choose a reason for hiding this comment

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

Why not using the key instead of the $i variable?

foreach($method->getParameters() as $i => $parameter) {
    $arguments[$parameter->getName()] = $i;
}

It should do the same thing: https://3v4l.org/Zpq4W

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated!

@dunglas
Copy link
Member

dunglas commented Jan 23, 2017

I'm still not a fond of having (non-standard) annotations for DI. I've explained my reasons in depth here: #21103 (comment)

However I recognize that it is practical for parameters injection. I've proposed a less intrusive alternative in #20738, but I'm not very convinced by it either.

Maybe should we keep with the current solution for now:

_defaults:
    autowire: true

parameters:
    mandrill_api_key: XXXXXXXXX

services:
    App\NewsletterManager: ['', '', '%mandrill_api_key%'] # This is already working

@ogizanagi
Copy link
Contributor

@dunglas :

services:
    App\NewsletterManager: ['', '', '%mandrill_api_key%'] # This is already working

This is not already working, until #21313 is merged 😄

@weaverryan
Copy link
Member Author

@dunglas I agree with much of what you are saying :), and I see that we're both after trying to solve the "parameter" problem in some decent way. I'm also not convinced by #20738, so we could either stay with the current YAML solution or use annotations (or maybe there's a 3rd option). About your comments (#21103 (comment)), I don't see these as a significant problem. Overall, the fact that this feature would be meant for end-users - and not library authors - puts most of those concerns to rest. And in app code, coupling your code to your framework isn't a problem (actually, I see it as an advantage for speed and clarity). Also, it may help the container to be seen as simply an "internal detail" to end-users. If auto-wiring were perfect, then life would be good. But since there will always be edge-cases (parameters being the big one), with annotations, the user could avoid needing to suddenly (and strangely) go into services.yml and add a service key + arguments key (e.g. where the first two args are empty strings). Instead, they would just annotate one arg in their class :).

But, we'll see what others think - I wanted to get some real code to help with this exact discussion!

Cheers!

@GuilhemN
Copy link
Contributor

GuilhemN commented Jan 23, 2017

For me, the BIG motivation is to augment autowiring, when you have just one argument that can't be autowired. Annotations read much better than, for example, setting the index "2" argument in YAML.

This made me think, can't we have named arguments in yaml? (e.g.

services:
    Foo:
        arguments:
            mandrillApiKey: %mandrill_api_key%

).

Having annotations look huge to me if this is the only motivation, don't you think ?

@dunglas
Copy link
Member

dunglas commented Jan 23, 2017

@GuilhemN I've proposed this exact solution to @nicolas-grekas today and I'm working on a PR to implement it 🙌

@weaverryan
Copy link
Member Author

@dunglas @GuilhemN I don't think I agree with that approach, as it's unexpected that changing argument names would have a side effect (this is true with annotations too, but at least the annotation & arg name are together). But, let's open that PR and decide the best solution!

@dunglas
Copy link
Member

dunglas commented Jan 23, 2017

Alternative proposal: #21383

@nicolas-grekas
Copy link
Member

Note that to me, this PR and #21383 do not compete. We could very well have both onboard.

return;
}

$this->reader = new AnnotationReader();
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 using $container->get('annotation_reader') instead? this service is already used at compile time by some other extensions.

Copy link
Member

Choose a reason for hiding this comment

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

and it already causes issues regarding incomplete service configuration (remember the burden when switching the cache configuration in 3.2 ?). We always documented that getting service instances during a compiler pass is an unsupported usage of the container (and so you are on your own and it may break in weird ways), so we should not do it in Symfony itself (especially when it can still break because of a change done by another bundle)

Copy link
Member

@nicolas-grekas nicolas-grekas Jan 24, 2017

Choose a reason for hiding this comment

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

I more than remember since I worked on that again in #21381.
Yet it allowed to fix a core issue, which is that there should be no cache enabled at build time.
Thus, using annotation_reader could be also seen as a way for us to support using it at compile time a first class citizen - which we have to anyway, as history proved us.


private $lazy;

public function __construct(array $data)
Copy link
Member

Choose a reason for hiding this comment

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

why not removing the constructor and using public properties, letting the annotation parser validate properties itself ? This is the recommended usage of doctrine/annotations when annnotation classes don't need to be reused for other purposes (which is why we don't do in the validator component: constraints themselves are used as annotations, but they are not only annotations)

@nicolas-grekas nicolas-grekas added this to the 3.3 milestone Jan 24, 2017
fabpot added a commit that referenced this pull request Feb 13, 2017
…(dunglas, nicolas-grekas)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[DependencyInjection] Add support for named arguments

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

This PR introduces named arguments for services definitions. It's especially useful to inject parameters in an autowired service. It is (at least partially) an alternative to #21376 and #20738.

Usage:

```yml
services:
    _defaults: { autowire: true }
    Acme\NewsletterManager: { $apiKey: "%mandrill_api_key%" }

# Alternative (traditional) syntax
services:
    newsletter_manager:
        class: Acme\NewsletterManager
        arguments:
            $apiKey: "%mandrill_api_key%"
        autowire: true
```
```php
use Doctrine\ORM\EntityManager;
use Psr\Log\LoggerInterface;

namespace Acme;

class NewsletterManager
{
    private $logger;
    private $em;
    private $apiKey;

    public function __construct(LoggerInterface $logger, EntityManager $em, $apiKey)
    {
        $this->logger = $logger;
        $this->em = $em;
        $this->apiKey = $apiKey;
    }
}
```

Commits
-------

8a126c8 [DI] Deprecate string keys in arguments
2ce36a6 [DependencyInjection] Add a new pass to check arguments validity
6e50129 [DependencyInjection] Add support for named arguments
@weaverryan
Copy link
Member Author

I'll close this for now (thanks to #21383). We may still want this in the future, to avoid needing to touch a YAML file once you need to map a custom, non-autowired arg... but let's see how this all sorts out.

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.

8 participants