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
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
},
"require-dev": {
"phpunit/phpunit": "^9.5 || ^7.5",
"psr/container": "^2 || ^1",
"react/async": "^4@dev || ^3@dev"
},
"autoload": {
Expand Down
53 changes: 42 additions & 11 deletions docs/best-practices/controllers.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,15 +305,46 @@ $container = new FrameworkX\Container([
// …
```

### PSR-11 compatibility
### PSR-11: Container interface

> ⚠️ **Feature preview**
>
> This is a feature preview, i.e. it might not have made it into the current beta.
> Give feedback to help us prioritize.
> We also welcome [contributors](../getting-started/community.md) to help out!

In the future, we will also allow you to pass in a custom
[PSR-11: Container interface](https://www.php-fig.org/psr/psr-11/) implementing
the well-established `Psr\Container\ContainerInterface`.
We love standards and interoperability.
X has a powerful, built-in dependency injection container (DI container or DIC)
that has a strong focus on simplicity and should cover most common use cases.
Sometimes, you might need a little more control over this and may want to use a
custom container implementation instead.

We love standards and interoperability, that's why we support the
[PSR-11: Container interface](https://www.php-fig.org/psr/psr-11/). This is a
common interface that is used by most DI containers in PHP. In the following
example, we're using [PHP-DI](https://php-di.org/), but you may likewise use any
other implementation of this interface:

```bash
composer require php-di/php-di
```

In order to use an external DI container, you first have to instantiate your
custom container as per its documentation. If this instance implements the
`Psr\Container\ContainerInterface`, you can then pass it into the X container that
acts as an adapter for the application like this:

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

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

// $builder = new DI\ContainerBuilder();
// $builder->...
// $container = $builder->build();
$container = new DI\Container();

$app = new FrameworkX\App(new FrameworkX\Container($container));

$app->get('/', Acme\Todo\HelloController::class);
$app->get('/users/{name}', Acme\Todo\UserController::class);

$app->run();
```

We expect most applications to work just fine with the built-in DI container.
If you need to use a custom container, the above logic should work with any of the
[PSR-11 container implementations](https://packagist.org/providers/psr/container-implementation).
27 changes: 19 additions & 8 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,36 @@

namespace FrameworkX;

use Psr\Container\ContainerInterface;
use Psr\Http\Message\ServerRequestInterface;

/**
* @final
*/
class Container
{
/** @var array<class-string,object|callable():(object|class-string)> */
/** @var array<class-string,object|callable():(object|class-string)>|ContainerInterface */
private $container;

/** @var array<class-string,callable():(object|class-string) | object | class-string> */
public function __construct(array $map = [])
/** @var array<class-string,callable():(object|class-string) | object | class-string>|ContainerInterface $loader */
public function __construct($loader = [])
{
foreach ($map as $name => $value) {
if (!\is_array($loader) && !$loader instanceof ContainerInterface) {
throw new \TypeError(
'Argument #1 ($loader) must be of type array|Psr\Container\ContainerInterface, ' . (\is_object($loader) ? get_class($loader) : gettype($loader)) . ' given'
);
}

foreach (($loader instanceof ContainerInterface ? [] : $loader) as $name => $value) {
if (\is_string($value)) {
$map[$name] = static function () use ($value) {
$loader[$name] = static function () use ($value) {
return $value;
};
} elseif (!$value instanceof \Closure && !$value instanceof $name) {
throw new \BadMethodCallException('Map for ' . $name . ' contains unexpected ' . (is_object($value) ? get_class($value) : gettype($value)));
}
}
$this->container = $map;
$this->container = $loader;
}

public function __invoke(ServerRequestInterface $request, callable $next = null)
Expand Down Expand Up @@ -52,12 +59,16 @@ public function callable(string $class): callable
{
return function (ServerRequestInterface $request, callable $next = null) use ($class) {
// Check `$class` references a valid class name that can be autoloaded
if (!\class_exists($class, true) && !interface_exists($class, false) && !trait_exists($class, false)) {
if (\is_array($this->container) && !\class_exists($class, true) && !interface_exists($class, false) && !trait_exists($class, false)) {
throw new \BadMethodCallException('Request handler class ' . $class . ' not found');
}

try {
$handler = $this->load($class);
if ($this->container instanceof ContainerInterface) {
$handler = $this->container->get($class);
} else {
$handler = $this->load($class);
}
} catch (\Throwable $e) {
throw new \BadMethodCallException(
'Request handler class ' . $class . ' failed to load: ' . $e->getMessage(),
Expand Down
53 changes: 53 additions & 0 deletions tests/ContainerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use FrameworkX\Container;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Message\Response;
Expand Down Expand Up @@ -336,6 +338,50 @@ public function testCallableReturnsCallableThatThrowsWhenFactoryIsRecursive()
$callable($request);
}

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

$controller = new class {
public function __invoke(ServerRequestInterface $request)
{
return new Response(200);
}
};

$psr = $this->createMock(ContainerInterface::class);
$psr->expects($this->never())->method('has');
$psr->expects($this->once())->method('get')->with(get_class($controller))->willReturn($controller);

$container = new Container($psr);

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

$response = $callable($request);
$this->assertInstanceOf(ResponseInterface::class, $response);
$this->assertEquals(200, $response->getStatusCode());
}

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

$exception = new class('Unable to load class') extends \RuntimeException implements NotFoundExceptionInterface { };

$psr = $this->createMock(ContainerInterface::class);
$psr->expects($this->never())->method('has');
$psr->expects($this->once())->method('get')->with('FooBar')->willThrowException($exception);

$container = new Container($psr);

$callable = $container->callable('FooBar');

$this->expectException(\BadMethodCallException::class);
$this->expectExceptionMessage('Request handler class FooBar failed to load: Unable to load class');
$callable($request);
}

public function testInvokeContainerAsMiddlewareReturnsFromNextRequestHandler()
{
$request = new ServerRequest('GET', 'http://example.com/');
Expand All @@ -357,4 +403,11 @@ public function testInvokeContainerAsFinalRequestHandlerThrows()
$this->expectExceptionMessage('Container should not be used as final request handler');
$container($request);
}

public function testCtorWithInvalidValueThrows()
{
$this->expectException(\TypeError::class);
$this->expectExceptionMessage('Argument #1 ($loader) must be of type array|Psr\Container\ContainerInterface, stdClass given');
new Container((object) []);
}
}