Skip to content

Commit

Permalink
Merge pull request #1 from ob-ivan/cyclic-deps
Browse files Browse the repository at this point in the history
Cyclic deps
  • Loading branch information
ob-ivan authored Aug 2, 2017
2 parents 84a4b9f + 381f76f commit dc01412
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 21 deletions.
51 changes: 31 additions & 20 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ class Container {
private $initializers = [];
private $services = [];

/**
* @var $usedNames string[] Already initialized names, used to detect cyclic dependencies.
**/
private $usedNames = [];

public function __construct(array $config = [], $selfName = '') {
$this->services = $config;
if ($selfName) {
Expand All @@ -15,7 +20,7 @@ public function __construct(array $config = [], $selfName = '') {

public function connect(ProviderInterface $provider) {
$this->initializers[$provider->getServiceName()] = function () use ($provider) {
$this->inject($provider);
$this->injectRecursive($provider);
return $provider->provide();
};
}
Expand Down Expand Up @@ -47,10 +52,22 @@ public function value($value) {
}

public function produce($initializer) {
$this->usedNames = [];
return $this->produceRecursive($initializer, []);
}

public function inject($consumer) {
$this->usedNames = [];
return $this->injectRecursive($consumer, []);
}

// Public for compatibility mode only.
public function get($name) {
$this->usedNames = [];
return $this->getRecursive($name);
}

private function injectRecursive($consumer) {
if (is_callable($consumer)) {
$parameters = $this->getParameterValues(new \ReflectionFunction($consumer));
return $consumer(...$parameters);
Expand All @@ -60,23 +77,18 @@ public function inject($consumer) {
}
}

// Public for compatibility mode only.
public function get($name) {
return $this->getRecursive($name, []);
}

private function produceRecursive($initializer, array $names) {
private function produceRecursive($initializer) {
if (is_string($initializer)) {
$class = new \ReflectionClass($initializer);
$constructor = $class->getConstructor();
if ($constructor) {
$parameters = $this->getParameterValues($constructor, $names);
$parameters = $this->getParameterValues($constructor);
$instance = new $initializer(...$parameters);
} else {
$instance = new $initializer();
}
} elseif (is_callable($initializer)) {
$parameters = $this->getParameterValues(new \ReflectionFunction($initializer), $names);
$parameters = $this->getParameterValues(new \ReflectionFunction($initializer));
$instance = $initializer(...$parameters);
} else {
$instance = $initializer;
Expand All @@ -95,13 +107,13 @@ private function injectDeclarer($object) {
return $object;
}

private function injectByNames($object, array $names) {
foreach ($names as $name) {
private function injectByNames($object, array $declaredNames) {
foreach ($declaredNames as $name) {
$setter = 'set' . implode('', array_map('ucfirst', explode('_', $name)));
if (!method_exists($object, $setter)) {
throw new Exception("Object declared $name dependency, but setter method $setter was not found");
}
$object->$setter($this->get($name));
$object->$setter($this->getRecursive($name));
}
return $object;
}
Expand All @@ -110,27 +122,26 @@ private function injectByNames($object, array $names) {
* Return an initialized service.
*
* @param $name string
* @param $names string[] Already initialized names, used to detect cyclic dependencies.
* @return mixed
**/
private function getRecursive(string $name, array $names) {
private function getRecursive(string $name) {
if (!isset($this->services[$name])) {
if (in_array($name, $names)) {
if (in_array($name, $this->usedNames)) {
throw new Exception("Cyclic dependency found while resolving $name");
}
$names[] = $name;
$this->usedNames[] = $name;
if (!isset($this->initializers[$name])) {
throw new Exception("No initializer for $name");
}
$this->services[$name] = $this->produceRecursive($this->initializers[$name], $names);
$this->services[$name] = $this->produceRecursive($this->initializers[$name]);
}
return $this->services[$name];
}

private function getParameterValues(\ReflectionFunctionAbstract $function, array $names = []) {
private function getParameterValues(\ReflectionFunctionAbstract $function) {
return array_map(
function ($parameter) use ($names) {
return $this->getRecursive($parameter->getName(), $names);
function ($parameter) {
return $this->getRecursive($parameter->getName());
},
$function->getParameters()
);
Expand Down
10 changes: 9 additions & 1 deletion tests/ContainerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,22 @@ public function testRegisterClassName() {
$this->assertEquals($container, $service->getContainer(), 'Must inject container by setter');
}

public function testDetectCyclicDependencies() {
public function testDetectCyclicDependenciesSimple() {
$container = new Container();
$container->register('a', function ($b) { return 1; });
$container->register('b', function ($a) { return 2; });
$this->expectException(Exception::class);
$container->get('a');
}

public function testDetectCyclicDependenciesComplex() {
$container = new Container();
$container->register('currencyStore', CurrencyStore::class);
$container->connect(new CurrencyProvider());
$this->expectException(Exception::class);
$container->get('currency');
}

public function testExtend() {
$name1 = 'Jar Jar Binks';
$name2 = 'Palpatine';
Expand Down
25 changes: 25 additions & 0 deletions tests/CurrencyProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php
namespace tests;

use SD\DependencyInjection\DeclarerInterface;
use SD\DependencyInjection\ProviderInterface;

class CurrencyProvider implements DeclarerInterface, ProviderInterface {
private $currencyStore;

public function declareDependencies() {
return ['currencyStore'];
}

public function setCurrencyStore($currencyStore) {
$this->currencyStore = $currencyStore;
}

public function getServiceName(): string {
return 'currency';
}

public function provide() {
return new \stdClass;
}
}
16 changes: 16 additions & 0 deletions tests/CurrencyStore.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php
namespace tests;

use SD\DependencyInjection\AutoDeclarerInterface;
use SD\DependencyInjection\AutoDeclarerTrait;

class CurrencyStore implements AutoDeclarerInterface {
use AutoDeclarerTrait;

private $autoDeclareCurrency = 'currency';
private $currency;

public function setCurrency($currency) {
$this->currency = $currency;
}
}

0 comments on commit dc01412

Please sign in to comment.