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

Skip to content

[DI] Replace container injection by explicit service locators #21553

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
Feb 13, 2017

Conversation

chalasr
Copy link
Member

@chalasr chalasr commented Feb 7, 2017

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

This adds a new ServiceLocatorArgument (!service_locator) argument type which takes a list of services, meant to be used as a concrete service locator in order to avoid the remaining needs for injecting the container when it's only a matter of dependency lazy-loading.

Config:

App\FooBar: [!service_locator { key1: '@service1', key2: '@service2' }]
<service class="App\FooBar" public="false">
    <argument type="service-locator">
        <argument type="service" key="key1" id="service1"/>
        <argument type="service" key="key2" id="service2"/>
     </argument>
</service>
new ServiceLocatorArgument(array('key1' => new Reference('service1'), 'key2' => new Reference('service2'));

Usage:

$locator->has('key1') // true
$locator->has('service1') // false, the defined key must be used
$locator->get('key1'); // service1 instance
$locator->get('service1'); // exception
$locator->has('not-specified') // false
$locator->get('not-specified'); // exception

We have some concrete use cases in the core where this would be useful (see e.g. SecurityBundle's FirewallMap), same in userland/3rd party code (see related RFC).

@ogizanagi
Copy link
Contributor

ogizanagi commented Feb 7, 2017

WDYT about using tags on each service available through a locator instead of listing the services as the locator arguments?

Something like:

<service id="MyServiceAvailableThroughALocator" class="...">
    <!-- ... -->
    <tag name="service.locator" locator="locator_name" alias="service_alias_within_the_locator" />
</service>

Then any service can declare it is available through a named locator, and can provide an alias used to identify it within the locator itself.

@jvasseur
Copy link
Contributor

jvasseur commented Feb 7, 2017

Does it allow mapping keys to services like this ?

App\FooBar:
    arguments:
        - =locator:
            foo: '@service1'
            bar: '@service2'

I think most of the time it's what is needed since you need to map the service id to some kind of alias used by the component receiving the locator.

@chalasr
Copy link
Member Author

chalasr commented Feb 7, 2017

@ogizanagi As discussed, I find more adapted to let the locator consumers defining explicitly which services they need rather than asking each service to define from which service locator(s) they can be accessed. The service being already in the container, to me it's fine to request it, as when using other lazy arguments. I don't see the need for extending the ServiceLocator itself in userland, nor restricting which services should be accessed by a locator elsewhere than the service using the locator itself, even if the idea is interesting.
Discussion stays open if others think it's a good idea.

@jvasseur Right now it doesn't, but it makes sense to me and adding it should not be hard.

@stof
Copy link
Member

stof commented Feb 7, 2017

@ogizanagi when a service needs a service locator, it has a single service locator, not multiple ones.
Services needing a service locator will often fill it using a tag. But it is better to keep using separate tags for each use case instead of transforming them all into <tag name="service.locator" locator="..." />

@ogizanagi
Copy link
Contributor

ogizanagi commented Feb 7, 2017

@stof

when a service needs a service locator, it has a single service locator, not multiple ones.
Services needing a service locator will often fill it using a tag. But it is better to keep using separate tags for each use case instead of transforming them all into

I think you misunderstood me (or am I misunderstanding you?). I updated my comment since, though, in order to show a more complete sample.
I don't want to tag a service needing a locator instance, but instead tag each service being available through a locator (maybe the service.locator tag name isn't right).
This means being able to extend the list of available services of a well identified locator on userland or from third-party bundles.

@stof
Copy link
Member

stof commented Feb 7, 2017

@ogizanagi bundles wanting to make the list extendable should use their own tags, allowing to keep things more understandable, and more flexible (the bundle can do extra processing of the tagged services in the compiler pass, or expect additional attributes).
Forcing to handle all collecting in a single compiler pass defined in Symfony itself is less powerful for bundles, and has a worse DX (and it leaks implementation details about the usage of the ServiceLocator feature in the bundle, while the bundle should be free to refactor the way it actually injects the services).

@stof
Copy link
Member

stof commented Feb 7, 2017

thus, it would also force to add an identifier on locators (to reference them in your proposed tag), which goes against the architecture of arguments

@chalasr
Copy link
Member Author

chalasr commented Feb 7, 2017

This means being able to add services to a well identified locator on userland or from third-party bundles.

I'm not sure about the usefulness of defining service locators as services, as use cases are quite specific (most of the times only one service needs a given range to be accessible f.i. CommandBus, FirewallMap).

If one needs two services with the same locator, then the services should be quite related IMO, so it can be solved by defining a parent service defining the locator argument and children would inherit this argument (e.g. Console command needing access to console helpers).
Right now I don't see a concrete need for having well identified locators nor creating custom ServiceLocator implementations in userland.

@ogizanagi
Copy link
Contributor

@stof : Understood. Finally, I agree. Thanks.

@chalasr
Copy link
Member Author

chalasr commented Feb 7, 2017

@jvasseur Updated to support mapping keys to services

$getterCode .= ' }';

$hasserCode = 'function ($id) {'."\n";
$hasserCode .= sprintf(' $references = array(%s);', implode(', ', $servicesMap));
Copy link
Contributor

@Taluu Taluu Feb 7, 2017

Choose a reason for hiding this comment

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

sprintf('$references = %s;', var_export($services, true)); ?

(Sorry, moved my comment here, as it seemed more appropriate)

Copy link
Member Author

Choose a reason for hiding this comment

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

@Taluu I tried it and in fact we only need a string map here, while var_export dumps Reference objects. Also looping over references is actually needed for generating the getter code so I think it can be kept as is, unless another existing helper exists for such

$hasserCode = 'function ($id) {'."\n";
$hasserCode .= sprintf(' $references = array(%s);', implode(', ', $servicesMap));
$hasserCode .= "\n\n".' return isset($references[$id]) || in_array($id, $references, true);';
$hasserCode .= "\n }";
Copy link
Contributor

Choose a reason for hiding this comment

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

More a question of tastes and all, but instead of several concatenations, I would have used a nowdoc instead :

<?php

// ...

$hasserCode = <<<'EOD'
$references = ...
EOD;

Not excluding a call to sprintf afterwards, of course

*
* @author Robin Chalas <[email protected]>
*/
class LocatorArgument extends IteratorArgument
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 not extend IteratorArgument. A LocatorArgument is not an IteratorArgument (something checking for iterator arguments should probably not be triggered for locator arguments)

Copy link
Member Author

Choose a reason for hiding this comment

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

It was a mistake, fixed.

@chalasr chalasr force-pushed the service-locator branch 5 times, most recently from 5a6a226 to b2b185f Compare February 7, 2017 19:15
@nicolas-grekas nicolas-grekas added this to the 3.x milestone Feb 7, 2017
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.

the map must be explicitly keyed

private $values;

/**
* @param Reference[] $values
Copy link
Member

Choose a reason for hiding this comment

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

doesn't have to be only references - the yaml loader currently supports any kind, which is nice I think - same for the xml loader

Copy link
Member Author

@chalasr chalasr Feb 7, 2017

Choose a reason for hiding this comment

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

Does that mean $v = $v instanceof Reference ? $v : new Reference($v); should be used, or do you have something else in mind (keep scalars, parameters)?

Copy link
Member

Choose a reason for hiding this comment

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

Nope, the values can really be anything, references or arrays or scalars

Copy link
Member Author

@chalasr chalasr Feb 7, 2017

Choose a reason for hiding this comment

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

Given ContainerInterface::get() must return an object, scalars should be returned by getParameter(), right?

/**
* @author Robin Chalas <[email protected]>
*/
final class ServiceLocator
Copy link
Member

Choose a reason for hiding this comment

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

should implement ContainerInterface to me

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 mark it as internal also so that we discourage it from being used in type hints?

Copy link
Member Author

@chalasr chalasr Feb 7, 2017

Choose a reason for hiding this comment

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

should implement ContainerInterface to me

I hesitated to, sounds good to me. However, since yaml/xml file loaders can return references, should we just ignore their invalid behavior and use the one passed to ServiceLocator::get()?
Also, should it make some noop for e.g. get/setParameter(), or should it truly implement that?

Copy link
Contributor

@jvasseur jvasseur Feb 7, 2017

Choose a reason for hiding this comment

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

Maybe we could make it implement the ArrayAccess interface. It would allow consumer interfaces to use array|ArrayAccess as a type hint and use it like an array.

Copy link
Contributor

Choose a reason for hiding this comment

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

@nicolas-grekas : Just to be sure: You're talking about PSR-11's ContainerInterface, right?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes psr-11 will solve part of the problem (it solves the coupling with the symfony framework).

But let's say we use this feature for some symfony component (Form Types could be an example), if i want to use this component outside the symfony framework I would need to : require an psr-11 container, instantiate it with the services I want and then pass it to the component when I could just use a PHP array.

Copy link
Contributor

Choose a reason for hiding this comment

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

You're just looking for psr-11

PSR-11 is still about DI containers. It semantically couples the code with dependency injection container concept. This is odd: I don't want to know whether it is container or simple array-like map of "command handlers". DI container is just specific type of a map.

Copy link
Member

Choose a reason for hiding this comment

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

@jvasseur @unkind you're missing eg exceptions - the wording about them in PSR-11 is a requirement here also. ArrayAccess is not an interface - it's just an magical incantation for the engine.

to use this component outside the symfony framework I would need to : require an psr-11 container

???: this would be an implementation of PSR-11, so you'd need nothing else really.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm talking about using a component outside of a DI container so yes you would need to require one.

Copy link
Contributor

Choose a reason for hiding this comment

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

you're missing eg exceptions - the wording about them in PSR-11 is a requirement here also

What exactly do we miss? You shouldn't access array field without checking whether it exists. So you won't face the NotFoundException (the only checked exception in PSR-11, btw) or your code is just broken.

} elseif ($value instanceof LocatorArgument) {
$references = $value->getValues();
$value = new ServiceLocator(function ($id) use ($references) {
return isset($references[$id]) || in_array($id, array_map(function ($ref) { return (string) $ref; }, $references), true);
Copy link
Member

Choose a reason for hiding this comment

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

the in_array part looks wrong to me - keys must be the ony way to reference values

return $this->get($references[$id], $references[$id]->getInvalidBehavior());
}

return $this->get($id, $references[array_search($id, $references)]->getInvalidBehavior());
Copy link
Member

Choose a reason for hiding this comment

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

keys must be the only way to reference values

$getterCode .= $singleGetter;
}
$getterCode .= ' }';
$hasserCode = 'function ($id) {'."\n";
Copy link
Member

Choose a reason for hiding this comment

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

given that keys should be the only way to reference values, instead of hasser+getter, I'd suggest generating a single Closure[], keyed by "public" service name

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Feb 7, 2017

This should be marked as experimental also.
And thanks for working on it :)

