Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose Container API to allow explicit DI container configuration #95

Merged
merged 3 commits into from
Dec 7, 2021
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
65 changes: 58 additions & 7 deletions docs/best-practices/controllers.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,15 +205,53 @@ constructor with type definitions to explicitly reference other classes.

### Container configuration

> ⚠️ **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!

Autowiring should cover most common use cases with zero configuration. If you
want to have more control over this behavior, you may also explicitly configure
the dependency injection container. This can be useful in these cases:
the dependency injection container like this:

=== "Arrow functions (PHP 7.4+)"

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

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

$container = new FrameworkX\Container([
Acme\Todo\HelloController::class => fn() => new Acme\Todo\HelloController();
]);



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

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

$app->run();
```

=== "Closure"

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

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

$container = new FrameworkX\Container([
Acme\Todo\HelloController::class => function () {
return new Acme\Todo\HelloController();
}
]);

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

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

$app->run();
```

This can be useful in these cases:

* Constructor parameter references an interface and you want to explicitly
define an instance that implements this interface.
Expand All @@ -222,6 +260,19 @@ the dependency injection container. This can be useful in these cases:
* Constructor parameter references a class, but you want to inject a specific
instance or subclass in place of a default class.

The configured container instance can be passed into the application like any
other middleware request handler. In most cases this means you create a single
`Container` instance with a number of factory methods and pass this instance as
the first argument to the `App`.

### PSR-11 compatibility

> ⚠️ **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`.
Expand Down
49 changes: 38 additions & 11 deletions docs/integrations/database.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,23 +379,50 @@ acme/
> see [controller classes](../best-practices/controllers.md) for more details.

The main entry point [registers a route](../api/app.md#routing) for our
controller and uses dependency injection (DI) to connect all classes:
controller and uses dependency injection (DI) or a
[DI container](../best-practices/controllers.md#container) to wire all classes:

```php title="public/index.php"
<?php
=== "Constructor dependency injection"

require __DIR__ . '/../vendor/autoload.php';
```php title="public/index.php"
<?php

$credentials = 'alice:secret@localhost/bookstore?idle=0.001';
$db = (new React\Mysql\Factory())->createLazyConnection($credentials);
$repository = new Acme\Todo\BookRepository($db);
require __DIR__ . '/../vendor/autoload.php';

$app = new FrameworkX\App();
$credentials = 'alice:secret@localhost/bookstore?idle=0.001';
$db = (new React\MySQL\Factory())->createLazyConnection($credentials);
$repository = new Acme\Todo\BookRepository($db);

$app->get('/book/{isbn}, new Acme\Todo\BookLookupController($repository));

$app->run();
```


$app = new FrameworkX\App();

$app->get('/book/{isbn}', new Acme\Todo\BookLookupController($repository));

$app->run();
```

=== "DI container"

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

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

$container = new FrameworkX\Container([
React\MySQL\ConnectionInterface::class => function () {
$credentials = 'alice:secret@localhost/bookstore?idle=0.001';
return (new React\MySQL\Factory())->createLazyConnection($credentials);
}
]);

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

$app->get('/book/{isbn}', Acme\Todo\BookLookupController::class);

$app->run();
```

