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

Skip to content

[DI] Add support for "wither" methods - for greater immutable services #30212

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
Apr 3, 2019

Conversation

nicolas-grekas
Copy link
Member

@nicolas-grekas nicolas-grekas commented Feb 12, 2019

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

Let's say we want to define an immutable service while still using traits for composing its optional features. A nice way to do so without hitting the downsides of setters is to use withers. Here would be an example:

 class MyService
{
    use LoggerAwareTrait;
}

trait LoggerAwareTrait
{
    private $logger;

    /**
     * @required
     * @return static
     */
    public function withLogger(LoggerInterface $logger)
    {
        $new = clone $this;
        $new->logger = $logger;

        return $new;
    }
}

$service = new MyService();
$service = $service->withLogger($logger);

As you can see, this nicely solves the setter issues.

BUT how do you make the service container create such a service? Right now, you need to resort to complex gymnastic using the "factory" setting - manageable for only one wither, but definitely not when more are involved and not compatible with autowiring.

So here we are: this PR allows configuring such services seamlessly.
Using explicit configuration, it adds a 3rd parameter to method calls configuration: after the method name and its parameters, you can pass true and done, you just declared a wither:

services:
    MyService:
        calls:
            - [withLogger, ['@logger'], true]

In XML, you could use the new returns-clone attribute on the <call> tag.

And when using autowiring, the code looks for the @return static annotation and turns the flag on if found.

