Skip to content

Commit 94b5526

Browse files
authored
Merge pull request #96 from clue-labs/container-class-names
Support class name references (aliases) in `Container` configuration
2 parents 5350ea2 + 010c23e commit 94b5526

3 files changed

Lines changed: 176 additions & 4 deletions

File tree

docs/best-practices/controllers.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,9 +262,35 @@ This can be useful in these cases:
262262

263263
The configured container instance can be passed into the application like any
264264
other middleware request handler. In most cases this means you create a single
265-
`Container` instance with a number of factory methods and pass this instance as
265+
`Container` instance with a number of factory functions and pass this instance as
266266
the first argument to the `App`.
267267

268+
In its most common form, each entry in the container configuration maps a class
269+
name to a factory function that will be invoked when this class is first
270+
requested. The factory function is responsible for returning an instance that
271+
implements the given class name.
272+
273+
The container configuration may also be used to map a class name to a different
274+
class name that implements the same interface, either by mapping between two
275+
class names or using a factory function that returns a class name. This is
276+
particularly useful when implementing an interface.
277+
278+
```php title="public/index.php"
279+
<?php
280+
281+
require __DIR__ . '/../vendor/autoload.php';
282+
283+
$container = new FrameworkX\Container([
284+
React\Cache\CacheInterface::class => React\Cache\ArrayCache::class,
285+
Psr\Http\Message\ResponseInterface::class => function () {
286+
// returns class implementing interface from factory function
287+
return React\Http\Message\Response::class;
288+
}
289+
]);
290+
291+
// …
292+
```
293+
268294
### PSR-11 compatibility
269295

270296
> ⚠️ **Feature preview**

