I’m working on a PHP project where I’m using PHP-DI for dependency injection, but I’m struggling to configure a hierarchical structure of classes cleanly and scalably. I have interfaces like ParentLoader
and ChildLoader
, with concrete implementations for different types: XML (e.g., XmlParentLoader
, XmlChildLoader
) that depend on a DOMDocument
, and SQL (e.g., SqlParentLoader
, SqlChildLoader
) that depend on a PDO
. These loaders are hierarchical (ParentLoader
depends on ChildLoader
), and I need to inject them conditionally based on a configuration (e.g., "type" => "xml"
or "type" => "sql"
from a settings file) into a service that uses two ParentLoader
instances.
Here’s a simplified example of my classes:
interface ParentLoader
{
public function load(): array; // Just an example
}
interface ChildLoader
{
public function load(): array; // Just an example
}
class XmlParentLoader implements ParentLoader
{
public function __construct(private XmlChildLoader $childs, private DOMDocument $source)
{
}
public function load(): array
{
// Logic using $source and $childs
return [];
}
}
class XmlChildLoader implements ChildLoader
{
public function __construct(private DOMDocument $source)
{
}
public function load(): array
{
// Logic using $source
return [];
}
}
class SqlParentLoader implements ParentLoader
{
public function __construct(private SqlChildLoader $childs, private PDO $source)
{
}
public function load(): array
{
// Logic using $source and $childs
return [];
}
}
class SqlChildLoader implements ChildLoader
{
public function __construct(private PDO $source)
{
}
public function load(): array
{
// Logic using $source
return [];
}
}
class MyService
{
public function __construct(
private ParentLoader $model, // Based on "model" from settings
private ParentLoader $target // Based on "target" from settings
) {
}
public function execute(): array
{
$dataModel = $this->model->load();
$dataTarget = $this->target->load();
// Combine or process data
return array_merge($dataModel, $dataTarget);
}
}
I’m using PHP-DI to conditionally inject these dependencies based on a configuration. For example, if the config specifies "model" => ["type" => "xml"]
, $model
should use XmlParentLoader
and XmlChildLoader
with a DOMDocument
; if "target" => ["type" => "sql"]
, $target
should use SqlParentLoader
and SqlChildLoader
with a PDO
. Here’s what I tried in my ContainerBuilder
:
use DIContainerBuilder;
use PsrContainerContainerInterface;
$builder = new ContainerBuilder();
$builder->addDefinitions([
'xml.source' => function () {
$dom = new DOMDocument();
$dom->load('some/path.xml'); // Simplified for example
return $dom;
},
'sql.source' => function () {
return new PDO('mysql:host=localhost;dbname=test', 'user', 'pass'); // Simplified
},
'parent.model' => function (ContainerInterface $container) {
$settings = $container->get('settings'); // Assume settings has "model" and "target"
$type = $settings['model']['type'];
if ($type === 'xml') {
return $container->get('xml.parent.loader.model');
} elseif ($type === 'sql') {
return $container->get('sql.parent.loader.model');
}
throw new Exception('Invalid type in model configuration');
},
'parent.target' => function (ContainerInterface $container) {
$settings = $container->get('settings');
$type = $settings['target']['type'];
if ($type === 'xml') {
return $container->get('xml.parent.loader.target');
} elseif ($type === 'sql') {
return $container->get('sql.parent.loader.target');
}
throw new Exception('Invalid type in target configuration');
},
'xml.parent.loader.model' => DIcreate(XmlParentLoader::class)
->constructor(DIget('xml.child.loader.model'), DIget('xml.source')),
'xml.child.loader.model' => DIcreate(XmlChildLoader::class)
->constructor(DIget('xml.source')),
'sql.parent.loader.model' => DIcreate(SqlParentLoader::class)
->constructor(DIget('sql.child.loader.model'), DIget('sql.source')),
'sql.child.loader.model' => DIcreate(SqlChildLoader::class)
->constructor(DIget('sql.source')),
'xml.parent.loader.target' => DIcreate(XmlParentLoader::class)
->constructor(DIget('xml.child.loader.target'), DIget('xml.source')),
'xml.child.loader.target' => DIcreate(XmlChildLoader::class)
->constructor(DIget('xml.source')),
'sql.parent.loader.target' => DIcreate(SqlParentLoader::class)
->constructor(DIget('sql.child.loader.target'), DIget('sql.source')),
'sql.child.loader.target' => DIcreate(SqlChildLoader::class)
->constructor(DIget('sql.source')),
MyService::class => DIcreate()
->constructor(DIget('parent.model'), DIget('parent.target'));
]);
This works, but it’s a mess. I have to manually define every loader in the hierarchy (e.g., xml.parent.loader.model
, xml.child.loader.model
, etc.) and their dependencies (DOMDocument
or PDO
) for each source (model
and target
). It gets worse when the hierarchy grows (e.g., adding a GrandChildLoader
). My goal is a solution where I don’t need to manually define every loader and its dependencies in the DI container for each source and type combination, making use of autowire for example, which currently I can’t because of the “conditional injection”. If wasn’t for the “source” parameter, I would be able to use autowire, but I need the source.
Is there a better way to structure this with or without PHP-DI? Maybe a factory pattern or some trick to avoid these repetitive definitions? I’d love to leverage autowiring more, but the specific sources (DOMDocument
and PDO
) make it tricky. Any ideas would be awesome!
Thanks!