1

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 DI\ContainerBuilder;
use Psr\Container\ContainerInterface;

$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' => \DI\create(XmlParentLoader::class)
        ->constructor(\DI\get('xml.child.loader.model'), \DI\get('xml.source')),
    'xml.child.loader.model' => \DI\create(XmlChildLoader::class)
        ->constructor(\DI\get('xml.source')),
    'sql.parent.loader.model' => \DI\create(SqlParentLoader::class)
        ->constructor(\DI\get('sql.child.loader.model'), \DI\get('sql.source')),
    'sql.child.loader.model' => \DI\create(SqlChildLoader::class)
        ->constructor(\DI\get('sql.source')),
    'xml.parent.loader.target' => \DI\create(XmlParentLoader::class)
        ->constructor(\DI\get('xml.child.loader.target'), \DI\get('xml.source')),
    'xml.child.loader.target' => \DI\create(XmlChildLoader::class)
        ->constructor(\DI\get('xml.source')),
    'sql.parent.loader.target' => \DI\create(SqlParentLoader::class)
        ->constructor(\DI\get('sql.child.loader.target'), \DI\get('sql.source')),
    'sql.child.loader.target' => \DI\create(SqlChildLoader::class)
        ->constructor(\DI\get('sql.source')),
    MyService::class => \DI\create()
        ->constructor(\DI\get('parent.model'), \DI\get('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!

4
  • 1
    Sounds like you need to check out why you should use composition over inheritance Commented Mar 26 at 9:19
  • 1
    For the sql.source and xml.source parameters, could you use the #[Inject] attribute in the class definitions which should better allow for autowiring? Commented Mar 26 at 13:21
  • The Loader class itself doesn't know where its source data comes from; it's injected based on settings. When used as $model, it might load from an XML file (e.g., model.xml) or an SQL database. Similarly, as $target, it could use target.xml or a different database. This flexibility means the source varies by context ($model or $target) and type (XML or SQL). Thus, #[Inject] alone can't handle this dynamic, context-dependent injection. @ChrisHaas Commented Mar 28 at 23:58
  • This question might be a bit opinion based so I wonder if it would be better suited for discussions? Personally, I would ask myself: "Do I really need to have definition for each variation of loader in DI container? Or would one factory that would create instance of MyService class based on current configuration be enough?" Commented Apr 17 at 9:44

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.