Poor-Mans DI Container with Reflection
I wanted a little more insight into how auto-wiring in containers could potentialy work, so I built myself a poor mains DI Container based on the PSR Container Interface, here it is in it's entirety:
<?php
namespace Ampedweb\Container;
use ReflectionClass;
use Ampedweb\Container\Exceptions\IdentifierNotKnownException;
class Container implements SimpleContainerInterface
{
private array $deps = [];
public function __construct(private bool $shouldUseReflection = true, private bool $cacheReflectedObjects = true)
{
}
/**
* @throws \ReflectionException
*/
protected function resolveDepsRecursively(string $id): ?object
{
$reflectedDep = new ReflectionClass($id);
$constructorParams = $reflectedDep->getConstructor()?->getParameters();
if (!$constructorParams) {
$dep = $reflectedDep->newInstance();
if ($this->cacheReflectedObjects) {
$this->share($id, $dep);
}
return $dep;
}
$params = [];
foreach ($constructorParams as $reflectionParameter) {
$depFQN = $reflectionParameter->getType()->getName();
if (class_exists($depFQN)) {
$params[] = $this->resolveDepsRecursively($depFQN);
}
}
$dep = $reflectedDep->newInstanceArgs($params);
if ($this->cacheReflectedObjects) {
$this->share($id, $dep);
}
return $dep;
}
protected function resolveWithReflection(string $id): mixed
{
if (!class_exists($id)) {
return false;
}
$dep = $this->resolveDepsRecursively($id);
if (!$dep) {
return false;
}
return $dep;
}
protected function resolveFromContainer(string $id): mixed
{
return $this->deps[$id] ?? false;
}
public function get(string $id): mixed
{
$dep = $this->resolveFromContainer($id);
if (!$dep && $this->shouldUseReflection) {
$dep = $this->resolveWithReflection($id);
}
if (!$dep) {
throw new IdentifierNotKnownException('Identifier "' . $id . '" is not known to the Container');
}
return $dep;
}
public function has(string $id): bool
{
return $this->deps[$id] ?? false;
}
public function share(string $id, mixed $paramOrObject): SimpleContainerInterface
{
$this->deps[$id] = $paramOrObject;
return $this;
}
}
So where the auto-wiring happens/doesn't happen is:
protected function resolveWithReflection(string $id): mixed
{
if (!class_exists($id)) {
return false;
}
$dep = $this->resolveDepsRecursively($id);
if (!$dep) {
return false;
}
return $dep;
}
Essentially quite straightforward, if the class we're looking for exists then try to resolve it and it's dependencies recursively:
protected function resolveDepsRecursively(string $id): ?object
{
$reflectedDep = new ReflectionClass($id);
//Do we have any constructor params?
$constructorParams = $reflectedDep->getConstructor()?->getParameters();
//No? Then just create an instance and return it
if (!$constructorParams) {
$dep = $reflectedDep->newInstance();
if ($this->cacheReflectedObjects) {
$this->share($id, $dep);
}
return $dep;
}
$params = [];
//Oh we do have params?
foreach ($constructorParams as $reflectionParameter) {
$depFQN = $reflectionParameter->getType()->getName();
//Is it an FQN and a does the class exist? great! pass that FQN back to this function
if (class_exists($depFQN)) {
$params[] = $this->resolveDepsRecursively($depFQN);
}
}
//We've got all our dependencies for our instance? Awesome make it, maybe cache it and return it!
$dep = $reflectedDep->newInstanceArgs($params);
if ($this->cacheReflectedObjects) {
$this->share($id, $dep);
}
return $dep;
}
So lets set up our container with a contrived example:
$container = new Container;
$container->share(Bozzer::class, new Bozzer);
$foo = $container->get(Foo::class);
$bozzer = $container->get(Bozzer::class);
$baz = $container->get(Baz::class);
dd($container);
And dump the container:
^ Ampedweb\Container\Container^ {#15
-deps: array:4 [
"Ampedweb\Container\TestClasses\Bozzer" => Ampedweb\Container\TestClasses\Bozzer^ {#24}
"Ampedweb\Container\TestClasses\Bar" => Ampedweb\Container\TestClasses\Bar^ {#21}
"Ampedweb\Container\TestClasses\Baz" => Ampedweb\Container\TestClasses\Baz^ {#22
#bozzer: Ampedweb\Container\TestClasses\Bozzer^ {#24}
}
"Ampedweb\Container\TestClasses\Foo" => Ampedweb\Container\TestClasses\Foo^ {#23
#bar: Ampedweb\Container\TestClasses\Bar^ {#21}
#baz: Ampedweb\Container\TestClasses\Baz^ {#22}
}
]
-shouldUseReflection: true
-cacheReflectedObjects: true
}
As you can see, all of our nested dependencies for each class have been included and reused from the cache where appropriate.