Skip to content

Commit 83e4777

Browse files
sstokNyholm
authored andcommitted
Add constraints for ServiceLocator services (#108)
1 parent 7d3bf40 commit 83e4777

6 files changed

+570
-0
lines changed

PhpUnit/AbstractContainerBuilderTestCase.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PHPUnit\Framework\TestCase;
77
use Symfony\Component\DependencyInjection\ContainerBuilder;
88
use Symfony\Component\DependencyInjection\Definition;
9+
use Symfony\Component\DependencyInjection\Reference;
910

1011
abstract class AbstractContainerBuilderTestCase extends TestCase
1112
{
@@ -167,6 +168,25 @@ protected function assertContainerBuilderHasServiceDefinitionWithArgument(
167168
);
168169
}
169170

171+
/**
172+
* Assert that the ContainerBuilder for this test has a service definition with the given id, which has an argument
173+
* at the given index, and its value is a ServiceLocator with a reference-map equal to the given value.
174+
*
175+
* @param string $serviceId
176+
* @param int|string $argumentIndex
177+
* @param array $expectedServiceMap an array of service-id references and their key in the map
178+
*/
179+
protected function assertContainerBuilderHasServiceDefinitionWithServiceLocatorArgument(
180+
$serviceId,
181+
$argumentIndex,
182+
array $expectedValue
183+
) {
184+
self::assertThat(
185+
$this->container,
186+
new DefinitionArgumentEqualsServiceLocatorConstraint($serviceId, $argumentIndex, $expectedValue)
187+
);
188+
}
189+
170190
/**
171191
* Assert that the ContainerBuilder for this test has a service definition with the given id, which has a method
172192
* call to the given method with the given arguments.
@@ -218,4 +238,22 @@ protected function assertContainerBuilderHasServiceDefinitionWithParent($service
218238

219239
self::assertThat($definition, new DefinitionIsChildOfConstraint($parentServiceId));
220240
}
241+
242+
/**
243+
* Assert that the ContainerBuilder for this test has a ServiceLocator service definition with the given id.
244+
*
245+
* @param string $serviceId
246+
* @param array $expectedServiceMap an array of service-id references and their key in the map
247+
*/
248+
protected function assertContainerBuilderHasServiceLocator(string $serviceId, array $expectedServiceMap = [])
249+
{
250+
$definition = $this->container->findDefinition($serviceId);
251+
252+
// Service locator was provided as context (and therefor a factory)
253+
if (isset($definition->getFactory()[1])) {
254+
$definition = $this->container->findDefinition((string) $definition->getFactory()[0]);
255+
}
256+
257+
self::assertThat($definition, new DefinitionEqualsServiceLocatorConstraint($expectedServiceMap));
258+
}
221259
}
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
<?php
2+
3+
namespace Matthias\SymfonyDependencyInjectionTest\PhpUnit;
4+
5+
use PHPUnit\Framework\Constraint\Constraint;
6+
use PHPUnit\Framework\Constraint\IsEqual;
7+
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
8+
use Symfony\Component\DependencyInjection\ContainerBuilder;
9+
use Symfony\Component\DependencyInjection\Definition;
10+
use Symfony\Component\DependencyInjection\Exception\OutOfBoundsException;
11+
use Symfony\Component\DependencyInjection\Reference;
12+
use Symfony\Component\DependencyInjection\ServiceLocator;
13+
14+
class DefinitionArgumentEqualsServiceLocatorConstraint extends Constraint
15+
{
16+
/**
17+
* @var int|string
18+
*/
19+
private $argumentIndex;
20+
private $expectedValue;
21+
private $serviceId;
22+
23+
public function __construct($serviceId, $argumentIndex, array $expectedValue)
24+
{
25+
parent::__construct();
26+
27+
if (!(is_string($argumentIndex) || (is_int($argumentIndex) && $argumentIndex >= 0))) {
28+
throw new \InvalidArgumentException('Expected either a string or a positive integer for $argumentIndex.');
29+
}
30+
31+
if (is_string($argumentIndex)) {
32+
if ('' === $argumentIndex) {
33+
throw new \InvalidArgumentException('A named argument must begin with a "$".');
34+
}
35+
36+
if ('$' !== $argumentIndex[0]) {
37+
throw new \InvalidArgumentException(
38+
sprintf('Unknown argument "%s". Did you mean "$%s"?', $argumentIndex, $argumentIndex)
39+
);
40+
}
41+
}
42+
43+
$this->serviceId = $serviceId;
44+
$this->argumentIndex = $argumentIndex;
45+
$this->expectedValue = array_map(
46+
function ($serviceId) {
47+
if (is_string($serviceId)) {
48+
return new ServiceClosureArgument(new Reference($serviceId));
49+
}
50+
51+
if (!$serviceId instanceof ServiceClosureArgument) {
52+
return new ServiceClosureArgument($serviceId);
53+
}
54+
55+
return $serviceId;
56+
},
57+
$expectedValue
58+
);
59+
}
60+
61+
public function toString(): string
62+
{
63+
if (is_string($this->argumentIndex)) {
64+
return sprintf(
65+
'has an argument named "%s" with the ServiceLocator',
66+
$this->argumentIndex
67+
);
68+
}
69+
70+
return sprintf(
71+
'has an argument with index %d with the ServiceLocator',
72+
$this->argumentIndex
73+
);
74+
}
75+
76+
public function evaluate($other, $description = '', $returnResult = false)
77+
{
78+
if (!($other instanceof ContainerBuilder)) {
79+
throw new \InvalidArgumentException(
80+
'Expected an instance of Symfony\Component\DependencyInjection\ContainerBuilder'
81+
);
82+
}
83+
84+
if (!$this->evaluateArgumentIndex($other->findDefinition($this->serviceId), $returnResult)) {
85+
return false;
86+
}
87+
88+
if (!$this->evaluateArgumentValue($other, $returnResult)) {
89+
return false;
90+
}
91+
92+
return true;
93+
}
94+
95+
private function evaluateArgumentIndex(Definition $definition, $returnResult)
96+
{
97+
try {
98+
$definition->getArgument($this->argumentIndex);
99+
} catch (\Exception $exception) {
100+
// Older versions of Symfony throw \OutOfBoundsException
101+
// Newer versions throw Symfony\Component\DependencyInjection\Exception\OutOfBoundsException
102+
if (!($exception instanceof \OutOfBoundsException || $exception instanceof OutOfBoundsException)) {
103+
// this was not the expected exception
104+
throw $exception;
105+
}
106+
107+
if ($returnResult) {
108+
return false;
109+
}
110+
111+
$this->fail(
112+
$definition,
113+
sprintf('The definition has no argument with index %s', $this->argumentIndex)
114+
);
115+
}
116+
117+
return true;
118+
}
119+
120+
private function evaluateArgumentValue(ContainerBuilder $container, $returnResult)
121+
{
122+
$definition = $container->findDefinition($this->serviceId);
123+
$actualValue = $definition->getArgument($this->argumentIndex);
124+
$serviceLocatorDef = $actualValue;
125+
126+
if ($actualValue instanceof Reference) {
127+
$serviceLocatorDef = $container->findDefinition((string) $actualValue);
128+
} elseif (!($actualValue instanceof Definition)) {
129+
if ($returnResult) {
130+
return false;
131+
}
132+
133+
$this->fail(
134+
$definition,
135+
sprintf(
136+
'The value of argument with index %s (%s) was expected to an instance of Symfony\Component\DependencyInjection\Reference or \Symfony\Component\DependencyInjection\Definition',
137+
$this->argumentIndex,
138+
$this->exporter->export($actualValue)
139+
)
140+
);
141+
}
142+
143+
if (!is_a($serviceLocatorDef->getClass(), ServiceLocator::class, true)) {
144+
if ($returnResult) {
145+
return false;
146+
}
147+
148+
$this->fail(
149+
$definition,
150+
sprintf(
151+
'The referenced service class of argument with index %s (%s) was expected to be an instance of Symfony\Component\DependencyInjection\ServiceLocator',
152+
$this->argumentIndex,
153+
$this->exporter->export($serviceLocatorDef->getClass())
154+
)
155+
);
156+
}
157+
158+
// Service locator was provided as context (and therefor a factory)
159+
if (isset($serviceLocatorDef->getFactory()[1])) {
160+
$serviceLocatorDef = $container->findDefinition((string) $serviceLocatorDef->getFactory()[0]);
161+
}
162+
163+
return $this->evaluateServiceDefinition($serviceLocatorDef, $definition, $returnResult);
164+
}
165+
166+
private function evaluateServiceDefinition(Definition $serviceLocatorDef, Definition $definition, $returnResult): bool
167+
{
168+
$actualValue = $serviceLocatorDef->getArgument(0);
169+
$constraint = new IsEqual($this->expectedValue);
170+
171+
if (!$constraint->evaluate($actualValue, '', true)) {
172+
if ($returnResult) {
173+
return false;
174+
}
175+
176+
$this->fail(
177+
$definition,
178+
sprintf(
179+
'The value of argument with index %s (%s) does not equal to the expected ServiceLocator service-map (%s)',
180+
$this->argumentIndex,
181+
$this->exporter->export($actualValue),
182+
$this->exporter->export($this->expectedValue)
183+
)
184+
);
185+
}
186+
187+
return true;
188+
}
189+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
namespace Matthias\SymfonyDependencyInjectionTest\PhpUnit;
4+
5+
use PHPUnit\Framework\Constraint\Constraint;
6+
use PHPUnit\Framework\Constraint\IsEqual;
7+
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
8+
use Symfony\Component\DependencyInjection\Definition;
9+
use Symfony\Component\DependencyInjection\Reference;
10+
use Symfony\Component\DependencyInjection\ServiceLocator;
11+
12+
class DefinitionEqualsServiceLocatorConstraint extends Constraint
13+
{
14+
private $expectedValue;
15+
16+
public function __construct($expectedValue)
17+
{
18+
parent::__construct();
19+
20+
$this->expectedValue = array_map(
21+
function ($serviceId) {
22+
if (is_string($serviceId)) {
23+
return new ServiceClosureArgument(new Reference($serviceId));
24+
}
25+
26+
if (!$serviceId instanceof ServiceClosureArgument) {
27+
return new ServiceClosureArgument($serviceId);
28+
}
29+
30+
return $serviceId;
31+
},
32+
$expectedValue
33+
);
34+
}
35+
36+
public function toString(): string
37+
{
38+
return sprintf('service definition is a service locator');
39+
}
40+
41+
public function evaluate($other, $description = '', $returnResult = false)
42+
{
43+
if (!($other instanceof Definition)) {
44+
throw new \InvalidArgumentException(
45+
'Expected an instance of Symfony\Component\DependencyInjection\Definition'
46+
);
47+
}
48+
49+
return $this->evaluateServiceDefinition($other, $returnResult);
50+
}
51+
52+
private function evaluateServiceDefinition(Definition $definition, $returnResult)
53+
{
54+
if (!$this->evaluateServiceDefinitionClass($definition, $returnResult)) {
55+
return false;
56+
}
57+
58+
return $this->evaluateArgumentIndex($definition, $returnResult);
59+
}
60+
61+
private function evaluateServiceDefinitionClass(Definition $definition, $returnResult)
62+
{
63+
if (is_a($definition->getClass(), ServiceLocator::class, true)) {
64+
return true;
65+
}
66+
67+
if ($returnResult) {
68+
return false;
69+
}
70+
71+
$this->fail(
72+
$definition,
73+
sprintf(
74+
'class %s was expected as service definition class, found %s instead',
75+
$this->exporter->export(ServiceLocator::class),
76+
$this->exporter->export($definition->getClass())
77+
)
78+
);
79+
}
80+
81+
private function evaluateArgumentIndex(Definition $definition, $returnResult)
82+
{
83+
$actualValue = $definition->getArgument(0);
84+
$constraint = new IsEqual($this->expectedValue);
85+
86+
if (!$constraint->evaluate($actualValue, '', true)) {
87+
if ($returnResult) {
88+
return false;
89+
}
90+
91+
$this->fail(
92+
$definition,
93+
sprintf(
94+
'The service-map %s does not equal to the expected service-map (%s)',
95+
$this->exporter->export($actualValue),
96+
$this->exporter->export($this->expectedValue)
97+
)
98+
);
99+
}
100+
101+
return true;
102+
}
103+
}

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,13 +311,18 @@ the given index.</dd>
311311
<dt><code>assertContainerBuilderHasServiceDefinitionWithArgument($serviceId, $argumentIndex, $expectedValue)</code></dt>
312312
<dd>Assert that the <code>ContainerBuilder</code> for this test has a service definition with the given id, which has an argument at
313313
the given index, and its value is the given value.</dd>
314+
<dt><code>assertContainerBuilderHasServiceDefinitionWithServiceLocatorArgument($serviceId, $argumentIndex, $expectedValue)</code></dt>
315+
<dd>Assert that the <code>ContainerBuilder</code> for this test has a service definition with the given id, which has an argument
316+
at the given index, and its value is a ServiceLocator with a reference-map equal to the given value.</dd>
314317
<dt><code>assertContainerBuilderHasServiceDefinitionWithMethodCall($serviceId, $method, array $arguments = array(), $index = null)</code></dt>
315318
<dd>Assert that the <code>ContainerBuilder</code> for this test has a service definition with the given id, which has a method call to
316319
the given method with the given arguments. If index is provided, invocation index order of method call is asserted as well.</dd>
317320
<dt><code>assertContainerBuilderHasServiceDefinitionWithTag($serviceId, $tag, array $attributes = array())</code></dt>
318321
<dd>Assert that the <code>ContainerBuilder</code> for this test has a service definition with the given id, which has the given tag with the given arguments.</dd>
319322
<dt><code>assertContainerBuilderHasServiceDefinitionWithParent($serviceId, $parentServiceId)</code></dt>
320323
<dd>Assert that the <code>ContainerBuilder</code> for this test has a service definition with the given id which is a decorated service and it has the given parent service.</dd>
324+
<dt><code>assertContainerBuilderHasServiceLocator($serviceId, $expectedServiceMap)</code></dt>
325+
<dd>Assert that the <code>ContainerBuilder</code> for this test has a ServiceLocator service definition with the given id.</dd>
321326
</dl>
322327

323328
## Available methods to set up container

0 commit comments

Comments
 (0)