-
-
Notifications
You must be signed in to change notification settings - Fork 324
Extend and decorate definitions (add new items to arrays, inherit definitions, decorate objects or factories…) #214
Description
Modular systems (aka plugins/bundles/extensions) usually work like this: where there is a base config and modules can override or extend it.
Overriding a definition is supported (just set a definition for the same name). However extending is not.
We could extend several kind of definitions:
- arrays: add new items (e.g. a list of log handlers, a module wants to add a new handler to the list)
- factories: we could want to wrap the previous factory (i.e. callable) in another factory to be able to manipulate or decorate the result (e.g. manipulate the event dispatcher to register new listeners)
- objects: we could want to decorate an object by another one (e.g. decorate an object with a proxy that cache method calls, …)
The big question is the syntax: how to make it simple and explicit?
The other question is performances… Should definitions be compiled to PHP code to guarantee minimum overhead? I guess that can be decided once we have a working solution depending on whether performance is a problem or not.
Symfony tags
Symfony's approach is to use tags: you add tag to a container entry. Then you write a compiler pass that collects all instances having a specific tag, and you do something with them.
Pros:
- no need to think of a syntax to decorate/manipulate/extend entries
- developers don't need to understand the principles of decorating objects or factories…
Cons:
- not as explicit: instead of configuring a list of "log handlers", you need to tag objects as "log handler" and write some stuff to inject all objects with that tag: setting up "a list of something" is native PHP stuff, "tagging" and "fetching tagged services" is not a PHP concept, it's less intuitive
- you likely need compiler passes (PHP-DI is not compiled, yet)
- you need to write compiler passes, which is very complex (you need to understand a lot more stuff about the container)
- doesn't work for the non-compiled container (which is confusing in Symfony because you can use the container not compiled)
Spring collections
For reference, Spring allows to define collections (list/map…).
How to extend a list in spring config?
Guice multibindings
Guice allows to define lists too, and let it possible for module (in a modular system) add items. They call that Multibindings.
By the way Guice has a "module" system that is pretty similar to what we want to achieve here (e.g. this article). A stripped down version (but understandable for PHP devs) can be found in Ray.Di which is inspired from Guice.
What follows is an attempt at a syntax. This is just a suggestion, I've been thinking about that for months now and there doesn't seem to be an obvious solution…
Arrays
Now that #207 is implemented, we can define collections/arrays (in v5):
'log.handlers' => [
DI\link(FileHandler::class),
DI\link(DatabaseHandler::class),
],To add entries in other config files:
'log.handlers' => DI\extend()->add([
DI\link(LogstashHandler::class),
]),We could go with a simpler DI\add() function:
'log.handlers' => DI\add([
DI\link(LogstashHandler::class),
]),but maybe being consistent with what follows is better?
Factories
Note: with a factory, we can extend any other definitions (that's cool!). E.g. here we'll extend an object definition:
ProductRepositoryInterface::class => DI\object(DatabaseProductRepository::class),To extend or decorate the previous one:
ProductRepositoryInterface::class => DI\extend()->factory(function($previous) {
return new CachedProductRepository($previous);
}),Alternative syntax:
ProductRepositoryInterface::class => DI\decorate(function($previous) {
return new CachedProductRepository($previous);
}),Objects
ProductRepositoryInterface::class => DI\object(DatabaseProductRepository::class),Note that currently for DI\object() it already extends previous definitions instead of overriding them. The reason for this is that we sometimes want to extend the Autowiring or Annotation definition (just set a parameter that is not type-hinted for example).
To extend another definition explicitly:
ProductRepositoryInterface::class => DI\object(CachedProductRepository::class)
->extend(/* provide an entry name or nothing */)
->constructor(DI\decorated()),Or with DI\extend():
ProductRepositoryInterface::class => DI\extend()->object(CachedProductRepository::class)
->constructor(DI\decorated()),or DI\link() without service ID could be used to reference the previous definition of the current service?