Skip to content

Commit 9fe083d

Browse files
committed
Support explicit container configuration for arguments with defaults
1 parent fa030e0 commit 9fe083d

3 files changed

Lines changed: 182 additions & 12 deletions

File tree

docs/best-practices/controllers.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,8 @@ covers most common use cases:
188188
* Class names need to be loadable through the autoloader. See
189189
[composer autoloading](#composer-autoloading) above.
190190
* Each class may or may not have a constructor.
191-
* If the constructor has an optional argument, it will be omitted.
191+
* If the constructor has an optional argument, it will be omitted unless an
192+
explicit [container configuration](#container-configuration) is used.
192193
* If the constructor has a nullable argument, it will be given a `null` value
193194
unless an explicit [container configuration](#container-configuration) is used.
194195
* If the constructor references another class, it will load this class next.
@@ -307,6 +308,25 @@ manual configuration like this:
307308
'hostname' => fn(): string => gethostname()
308309
]);
309310

311+
// …
312+
```
313+
314+
=== "Default values"
315+
316+
```php title="public/index.php"
317+
<?php
318+
319+
require __DIR__ . '/../vendor/autoload.php';
320+
321+
$container = new FrameworkX\Container([
322+
Acme\Todo\UserController::class => function (bool $debug = false) {
323+
// example UserController class uses $debug, apply default if not set
324+
return new Acme\Todo\UserController($debug);
325+
},
326+
'debug' => true
327+
]);
328+
329+
310330
// …
311331
```
312332

src/Container.php

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -197,13 +197,6 @@ private function loadFunctionParams(\ReflectionFunctionAbstract $function, int $
197197
{
198198
$params = [];
199199
foreach ($function->getParameters() as $parameter) {
200-
assert($parameter instanceof \ReflectionParameter);
201-
202-
// stop building parameters when encountering first optional parameter
203-
if ($parameter->isOptional()) {
204-
break;
205-
}
206-
207200
$params[] = $this->loadParameter($parameter, $depth, $allowVariables);
208201
}
209202

@@ -219,13 +212,18 @@ private function loadParameter(\ReflectionParameter $parameter, int $depth, bool
219212
// ensure parameter is typed
220213
$type = $parameter->getType();
221214
if ($type === null) {
215+
if ($parameter->isDefaultValueAvailable()) {
216+
return $parameter->getDefaultValue();
217+
}
222218
throw new \BadMethodCallException(self::parameterError($parameter) . ' has no type');
223219
}
224220

221+
$hasDefault = $parameter->isDefaultValueAvailable() || $parameter->allowsNull();
222+
225223
// abort for union types (PHP 8.0+) and intersection types (PHP 8.1+)
226224
if ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) { // @codeCoverageIgnoreStart
227-
if ($type->allowsNull()) {
228-
return null;
225+
if ($hasDefault) {
226+
return $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null;
229227
}
230228
throw new \BadMethodCallException(self::parameterError($parameter) . ' expects unsupported type ' . $type);
231229
} // @codeCoverageIgnoreEnd
@@ -238,8 +236,8 @@ private function loadParameter(\ReflectionParameter $parameter, int $depth, bool
238236
}
239237

240238
// use null for nullable arguments if not already loaded above
241-
if ($type->allowsNull() && !isset($this->container[$type->getName()])) {
242-
return null;
239+
if ($hasDefault && !isset($this->container[$type->getName()])) {
240+
return $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null;
243241
}
244242

245243
// abort if required container variable is not defined

tests/ContainerTest.php

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,124 @@ public function __invoke(ServerRequestInterface $request)
156156
$this->assertEquals('null', (string) $response->getBody());
157157
}
158158

159+
public function testCallableReturnsCallableForClassWithNullDefaultViaAutowiringWillDefaultToNullValue()
160+
{
161+
$request = new ServerRequest('GET', 'http://example.com/');
162+
163+
$controller = new class(null) {
164+
private $data = false;
165+
166+
public function __construct(\stdClass $data = null)
167+
{
168+
$this->data = $data;
169+
}
170+
171+
public function __invoke(ServerRequestInterface $request)
172+
{
173+
return new Response(200, [], json_encode($this->data));
174+
}
175+
};
176+
177+
$container = new Container([]);
178+
179+
$callable = $container->callable(get_class($controller));
180+
$this->assertInstanceOf(\Closure::class, $callable);
181+
182+
$response = $callable($request);
183+
$this->assertInstanceOf(ResponseInterface::class, $response);
184+
$this->assertEquals(200, $response->getStatusCode());
185+
$this->assertEquals('null', (string) $response->getBody());
186+
}
187+
188+
public function testCallableReturnsCallableForClassWithNullDefaultViaContainerConfiguration()
189+
{
190+
$request = new ServerRequest('GET', 'http://example.com/');
191+
192+
$controller = new class(null) {
193+
private $data = false;
194+
195+
public function __construct(\stdClass $data = null)
196+
{
197+
$this->data = $data;
198+
}
199+
200+
public function __invoke(ServerRequestInterface $request)
201+
{
202+
return new Response(200, [], json_encode($this->data));
203+
}
204+
};
205+
206+
$container = new Container([
207+
\stdClass::class => (object) []
208+
]);
209+
210+
$callable = $container->callable(get_class($controller));
211+
$this->assertInstanceOf(\Closure::class, $callable);
212+
213+
$response = $callable($request);
214+
$this->assertInstanceOf(ResponseInterface::class, $response);
215+
$this->assertEquals(200, $response->getStatusCode());
216+
$this->assertEquals('{}', (string) $response->getBody());
217+
}
218+
219+
/**
220+
* @requires PHP 8
221+
*/
222+
public function testCallableReturnsCallableForUnionWithIntDefaultValueViaAutowiringWillDefaultToIntValue()
223+
{
224+
$request = new ServerRequest('GET', 'http://example.com/');
225+
226+
$controller = new class(null) {
227+
private $data = false;
228+
229+
#[PHP8] public function __construct(string|int|null $data = 42) { $this->data = $data; }
230+
231+
public function __invoke(ServerRequestInterface $request)
232+
{
233+
return new Response(200, [], json_encode($this->data));
234+
}
235+
};
236+
237+
$container = new Container([]);
238+
239+
$callable = $container->callable(get_class($controller));
240+
$this->assertInstanceOf(\Closure::class, $callable);
241+
242+
$response = $callable($request);
243+
$this->assertInstanceOf(ResponseInterface::class, $response);
244+
$this->assertEquals(200, $response->getStatusCode());
245+
$this->assertEquals('42', (string) $response->getBody());
246+
}
247+
248+
public function testCallableReturnsCallableForUndefaultWithStringDefaultViaAutowiringWillDefaultToStringValue()
249+
{
250+
$request = new ServerRequest('GET', 'http://example.com/');
251+
252+
$controller = new class(null) {
253+
private $data = false;
254+
255+
public function __construct($data = 'empty')
256+
{
257+
$this->data = $data;
258+
}
259+
260+
public function __invoke(ServerRequestInterface $request)
261+
{
262+
return new Response(200, [], json_encode($this->data));
263+
}
264+
};
265+
266+
$container = new Container([]);
267+
268+
$callable = $container->callable(get_class($controller));
269+
$this->assertInstanceOf(\Closure::class, $callable);
270+
271+
$response = $callable($request);
272+
$this->assertInstanceOf(ResponseInterface::class, $response);
273+
$this->assertEquals(200, $response->getStatusCode());
274+
$this->assertEquals('"empty"', (string) $response->getBody());
275+
}
276+
159277
public function testCallableReturnsCallableForClassNameViaAutowiringWithFactoryFunctionForDependency()
160278
{
161279
$request = new ServerRequest('GET', 'http://example.com/');
@@ -469,6 +587,40 @@ public function __invoke()
469587
$this->assertEquals('{"user":{},"data":null}', (string) $response->getBody());
470588
}
471589

590+
public function testCallableReturnsCallableForClassNameWithDependencyMappedWithFactoryThatRequiresContainerVariablesWithDefaultValues()
591+
{
592+
$request = new ServerRequest('GET', 'http://example.com/');
593+
594+
$controller = new class(new Response()) {
595+
private $response;
596+
597+
public function __construct(ResponseInterface $response)
598+
{
599+
$this->response = $response;
600+
}
601+
602+
public function __invoke()
603+
{
604+
return $this->response;
605+
}
606+
};
607+
608+
$container = new Container([
609+
ResponseInterface::class => function (string $name = 'Alice', int $age = 0) {
610+
return new Response(200, [], json_encode(['name' => $name, 'age' => $age]));
611+
},
612+
'age' => 42
613+
]);
614+
615+
$callable = $container->callable(get_class($controller));
616+
$this->assertInstanceOf(\Closure::class, $callable);
617+
618+
$response = $callable($request);
619+
$this->assertInstanceOf(ResponseInterface::class, $response);
620+
$this->assertEquals(200, $response->getStatusCode());
621+
$this->assertEquals('{"name":"Alice","age":42}', (string) $response->getBody());
622+
}
623+
472624
public function testCallableReturnsCallableForClassNameWithDependencyMappedWithFactoryThatRequiresScalarVariables()
473625
{
474626
$request = new ServerRequest('GET', 'http://example.com/');

0 commit comments

Comments
 (0)