Skip to content

Commit

Permalink
improve external container support
Browse files Browse the repository at this point in the history
  • Loading branch information
klimov-paul committed Jul 28, 2023
1 parent cec90ce commit 0052e5c
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 20 deletions.
124 changes: 113 additions & 11 deletions README.md
Expand Up @@ -105,13 +105,16 @@ For example:
require __DIR__ . '../vendor/autoload.php';
// ...

use yii1tech\di\Container;
use yii1tech\di\DI;

// setup DI container:
yii1tech\di\DI::setContainer(
yii1tech\di\Container::new()
DI::setContainer(
Container::new()
->config(CDbConnection::class, [
'connectionString' => 'sqlite::memory:',
])
->lazy(ICache::class, function (Psr\Container\ContainerInterface $container) {
->lazy(ICache::class, function (Container $container) {
$cache = new CDbCache();
$cache->setDbConnection($container->get(CDbConnection::class))

Expand All @@ -134,9 +137,11 @@ For example:
// file '/public/index.php'
require __DIR__ . '../vendor/autoload.php';
// ...
use yii1tech\di\Container;
use yii1tech\di\DI;

// setup DI container:
yii1tech\di\DI::setContainer(function () {
DI::setContainer(function () {
return ContainerFactory::create();
});

Expand All @@ -148,7 +153,7 @@ class ContainerFactory
{
public static function create(): \Psr\Container\ContainerInterface
{
$container = yii1tech\di\Container::new();
$container = Container::new();
// fill up container

return $container;
Expand Down Expand Up @@ -179,11 +184,14 @@ For example:
// file '/public/index.php'
require __DIR__ . '../vendor/autoload.php';
// ...
use yii1tech\di\DI;
use yii1tech\di\web\WebApplication;

// setup DI container:
yii1tech\di\DI::setContainer(/* ... */);
DI::setContainer(/* ... */);

// create and run Yii DI-aware application:
Yii::createApplication(yii1tech\di\web\WebApplication::class, $config)->run();
Yii::createApplication(WebApplication::class, $config)->run();
```

Being DI-aware application will resolve configured components via DI Container according to their configured class.
Expand All @@ -193,9 +201,13 @@ For example:
```php
<?php
// ...
use yii1tech\di\Container;
use yii1tech\di\DI;
use yii1tech\di\web\WebApplication;

// setup DI container:
yii1tech\di\DI::setContainer(
yii1tech\di\Container::new()
DI::setContainer(
Container::new()
->lazy(CDbConnection::class, function () {
$db = new CDbConnection();
$db->connectionString = 'mysql:host=127.0.0.1;dbname=example';
Expand All @@ -206,7 +218,7 @@ yii1tech\di\DI::setContainer(

return $db;
})
->lazy(ICache::class, function (Psr\Container\ContainerInterface $container) {
->lazy(ICache::class, function (Container $container) {
$cache = new CDbCache();
$cache->setDbConnection($container->get(CDbConnection::class));

Expand All @@ -232,7 +244,7 @@ $config = [
];

// create and run Yii DI-aware application:
Yii::createApplication(yii1tech\di\web\WebApplication::class, $config)->run();
Yii::createApplication(WebApplication::class, $config)->run();
//...
$db = Yii::app()->getComponent('db');
var_dump($db->username); // outputs 'container_user'
Expand Down Expand Up @@ -342,3 +354,93 @@ and passes them to the parent constructor. Otherwise, console command may not fu

### External (3rd party) Container Usage <span id="external-contatiner-usage"></span>

Container `\yii1tech\di\Container` is very basic, and you may want to use something more sophisticated like [PHP-DI](https://php-di.org/) instead of it.
Any PSR-11 compatible container can be used within this extension. All you need is pass your container to `\yii1tech\di\DI::setContainer()`.
For example:

```php
<?php
// file '/public/index.php'
require __DIR__ . '../vendor/autoload.php';
// ...
use DI\ContainerBuilder;
use yii1tech\di\DI;
use yii1tech\di\web\WebApplication;

// use 'PHP-DI' for the container:
DI::setContainer(function () {
$builder = new ContainerBuilder();

$builder->useAutowiring(true);
// ...

return $builder->build();
});

// create and run Yii DI-aware application:
Yii::createApplication(WebApplication::class, $config)->run();
```

Many existing PSR-11 compatible solutions are coming with built-in injection mechanism, which you may want to utilize instead of the one,
provided by this extension. In order to do this you should create your own injector, implementing `\yii1tech\di\InjectorContract` interface,
and pass it to `\yii1tech\di\DI::setInjector()`.

Many of the existing PSR-11 containers (including the 'PHP-DI') already implements dependency injection methods `make()` and `call()`.
You can use `\yii1tech\di\external\ContainerBasedInjector` to utilize these methods. For example:

```php
<?php
// file '/public/index.php'
require __DIR__ . '../vendor/autoload.php';
// ...
use DI\ContainerBuilder;
use yii1tech\di\DI;
use yii1tech\di\external\ContainerBasedInjector;
use yii1tech\di\web\WebApplication;

// use 'PHP-DI' for the container:
DI::setContainer(function () {
$builder = new ContainerBuilder();
// ...

return $builder->build();
})
->setInjector(new ContainerBasedInjector()); // use `DI\Container::make()` and `DI\Container::call()` for dependency injection

// create and run Yii DI-aware application:
Yii::createApplication(WebApplication::class, $config)->run();
```

**Heads up!** Many of the existing PSR-11 containers, which provide built-in injection methods, implements method `\Psr\Container\ContainerInterface::has()`
in inconsistent way. While standard demands it should return `true` only if there is explicit binding inside the container, many solutions return
`true` even if binding does not exist, but requested class **can be** resolved, e.g. its constructor arguments can be filled up using other bindings
from container. Such check is always comes with extra class and method reflection creation, which reduces the overall performance. This may cause a
significant impact, while resolving Yii application components via DI container.

It is recommended to resolve any inconsistency in `\Psr\Container\ContainerInterface::has()` implementation before using container with this extension.
This can be easily achieved using `\yii1tech\di\external\ContainerProxy` class. For example:

```php
<?php

// file '/public/index.php'
require __DIR__ . '../vendor/autoload.php';
// ...

use DI\Container;
use DI\ContainerBuilder;
use yii1tech\di\DI;
use yii1tech\di\external\ContainerProxy;

// use 'PHP-DI' for the container:
DI::setContainer(function () {
$builder = new ContainerBuilder();
// ...

return ContainerProxy::new($builder->build())
->setCallbackForHas(function (Container $container, string $id) {
return in_array($id, $container->getKnownEntryNames(), true);
});
});
// ...
```
70 changes: 61 additions & 9 deletions src/external/ContainerProxy.php
Expand Up @@ -13,13 +13,13 @@
* For example:
*
* ```php
* class PhpDiContainerProxy extends ContainerProxy
* {
* public function has(string $id): bool
* {
* return in_array($id, $this->container->getKnownEntryNames(), true);
* }
* }
* use DI\Container;
* use yii1tech\di\external\ContainerProxy;
*
* $container = ContainerProxy::new(new Container())
* ->setCallbackForHas(function (Container $container, string $id) {
* return in_array($id, $container->getKnownEntryNames(), true);
* });
* ```
*
* @author Paul Klimov <klimov.paul@gmail.com>
Expand All @@ -31,6 +31,14 @@ class ContainerProxy implements ContainerInterface
* @var \Psr\Container\ContainerInterface wrapped container instance.
*/
protected $container;
/**
* @var callable|null PHP callback, which should be used to implement `get()` method.
*/
protected $callbackForGet;
/**
* @var callable|null PHP callback, which should be used to implement `has()` method.
*/
protected $callbackForHas;

/**
* Constructor.
Expand All @@ -47,15 +55,59 @@ public function __construct(ContainerInterface $container)
*/
public function get(string $id)
{
return $this->container->get($id);
if ($this->callbackForGet === null) {
return $this->container->get($id);
}

return call_user_func($this->callbackForGet, $this->container, $id);
}

/**
* {@inheritdoc}
*/
public function has(string $id): bool
{
return $this->container->has($id);
if ($this->callbackForHas == null) {
return $this->container->has($id);
}

return call_user_func($this->callbackForHas, $this->container, $id);
}

/**
* Specifies a PHP callback, which should be invoked to implement method `get()`.
* The callback signature:
*
* ```
* function (\Psr\Container\ContainerInterface $container, string $id): mixed
* ```
*
* @param callable|null $callback PHP callback.
* @return static self reference.
*/
public function setCallbackForGet(?callable $callback): self
{
$this->callbackForGet = $callback;

return $this;
}

/**
* Specifies a PHP callback, which should be invoked to implement method `has()`.
* The callback signature:
*
* ```
* function (\Psr\Container\ContainerInterface $container, string $id): bool
* ```
*
* @param callable|null $callback PHP callback.
* @return static self reference.
*/
public function setCallbackForHas(?callable $callback): self
{
$this->callbackForHas = $callback;

return $this;
}

/**
Expand Down
40 changes: 40 additions & 0 deletions tests/external/ContainerProxyTest.php
Expand Up @@ -46,4 +46,44 @@ public function testForwardCall(): void

$this->assertTrue($proxy->has(ArrayObject::class));
}

/**
* @depends testHas
*/
public function testCallbackForHas(): void
{
$container = new Container();

$proxy = new ContainerProxy($container);
$proxy->setCallbackForHas(function (Container $container, $id) {
return $id === ArrayObject::class;
});

$this->assertTrue($proxy->has(ArrayObject::class));
$this->assertFalse($proxy->has('unexistint-id'));
}

/**
* @depends testGet
*/
public function testCallbackForGet(): void
{
$container = new Container();

$proxy = new ContainerProxy($container);
$proxy->setCallbackForGet(function (Container $container, $id) {
if ($id === ArrayObject::class) {
return new ArrayObject();
}

return null;
});

$object = $proxy->get(ArrayObject::class);

$this->assertTrue($object instanceof ArrayObject);

$object = $proxy->get('any');
$this->assertNull($object);
}
}

0 comments on commit 0052e5c

Please sign in to comment.