Skip to content

[DependencyInjection] added a simple way to replace a service by keeping a reference to the old one #9003

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 1 commit into from

Conversation

fabpot
Copy link
Member

@fabpot fabpot commented Sep 12, 2013

Q A
Bug fix? no
New feature? no
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets #5920
License MIT
Doc PR not yet

TODO:

  • add a cookbook entry for this new feature
  • add support in all loaders/dumpers
  • add unit tests
  • see if there are use cases in Symfony that would benefit from this new feature
  • find the best name for this feature

When overriding an existing definition, the old service is lost:

$c->set('foo', 'stdClass');

// this is going to remove the old definition with the new one
// old definition is lost
$c->set('foo', 'stdClass');

Most of the time, that's exactly what you want to do, but sometimes, you might want to decorate the old one instead. In this case, the old service should be kept around to be able to reference it in the new one. This PR implements this new feature:

$c->set('foo', 'stdClass');

// I want to replace foo with a new one, but I need a reference of the old one (foo1.parent here)
$c->set('foo1', 'stdClass')->addArgument('foo1.parent')->setDecoratedService('foo');

Here is what's going on here: the setDecoratedService() method tells the container that the foo1 service should replace the foo service. By convention, the old foo service is going to be renamed foo1.parent, so I can inject it into my new service.

You can change the parent service name if you want to:

$c->set('foo1', 'stdClass')->addArgument('foo1.wooz')->setDecoratedService('foo', 'foo1.wooz');

Also, several service can decorate a single one and everything should work fine:

$c->set('foo', 'stdClass');

$c->set('foo1', 'stdClass')->addArgument('foo1.parent')->setDecoratedService('foo');
$c->set('foo2', 'stdClass')->addArgument('foo2.parent')->setDecoratedService('foo');

Note that this is different from the existing parent definition attribute, where a service inherit/extends another one (as the two services co-exist independently).

In XML, that would work like this:

<service id="foo1" class="stdClass" decorate="foo">
    <argument type="service" id="foo1.parent" />
</service>

