Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions docs/best-practices/controllers.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,9 +285,10 @@ $container = new FrameworkX\Container([
```

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

```php title="public/index.php"
<?php
Expand All @@ -296,7 +297,7 @@ require __DIR__ . '/../vendor/autoload.php';

$container = new FrameworkX\Container([
Acme\Todo\UserController::class => function (bool $debug, string $hostname) {
// example UserController class requires two scalar arguments
// example UserController class uses two container variables
return new Acme\Todo\UserController($debug, $hostname);
},
'debug' => false,
Expand All @@ -306,9 +307,9 @@ $container = new FrameworkX\Container([
// …
```

> ℹ️ **Avoiding name conflicts**
> ℹ️ **Avoiding name collisions**
>
> Note that class names and scalar variables share the same container
> Note that class names and container variables share the same container
> configuration map and as such might be subject to name collisions as a single
> entry may only have a single value. For this reason, container variables will
> only be used for container functions by default. We highly recommend using
Expand Down
39 changes: 24 additions & 15 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ public function __construct($loader = [])
}

foreach (($loader instanceof ContainerInterface ? [] : $loader) as $name => $value) {
if (!\is_scalar($value) && !$value instanceof \Closure && !$value instanceof $name) {
if (
(!\is_object($value) && !\is_scalar($value)) ||
(!$value instanceof $name && !$value instanceof \Closure && !\is_string($value) && \strpos($name, '\\') !== false)
) {
throw new \BadMethodCallException('Map for ' . $name . ' contains unexpected ' . (is_object($value) ? get_class($value) : gettype($value)));
}
}
Expand Down Expand Up @@ -154,8 +157,8 @@ private function loadObject(string $name, int $depth = 64) /*: object (PHP 7.2+)
}

$this->container[$name] = $value;
} elseif (\is_scalar($this->container[$name])) {
throw new \BadMethodCallException('Map for ' . $name . ' contains unexpected ' . \gettype($this->container[$name]));
} elseif (!$this->container[$name] instanceof $name) {
throw new \BadMethodCallException('Map for ' . $name . ' contains unexpected ' . (\is_object($this->container[$name]) ? \get_class($this->container[$name]) : \gettype($this->container[$name])));
}

assert($this->container[$name] instanceof $name);
Expand Down Expand Up @@ -237,17 +240,21 @@ private function loadFunctionParams(\ReflectionFunctionAbstract $function, int $
throw new \BadMethodCallException(self::parameterError($parameter) . ' is recursive');
}

$params[] = $this->loadObject($type->getName(), $depth - 1);
if ($allowVariables && isset($this->container[$parameter->getName()])) {
$params[] = $this->loadVariable($parameter->getName(), $type->getName(), $depth);
} else {
$params[] = $this->loadObject($type->getName(), $depth - 1);
}
}

return $params;
}

/**
* @return string|int|float|bool
* @throws \BadMethodCallException if $name is not a valid scalar variable
* @return object|string|int|float|bool
* @throws \BadMethodCallException if $name is not a valid container variable
*/
private function loadVariable(string $name, string $type, int $depth) /*: string|int|float|bool (PHP 8.0+) */
private function loadVariable(string $name, string $type, int $depth) /*: object|string|int|float|bool (PHP 8.0+) */
{
if (!isset($this->container[$name])) {
throw new \BadMethodCallException('Container variable $' . $name . ' is not defined');
Expand All @@ -265,20 +272,22 @@ private function loadVariable(string $name, string $type, int $depth) /*: string
// invoke factory with list of parameters
$value = $params === [] ? ($this->container[$name])() : ($this->container[$name])(...$params);

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

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

$value = $this->container[$name];
if (!\is_scalar($value)) {
throw new \BadMethodCallException('Container variable $' . $name . ' expected scalar type, but got ' . (\is_object($value) ? \get_class($value) : \gettype($value)));
}

if (($type === 'string' && !\is_string($value)) || ($type === 'int' && !\is_int($value)) || ($type === 'float' && !\is_float($value)) || ($type === 'bool' && !\is_bool($value))) {
throw new \BadMethodCallException('Container variable $' . $name . ' expected type ' . $type . ', but got ' . \gettype($value));
assert(\is_object($value) || \is_scalar($value));

if (
(\is_object($value) && !$value instanceof $type) ||
(!\is_object($value) && !\in_array($type, ['string', 'int', 'float', 'bool'])) ||
($type === 'string' && !\is_string($value)) || ($type === 'int' && !\is_int($value)) || ($type === 'float' && !\is_float($value)) || ($type === 'bool' && !\is_bool($value))
) {
throw new \BadMethodCallException('Container variable $' . $name . ' expected type ' . $type . ', but got ' . (\is_object($value) ? \get_class($value) : \gettype($value)));
}

return $value;
Expand Down
171 changes: 166 additions & 5 deletions tests/ContainerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,76 @@ public function __invoke()
$this->assertEquals('{"name":"Alice"}', (string) $response->getBody());
}

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

$controller = new class(new Response()) {
private $response;

public function __construct(ResponseInterface $response)
{
$this->response = $response;
}

public function __invoke()
{
return $this->response;
}
};

$container = new Container([
ResponseInterface::class => function (\stdClass $data) {
return new Response(200, [], json_encode($data));
},
'data' => (object) ['name' => 'Alice']
]);

$callable = $container->callable(get_class($controller));
$this->assertInstanceOf(\Closure::class, $callable);

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

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

$controller = new class(new Response()) {
private $response;

public function __construct(ResponseInterface $response)
{
$this->response = $response;
}

public function __invoke()
{
return $this->response;
}
};

$container = new Container([
ResponseInterface::class => function (\stdClass $data) {
return new Response(200, [], json_encode($data));
},
'data' => function () {
return (object) ['name' => 'Alice'];
}
]);

$callable = $container->callable(get_class($controller));
$this->assertInstanceOf(\Closure::class, $callable);

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

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

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

Expand Down Expand Up @@ -440,7 +510,7 @@ public function __invoke(ServerRequestInterface $request)
$callable = $container->callable(get_class($controller));

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

Expand Down Expand Up @@ -474,7 +544,39 @@ public function __invoke(ServerRequestInterface $request)
$callable = $container->callable(get_class($controller));

$this->expectException(\BadMethodCallException::class);
$this->expectExceptionMessage('Container variable $http expected scalar type from factory, but got resource');
$this->expectExceptionMessage('Container variable $http expected type object|scalar from factory, but got resource');
$callable($request);
}

public function testCallableReturnsCallableThatThrowsWhenFactoryReferencesObjectVariableMappedFromFactoryWithReturnsUnexpectedInteger()
{
$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 (\stdClass $http) {
return (object) ['name' => $http];
},
'http' => 1
]);

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

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

Expand Down Expand Up @@ -664,6 +766,35 @@ public function __invoke(ServerRequestInterface $request)
$callable($request);
}

public function testCallableReturnsCallableThatThrowsWhenFactoryReferencesClassMappedToUnexpectedObject()
{
$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 => new Response()
]);

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

$this->expectException(\BadMethodCallException::class);
$this->expectExceptionMessage('Map for stdClass contains unexpected React\Http\Message\Response');
$callable($request);
}

public function testCallableReturnsCallableThatThrowsWhenConstructorWithoutFactoryFunctionReferencesStringVariable()
{
$request = new ServerRequest('GET', 'http://example.com/');
Expand Down Expand Up @@ -693,13 +824,43 @@ public function __invoke(ServerRequestInterface $request)
$callable($request);
}

public function testCtorThrowsWhenMapContainsInvalidArray()
{
$this->expectException(\BadMethodCallException::class);
$this->expectExceptionMessage('Map for all contains unexpected array');

new Container([
'all' => []
]);
}

public function testCtorThrowsWhenMapContainsInvalidNull()
{
$this->expectException(\BadMethodCallException::class);
$this->expectExceptionMessage('Map for user contains unexpected NULL');

new Container([
'user' => null
]);
}

public function testCtorThrowsWhenMapContainsInvalidResource()
{
$this->expectException(\BadMethodCallException::class);
$this->expectExceptionMessage('Map for stdClass contains unexpected resource');
$this->expectExceptionMessage('Map for file contains unexpected resource');

new Container([
'file' => tmpfile()
]);
}

public function testCtorThrowsWhenMapForClassContainsInvalidObject()
{
$this->expectException(\BadMethodCallException::class);
$this->expectExceptionMessage('Map for Psr\Http\Message\ResponseInterface contains unexpected stdClass');

new Container([
\stdClass::class => tmpfile()
ResponseInterface::class => new \stdClass()
]);
}

Expand Down