A dependency injection container for PHP, please enjoy responsibly.
OpenContainer was created as an attempt to leverage strict type checking available in full-featured PHP development environments, such as JetBrains PHPStorm, or the native strict type checking of PHP 7+, when managing dependencies from a centralized container. In addition, it contains some experiments with reflection and proxies in order to avoid problems when circular dependencies are introduced in the container's dependency chain.
In addition to exposing dependencies as type-checked class properties, OpenContainer is also compatible with the PSR-11 Container Interface, for interoperability with several PHP frameworks.
- PHP 7.4 (main, 2.x)
Use Composer. There are two ways to add OpenContainer to your project.
From the composer CLI:
./composer.phar require modethirteen/opencontainer
Or add modethirteen/opencontainer to your project's composer.json:
{
"require": {
"modethirteen/opencontainer": "dev-main"
}
}
dev-main
is the main development branch. If you are using OpenContainer in a production environment, it is advised that you use a stable release.
Assuming you have setup Composer's autoloader, OpenContainer can be found in the modethirteen\OpenContainer\
namespace.
Simply instantiate OpenContainer and you're ready to go.
$container = new OpenContainer();
An injectable class is instantiated by injecting the container at construction time. In this example, Foo, Bar, and Baz are all types registered in the container. If a registered type in the container requires Baz's method doSomething(), Baz must first pull it's dependencies, Foo and Bar, from the container (and furthermore, their dependencies, creating a dependency tree).
class Baz {
private Foo $foo;
private Bar $bar;
public function __construct(IContainer $container) {
$this->foo = $container->Foo;
$this->bar = $container->Bar;
}
public function doSomething() : string {
return $this->foo->myMethod();
}
}
Registering a type requires a symbol to identify the type when fetching it's instantiated instance from the container, and the fully qualified class name to build.
/**
* setup the type as a virtual property so that IDE's that support type checking can take advantage
*
* @property Foo $Foo
*/
class MyContainer extends OpenContainer {
}
$container = new MyContainer();
$container->registerType('Foo', Foo::class);
// type checks will infer this object to be an instance of Foo
$instance = $container->Foo;
Registering an instance requires a symbol to identify the instance when fetching from the container, and the already-created instance itself. Registering an instance is useful when the type's constructor cannot meet the requirements of an injectable class.
/**
* setup the type as a virtual property so that IDE's that support type checking can take advantage
*
* @property Bar $Bar
*/
class MyContainer extends OpenContainer {
}
$container = new MyContainer();
$container->registerInstance('Bar', new Bar($dependency, $outside, $of, $container));
// type checks will infer this object to be an instance of Bar
$instance = $container->Bar;
Registering a builder requires a symbol to identify the instance when fetching it from the container, and a closure function to execute the first time it is fetched. Registering a builder is useful if there are specialized steps that must be taken before the instance is created.
/**
* setup the type as a virtual property so that IDE's that support type checking can take advantage
*
* @property Qux $Qux
*/
class MyContainer extends OpenContainer {
}
$container = new MyContainer();
$container->registerBuilder('Qux', function(MyContainer $container) : Qux {
// builder functions only have one argument, access to the container itself
return new Qux($container, $some, $other, $dependency);
});
// type checks will infer this object to be an instance of Qux
$instance = $container->Qux;
A deferred container attempts to use reflection and class proxies to avoid circular dependencies. A deferred container returns proxies, which are not materialized until a method or property is accessed on the proxy. This behavior is useful as without it, every dependency in the tree is instantiated when the root dependency is first instantiated (whether those downstream dependencies will be eventually used or not). Without deferring dependency instantiation until those dependencies are actually needed, any circular dependency returns a null value, an endless nested function loop, or raises an Undefined Property warning if they can't be resolved.
$container = (new OpenContainer)->toDeferredContainer();
// all methods on a deferred container are identical to a non-deferred container
$container->registerType('Plugh', Plugh::class);
$container->registerInstance('Plugh', new Plugh());
// closure function style builders require a return type, otherwise an OpenContainerCannotBuildDeferredInstanceException will be thrown
$container->registerBuilder('Plugh', function(IContainer $container) : Plugh { ... });
$container->Plugh;