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!