Browse Source

Fix #17607: Added Yii version 3 DI config compatibility

- Allow `__class` and `__construct()` in configurations
- Allow wider use of `Instance::of`
- Allow static call DI definition like: `[SomeFactory::class, 'createMethod']`
- Add support for `__class` in `createObject`
tags/2.0.29
Andrii Vasyliev 5 years ago committed by Alexander Makarov
parent
commit
da626f507f
  1. 40
      docs/guide/concept-di-container.md
  2. 20
      framework/BaseYii.php
  3. 1
      framework/CHANGELOG.md
  4. 20
      framework/di/Container.php
  5. 2
      framework/di/ServiceLocator.php
  6. 16
      tests/framework/BaseYiiTest.php
  7. 122
      tests/framework/di/ContainerTest.php
  8. 7
      tests/framework/di/InstanceTest.php
  9. 18
      tests/framework/di/stubs/QuxFactory.php

40
docs/guide/concept-di-container.md

@ -169,6 +169,9 @@ $container->set('yii\mail\MailInterface', 'yii\swiftmailer\Mailer');
// to create an instance of Connection
$container->set('foo', 'yii\db\Connection');
// register an alias with `Instance::of`
$container->set('bar', Instance::of('foo'));
// register a class with configuration. The configuration
// will be applied when the class is instantiated by get()
$container->set('yii\db\Connection', [
@ -179,7 +182,7 @@ $container->set('yii\db\Connection', [
]);
// register an alias name with class configuration
// In this case, a "class" element is required to specify the class
// In this case, a "class" or "__class" element is required to specify the class
$container->set('db', [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
@ -188,11 +191,12 @@ $container->set('db', [
'charset' => 'utf8',
]);
// register a PHP callable
// register callable closure or array
// The callable will be executed each time when $container->get('db') is called
$container->set('db', function ($container, $params, $config) {
return new \yii\db\Connection($config);
});
$container->set('db', ['app\db\DbFactory', 'create']);
// register a component instance
// $container->get('pageCache') will return the same instance each time it is called
@ -215,7 +219,6 @@ $container->setSingleton('yii\db\Connection', [
]);
```
Resolving Dependencies <span id="resolving-dependencies"></span>
----------------------
@ -440,28 +443,23 @@ we shall copy-paste the line that creates `FileStorage` object, that is not the
As described in the [Resolving Dependencies](#resolving-dependencies) subsection, [[yii\di\Container::set()|set()]]
and [[yii\di\Container::setSingleton()|setSingleton()]] can optionally take dependency's constructor parameters as
a third argument. To set the constructor parameters, you may use the following configuration array format:
- `key`: class name, interface name or alias name. The key will be passed to the
[[yii\di\Container::set()|set()]] method as a first argument `$class`.
- `value`: array of two elements. The first element will be passed to the [[yii\di\Container::set()|set()]] method as the
second argument `$definition`, the second one — as `$params`.
a third argument. To set the constructor parameters, you may use the `__construct()` option:
Let's modify our example:
```php
$container->setDefinitions([
'tempFileStorage' => [ // we've created an alias for convenience
['class' => 'app\storage\FileStorage'],
['/var/tempfiles'] // could be extracted from some config files
'class' => 'app\storage\FileStorage',
'__construct()' => ['/var/tempfiles'], // could be extracted from some config files
],
'app\storage\DocumentsReader' => [
['class' => 'app\storage\DocumentsReader'],
[Instance::of('tempFileStorage')]
'class' => 'app\storage\DocumentsReader',
'__construct()' => [Instance::of('tempFileStorage')],
],
'app\storage\DocumentsWriter' => [
['class' => 'app\storage\DocumentsWriter'],
[Instance::of('tempFileStorage')]
'class' => 'app\storage\DocumentsWriter',
'__construct()' => [Instance::of('tempFileStorage')]
]
]);
@ -488,19 +486,19 @@ create its instance once and use it multiple times.
```php
$container->setSingletons([
'tempFileStorage' => [
['class' => 'app\storage\FileStorage'],
['/var/tempfiles']
'class' => 'app\storage\FileStorage',
'__construct()' => ['/var/tempfiles']
],
]);
$container->setDefinitions([
'app\storage\DocumentsReader' => [
['class' => 'app\storage\DocumentsReader'],
[Instance::of('tempFileStorage')]
'class' => 'app\storage\DocumentsReader',
'__construct()' => [Instance::of('tempFileStorage')],
],
'app\storage\DocumentsWriter' => [
['class' => 'app\storage\DocumentsWriter'],
[Instance::of('tempFileStorage')]
'class' => 'app\storage\DocumentsWriter',
'__construct()' => [Instance::of('tempFileStorage')],
]
]);

20
framework/BaseYii.php

@ -343,14 +343,26 @@ class BaseYii
{
if (is_string($type)) {
return static::$container->get($type, $params);
} elseif (is_array($type) && isset($type['class'])) {
}
if (is_array($type) && isset($type['class'])) {
$class = $type['class'];
unset($type['class']);
return static::$container->get($class, $params, $type);
} elseif (is_callable($type, true)) {
}
if (is_array($type) && isset($type['__class'])) {
$class = $type['__class'];
unset($type['__class']);
return static::$container->get($class, $params, $type);
}
if (is_callable($type, true)) {
return static::$container->invoke($type, $params);
} elseif (is_array($type)) {
throw new InvalidConfigException('Object configuration must be an array containing a "class" element.');
}
if (is_array($type)) {
throw new InvalidConfigException('Object configuration must be an array containing a "class" or "__class" element.');
}
throw new InvalidConfigException('Unsupported configuration type: ' . gettype($type));

1
framework/CHANGELOG.md

@ -4,6 +4,7 @@ Yii Framework 2 Change Log
2.0.29 under development
------------------------
- Enh #17607: Added Yii version 3 DI config compatibility (hiqsol)
- Bug #17573: `EmailValidator` with `checkDNS=true` throws `ErrorException` on bad domains on Alpine (batyrmastyr)
- Bug #17606: Fix error in `AssetBundle` when a disabled bundle with custom init() was still published (onmotion)

20
framework/di/Container.php

@ -137,7 +137,7 @@ class Container extends Component
* In this case, the constructor parameters and object configurations will be used
* only if the class is instantiated the first time.
*
* @param string $class the class name or an alias name (e.g. `foo`) that was previously registered via [[set()]]
* @param string|Instance $class the class Instance, name or an alias name (e.g. `foo`) that was previously registered via [[set()]]
* or [[setSingleton()]].
* @param array $params a list of constructor parameter values. The parameters should be provided in the order
* they appear in the constructor declaration. If you want to skip some parameters, you should index the remaining
@ -149,6 +149,9 @@ class Container extends Component
*/
public function get($class, $params = [], $config = [])
{
if ($class instanceof Instance) {
$class = $class->id;
}
if (isset($this->_singletons[$class])) {
// singleton
return $this->_singletons[$class];
@ -323,9 +326,15 @@ class Container extends Component
return ['class' => $class];
} elseif (is_string($definition)) {
return ['class' => $definition];
} elseif ($definition instanceof Instance) {
return ['class' => $definition->id];
} elseif (is_callable($definition, true) || is_object($definition)) {
return $definition;
} elseif (is_array($definition)) {
if (!isset($definition['class']) && isset($definition['__class'])) {
$definition['class'] = $definition['__class'];
unset($definition['__class']);
}
if (!isset($definition['class'])) {
if (strpos($class, '\\') !== false) {
$definition['class'] = $class;
@ -364,6 +373,13 @@ class Container extends Component
/* @var $reflection ReflectionClass */
list($reflection, $dependencies) = $this->getDependencies($class);
if (isset($config['__construct()'])) {
foreach ($config['__construct()'] as $index => $param) {
$dependencies[$index] = $param;
}
unset($config['__construct()']);
}
foreach ($params as $index => $param) {
$dependencies[$index] = $param;
}
@ -630,7 +646,7 @@ class Container extends Component
public function setDefinitions(array $definitions)
{
foreach ($definitions as $class => $definition) {
if (is_array($definition) && count($definition) === 2 && array_values($definition) === $definition) {
if (is_array($definition) && count($definition) === 2 && array_values($definition) === $definition && is_array($definition[1])) {
$this->set($class, $definition[0], $definition[1]);
continue;
}

2
framework/di/ServiceLocator.php

@ -199,7 +199,7 @@ class ServiceLocator extends Component
$this->_definitions[$id] = $definition;
} elseif (is_array($definition)) {
// a configuration array
if (isset($definition['class'])) {
if (isset($definition['class']) || isset($definition['__class'])) {
$this->_definitions[$id] = $definition;
} else {
throw new InvalidConfigException("The configuration for the \"$id\" component must contain a \"class\" element.");

16
tests/framework/BaseYiiTest.php

@ -14,6 +14,7 @@ use yii\log\Logger;
use yiiunit\data\base\Singer;
use yiiunit\TestCase;
use yiiunit\data\base\CallableClass;
use yiiunit\framework\di\stubs\Qux;
/**
* BaseYiiTest.
@ -72,6 +73,19 @@ class BaseYiiTest extends TestCase
$this->assertInternalType('string', Yii::powered());
}
public function testCreateObjectArray()
{
Yii::$container = new Container();
$qux = Yii::createObject([
'__class' => Qux::className(),
'a' => 42,
]);
$this->assertInstanceOf(Qux::className(), $qux);
$this->assertSame(42, $qux->a);
}
public function testCreateObjectCallable()
{
Yii::$container = new Container();
@ -99,7 +113,7 @@ class BaseYiiTest extends TestCase
public function testCreateObjectEmptyArrayException()
{
$this->expectException('yii\base\InvalidConfigException');
$this->expectExceptionMessage('Object configuration must be an array containing a "class" element.');
$this->expectExceptionMessage('Object configuration must be an array containing a "class" or "__class" element.');
Yii::createObject([]);
}

122
tests/framework/di/ContainerTest.php

@ -20,6 +20,7 @@ use yiiunit\framework\di\stubs\Foo;
use yiiunit\framework\di\stubs\FooProperty;
use yiiunit\framework\di\stubs\Qux;
use yiiunit\framework\di\stubs\QuxInterface;
use yiiunit\framework\di\stubs\QuxFactory;
use yiiunit\TestCase;
/**
@ -282,6 +283,127 @@ class ContainerTest extends TestCase
}
}
public function testStaticCall()
{
$container = new Container();
$container->setDefinitions([
'qux' => [QuxFactory::className(), 'create'],
]);
$qux = $container->get('qux');
$this->assertInstanceOf(Qux::className(), $qux);
$this->assertSame(42, $qux->a);
}
public function testObject()
{
$container = new Container();
$container->setDefinitions([
'qux' => new Qux(42),
]);
$qux = $container->get('qux');
$this->assertInstanceOf(Qux::className(), $qux);
$this->assertSame(42, $qux->a);
}
public function testDi3Compatibility()
{
$container = new Container();
$container->setDefinitions([
'test\TraversableInterface' => [
'__class' => 'yiiunit\data\base\TraversableObject',
'__construct()' => [['item1', 'item2']],
],
'qux' => [
'__class' => Qux::className(),
'a' => 42,
],
]);
$qux = $container->get('qux');
$this->assertInstanceOf(Qux::className(), $qux);
$this->assertSame(42, $qux->a);
$traversable = $container->get('test\TraversableInterface');
$this->assertInstanceOf('yiiunit\data\base\TraversableObject', $traversable);
$this->assertEquals('item1', $traversable->current());
}
public function testInstanceOf()
{
$container = new Container();
$container->setDefinitions([
'qux' => [
'class' => Qux::className(),
'a' => 42,
],
'bar' => [
'__class' => Bar::className(),
'__construct()' => [
Instance::of('qux')
],
],
]);
$bar = $container->get('bar');
$this->assertInstanceOf(Bar::className(), $bar);
$qux = $bar->qux;
$this->assertInstanceOf(Qux::className(), $qux);
$this->assertSame(42, $qux->a);
}
public function testGetByInstance()
{
$container = new Container();
$container->setSingletons([
'one' => Qux::className(),
'two' => Instance::of('one'),
]);
$one = $container->get(Instance::of('one'));
$two = $container->get(Instance::of('two'));
$this->assertInstanceOf(Qux::className(), $one);
$this->assertSame($one, $two);
$this->assertSame($one, $container->get('one'));
$this->assertSame($one, $container->get('two'));
}
public function testWithoutDefinition()
{
$container = new Container();
$one = $container->get(Qux::className());
$two = $container->get(Qux::className());
$this->assertInstanceOf(Qux::className(), $one);
$this->assertInstanceOf(Qux::className(), $two);
$this->assertSame(1, $one->a);
$this->assertSame(1, $two->a);
$this->assertNotSame($one, $two);
}
public function testGetByClassIndirectly()
{
$container = new Container();
$container->setSingletons([
'qux' => Qux::className(),
Qux::className() => [
'a' => 42,
],
]);
$qux = $container->get('qux');
$this->assertInstanceOf(Qux::className(), $qux);
$this->assertSame(42, $qux->a);
}
/**
* @expectedException \yii\base\InvalidConfigException
*/
public function testThrowingNotFoundException()
{
$container = new Container();
$container->get('non_existing');
}
public function testContainerSingletons()
{
$container = new Container();

7
tests/framework/di/InstanceTest.php

@ -171,12 +171,7 @@ class InstanceTest extends TestCase
$instance = Instance::of('something');
$export = var_export($instance, true);
$this->assertRegExp(<<<'PHP'
@yii\\di\\Instance::__set_state\(array\(
\s+'id' => 'something',
\)\)@
PHP
, $export);
$this->assertRegExp('~yii\\\\di\\\\Instance::__set_state\(array\(\s+\'id\' => \'something\',\s+\)\)~', $export);
$this->assertEquals($instance, Instance::__set_state([
'id' => 'something',

18
tests/framework/di/stubs/QuxFactory.php

@ -0,0 +1,18 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yiiunit\framework\di\stubs;
use yii\di\Container;
class QuxFactory extends \yii\base\BaseObject
{
public static function create(Container $container)
{
return new Qux(42);
}
}
Loading…
Cancel
Save