@chalasr
Copy link
Member Author

chalasr commented Feb 7, 2017

Status: needs work

😅

private $values;

/**
* @param array $values An array of mixed entries indexed by identifier
Copy link
Member

Choose a reason for hiding this comment

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

That does not help that much. But now I realize that I've missed something in this PR. This allows to inject both services and parameters, right? But a service locator is really about giving access to services only, not parameters. Any use case for having parameters as well?

Copy link
Member

Choose a reason for hiding this comment

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

That's PSR-11 - return type of "get" is mixed for this reason - same as eg Pimple ;)
I see no reason to forbid non-objects - that won't provide any technical benefit (even if I won't use that much myself).

Copy link
Member Author

@chalasr chalasr Feb 13, 2017

Choose a reason for hiding this comment

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

Please see #21553 (comment). There's no explicit notion of parameters here, only services to get() but services can be any value. I have no strong opinion about, but that's PSR-11 compliant and I don't see any reason to forbid it in fact.

Copy link
Member

Choose a reason for hiding this comment

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

Let me be very clear: I don't care about PSR-11. Let's go back to the basics instead. A service locator must return services. So, let's do that.

And as far as Pimple is concerned, please keep in mind that this is a toy, a joke, not the holy grail of dependency injection, so mentioning it here is pointless :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Docblock and tests updated to expect only references.

Copy link
Member Author

Choose a reason for hiding this comment

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

It now throws on non-reference values

@chalasr chalasr force-pushed the service-locator branch 2 times, most recently from bb455aa to 34443bb Compare February 13, 2017 09:47
private $values;

/**
* @param array $values An array of references entries indexed by identifier
Copy link
Member

Choose a reason for hiding this comment

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

Reference[] + validity check in the constructor? maybe also in loaders if that can provide more useful error messages?

Copy link
Member Author

Choose a reason for hiding this comment

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

done

@chalasr chalasr force-pushed the service-locator branch 2 times, most recently from d4db8fc to 1c0c22d Compare February 13, 2017 10:02
[SecurityBundle] Avoid container injection in FirewallMap
@fabpot
Copy link
Member

fabpot commented Feb 13, 2017

Thank you @chalasr.

@fabpot fabpot merged commit e7935c0 into symfony:master Feb 13, 2017
fabpot added a commit that referenced this pull request Feb 13, 2017
…ocators (chalasr)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[DI] Replace container injection by explicit service locators

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

This adds a new `ServiceLocatorArgument` (`!service_locator`) argument type which takes a list of services, meant to be used as a concrete service locator in order to avoid the remaining needs for injecting the container when it's only a matter of dependency lazy-loading.

Config:
```yaml
App\FooBar: [!service_locator { key1: '@Service1', key2: '@service2' }]
```

```xml
<service class="App\FooBar" public="false">
    <argument type="service-locator">
        <argument type="service" key="key1" id="service1"/>
        <argument type="service" key="key2" id="service2"/>
     </argument>
</service>
```

```php
new ServiceLocatorArgument(array('key1' => new Reference('service1'), 'key2' => new Reference('service2'));
```

Usage:
```php
$locator->has('key1') // true
$locator->has('service1') // false, the defined key must be used
$locator->get('key1'); // service1 instance
$locator->get('service1'); // exception
$locator->has('not-specified') // false
$locator->get('not-specified'); // exception
```

We have some concrete use cases in the core where this would be useful (see e.g. SecurityBundle's FirewallMap), same in userland/3rd party code (see related RFC).

Commits
-------

e7935c0 [DI] Replace container injection by explicit service locators
@chalasr chalasr deleted the service-locator branch February 14, 2017 15:43
@lsmith77
Copy link
Contributor

https://github.com/liip/LiipContainerWrapperBundle :)

@chalasr
Copy link
Member Author

chalasr commented Feb 14, 2017

@lsmith77 A great precursor :)

fabpot added a commit that referenced this pull request Feb 28, 2017
…ocators (nicolas-grekas, chalasr)

This PR was merged into the 3.3-dev branch.

Discussion
----------

Remove some container injections in favor of service locators

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #21553 (comment)
| License       | MIT
| Doc PR        | n/a

Commits
-------

8293b75 Replace some container injections by service locators
0be9ea8 [EventDispatcher] Fix abstract event subscribers registration
symfony-splitter pushed a commit to symfony/event-dispatcher that referenced this pull request Feb 28, 2017
…ocators (nicolas-grekas, chalasr)

This PR was merged into the 3.3-dev branch.

Discussion
----------

Remove some container injections in favor of service locators

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | symfony/symfony#21553 (comment)
| License       | MIT
| Doc PR        | n/a

Commits
-------

8293b753cf Replace some container injections by service locators
0be9ea8ba1 [EventDispatcher] Fix abstract event subscribers registration
symfony-splitter pushed a commit to symfony/http-kernel that referenced this pull request Feb 28, 2017
…ocators (nicolas-grekas, chalasr)

This PR was merged into the 3.3-dev branch.

Discussion
----------

Remove some container injections in favor of service locators

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | symfony/symfony#21553 (comment)
| License       | MIT
| Doc PR        | n/a

Commits
-------

8293b75 Replace some container injections by service locators
0be9ea8 [EventDispatcher] Fix abstract event subscribers registration
symfony-splitter pushed a commit to symfony/framework-bundle that referenced this pull request Feb 28, 2017
…ocators (nicolas-grekas, chalasr)

This PR was merged into the 3.3-dev branch.

Discussion
----------

Remove some container injections in favor of service locators

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | symfony/symfony#21553 (comment)
| License       | MIT
| Doc PR        | n/a

Commits
-------

8293b753cf Replace some container injections by service locators
0be9ea8ba1 [EventDispatcher] Fix abstract event subscribers registration
@nicolas-grekas nicolas-grekas modified the milestones: 3.x, 3.3 Mar 24, 2017
@fabpot fabpot mentioned this pull request May 1, 2017
javiereguiluz added a commit to symfony/symfony-docs that referenced this pull request May 4, 2017
…guiluz)

This PR was merged into the master branch.

Discussion
----------

[DI] Add section about service locators

Adds documentation for symfony/symfony#21553 and symfony/symfony#22024.
Any suggestion will be much appreciated, as usual.

Commits
-------

fa19770 Fix service locator declaration
f5e4942 Rewords
5efacd0 [DI] Add section about Service Locators
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.