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

Skip to content

[DependencyInjection] add ServiceSubscriberTrait #27077

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
Jun 4, 2018
Merged

[DependencyInjection] add ServiceSubscriberTrait #27077

merged 1 commit into from
Jun 4, 2018

Conversation

kbond
Copy link
Member

@kbond kbond commented Apr 27, 2018

Q A
Branch? master
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets #23898
License MIT
Doc PR symfony/symfony-docs#9809

This allows you to easily configure Service Subscribers with the following convention:

class MyService implements ServiceSubscriberInterface
{
    use ServiceSubscriberTrait;

    public function doSomething()
    {
        // $this->router() ...
    }

    private function router(): RouterInterface
    {
        return $this->container->get(__METHOD__);
    }
}

This also allows you to create helper traits like RouterAware, LoggerAware etc... and compose your services with them (not using __METHOD__ in traits because it doesn't behave as expected.).

trait LoggerAware
{
    private function logger(): LoggerInterface
    {
        return $this->container->get(__CLASS__.'::'.__FUNCTION__);
    }
}
trait RouterAware
{
    private function router(): RouterInterface
    {
        return $this->container->get(__CLASS__.'::'.__FUNCTION__);
    }
}
class MyService implements ServiceSubscriberInterface
{
    use ServiceSubscriberTrait, LoggerAware, RouterAware;

    public function doSomething()
    {
        // $this->router() ...
        // $this->logger() ...
    }
}

@@ -13,6 +13,11 @@
<!-- dummy arg to register class_exists as annotation loader only when required -->
<argument type="service" id="annotations.dummy_registry" />
</call>
<call method="addGlobalIgnoredName">
<argument>service</argument>
<!-- dummy arg to register class_exists as annotation loader only when required -->
Copy link
Member

Choose a reason for hiding this comment

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

no need to make multiple dummy registration


$returnType = $returnType->getName();

if (!class_exists($returnType) && !interface_exists($returnType)) {
Copy link
Member

Choose a reason for hiding this comment

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

the second call could avoid triggering the autoloader, as it was already triggered by the first one

Copy link
Member Author

Choose a reason for hiding this comment

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

Can you explain? I don't understand what should be done instead.

Copy link
Member

Choose a reason for hiding this comment

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

if (!class_exists($returnType) && !interface_exists($returnType, false)) {

As class_exists already triggered the autoloader, there is no need to trigger it for interfaces. If the interface exists, it will already have been loaded by the class_exists check.

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 see, thanks!

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.

Cool, thank you for starting this!
I think we need additional logic to deal with inheritance (i.e. call the parent method if any)

$services = array();

foreach ((new \ReflectionClass(static::class))->getMethods() as $method) {
if (false === strpos($method->getDocComment(), '@service')) {
Copy link
Member

Choose a reason for hiding this comment

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

how would it look like if we were to remove this condition?
instead, we could just make all services optional ( $services[$method->getName()] = '?'.$returnType; )
I feel like it could work quite nicely in practice.

Copy link
Member Author

@kbond kbond Apr 27, 2018

Choose a reason for hiding this comment

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

I tried this change and it seems to work but can't help thinking their may be side effects I can't think of...

One thing that comes to mind is developers wouldn't get immediate feedback if their services aren't wired correctly.

Thoughts?

Copy link
Member

Choose a reason for hiding this comment

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

Not having to declare anything creates a great experience.
About the error, I'm not sure how it would/should behave, we should try and figure out.


$returnType = $returnType->getName();

if (!class_exists($returnType) && !interface_exists($returnType, false)) {
Copy link
Member

Choose a reason for hiding this comment

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

this can also be removed if the map contains only optional services :)

continue;
}

$services[$method->getName()] = $returnType;
Copy link
Member

Choose a reason for hiding this comment

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

so: $services[$method->getName()] = '?'.$returnType;

$this->container = $container;
}

protected function service(string $id)
Copy link
Member

Choose a reason for hiding this comment

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

no need for this method, ppl should just have to write $this->container->get(__FUNCTION__);
(the property is private but is in the class' scope so one can access it)

Copy link
Member Author

Choose a reason for hiding this comment

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

What if there is a subclass that adds its own service methods?

Copy link
Member

Choose a reason for hiding this comment

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

maybe we should use $this->container->get(__METHOD__); instead?

Copy link
Member Author

Choose a reason for hiding this comment

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

But if $container is private, subclasses can't access it in their methods.

Copy link
Member

Choose a reason for hiding this comment

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

we don't care, do we? each such class would just have to use the trait (provided we add the logic to call the method on the parent)

Copy link
Member

@nicolas-grekas nicolas-grekas Apr 27, 2018

Choose a reason for hiding this comment

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

Then we should use $this->service(__CLASS__, __FUNCTION__);, isn't it?

OR, we could make the trait work only with public and protected methods. Any preference? Does it make sense to use a private method for these anyway? (could be, you'll tell me what you think about this :) )

Copy link
Member Author

Choose a reason for hiding this comment

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

Why can't we just use __FUNCTION__ as I had originally?

Copy link
Member

Choose a reason for hiding this comment

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

using function works when used in public/protected methods, but might break when mixed with private methods: a private method with same name but different return type can be defined in a parent class. When the parent will call its private method, it will get the service that was injected for the child definition. And this can just break...

Copy link
Member Author

Choose a reason for hiding this comment

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

Understood.

Using $this->service(__CLASS__, __FUNCTION__); would get my vote then.

Copy link
Member

@nicolas-grekas nicolas-grekas Apr 28, 2018

Choose a reason for hiding this comment

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

Cool. I updated my proposal above, it should be ok so you can borrow it. For the changelog, this should be in the 4.2 section. With a few tests, I think we might be good :)

@nicolas-grekas nicolas-grekas added this to the next milestone Apr 27, 2018
@kbond
Copy link
Member Author

kbond commented May 2, 2018

I have pushed the changes @nicolas-grekas and I have discussed and updated the PR description. If this looks good, I'll add some tests.

@nicolas-grekas
Copy link
Member

Looks good to me :)

@lyrixx
Copy link
Member

lyrixx commented May 2, 2018

Even if the implementation is good, I don't like this feature.
It adds another way of doing things without really reducing the line of code needed.

And this is going to be accepted, it will needs some tests

@nicolas-grekas
Copy link
Member

nicolas-grekas commented May 2, 2018

@lyrixx I didn't like autowiring first. Now I like it. Might be the same for you here. About tests, we agree, see mentions of it above :) Another way of doing things? For sure, but there is a huge benefit for DX: enabling autocompletion (as described in linked issue #23898). And this doesn't leak to the outside world: it stays a pure implementation detail. Definitely worth it IMHO.

use Psr\Container\ContainerInterface;

/**
* @author Kevin Bond <[email protected]>
Copy link
Member

Choose a reason for hiding this comment

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

could be worth some notes in the docblock

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.

getDeclaringClass should be used instead of getProrotype, my bad

}

try {
$method = $method->getPrototype();
Copy link
Member

Choose a reason for hiding this comment

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

the entire try/catch should be removed actually

}

if (($returnType = $method->getReturnType()) && !$returnType->isBuiltin()) {
$services[$method->class.'::'.$method->name] = $returnType->getName();
Copy link
Member

@nicolas-grekas nicolas-grekas May 6, 2018

Choose a reason for hiding this comment

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

$services[$method->getDeclaringClass()->name.'::'.$method->name] = '?'.$returnType->getName();

@kbond
Copy link
Member Author

kbond commented May 10, 2018

I added a happy path test.

continue;
}

if (($returnType = $method->getReturnType()) && !$returnType->isBuiltin()) {
Copy link
Member

@nicolas-grekas nicolas-grekas May 10, 2018

Choose a reason for hiding this comment

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

I feel like we should be even more strict and not wire methods from parents, as they should be the one declaring their deps.
$method->getDeclaringClass() === self::class should be added first in the "if" (then we can use self::class also on the next line.)

public function setContainer(ContainerInterface $container): void
{
if (\is_callable(array('parent', __FUNCTION__))) {
parent::setContainer($container);
Copy link
Member

Choose a reason for hiding this comment

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

In order to be compatible with AbstractController, we should return the return value of the parent (thus set the property before this block and remove the void on the signature.)

Copy link
Member Author

Choose a reason for hiding this comment

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

If there is no parent should I return something?

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

@nicolas-grekas
Copy link
Member

Would be nice to have a few more test cases covering:

  • that methods from parents are ignored
  • that methods from children are ignored
  • that parent getSubscribedServices/setContainer methods are called

Otherwise, it looks great to me, thank you!

@kbond
Copy link
Member Author

kbond commented May 10, 2018

I have added the requested tests.

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.

I like it :)

@nicolas-grekas nicolas-grekas changed the title [RFC][DependencyInjection] add ServiceSubscriberTrait [DependencyInjection] add ServiceSubscriberTrait May 10, 2018
@lyrixx
Copy link
Member

lyrixx commented May 28, 2018

Just one question, insteand of writing return $this->service(__CLASS__, __FUNCTION__);,
Can't we play with the backtrace to get __CLASS__, __FUNCTION__ Automatically ?
Or at least __METHOD__ ?

@nicolas-grekas
Copy link
Member

nicolas-grekas commented May 28, 2018

can't we play with the backtrace to get __CLASS__, __FUNCTION__

this is a runtime call, that'd be too slow

least __METHOD__

we discussed about it in #27077 (comment)

actually, I think removing the "service" helper might be nice.
Ppl would have to write $this->container->get(__METHOD__) instead of the current $this->service(__CLASS__, __FUNCTION__). I like it because it means one less indirection. For traits authors, they'd have to write $this->container->get(__CLASS__.'::'.__FUNCTION__). But their pb I think, that's a small overhead for an uncommon case.

@kbond ok for you?

@kbond
Copy link
Member Author

kbond commented May 30, 2018

My only hesitation is it makes creating composable traits (which I think/hope will not be uncommon) not as intuitive. I can see people putting $this->container->get(__METHOD__) in their trait methods and being confused as to why it isn't working - I was.

That being said, if composable traits aren't common, it is easier to do $this->container->get(__METHOD__).

Seems prudent to keep it simple now and add a service() helper later if it becomes problematic. I will update the PR.

@kbond
Copy link
Member Author

kbond commented May 31, 2018

I removed the service() method and updated docs and this PR description.

@nicolas-grekas
Copy link
Member

Thank you @kbond.

@nicolas-grekas nicolas-grekas merged commit 238e793 into symfony:master Jun 4, 2018
nicolas-grekas added a commit that referenced this pull request Jun 4, 2018
This PR was squashed before being merged into the 4.2-dev branch (closes #27077).

Discussion
----------

[DependencyInjection] add ServiceSubscriberTrait

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #23898
| License       | MIT
| Doc PR        | symfony/symfony-docs#9809

This allows you to easily configure Service Subscribers with the following convention:

```php
class MyService implements ServiceSubscriberInterface
{
    use ServiceSubscriberTrait;

    public function doSomething()
    {
        // $this->router() ...
    }

    private function router(): RouterInterface
    {
        return $this->container->get(__METHOD__);
    }
}
```

This also allows you to create helper traits like `RouterAware`, `LoggerAware` etc... and compose your services with them (*not* using `__METHOD__` in traits because it doesn't behave as expected.).

```php
trait LoggerAware
{
    private function logger(): LoggerInterface
    {
        return $this->container->get(__CLASS__.'::'.__FUNCTION__);
    }
}
```

```php
trait RouterAware
{
    private function router(): RouterInterface
    {
        return $this->container->get(__CLASS__.'::'.__FUNCTION__);
    }
}
```

```php
class MyService implements ServiceSubscriberInterface
{
    use ServiceSubscriberTrait, LoggerAware, RouterAware;

    public function doSomething()
    {
        // $this->router() ...
        // $this->logger() ...
    }
}
```

Commits
-------

238e793 [DependencyInjection] add ServiceSubscriberTrait
@kbond kbond deleted the service-subscriber branch June 4, 2018 19:58
javiereguiluz added a commit to symfony/symfony-docs that referenced this pull request Jun 11, 2018
…ond)

This PR was merged into the master branch.

Discussion
----------

[DependencyInjection] Document ServiceSubscriberTrait

Documentation for symfony/symfony#27077

Commits
-------

b0ac3a4 document ServiceSubscriberTrait
@nicolas-grekas nicolas-grekas modified the milestones: next, 4.2 Nov 1, 2018
This was referenced Nov 3, 2018
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.

6 participants