Skip to content

Commit 52e78c6

Browse files
committed
Support class name references (aliases) in Container configuration
1 parent 1009341 commit 52e78c6

3 files changed

Lines changed: 140 additions & 4 deletions

File tree

docs/best-practices/controllers.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,32 @@ name to a factory function that will be invoked when this class is first
270270
requested. The factory function is responsible for returning an instance that
271271
implements the given class name.
272272

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+
use FrameworkX\Container;
282+
use Psr\Http\Message\ResponseInterface;
283+
use React\Cache\ArrayCache;
284+
use React\Cache\CacheInterface;
285+
use React\Http\Message\Response;
286+
287+
// …
288+
289+
$container = new Container([
290+
ResponseInterface::class => Response::class,
291+
CacheInterface::class => function () {
292+
return ArrayCache::class;
293+
}
294+
]);
295+
296+
// …
297+
```
298+
273299
### PSR-11 compatibility
274300

275301
> ⚠️ **Feature preview**

src/Container.php

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,18 @@
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
{
1818
foreach ($map as $name => $value) {
19-
if (!$value instanceof \Closure && !$value instanceof $name) {
19+
if (\is_string($value)) {
20+
$map[$name] = static function () use ($value) {
21+
return $value;
22+
};
23+
} elseif (!$value instanceof \Closure && !$value instanceof $name) {
2024
throw new \BadMethodCallException('Map for ' . $name . ' contains unexpected ' . (is_object($value) ? get_class($value) : gettype($value)));
2125
}
2226
}
@@ -88,7 +92,13 @@ private function load(string $name, int $depth = 64)
8892
if ($this->container[$name] instanceof \Closure) {
8993
$value = ($this->container[$name])();
9094

91-
if (!$value instanceof $name) {
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) {
92102
throw new \BadMethodCallException('Factory for ' . $name . ' returned unexpected ' . (is_object($value) ? get_class($value) : gettype($value)));
93103
}
94104

tests/ContainerTest.php

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,76 @@ 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+
135205
public function testCtorThrowsWhenMapContainsInvalidInteger()
136206
{
137207
$this->expectException(\BadMethodCallException::class);
@@ -142,6 +212,21 @@ public function testCtorThrowsWhenMapContainsInvalidInteger()
142212
]);
143213
}
144214

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+
145230
public function testCallableReturnsCallableThatThrowsWhenFactoryReturnsInvalidInteger()
146231
{
147232
$request = new ServerRequest('GET', 'http://example.com/');
@@ -157,6 +242,21 @@ public function testCallableReturnsCallableThatThrowsWhenFactoryReturnsInvalidIn
157242
$callable($request);
158243
}
159244

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+
160260
public function testInvokeContainerAsMiddlewareReturnsFromNextRequestHandler()
161261
{
162262
$request = new ServerRequest('GET', 'http://example.com/');

0 commit comments

Comments
 (0)