The main entity we're dealing with in this example is a plain PHP class which
makes it super easy to write and to use in our code:
Expand Down
18 changes: 10 additions & 8 deletions src/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,18 @@ class App
*/
public function __construct(...$middleware)
{
$container = new Container();
$errorHandler = new ErrorHandler();
$this->router = new RouteHandler($container);

$container = new Container();
if ($middleware) {
$middleware = array_map(
function ($handler) use ($container) {
return is_callable($handler) ? $handler : $container->callable($handler);
},
$middleware
);
foreach ($middleware as $i => $handler) {
if ($handler instanceof Container) {
$container = $handler;
unset($middleware[$i]);
} elseif (!\is_callable($handler)) {
$middleware[$i] = $container->callable($handler);
}
}
}

// new MiddlewareHandler([$accessLogHandler, $errorHandler, ...$middleware, $routeHandler])
Expand All @@ -58,6 +59,7 @@ function ($handler) use ($container) {
\array_unshift($middleware, new AccessLogHandler());
}

$this->router = new RouteHandler($container);
$middleware[] = $this->router;
$this->handler = new MiddlewareHandler($middleware);
$this->sapi = new SapiHandler();
Expand Down
33 changes: 30 additions & 3 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,39 @@
use Psr\Http\Message\ServerRequestInterface;

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

/** @var array<class-string,callable():object | object> */
public function __construct(array $map = [])
{
$this->container = $map;
}

public function __invoke(ServerRequestInterface $request, callable $next = null)
{
if ($next === null) {
// You don't want to end up here. This only happens if you use the
// container as a final request handler instead of as a middleware.
// In this case, you should omit the container or add another final
// request handler behind the container in the middleware chain.
throw new \BadMethodCallException('Container should not be used as final request handler');
}

// If the container is used as a middleware, simply forward to the next
// request handler. As an additional optimization, the container would
// usually be filtered out from a middleware chain as this is a NO-OP.
return $next($request);
}

/**
* @param class-string $class
* @return callable
* @return callable(ServerRequestInterface,?callable=null)
* @internal
*/
public function callable(string $class): callable
{
Expand Down Expand Up @@ -57,6 +80,10 @@ public function callable(string $class): callable
private function load(string $name, int $depth = 64)
{
if (isset($this->container[$name])) {
if ($this->container[$name] instanceof \Closure) {
$this->container[$name] = ($this->container[$name])();
}

return $this->container[$name];
}

Expand Down
24 changes: 16 additions & 8 deletions src/RouteHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,24 @@ public function __construct(Container $container = null)
public function map(array $methods, string $route, $handler, ...$handlers): void
{
if ($handlers) {
$handler = new MiddlewareHandler(array_map(
function ($handler) {
return is_callable($handler) ? $handler : $this->container->callable($handler);
},
array_merge([$handler], $handlers)
));
} elseif (!is_callable($handler)) {
$handler = $this->container->callable($handler);
\array_unshift($handlers, $handler);
\end($handlers);
} else {
$handlers = [$handler];
}

$last = key($handlers);
$container = $this->container;
foreach ($handlers as $i => $handler) {
if ($handler instanceof Container && $i !== $last) {
$container = $handler;
unset($handlers[$i]);
} elseif (!\is_callable($handler)) {
$handlers[$i] = $container->callable($handler);
}
}

$handler = \count($handlers) > 1 ? new MiddlewareHandler(array_values($handlers)) : \reset($handlers);
$this->routeDispatcher = null;
$this->routeCollector->addRoute($methods, $route, $handler);
}
Expand Down
56 changes: 56 additions & 0 deletions tests/AppTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use FrameworkX\AccessLogHandler;
use FrameworkX\App;
use FrameworkX\Container;
use FrameworkX\ErrorHandler;
use FrameworkX\MiddlewareHandler;
use FrameworkX\RouteHandler;
Expand Down Expand Up @@ -55,6 +56,61 @@ public function testConstructWithMiddlewareAssignsGivenMiddleware()
$this->assertInstanceOf(RouteHandler::class, $handlers[3]);
}

public function testConstructWithContainerAssignsContainerForRouteHandlerOnly()
{
$container = new Container();
$app = new App($container);

$ref = new ReflectionProperty($app, 'handler');
$ref->setAccessible(true);
$handler = $ref->getValue($app);

$this->assertInstanceOf(MiddlewareHandler::class, $handler);
$ref = new ReflectionProperty($handler, 'handlers');
$ref->setAccessible(true);
$handlers = $ref->getValue($handler);

$this->assertCount(3, $handlers);
$this->assertInstanceOf(AccessLogHandler::class, $handlers[0]);
$this->assertInstanceOf(ErrorHandler::class, $handlers[1]);
$this->assertInstanceOf(RouteHandler::class, $handlers[2]);

$routeHandler = $handlers[2];
$ref = new ReflectionProperty($routeHandler, 'container');
$ref->setAccessible(true);
$this->assertSame($container, $ref->getValue($routeHandler));
}

public function testConstructWithContainerAndMiddlewareClassNameAssignsCallableFromContainerAsMiddleware()
{
$middleware = function (ServerRequestInterface $request, callable $next) { };

$container = $this->createMock(Container::class);
$container->expects($this->once())->method('callable')->with('stdClass')->willReturn($middleware);

$app = new App($container, \stdClass::class);

$ref = new ReflectionProperty($app, 'handler');
$ref->setAccessible(true);
$handler = $ref->getValue($app);

$this->assertInstanceOf(MiddlewareHandler::class, $handler);
$ref = new ReflectionProperty($handler, 'handlers');
$ref->setAccessible(true);
$handlers = $ref->getValue($handler);

$this->assertCount(4, $handlers);
$this->assertInstanceOf(AccessLogHandler::class, $handlers[0]);
$this->assertInstanceOf(ErrorHandler::class, $handlers[1]);
$this->assertSame($middleware, $handlers[2]);
$this->assertInstanceOf(RouteHandler::class, $handlers[3]);

$routeHandler = $handlers[3];
$ref = new ReflectionProperty($routeHandler, 'container');
$ref->setAccessible(true);
$this->assertSame($container, $ref->getValue($routeHandler));
}

public function testRunWillReportListeningAddressAndRunLoopWithSocketServer()
{
$socket = @stream_socket_server('127.0.0.1:8080');
Expand Down
Loading