Skip to content

Instantly share code, notes, and snippets.

@marcelomx
Last active August 30, 2021 12:35
Show Gist options
  • Save marcelomx/285741e22915c32bd7005acc45a49e9d to your computer and use it in GitHub Desktop.
Save marcelomx/285741e22915c32bd7005acc45a49e9d to your computer and use it in GitHub Desktop.
Simple micro DI container with PHP
<?php
use Psr\Container\NotFoundExceptionInterface;
class ContainerException extends \Exception implements NotFoundExceptionInterface
{
}
<?php
use Psr\Container\ContainerInterface;
class MicroContainer implements ContainerInterface
{
private array $instances = [];
public function __construct(private array $defs = [])
{
}
public function get(string $id)
{
if (!$this->has($id)) {
throw new ContainerException(
sprintf('Dependency %s is not defined', $id)
);
}
// Singleton!
if (isset($this->instances[$id])) {
return $this->instances[$id];
}
$def = $this->defs[$id];
// Try simple autowiring
if (is_string($def) && class_exists($def)) {
$refClass = new \ReflectionClass($def);
$newArgs = $this->resolveClassParams($refClass);
$newInstance = $refClass->newInstanceArgs($newArgs);
return $this->registerNewInstance($newInstance, $id, $def);
}
if (\is_callable($def)) {
$newInstance = call_user_func_array($def, [$this]);
$this->instances[$id] = $newInstance;
$classDef = get_class($newInstance);
return $this->registerNewInstance($newInstance, $id, $classDef);
}
return $def;
}
private function registerNewInstance($newInstance, $id, $classDef)
{
$this->instances[$id] = $newInstance;
if (
$classDef !== $id &&
!$this->has($classDef) &&
!isset($this->instances[$classDef])
) {
$this->instances[$classDef] = $newInstance;
}
return $newInstance;
}
private function resolveClassParams(\ReflectionClass $refClass)
{
$constructor = $refClass->getConstructor();
return !$constructor ? [] : array_map(
function (\ReflectionParameter $p) {
$type = $p->getType();
if ($type->isBuiltin()) {
return $p->isDefaultValueAvailable()
? $p->getDefaultValue()
: $this->get($p->getName());
}
$classDef = $type->getName();
if (class_exists($classDef) && !$this->has($classDef)) {
$this->defs[$classDef] = $classDef;
}
return $this->get($classDef);
},
$constructor->getParameters()
);
}
public function has(string $id): bool
{
return isset($this->defs[$id]);
}
}
<?php
use Inner\Deep\Dependency;
use PHPUnit\Framework\TestCase;
use Psr\Container\NotFoundExceptionInterface;
class MicroContainerTest extends TestCase
{
function testNotFoundDefs()
{
$this->expectException(NotFoundExceptionInterface::class);
$container = new MicroContainer();
$container->get('invalidDefinition');
}
function testBuildContainerRawValues()
{
$container = new MicroContainer([
'foo' => 1,
'bar' => 2
]);
$this->assertEquals(1, $container->get('foo'));
$this->assertEquals(2, $container->get('bar'));
}
function testBuildContainerClassDefs()
{
$container = new MicroContainer([
'foo' => 'foostring',
\stdClass::class => fn () => new \stdClass,
Foo::class => fn (MicroContainer $c) => new Foo($c->get('foo'), $c->get(stdClass::class)),
Bar::class => fn (MicroContainer $c) => new Bar($c->get(Foo::class))
]);
$fooValue = $container->get('foo');
$stdDep = $container->get(\stdClass::class);
$fooObj = $container->get(\Foo::class);
$barObj = $container->get(\Bar::class);
$this->assertEquals('foostring', $fooValue);
$this->assertInstanceOf(\stdClass::class, $stdDep);
$this->assertInstanceOf(Foo::class, $fooObj);
$this->assertEquals($fooValue, $fooObj->foo);
$this->assertSame($stdDep, $fooObj->stdDep);
$this->assertInstanceOf(Bar::class, $barObj);
$this->assertSame($fooObj, $barObj->fooDep);
}
function testBuildDepsWithAutowiring()
{
$container = new MicroContainer([
'foo' => 'foostring',
\stdClass::class => fn () => new \stdClass,
Foo::class => Foo::class,
'foo.service' => fn (MicroContainer $c) => $c->get(Foo::class),
FooInterface::class => Foo::class,
Bar::class => Bar::class,
Baz::class => Baz::class
]);
$fooValue = $container->get('foo');
$stdDep = $container->get(\stdClass::class);
$fooObj = $container->get(\FooInterface::class);
$barObj = $container->get(\Bar::class);
$bazObj = $container->get(\Baz::class);
$this->assertEquals('foostring', $fooValue);
$this->assertInstanceOf(\stdClass::class, $stdDep);
$this->assertInstanceOf(Foo::class, $fooObj);
$this->assertEquals($fooValue, $fooObj->foo);
$this->assertSame($stdDep, $fooObj->stdDep);
$this->assertInstanceOf(Bar::class, $barObj);
$this->assertNotSame($fooObj, $barObj->fooDep);
$this->assertInstanceOf(Baz::class, $bazObj);
}
}
class Foo implements FooInterface
{
public function __construct(
public string $foo,
public \stdClass $stdDep
) {
}
}
class Bar
{
public function __construct(public Foo $fooDep)
{
}
}
class Baz
{
public function __construct(
public string $name = 'Default Value',
public Foo $foo,
public Bar $bar,
public Dependency $dependency
) {
}
}
interface FooInterface
{
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment