Problems with complexity in dependency conditional injection with PHP-DI

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!