How to take Advantage of 3rd party Dependency Injection Container

When Nuno sent me a Pest plugin for type coverage that runs TomasVotruba/type-coverage, I looked for the PHPStan container use.

Why? Because the type-coverage package is PHPStan rules that easily plugin into PHPStan. But what if you want to use them in a tool that has a different container?

I've found the solution the hard way - so it might be useful to share it with you to save you the trouble.

In the example above, there was no PHPStan container. The PHPStan services are made from scratch, which is a painful process and can lead to bugs in the next PHPStan patch version.


But don't narrow your focus only on this specific situation. It can be anything from:

...and so on.


It's not universal. You can't connect every PHP container into another container.


Some projects even don't have containers and are made manually with new instances.


Minimal Requirements

Despite the messy complexity and wide variety of PHP containers, there are compatibility requirements to be able to connect 2 containers:

Have you met these conditions? We're good to go.


From my experience, Laravel, Symfony, and Nette containers fit perfectly and are mutually reusable.


Practical Use Case: We need PHPStan service in Rector

Let's look at a practical example - we need a PHPStan PHPStan\Analyser\NodeScopeResolver service inside a Rector container.


How does injecting from another container work?


Rector uses Laravel container since 0.18, so we build the example using illuminate/container package (same pseudo-code works for Symfony, too):

new Illuminate\Container\Container;

use PHPStan\DependencyInjection\Container as PHPStanContainer;
use PHPStan\Analyser\NodeScopeResolver::class;

$container = new Container();

// we register a service from PHPStan that we want in our project
$container->singleton(NodeScopeResolver::class, function (Container $container) {
    // we ask for the PHPStan container
    $phpstanContainer = $container->make(PHPStanContainer::class);

    return $phpstanContainer->getByType(NodeScopeResolver::class);
});

We asked for PHPStanContainer service, but where does it come from?

Fortunately, PHPStan has a ContainerFactory, so we register it and create PHPStan container:

use PHPStan\DependencyInjection\ContainerFactory as PHPStanContainerFactory;
use PHPStan\DependencyInjection\Container as PHPStanContainer;

// ...

$container->singleton(PHPStanContainer::class, function (Con) {
    $phpStanContainerFactory = new PHPStanContainerFactory();

    return $phpStanContainerFactory->create(
        '/tmp/our_phpstan',
        additionalConfigFiles: [],
        analysedPaths: []
    );
});

Now, when we ask for PHPStanContainer, it will be created once and stored in our container.

Then PHPStan container will provide any service we need!


For more inspiration, check this pattern in Rector.


Make your Tools with Usability in Mind

However, this is not standard - not every tool ships with a container factory class. That means we have to manually dig in and construct the whole service and its tree with new instances.

PHPStan, ECS, Rector and all the tools I make have a container factory as first class citizens.

You can require them in your project and build on top of their features quickly.


Next time you make a tool, consider dropping in a container factory class too. Even if it is bare new instances with a single get() method, it will allow your fellow PHP developers to use them easily.


Happy coding!




Do you learn from my contents or use open-souce packages like Rector every day?
Consider supporting it on GitHub Sponsors. I'd really appreciate it!