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

Commit 0105775

Browse files
committed
Merge branch 'hotfix/89' into develop
Forward port #89
2 parents ca4ab88 + 169f2a7 commit 0105775

File tree

6 files changed

+340
-15
lines changed

6 files changed

+340
-15
lines changed

CHANGELOG.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ All notable changes to this project will be documented in this file, in reverse
66

77
### Added
88

9-
- [#64](https://github.com/zendframework/zend-servicemanager/pull/64) performance optimizations
10-
when dealing with alias resolution during service manager instantiation
9+
- Nothing.
1110

1211
### Deprecated
1312

@@ -25,7 +24,10 @@ All notable changes to this project will be documented in this file, in reverse
2524

2625
### Added
2726

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

3032
### Deprecated
3133

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
}

0 commit comments

Comments
 (0)