PHP-DI
PHP-DI copied to clipboard
Extending other object definitions (e.g. parent classes)
Hi. Is there a way in PHP-DI to define dependencies for an abstract object? Most logical way, to define parameters for a constructor for abstract object which would later be resolved for child object, does not work:
abstract class AbstractObject {
protected $a;
public function __construct($a) { $this->a = $a; }
}
class ConcreteObject extends AbstractObject {}
$builder = new DI\ContainerBuilder;
$builder->useAutowiring(true);
$builder->addDefinitions([
'AbstractObject' => DI\object()->constructorParameter('a', 'abc')
]);
$container = $builder->build();
$container->make('ConcreteObject');
This code result in
PHP Fatal error: Uncaught exception 'DI\Definition\Exception\DefinitionException'
with message 'Entry ConcreteObject cannot be resolved: The parameter 'a' of
AbstractObject::__construct has no value defined or guessable
Full definition:
Object (
class = ConcreteObject
scope = singleton
lazy = false
__construct(
$a = #UNDEFINED#
)
)'
Am I missing something from the documentation or PHP-DI is unable at this point to realize this scenario?
Hi! Sorry for the late answer (long holidays). You are right this doesn't work at the moment. This is something I considered adding in 5.0 but it was too much work (considering all the rest of the features I wanted to do).
Ideally it would be something like this (unless a better solution comes up):
[
'AbstractObject' => DI\object()
->constructorParameter('a', 'abc'),
'ConcreteObject' => DI\object()
->extends('AbstractObject'),
]
That way it's explicit (no surprises) and you could extend any definition, e.g.:
[
'logger.base' => DI\object()
->constructorParameter('a', 'abc'),
'logger' => DI\object()
->extends('logger.base'),
]
To be transparent, one main reason I don't consider this a high priority feature is that in theory an abstract class/interface shouldn't know (or care) how its implementations are constructed. Dependencies are an implementation detail, so defining dependencies for the concrete classes is "the right way".
But I can understand how it could be useful to factorize some configuration sometimes.
Pasting this here FWIW:
I'd prefer to just put the definitions on the base class, and have the container automatically read/extend them without having to add anything for the subclass. This is possible with Aura.Di using $container->setters.
At first I thought this sounded like a great feature and classes should implicitly extend their parent definitions. However, after thinking it through I think such a feature could be far too easily abused and I question whether it's good design at all. For instance, one might create a class hierarchy with nodes whose purpose is to override their parent definitions.
As an alternative, a goal of reusing definitions as opposed to extending other definitions may solve this problem. This can actually be achieved quite readily in 5.x using a custom helper function.
namespace ABCDI;
function object() {
return DI\object()->constructorParameter('a', 'abc');
}
return [
'AbstractObject' => ABCDI\object(),
'ConcreteObject' => ABCDI\object()
];
Another approach with the same goal of reuse would be to allow a definition to apply to multiple classes. The syntax might look like one of the following examples.
return [
'(Abstract|Concrete)Object' => DI\object()->constructorParameter('a', 'abc')
];
or
return [
[
'types' => ['AbstractObject', 'ConcreteObject'],
'definition' => DI\object()->constructorParameter('a', 'abc')
]
];
@cogentParadigm Function helpers are a really good idea! I've never thought of that, but it's really simple, and clear. It's also really easy to set up, we can write such functions directly above the return of the definition array so it's clear.
I'm also more and more convinced that writing reusable definitions (like with parent classes) is complex and confusing.
Extending the definition of another config file could be another use case, but I don't think many containers support that. And when there's no support for that, it's easily solved:
- either redefine the service completely
- or make the parameter you want to override a separate container entry that you can override
That's good enough.
However autowiring is a separate use case and I think we need to keep it (i.e. extend from the autowired definition and refine it), but it can be a specific name:
MyClass::class => autowire()
->constructorParameter('a', get('abc')),
This seems to work (as an alternative to configuring the base class):
'*\Controller\*Controller' => DI\autowire()->method('setView', DI\get('view'))
@mnapoli Would it make sense to have a way to "hint" to the compiler that a certain set of classes should be included? (f.e. when using wildcards and/or auto-wiring)?