Skip to content

Commit

Permalink
Support container variables from factory function that returns string
Browse files Browse the repository at this point in the history
  • Loading branch information
clue committed Jul 28, 2022
1 parent 9b1d235 commit 350cd67
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 14 deletions.
15 changes: 8 additions & 7 deletions docs/best-practices/controllers.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,21 +285,22 @@ $container = new FrameworkX\Container([
```

Factory functions used in the container configuration map may also reference
string variables defined in the container configuration. This can be
particularly useful when combining autowiring with some manual configuration
like this:
string variables defined in the container configuration. You may also use
factory functions that return string variables. This can be particularly useful
when combining autowiring with some manual configuration like this:

```php title="public/index.php"
<?php

require __DIR__ . '/../vendor/autoload.php';

$container = new FrameworkX\Container([
Acme\Todo\UserController::class => function (string $name) {
// example UserController class requires single string argument
return new Acme\Todo\UserController($name);
Acme\Todo\UserController::class => function (string $name, string $hostname) {
// example UserController class requires two string arguments
return new Acme\Todo\UserController($name, $hostname);
},
'name' => 'Acme'
'name' => 'Acme',
'hostname' => fn(): string => gethostname()
]);

// …
Expand Down
27 changes: 23 additions & 4 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
*/
class Container
{
/** @var array<string,object|callable():(object|class-string)|string>|ContainerInterface */
/** @var array<string,object|callable():(object|string)|string>|ContainerInterface */
private $container;

/** @var array<string,callable():(object|class-string) | object | string>|ContainerInterface $loader */
/** @var array<string,callable():(object|string) | object | string>|ContainerInterface $loader */
public function __construct($loader = [])
{
if (!\is_array($loader) && !$loader instanceof ContainerInterface) {
Expand Down Expand Up @@ -221,7 +221,7 @@ private function loadFunctionParams(\ReflectionFunctionAbstract $function, int $

// load string variables from container
if ($allowVariables && $type->getName() === 'string') {
$params[] = $this->loadVariable($parameter->getName());
$params[] = $this->loadVariable($parameter->getName(), $depth);
continue;
}

Expand All @@ -242,12 +242,31 @@ private function loadFunctionParams(\ReflectionFunctionAbstract $function, int $
}

/** @throws \BadMethodCallException if $name is not a valid string variable */
private function loadVariable(string $name): string
private function loadVariable(string $name, int $depth): string
{
if (!isset($this->container[$name])) {
throw new \BadMethodCallException('Container variable $' . $name . ' is not defined');
}

if ($this->container[$name] instanceof \Closure) {
if ($depth < 1) {
throw new \BadMethodCallException('Container variable $' . $name . ' is recursive');
}

// build list of factory parameters based on parameter types
$closure = new \ReflectionFunction($this->container[$name]);
$params = $this->loadFunctionParams($closure, $depth - 1, true);

// invoke factory with list of parameters
$value = $params === [] ? ($this->container[$name])() : ($this->container[$name])(...$params);

if (!\is_string($value)) {
throw new \BadMethodCallException('Container variable $' . $name . ' expected type string from factory, but got ' . (\is_object($value) ? \get_class($value) : \gettype($value)));
}

$this->container[$name] = $value;
}

$value = $this->container[$name];
if (!\is_string($value)) {
throw new \BadMethodCallException('Container variable $' . $name . ' expected type string, but got ' . (\is_object($value) ? \get_class($value) : \gettype($value)));
Expand Down
141 changes: 138 additions & 3 deletions tests/ContainerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,77 @@ public function __invoke(ServerRequestInterface $request)
$this->assertEquals('{"name":"Alice"}', (string) $response->getBody());
}

public function testCallableReturnsCallableForClassNameMappedFromFactoryWithStringVariableMappedFromFactory()
{
$request = new ServerRequest('GET', 'http://example.com/');

$controller = new class(new \stdClass()) {
private $data;

public function __construct(\stdClass $data)
{
$this->data = $data;
}

public function __invoke(ServerRequestInterface $request)
{
return new Response(200, [], json_encode($this->data));
}
};

$container = new Container([
\stdClass::class => function (string $username) {
return (object) ['name' => $username];
},
'username' => function () {
return 'Alice';
}
]);

$callable = $container->callable(get_class($controller));

$response = $callable($request);
$this->assertInstanceOf(ResponseInterface::class, $response);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('{"name":"Alice"}', (string) $response->getBody());
}

public function testCallableReturnsCallableForClassNameReferencingVariableMappedFromFactoryReferencingVariable()
{
$request = new ServerRequest('GET', 'http://example.com/');

$controller = new class(new \stdClass()) {
private $data;

public function __construct(\stdClass $data)
{
$this->data = $data;
}

public function __invoke(ServerRequestInterface $request)
{
return new Response(200, [], json_encode($this->data));
}
};

$container = new Container([
\stdClass::class => function (string $username) {
return (object) ['name' => $username];
},
'username' => function (string $role) {
return strtoupper($role);
},
'role' => 'admin'
]);

$callable = $container->callable(get_class($controller));

$response = $callable($request);
$this->assertInstanceOf(ResponseInterface::class, $response);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('{"name":"ADMIN"}', (string) $response->getBody());
}

public function testCallableReturnsCallableThatThrowsWhenFactoryReferencesUnknownVariable()
{
$request = new ServerRequest('GET', 'http://example.com/');
Expand Down Expand Up @@ -305,7 +376,71 @@ public function __invoke(ServerRequestInterface $request)
$callable($request);
}

public function testCallableReturnsCallableThatThrowsWhenFactoryReferencesVariableOfUnexpectedType()
public function testCallableReturnsCallableThatThrowsWhenFactoryReferencesRecursiveVariable()
{
$request = new ServerRequest('GET', 'http://example.com/');

$controller = new class(new \stdClass()) {
private $data;

public function __construct(\stdClass $data)
{
$this->data = $data;
}

public function __invoke(ServerRequestInterface $request)
{
return new Response(200, [], json_encode($this->data));
}
};

$container = new Container([
\stdClass::class => function (string $stdClass) {
return (object) ['name' => $stdClass];
}
]);

$callable = $container->callable(get_class($controller));

$this->expectException(\BadMethodCallException::class);
$this->expectExceptionMessage('Container variable $stdClass is recursive');
$callable($request);
}

public function testCallableReturnsCallableThatThrowsWhenFactoryReferencesVariableMappedWithUnexpectedType()
{
$request = new ServerRequest('GET', 'http://example.com/');

$controller = new class('') {
private $data;

public function __construct(string $stdClass)
{
$this->data = $stdClass;
}

public function __invoke(ServerRequestInterface $request)
{
return new Response(200, [], json_encode($this->data));
}
};

$container = new Container([
get_class($controller) => function (string $stdClass) use ($controller) {
$class = get_class($controller);
return new $class($stdClass);
},
\stdClass::class => (object) []
]);

$callable = $container->callable(get_class($controller));

$this->expectException(\BadMethodCallException::class);
$this->expectExceptionMessage('Container variable $stdClass expected type string, but got stdClass');
$callable($request);
}

public function testCallableReturnsCallableThatThrowsWhenFactoryReferencesVariableMappedFromFactoryWithUnexpectedReturnType()
{
$request = new ServerRequest('GET', 'http://example.com/');

Expand All @@ -328,14 +463,14 @@ public function __invoke(ServerRequestInterface $request)
return (object) ['name' => $http];
},
'http' => function () {
return 'http/3';
return 3;
}
]);

$callable = $container->callable(get_class($controller));

$this->expectException(\BadMethodCallException::class);
$this->expectExceptionMessage('Container variable $http expected type string, but got Closure');
$this->expectExceptionMessage('Container variable $http expected type string from factory, but got integer');
$callable($request);
}

Expand Down

0 comments on commit 350cd67

Please sign in to comment.