public function setSquashedService($id, $renamedId = null)
{
if ($renamedId && $id == $renamedId) {
throw new \LogicException(sprintf('A squashed service must have a unique name ("%s")', $id));
Copy link
Member

Choose a reason for hiding this comment

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

This exception message looks weird to me

@stof
Copy link
Member

stof commented Sep 12, 2013

I like this implementation as it does not even require registering our own compiler pass.

@fabpot
Copy link
Member Author

fabpot commented Sep 12, 2013

Before implemeting the missing features (loaders/dumpers), I'd like to settle on a name for this feature. Is squash the best word to describe what we are doing here? Any other suggestions?

@stof
Copy link
Member

stof commented Sep 12, 2013

@fabpot what about decorate="foo" ? I think it would be easier to understand than squash="true"

@fabpot
Copy link
Member Author

fabpot commented Sep 12, 2013

decorate was on my list as well, I thought that it does not convey the fact that we are actually overriding/replacing the service that we are decorating. But I'm fine with decorate if that's the preferred choice.

@stof
Copy link
Member

stof commented Sep 12, 2013

@fabpot squash seems quite hard to understand IMO. another solution could be wrap

@igorw
Copy link
Contributor

igorw commented Sep 12, 2013

Have you considered a generic decorator compiler pass? Something like a generic version of this. Could possibly even be extended with priorities.

The basic premise is that adding tags (metadata) is much more generic and requires less adjustments to loaders/dumpers/schemata than adding a new service attribute.

I agree that if the intent of this feature is decoration, then that term should be used.

@fabpot
Copy link
Member Author

fabpot commented Sep 12, 2013

@iGrow I'm not a big fan of using tags for built-in features. In fact (I might be wrong as I haven't checked), I think we don't have any tags in the component.

@stof
Copy link
Member

stof commented Sep 12, 2013

@igorw This is how I suggested to implement it in the original issue, by simply making the 3 ids configurable in my original code
But allowing to define it directly in the service definition is easier than registering a compiler pass each time IMO.

@fabpot we indeed don't have any tag defined in the DI component itself.

@Taluu
Copy link
Contributor

Taluu commented Sep 12, 2013

Hum, wrap or decorate seems better IMO. But something I don't really get ; you declare a new service, which will kinda redefine the parent (service "foo1" ?), or could it have the same name as the parent (which would then effectively erase the old one, unless we set the squash / decorator / wrapper / whatever on it) ? Or have I got something wrong along the way ?

@stof
Copy link
Member

stof commented Sep 12, 2013

@Taluu At the end of this, the foo service will be an alias of your foo1 service, and foo1.parent will be a private service with the original definition of foo, so that you can inject it as an argument in foo1 (which is needed to decorate it).
When using this, foo1 will generally be a private service (you have no reason to access it as `foo1directly but only by asking forfoo``.
If you reuse the id of the parent, it erases it while keeping no info at all about the parent, so you have no way to wrap the parent implementation (it does not exist anymore in the ContainerBuilder).

the goal of this feature is not to replace a service totally (which can already be done currently) but to apply a decorator on it.

@wcluijt
Copy link

wcluijt commented Sep 13, 2013

How about mimic or mimicked?

@fabpot
Copy link
Member Author

fabpot commented Sep 14, 2013

renamed squash to decorate and setSquashedService to setDecoratedService

@yjv
Copy link

yjv commented Sep 16, 2013

does decorates perhaps better convey the fact that service foo1 is decorating foo?

@toaotc
Copy link

toaotc commented Sep 17, 2013

what about intersect?
because of decorate is used very often (almost 150 times).

@yjv
Copy link

yjv commented Sep 17, 2013

It seems like all of these suggestions are of something the service definition should do but it seems like all the other attributes (name, class etc) are about describing what the definition is that's why i like say decorates over decorate.

@docteurklein
Copy link
Contributor

would it be possible for a (non shared) service to decorate many (non shared) services ?

@stof
Copy link
Member

stof commented Oct 25, 2013

@docteurklein I don't understand what you mean when saying "decorate many services" ?

@elnur
Copy link
Contributor

elnur commented Oct 25, 2013

What about transclude as the name for it?

This term is used in AngularJS when replacing a view element with something else and getting a copy/reference to the replaced element to be able to insert it inside the replacing element.

@docteurklein
Copy link
Contributor

@stof here's an example of the resulting object graph i'd like to have:

$firstVoter = new FirstVoter;
$secondVoter = new SecondVoter;

$registry->addVoter(new LoggerVoter($firstVoter));
$registry->addVoter(new LoggerVoter($secondVoter));

The LoggerVoter decorates $firstVoter and $secondVoter.

I hope what I say is comprehensible :)

@stof
Copy link
Member

stof commented Oct 25, 2013

@docteurklein You will need 2 service definitions (one per LoggerVoter) as you have 2 services (the can use the definition inheritance to share common stuff though).

@docteurklein
Copy link
Contributor

@stof I implemented the behavior I describe using a compiler pass (trying to make a generic version of @igorw and yours). I know tags are not the preferred solution, I just can't use something else ATM.

https://gist.github.com/docteurklein/7154758

@stof
Copy link
Member

stof commented Oct 25, 2013

@docteurklein you are doing the config in the opposite way in your case, which makes it much less useful as a sevrice needs to know who is decorating it (and it limits it to doing it once).

this feature is about making 1 service declare that it will overwirte another service using composition (and so we want to be able to access the previous definition, not just overwriting it totally by reusing the same id). This approach is much more flexible: the I18nRoutingBundle will be able to decorate the core router without requiring changing the core service, and applying both the i18n decorator and another decorator (doing something else) would work without having to make one bundle aware of the other one.
Note that I'm using the example of I18nRoutingBundle because https://github.com/BeSimple/BeSimpleI18nRoutingBundle is one of the bundle in which I implemented such decorator pattern using a compiler pass (not sure if it was the first one or no)


list ($decorated, $renamedId) = $decorated;
if (!$renamedId) {
$renamedId = $id.'.parent';
Copy link
Contributor

Choose a reason for hiding this comment

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

should this be .decorator instead of parent?

Copy link
Member

Choose a reason for hiding this comment

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

It is not the decorator. It is the decorated service.

I agree that .parent may not be the best choice when using composition through. .inner may be easier to understand

Copy link
Contributor

Choose a reason for hiding this comment

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

or .overridden

Copy link
Member

Choose a reason for hiding this comment

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

@cordoval It would be weird to use be_simple.i18n_router.overriden to store the router service being decorated by be_simple.i18n_router. .overridden would be even worse than .parent in term of understanding when looking at the bundle services.

Copy link
Contributor

Choose a reason for hiding this comment

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

.wrapped then?

Copy link
Member Author

Choose a reason for hiding this comment

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

renamed to .inner.

@fabpot
Copy link
Member Author

fabpot commented Apr 1, 2014

closing in favor of #10600

@fabpot fabpot closed this Apr 1, 2014
fabpot added a commit that referenced this pull request Apr 1, 2014
…service by keeping a reference to the old one (romainneutron)

This PR was merged into the 2.5-dev branch.

Discussion
----------

[DependencyInjection] added a simple way to replace a service by keeping a reference to the old one

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

This PR replaces #9003. Here's the todo list:
 - [x] add a cookbook entry for this new feature
 - [x] add support in all loaders/dumpers
 - [x] add unit tests
 - [x] see if there are use cases in Symfony that would benefit from this new feature
 - [x] find the best name for this feature

I've implemented YAML and XML Loader / Dumper.
From what I see, PhpDumper, PhpLoader, IniLoader, GraphvizDumper do not require an update, am I wrong?

Commits
-------

140f807 [DependencyInjection] Update dumpers and loaders, add unit tests
1eb1f4d [DependencyInjection] added a simple way to replace a service by keeping a reference to the old one
lunetics added a commit to prooph/service-bus-symfony-bundle that referenced this pull request Apr 21, 2017
Bad practice and error prone:
also see symfony/symfony#11881
and symfony/symfony#9003
codeliner pushed a commit to prooph/service-bus-symfony-bundle that referenced this pull request Sep 29, 2017
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.

10 participants