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.