There is only one limitation: unlike services with regular setters, services with withers cannot be part of circular loops that involve calls to wither methods (unless they're declared lazy of course).

@rvanlaak
Copy link
Contributor

The third parameter does not feel as explicit as it should.

On the other hand is the following format a bit overkill:

services:
    MyService:
        with:
            - [withLogger, '@logger']

Did you think about some other formats as well?

@tsantos84
Copy link
Contributor

Its ok to access private members as public ones? Sounds weird to me. Its ok this approach in this context?

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Feb 12, 2019

Did you think about some other formats as well?

None that clicked - that's the most simple I could think of - like for you, "with" doesn't click. I'm good with the 3rd arg - withers already start with "with" and that's enough maybe.

Its ok to access private members as public ones? Sounds weird to me. Its ok this approach in this context?

of course it is, that's pretty common for this and similar cases.

@Tobion
Copy link
Contributor

Tobion commented Feb 13, 2019

services:
    MyService:
        calls:
            - [withLogger, '@logger', true]

It does not look like this works. The second array element needs to be an array of arguments (['@logger']). Ref. #25663
And the third element was not implemented in YamlFileLoader as far as I see (which is good IMO).

So it needs to be written in the following way which is also alot more readable:

services:
    MyService:
        calls:
            - 
                method: withLogger
                arguments: ['@logger']
                use_result: true

I would go so far that we should think about deprecating the yaml syntax with array list in favour of named properties (#13892)

@nicolas-grekas nicolas-grekas force-pushed the di-withers branch 2 times, most recently from 4ce5f60 to 1e41685 Compare February 13, 2019 08:14
@nicolas-grekas
Copy link
Member Author

Thanks @Tobion and everyone, comments addressed, PR ready.

I would go so far that we should think about deprecating the yaml syntax with array list in favour of named properties

that's too far for me :) but that can be discussed in a RFC if you want to.

@fancyweb
Copy link
Contributor

Shouldn't there be a check that the "wither" method actually returns an instance of the same class ? What prevents people from returning something else ?

@nicolas-grekas
Copy link
Member Author

That's not the job of a DI container, eg we don't check you won't inject a logger where an event dispatcher is expected. That's a job for the language runtime, or a separate compiler pass / inspection process.

Copy link

@beoboo beoboo left a comment

Choose a reason for hiding this comment

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

I tried this locally and works very well.

@stof
Copy link
Member

stof commented Feb 20, 2019

On the other hand is the following format a bit overkill:

services:
    MyService:
        with:
            - [withLogger, '@logger']

Did you think about some other formats as well?

@rvanlaak Registering withers separately from other calls would have a big drawback: if you have both with and calls, what is their order ? Do we call methods on the initial object or on the final one ?
Marking calls themselves as wither allows to support any order (even having withers in the middle of non-wither calls) as the order is explicit.

@@ -136,15 +136,27 @@ protected function processValue($value, $isRoot = false)
}
$this->lazy = false;

// Any calls before a "wither" are part of the constructor-instantiation graph
Copy link
Member

Choose a reason for hiding this comment

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

Aren't any call to a non-wither performed before a wither call also part of it ?

Copy link
Member

Choose a reason for hiding this comment

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

Please add some tests covering mixing wither calls with non-wither calls (in different orders) to make sure this works right.

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 argue that non-wither calls should happen after wither calls, on the off chance that some calls register themselves with other services and the object hashes differ and causes unintended problems.

Copy link
Member

Choose a reason for hiding this comment

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

Well, I would say that mixing things are an edge case indeed. But the current configuration syntax supports it, and so we should deal with it properly.

The code currently already respects the order of method calls, be them withers or no, when instantiating the service. Only this place is missing that.

Copy link
Member Author

Choose a reason for hiding this comment

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

Aren't any call to a non-wither performed before a wither call also part of it ?

sure, that'swhat I mean by "any calls" - regular setters or withers

non-wither calls should happen after wither calls

why not, but not something I would make a "must"

@nicolas-grekas
Copy link
Member Author

@stof thanks for the review, issue addressed.

Statuts: needs review

@nicolas-grekas
Copy link
Member Author

friendly ping @symfony/deciders

Copy link
Member

@fabpot fabpot left a comment

Choose a reason for hiding this comment

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

Looks good to me. Not a big fan of use-result though. use-returned-value would be more explicit IMHO, but perhaps a bit verbose. What about use-returned? Or anyone with a better idea?

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Apr 3, 2019

use-result? use-returned-value? use-returned?
is-wither-factory? is-wither?
as-factory? is-factory?
is-wither-factory?

@nicolas-grekas
Copy link
Member Author

PR and description updated to use returns-clone, as discussed on Slack.

@fabpot
Copy link
Member

fabpot commented Apr 3, 2019

Thank you @nicolas-grekas.

@fabpot fabpot merged commit f455d1b into symfony:master Apr 3, 2019
fabpot added a commit that referenced this pull request Apr 3, 2019
…mutable services (nicolas-grekas)

This PR was merged into the 4.3-dev branch.

Discussion
----------

[DI] Add support for "wither" methods - for greater immutable 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        | symfony/symfony-docs#10991

Let's say we want to define an immutable service while still using traits for composing its optional features. A nice way to do so without hitting [the downsides of setters](https://symfony.com/doc/current/service_container/injection_types.html#setter-injection) is to use withers. Here would be an example:

```php
 class MyService
{
    use LoggerAwareTrait;
}

trait LoggerAwareTrait
{
    private $logger;

    /**
     * @required
     * @return static
     */
    public function withLogger(LoggerInterface $logger)
    {
        $new = clone $this;
        $new->logger = $logger;

        return $new;
    }
}

$service = new MyService();
$service = $service->withLogger($logger);
```

As you can see, this nicely solves the setter issues.

BUT how do you make the service container create such a service? Right now, you need to resort to complex gymnastic using the "factory" setting - manageable for only one wither, but definitely not when more are involved and not compatible with autowiring.

So here we are: this PR allows configuring such services seamlessly.
Using explicit configuration, it adds a 3rd parameter to method calls configuration: after the method name and its parameters, you can pass `true` and done, you just declared a wither:
```yaml
services:
    MyService:
        calls:
            - [withLogger, ['@logger'], true]
```

In XML, you could use the new `returns-clone` attribute on the `<call>` tag.

And when using autowiring, the code looks for the `@return static` annotation and turns the flag on if found.

There is only one limitation: unlike services with regular setters, services with withers cannot be part of circular loops that involve calls to wither methods (unless they're declared lazy of course).

Commits
-------

f455d1b [DI] Add support for "wither" methods - for greater immutable services
@nicolas-grekas nicolas-grekas deleted the di-withers branch April 3, 2019 10:52
@nicolas-grekas nicolas-grekas modified the milestones: next, 4.3 Apr 30, 2019
@fabpot fabpot mentioned this pull request May 9, 2019
wouterj added a commit to symfony/symfony-docs that referenced this pull request Nov 23, 2019
This PR was merged into the 4.3 branch.

Discussion
----------

[DIC] Static injection

Hi everyone,

As discussed with @nicolas-grekas, the DIC is planned to be capable of ensuring services immutability (using "wither" calls), the implementation can be found here:

- symfony/symfony#30212

Commits
-------

a598bc0 feat(DI): static injection
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.