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

Commit 129f00d

Browse files
committed
Merge branch 'hotfix/89'
Close #89
2 parents 6e46966 + 169f2a7 commit 129f00d

File tree

6 files changed

+339
-13
lines changed

6 files changed

+339
-13
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ All notable changes to this project will be documented in this file, in reverse
66

77
### Added
88

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

1114
### Deprecated
1215

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<?php
2+
/**
3+
* Zend Framework (http://framework.zend.com/)
4+
*
5+
* @link http://github.com/zendframework/zf2 for the canonical source repository
6+
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
7+
* @license http://framework.zend.com/license/new-bsd New BSD License
8+
*/
9+
10+
namespace Zend\ServiceManager\Exception;
11+
12+
class CyclicAliasException extends InvalidArgumentException
13+
{
14+
/**
15+
* @param string[] $aliases map of referenced services, indexed by alias name (string)
16+
*
17+
* @return self
18+
*/
19+
public static function fromAliasesMap(array $aliases)
20+
{
21+
$detectedCycles = array_filter(array_map(
22+
function ($alias) use ($aliases) {
23+
return self::getCycleFor($aliases, $alias);
24+
},
25+
array_keys($aliases)
26+
));
27+
28+
if (! $detectedCycles) {
29+
return new self(sprintf(
30+
"A cycle was detected within the following aliases map:\n\n%s",
31+
self::printReferencesMap($aliases)
32+
));
33+
}
34+
35+
return new self(sprintf(
36+
"Cycles were detected within the provided aliases:\n\n%s\n\n"
37+
. "The cycle was detected in the following alias map:\n\n%s",
38+
self::printCycles(self::deDuplicateDetectedCycles($detectedCycles)),
39+
self::printReferencesMap($aliases)
40+
));
41+
}
42+
43+
/**
44+
* Retrieves the cycle detected for the given $alias, or `null` if no cycle was detected
45+
*
46+
* @param string[] $aliases
47+
* @param string $alias
48+
*
49+
* @return array|null
50+
*/
51+
private static function getCycleFor(array $aliases, $alias)
52+
{
53+
$cycleCandidate = [];
54+
$targetName = $alias;
55+
56+
while (isset($aliases[$targetName])) {
57+
if (isset($cycleCandidate[$targetName])) {
58+
return $cycleCandidate;
59+
}
60+
61+
$cycleCandidate[$targetName] = true;
62+
63+
$targetName = $aliases[$targetName];
64+
}
65+
66+
return null;
67+
}
68+
69+
/**
70+
* @param string[] $aliases
71+
*
72+
* @return string
73+
*/
74+
private static function printReferencesMap(array $aliases)
75+
{
76+
$map = [];
77+
78+
foreach ($aliases as $alias => $reference) {
79+
$map[] = '"' . $alias . '" => "' . $reference . '"';
80+
}
81+
82+
return "[\n" . implode("\n", $map) . "\n]";
83+
}
84+
85+
/**
86+
* @param string[][] $detectedCycles
87+
*
88+
* @return string
89+
*/
90+
private static function printCycles(array $detectedCycles)
91+
{
92+
return "[\n" . implode("\n", array_map([__CLASS__, 'printCycle'], $detectedCycles)) . "\n]";
93+
}
94+
95+
/**
96+
* @param string[] $detectedCycle
97+
*
98+
* @return string
99+
*/
100+
private static function printCycle(array $detectedCycle)
101+
{
102+
$fullCycle = array_keys($detectedCycle);
103+
$fullCycle[] = reset($fullCycle);
104+
105+
return implode(
106+
' => ',
107+
array_map(
108+
function ($cycle) {
109+
return '"' . $cycle . '"';
110+
},
111+
$fullCycle
112+
)
113+
);
114+
}
115+
116+
/**
117+
* @param bool[][] $detectedCycles
118+
*
119+
* @return bool[][] de-duplicated
120+
*/
121+
private static function deDuplicateDetectedCycles(array $detectedCycles)
122+
{
123+
$detectedCyclesByHash = [];
124+
125+
foreach ($detectedCycles as $detectedCycle) {
126+
$cycleAliases = array_keys($detectedCycle);
127+
128+
sort($cycleAliases);
129+
130+
$hash = serialize(array_values($cycleAliases));
131+
132+
$detectedCyclesByHash[$hash] = isset($detectedCyclesByHash[$hash])
133+
? $detectedCyclesByHash[$hash]
134+
: $detectedCycle;
135+
}
136+
137+
return array_values($detectedCyclesByHash);
138+
}
139+
}

