Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

use Closure;
use Iterator;
use ReflectionClass;
use ReflectionNamedType;
use RuntimeException;
use Psr\Container\ContainerInterface;

class Container implements ContainerInterface, Iterator
Expand Down Expand Up @@ -71,6 +74,70 @@ public function get(string $id, ...$parameters): mixed
return $this->instances[$id];
}

/**
* 构造器注入:按参数类型从容器解析依赖,未注册的类递归构建。
*/
public function make(string $class): object
{
return $this->build($class, []);
}

private function build(string $class, array $resolving): object
{
if ($this->has($class)) {
return $this->get($class);
}

if (!class_exists($class)) {
throw new RuntimeException(sprintf('Container cannot make unknown class "%s".', $class));
}

if (in_array($class, $resolving, true)) {
throw new RuntimeException(sprintf(
'Circular dependency detected: %s.',
implode(' -> ', [...$resolving, $class])
));
}

$resolving[] = $class;
$reflection = new ReflectionClass($class);
$constructor = $reflection->getConstructor();

if ($constructor === null || $constructor->getNumberOfParameters() === 0) {
return $reflection->newInstance();
}

$arguments = [];

foreach ($constructor->getParameters() as $parameter) {
$type = $parameter->getType();

if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
$name = $type->getName();
$arguments[] = $this->has($name) ? $this->get($name) : $this->build($name, $resolving);
continue;
}

if ($parameter->isDefaultValueAvailable()) {
$arguments[] = $parameter->getDefaultValue();
continue;
}

if ($parameter->allowsNull()) {
$arguments[] = null;
continue;
}

throw new RuntimeException(sprintf(
'Cannot resolve parameter $%s of %s::__construct().',
$parameter->getName(),
$class
));
}

return $reflection->newInstanceArgs($arguments);
}

public function clear(string $id): void
{
if (array_key_exists($id, $this->instances)) {
Expand Down
87 changes: 87 additions & 0 deletions tests/MakeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

declare(strict_types=1);

use FastD\Container\Container;
use PHPUnit\Framework\TestCase;

final class DemoDependency
{
}

final class DemoService
{
public function __construct(public readonly DemoDependency $dependency)
{
}
}

final class DemoController
{
public function __construct(public readonly DemoService $service)
{
}
}


interface DemoContract
{
}

final class DemoImplementation implements DemoContract
{
}

final class DemoConsumer
{
public function __construct(public readonly DemoContract $contract)
{
}
}

final class DemoCircularA
{
public function __construct(DemoCircularB $b)
{
}
}

final class DemoCircularB
{
public function __construct(DemoCircularA $a)
{
}
}

final class MakeTest extends TestCase
{
public function testMakeResolvesConstructorDependencies(): void
{
$container = new Container();
$dependency = new DemoDependency();
$container->add(DemoDependency::class, $dependency);

$controller = $container->make(DemoController::class);

$this->assertInstanceOf(DemoController::class, $controller);
$this->assertSame($dependency, $controller->service->dependency);
}


public function testInterfaceBindingIsInjected(): void
{
$container = new Container();
$container->add(DemoContract::class, new DemoImplementation());

$consumer = $container->make(DemoConsumer::class);

$this->assertInstanceOf(DemoImplementation::class, $consumer->contract);
}

public function testCircularDependencyIsDetected(): void
{
$this->expectException(RuntimeException::class);

(new Container())->make(DemoCircularA::class);
}
}
Loading