Skip to content
This repository has been archived by the owner on Feb 6, 2020. It is now read-only.

Commit

Permalink
Merge branch 'hotfix/89'
Browse files Browse the repository at this point in the history
Close #89
  • Loading branch information
weierophinney committed Feb 1, 2016
2 parents 6e46966 + 169f2a7 commit 129f00d
Show file tree
Hide file tree
Showing 6 changed files with 339 additions and 13 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ All notable changes to this project will be documented in this file, in reverse

### Added

- Nothing.
- [#89](https://github.com/zendframework/zend-servicemanager/pull/89) adds
cyclic alias detection to the `ServiceManager`; it now raises a
`Zend\ServiceManager\Exception\CyclicAliasException` when one is detected,
detailing the cycle detected.

### Deprecated

Expand Down
139 changes: 139 additions & 0 deletions src/Exception/CyclicAliasException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/

namespace Zend\ServiceManager\Exception;

class CyclicAliasException extends InvalidArgumentException
{
/**
* @param string[] $aliases map of referenced services, indexed by alias name (string)
*
* @return self
*/
public static function fromAliasesMap(array $aliases)
{
$detectedCycles = array_filter(array_map(
function ($alias) use ($aliases) {
return self::getCycleFor($aliases, $alias);
},
array_keys($aliases)
));

if (! $detectedCycles) {
return new self(sprintf(
"A cycle was detected within the following aliases map:\n\n%s",
self::printReferencesMap($aliases)
));
}

return new self(sprintf(
"Cycles were detected within the provided aliases:\n\n%s\n\n"
. "The cycle was detected in the following alias map:\n\n%s",
self::printCycles(self::deDuplicateDetectedCycles($detectedCycles)),
self::printReferencesMap($aliases)
));
}

/**
* Retrieves the cycle detected for the given $alias, or `null` if no cycle was detected
*
* @param string[] $aliases
* @param string $alias
*
* @return array|null
*/
private static function getCycleFor(array $aliases, $alias)
{
$cycleCandidate = [];
$targetName = $alias;

while (isset($aliases[$targetName])) {
if (isset($cycleCandidate[$targetName])) {
return $cycleCandidate;
}

$cycleCandidate[$targetName] = true;

$targetName = $aliases[$targetName];
}

return null;
}

/**
* @param string[] $aliases
*
* @return string
*/
private static function printReferencesMap(array $aliases)
{
$map = [];

foreach ($aliases as $alias => $reference) {
$map[] = '"' . $alias . '" => "' . $reference . '"';
}

return "[\n" . implode("\n", $map) . "\n]";
}

/**
* @param string[][] $detectedCycles
*
* @return string
*/
private static function printCycles(array $detectedCycles)
{
return "[\n" . implode("\n", array_map([__CLASS__, 'printCycle'], $detectedCycles)) . "\n]";
}

/**
* @param string[] $detectedCycle
*
* @return string
*/
private static function printCycle(array $detectedCycle)
{
$fullCycle = array_keys($detectedCycle);
$fullCycle[] = reset($fullCycle);

return implode(
' => ',
array_map(
function ($cycle) {
return '"' . $cycle . '"';
},
$fullCycle
)
);
}

/**
* @param bool[][] $detectedCycles
*
* @return bool[][] de-duplicated
*/
private static function deDuplicateDetectedCycles(array $detectedCycles)
{
$detectedCyclesByHash = [];

foreach ($detectedCycles as $detectedCycle) {
$cycleAliases = array_keys($detectedCycle);

sort($cycleAliases);

$hash = serialize(array_values($cycleAliases));

$detectedCyclesByHash[$hash] = isset($detectedCyclesByHash[$hash])
? $detectedCyclesByHash[$hash]
: $detectedCycle;
}

return array_values($detectedCyclesByHash);
}
}
13 changes: 10 additions & 3 deletions src/ServiceManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@
use ProxyManager\Factory\LazyLoadingValueHolderFactory;
use ProxyManager\GeneratorStrategy\EvaluatingGeneratorStrategy;
use Zend\ServiceManager\Exception\ContainerModificationsNotAllowedException;
use Zend\ServiceManager\Exception\CyclicAliasException;
use Zend\ServiceManager\Exception\InvalidArgumentException;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\AbstractFactoryInterface;
use Zend\ServiceManager\Factory\DelegatorFactoryInterface;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Initializer\InitializerInterface;

/**
Expand Down Expand Up @@ -569,10 +570,16 @@ private function resolveInitializers(array $initializers)
private function resolveAliases(array $aliases)
{
foreach ($aliases as $alias => $service) {
$name = $alias;
$visited = [];
$name = $alias;

while (isset($this->aliases[$name])) {
$name = $this->aliases[$name];
if (isset($visited[$name])) {
throw CyclicAliasException::fromAliasesMap($aliases);
}

$visited[$name] = true;
$name = $this->aliases[$name];
}

$this->resolvedAliases[$alias] = $name;
Expand Down
19 changes: 17 additions & 2 deletions test/CommonServiceLocatorBehaviorsTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@

use DateTime;
use Interop\Container\Exception\ContainerException;
use PHPUnit_Framework_TestCase as TestCase;
use ReflectionProperty;
use stdClass;
use Zend\ServiceManager\Exception\ContainerModificationsNotAllowedException;
use Zend\ServiceManager\Exception\CyclicAliasException;
use Zend\ServiceManager\Exception\InvalidArgumentException;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Factory\FactoryInterface;
use Zend\ServiceManager\Factory\InvokableFactory;
use Zend\ServiceManager\Initializer\InitializerInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\ServiceManager\Factory\InvokableFactory;
use ZendTest\ServiceManager\TestAsset\FailingAbstractFactory;
use ZendTest\ServiceManager\TestAsset\FailingFactory;
use ZendTest\ServiceManager\TestAsset\InvokableObject;
Expand Down Expand Up @@ -783,4 +783,19 @@ public function testCanRetrieveParentContainerViaGetServiceLocatorWithDeprecatio
$this->assertSame($this->creationContext, $container->getServiceLocator());
restore_error_handler();
}

/**
* @group zendframework/zend-servicemanager#83
*/
public function testCrashesOnCyclicAliases()
{
$this->setExpectedException(CyclicAliasException::class);

$this->createContainer([
'aliases' => [
'a' => 'b',
'b' => 'a',
],
]);
}
}
169 changes: 169 additions & 0 deletions test/Exception/CyclicAliasExceptionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/