src/Container.php

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,21 @@
99
*/
1010
class Container
1111
{
12-
/** @var array<class-string,object|callable():object> */
12+
/** @var array<class-string,object|callable():(object|class-string)> */
1313
private $container;
1414

15-
/** @var array<class-string,callable():object | object> */
15+
/** @var array<class-string,callable():(object|class-string) | object | class-string> */
1616
public function __construct(array $map = [])
1717
{
18+
foreach ($map as $name => $value) {
19+
if (\is_string($value)) {
20+
$map[$name] = static function () use ($value) {
21+
return $value;
22+
};
23+
} elseif (!$value instanceof \Closure && !$value instanceof $name) {
24+
throw new \BadMethodCallException('Map for ' . $name . ' contains unexpected ' . (is_object($value) ? get_class($value) : gettype($value)));
25+
}
26+
}
1827
$this->container = $map;
1928
}
2029

@@ -81,7 +90,19 @@ private function load(string $name, int $depth = 64)
8190
{
8291
if (isset($this->container[$name])) {
8392
if ($this->container[$name] instanceof \Closure) {
84-
$this->container[$name] = ($this->container[$name])();
93+
$value = ($this->container[$name])();
94+
95+
if (\is_string($value)) {
96+
if ($depth < 1) {
97+
throw new \BadMethodCallException('Factory for ' . $name . ' is recursive');
98+
}
99+
100+
$value = $this->load($value, $depth - 1);
101+
} elseif (!$value instanceof $name) {
102+
throw new \BadMethodCallException('Factory for ' . $name . ' returned unexpected ' . (is_object($value) ? get_class($value) : gettype($value)));
103+
}
104+
105+
$this->container[$name] = $value;
85106
}
86107

87108
return $this->container[$name];

tests/ContainerTest.php

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,131 @@ public function __invoke(ServerRequestInterface $request)
132132
$this->assertEquals('{"num":1}', (string) $response->getBody());
133133
}
134134

135+
public function testCallableReturnsCallableForClassNameWithExplicitlyMappedSubclassForDependency()
136+
{
137+
$request = new ServerRequest('GET', 'http://example.com/');
138+
139+
$dto = new class extends \stdClass { };
140+
$dto->name = 'Alice';
141+
142+
$controller = new class(new \stdClass()) {
143+
private $data;
144+
145+
public function __construct(\stdClass $data)
146+
{
147+
$this->data = $data;
148+
}
149+
150+
public function __invoke(ServerRequestInterface $request)
151+
{
152+
return new Response(200, [], json_encode($this->data));
153+
}
154+
};
155+
156+
$container = new Container([
157+
\stdClass::class => get_class($dto),
158+
get_class($dto) => $dto
159+
]);
160+
161+
$callable = $container->callable(get_class($controller));
162+
$this->assertInstanceOf(\Closure::class, $callable);
163+
164+
$response = $callable($request);
165+
$this->assertInstanceOf(ResponseInterface::class, $response);
166+
$this->assertEquals(200, $response->getStatusCode());
167+
$this->assertEquals('{"name":"Alice"}', (string) $response->getBody());
168+
}
169+
170+
public function testCallableReturnsCallableForClassNameWithSubclassMappedFromFactoryForDependency()
171+
{
172+
$request = new ServerRequest('GET', 'http://example.com/');
173+
174+
$dto = new class extends \stdClass { };
175+
$dto->name = 'Alice';
176+
177+
$controller = new class(new \stdClass()) {
178+
private $data;
179+
180+
public function __construct(\stdClass $data)
181+
{
182+
$this->data = $data;
183+
}
184+
185+
public function __invoke(ServerRequestInterface $request)
186+
{
187+
return new Response(200, [], json_encode($this->data));
188+
}
189+
};
190+
191+
$container = new Container([
192+
\stdClass::class => function () use ($dto) { return get_class($dto); },
193+
get_class($dto) => function () use ($dto) { return $dto; }
194+
]);
195+
196+
$callable = $container->callable(get_class($controller));
197+
$this->assertInstanceOf(\Closure::class, $callable);
198+
199+
$response = $callable($request);
200+
$this->assertInstanceOf(ResponseInterface::class, $response);
201+
$this->assertEquals(200, $response->getStatusCode());
202+
$this->assertEquals('{"name":"Alice"}', (string) $response->getBody());
203+
}
204+
205+
public function testCtorThrowsWhenMapContainsInvalidInteger()
206+
{
207+
$this->expectException(\BadMethodCallException::class);
208+
$this->expectExceptionMessage('Map for stdClass contains unexpected integer');
209+
210+
new Container([
211+
\stdClass::class => 42
212+
]);
213+
}
214+
215+
public function testCallableReturnsCallableThatThrowsWhenFactoryReturnsInvalidClassName()
216+
{
217+
$request = new ServerRequest('GET', 'http://example.com/');
218+
219+
$container = new Container([
220+
\stdClass::class => function () { return 'invalid'; }
221+
]);
222+
223+
$callable = $container->callable(\stdClass::class);
224+
225+
$this->expectException(\BadMethodCallException::class);
226+
$this->expectExceptionMessage('Class invalid not found');
227+
$callable($request);
228+
}
229+
230+
public function testCallableReturnsCallableThatThrowsWhenFactoryReturnsInvalidInteger()
231+
{
232+
$request = new ServerRequest('GET', 'http://example.com/');
233+
234+
$container = new Container([
235+
\stdClass::class => function () { return 42; }
236+
]);
237+
238+
$callable = $container->callable(\stdClass::class);
239+
240+
$this->expectException(\BadMethodCallException::class);
241+
$this->expectExceptionMessage('Factory for stdClass returned unexpected integer');
242+
$callable($request);
243+
}
244+
245+
public function testCallableReturnsCallableThatThrowsWhenFactoryIsRecursive()
246+
{
247+
$request = new ServerRequest('GET', 'http://example.com/');
248+
249+
$container = new Container([
250+
\stdClass::class => \stdClass::class
251+
]);
252+
253+
$callable = $container->callable(\stdClass::class);
254+
255+
$this->expectException(\BadMethodCallException::class);
256+
$this->expectExceptionMessage('Factory for stdClass is recursive');
257+
$callable($request);
258+
}
259+
135260
public function testInvokeContainerAsMiddlewareReturnsFromNextRequestHandler()
136261
{
137262
$request = new ServerRequest('GET', 'http://example.com/');

0 commit comments

Comments
 (0)