src/ServiceManager.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@
1616
use ProxyManager\Factory\LazyLoadingValueHolderFactory;
1717
use ProxyManager\GeneratorStrategy\EvaluatingGeneratorStrategy;
1818
use Zend\ServiceManager\Exception\ContainerModificationsNotAllowedException;
19+
use Zend\ServiceManager\Exception\CyclicAliasException;
1920
use Zend\ServiceManager\Exception\InvalidArgumentException;
2021
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
22+
use Zend\ServiceManager\Exception\ServiceNotFoundException;
2123
use Zend\ServiceManager\Factory\AbstractFactoryInterface;
2224
use Zend\ServiceManager\Factory\DelegatorFactoryInterface;
23-
use Zend\ServiceManager\Exception\ServiceNotFoundException;
2425
use Zend\ServiceManager\Initializer\InitializerInterface;
2526

2627
/**
@@ -569,10 +570,16 @@ private function resolveInitializers(array $initializers)
569570
private function resolveAliases(array $aliases)
570571
{
571572
foreach ($aliases as $alias => $service) {
572-
$name = $alias;
573+
$visited = [];
574+
$name = $alias;
573575

574576
while (isset($this->aliases[$name])) {
575-
$name = $this->aliases[$name];
577+
if (isset($visited[$name])) {
578+
throw CyclicAliasException::fromAliasesMap($aliases);
579+
}
580+
581+
$visited[$name] = true;
582+
$name = $this->aliases[$name];
576583
}
577584

578585
$this->resolvedAliases[$alias] = $name;

test/CommonServiceLocatorBehaviorsTrait.php

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@
1111

1212
use DateTime;
1313
use Interop\Container\Exception\ContainerException;
14-
use PHPUnit_Framework_TestCase as TestCase;
1514
use ReflectionProperty;
1615
use stdClass;
1716
use Zend\ServiceManager\Exception\ContainerModificationsNotAllowedException;
17+
use Zend\ServiceManager\Exception\CyclicAliasException;
1818
use Zend\ServiceManager\Exception\InvalidArgumentException;
1919
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
2020
use Zend\ServiceManager\Factory\FactoryInterface;
21+
use Zend\ServiceManager\Factory\InvokableFactory;
2122
use Zend\ServiceManager\Initializer\InitializerInterface;
2223
use Zend\ServiceManager\ServiceLocatorInterface;
23-
use Zend\ServiceManager\Factory\InvokableFactory;
2424
use ZendTest\ServiceManager\TestAsset\FailingAbstractFactory;
2525
use ZendTest\ServiceManager\TestAsset\FailingFactory;
2626
use ZendTest\ServiceManager\TestAsset\InvokableObject;
@@ -783,4 +783,19 @@ public function testCanRetrieveParentContainerViaGetServiceLocatorWithDeprecatio
783783
$this->assertSame($this->creationContext, $container->getServiceLocator());
784784
restore_error_handler();
785785
}
786+
787+
/**
788+
* @group zendframework/zend-servicemanager#83
789+
*/
790+
public function testCrashesOnCyclicAliases()
791+
{
792+
$this->setExpectedException(CyclicAliasException::class);
793+
794+
$this->createContainer([
795+
'aliases' => [
796+
'a' => 'b',
797+
'b' => 'a',
798+
],
799+
]);
800+
}
786801
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
<?php
2+
/**
3+
* Zend Framework (http://framework.zend.com/)
4+
*
5+
* @link http://github.com/zendframework/zf2 for the canonical source repository
6+
* @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
7+
* @license http://framework.zend.com/license/new-bsd New BSD License
8+
*/
9+
10+
namespace ZendTest\ServiceManager\Exception;
11+
12+
use PHPUnit_Framework_TestCase as TestCase;
13+
use ProxyManager\Autoloader\AutoloaderInterface;
14+
use RecursiveDirectoryIterator;
15+
use RecursiveIteratorIterator;
16+
use RecursiveRegexIterator;
17+
use RegexIterator;
18+
use stdClass;
19+
use Zend\ServiceManager\Exception\CyclicAliasException;
20+
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
21+
use Zend\ServiceManager\Exception\ServiceNotFoundException;
22+
use Zend\ServiceManager\Factory\InvokableFactory;
23+
use Zend\ServiceManager\Proxy\LazyServiceFactory;
24+
use Zend\ServiceManager\ServiceManager;
25+
use ZendTest\ServiceManager\TestAsset\InvokableObject;
26+
27+
/**
28+
* @covers \Zend\ServiceManager\Exception\CyclicAliasException
29+
*/
30+
class CyclicAliasExceptionTest extends TestCase
31+
{
32+
/**
33+
* @dataProvider aliasesProvider
34+
*
35+
* @param string[] $aliases
36+
* @param string $expectedMessage
37+
*
38+
* @return void
39+
*/
40+
public function testFromAliasesMap(array $aliases, $expectedMessage)
41+
{
42+
$exception = CyclicAliasException::fromAliasesMap($aliases);
43+
44+
self::assertInstanceOf(CyclicAliasException::class, $exception);
45+
self::assertSame($expectedMessage, $exception->getMessage());
46+
}
47+
48+
/**
49+
* @return string[][]|string[][][]
50+
*/
51+
public function aliasesProvider()
52+
{
53+
return [
54+
'empty set' => [
55+
[],
56+
'A cycle was detected within the following aliases map:
57+
58+
[
59+
60+
]'
61+
],
62+
'acyclic set' => [
63+
[
64+
'b' => 'a',
65+
'd' => 'c',
66+
],
67+
'A cycle was detected within the following aliases map:
68+
69+
[
70+
"b" => "a"
71+
"d" => "c"
72+
]'
73+
],
74+
'acyclic self-referencing set' => [
75+
[
76+
'b' => 'a',
77+
'c' => 'b',
78+
'd' => 'c',
79+
],
80+
'A cycle was detected within the following aliases map:
81+
82+
[
83+
"b" => "a"
84+
"c" => "b"
85+
"d" => "c"
86+
]'
87+
],
88+
'cyclic set' => [
89+
[
90+
'b' => 'a',
91+
'a' => 'b',
92+
],
93+
'Cycles were detected within the provided aliases:
94+
95+
[
96+
"b" => "a" => "b"
97+
]
98+
99+
The cycle was detected in the following alias map:
100+
101+
[
102+
"b" => "a"
103+
"a" => "b"
104+
]'
105+
],
106+
'cyclic set (indirect)' => [
107+
[
108+
'b' => 'a',
109+
'c' => 'b',
110+
'a' => 'c',
111+
],
112+
'Cycles were detected within the provided aliases:
113+
114+
[
115+
"b" => "a" => "c" => "b"
116+
]
117+
118+
The cycle was detected in the following alias map:
119+
120+
[
121+
"b" => "a"
122+
"c" => "b"
123+
"a" => "c"
124+
]'
125+
],
126+
'cyclic set + acyclic set' => [
127+
[
128+
'b' => 'a',
129+
'a' => 'b',
130+
'd' => 'c',
131+
],
132+
'Cycles were detected within the provided aliases:
133+
134+
[
135+
"b" => "a" => "b"
136+
]
137+
138+
The cycle was detected in the following alias map:
139+
140+
[
141+
"b" => "a"
142+
"a" => "b"
143+
"d" => "c"
144+
]'
145+
],
146+
'cyclic set + reference to cyclic set' => [
147+
[
148+
'b' => 'a',
149+
'a' => 'b',
150+
'c' => 'a',
151+
],
152+
'Cycles were detected within the provided aliases:
153+
154+
[
155+
"b" => "a" => "b"
156+
"c" => "a" => "b" => "c"
157+
]
158+
159+
The cycle was detected in the following alias map:
160+
161+
[
162+
"b" => "a"
163+
"a" => "b"
164+
"c" => "a"
165+
]'
166+
],
167+
];
168+
}
169+
}

0 commit comments

Comments
 (0)