namespace ZendTest\ServiceManager\Exception;

use PHPUnit_Framework_TestCase as TestCase;
use ProxyManager\Autoloader\AutoloaderInterface;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RecursiveRegexIterator;
use RegexIterator;
use stdClass;
use Zend\ServiceManager\Exception\CyclicAliasException;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\InvokableFactory;
use Zend\ServiceManager\Proxy\LazyServiceFactory;
use Zend\ServiceManager\ServiceManager;
use ZendTest\ServiceManager\TestAsset\InvokableObject;

/**
* @covers \Zend\ServiceManager\Exception\CyclicAliasException
*/
class CyclicAliasExceptionTest extends TestCase
{
/**
* @dataProvider aliasesProvider
*
* @param string[] $aliases
* @param string $expectedMessage
*
* @return void
*/
public function testFromAliasesMap(array $aliases, $expectedMessage)
{
$exception = CyclicAliasException::fromAliasesMap($aliases);

self::assertInstanceOf(CyclicAliasException::class, $exception);
self::assertSame($expectedMessage, $exception->getMessage());
}

/**
* @return string[][]|string[][][]
*/
public function aliasesProvider()
{
return [
'empty set' => [
[],
'A cycle was detected within the following aliases map:
[
]'
],
'acyclic set' => [
[
'b' => 'a',
'd' => 'c',
],
'A cycle was detected within the following aliases map:
[
"b" => "a"
"d" => "c"
]'
],
'acyclic self-referencing set' => [
[
'b' => 'a',
'c' => 'b',
'd' => 'c',
],
'A cycle was detected within the following aliases map:
[
"b" => "a"
"c" => "b"
"d" => "c"
]'
],
'cyclic set' => [
[
'b' => 'a',
'a' => 'b',
],
'Cycles were detected within the provided aliases:
[
"b" => "a" => "b"
]
The cycle was detected in the following alias map:
[
"b" => "a"
"a" => "b"
]'
],
'cyclic set (indirect)' => [
[
'b' => 'a',
'c' => 'b',
'a' => 'c',
],
'Cycles were detected within the provided aliases:
[
"b" => "a" => "c" => "b"
]
The cycle was detected in the following alias map:
[
"b" => "a"
"c" => "b"
"a" => "c"
]'
],
'cyclic set + acyclic set' => [
[
'b' => 'a',
'a' => 'b',
'd' => 'c',
],
'Cycles were detected within the provided aliases:
[
"b" => "a" => "b"
]
The cycle was detected in the following alias map:
[
"b" => "a"
"a" => "b"
"d" => "c"
]'
],
'cyclic set + reference to cyclic set' => [
[
'b' => 'a',
'a' => 'b',
'c' => 'a',
],
'Cycles were detected within the provided aliases:
[
"b" => "a" => "b"
"c" => "a" => "b" => "c"
]
The cycle was detected in the following alias map:
[
"b" => "a"
"a" => "b"
"c" => "a"
]'
],
];
}
}
Loading

0 comments on commit 129f00d

Please sign in to comment.