diff --git a/docs/guide-ru/concept-di-container.md b/docs/guide-ru/concept-di-container.md index 3ca34c108d..5ad5361761 100644 --- a/docs/guide-ru/concept-di-container.md +++ b/docs/guide-ru/concept-di-container.md @@ -358,8 +358,11 @@ $container->setSingleton('yii\db\Connection', [ // "db" ранее зарегистрированный псевдоним $db = $container->get('db'); -// эквивалентно: $engine = new \app\components\SearchEngine($apiKey, ['type' => 1]); -$engine = $container->get('app\components\SearchEngine', [$apiKey], ['type' => 1]); +// эквивалентно: $engine = new \app\components\SearchEngine($apiKey, $apiSecret, ['type' => 1]); +$engine = $container->get('app\components\SearchEngine', [$apiKey, $apiSecret], ['type' => 1]); + +// эквивалентно: $api = new \app\components\Api($host, $apiKey); +$api = $container->get('app\components\Api', ['host' => $host, 'apiKey' => $apiKey]); ``` За кулисами, контейнер внедрения зависимостей делает гораздо больше работы, чем просто создание нового объекта. diff --git a/docs/guide/concept-di-container.md b/docs/guide/concept-di-container.md index cbb050a4d2..b771a3d175 100644 --- a/docs/guide/concept-di-container.md +++ b/docs/guide/concept-di-container.md @@ -241,6 +241,9 @@ $db = $container->get('db'); // equivalent to: $engine = new \app\components\SearchEngine($apiKey, $apiSecret, ['type' => 1]); $engine = $container->get('app\components\SearchEngine', [$apiKey, $apiSecret], ['type' => 1]); + +// equivalent to: $api = new \app\components\Api($host, $apiKey); +$api = $container->get('app\components\Api', ['host' => $host, 'apiKey' => $apiKey]); ``` Behind the scene, the DI container does much more work than just creating a new object. diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index fc3c864bfe..419fc6dd20 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -16,8 +16,10 @@ Yii Framework 2 Change Log - Bug #18313: Fix multipart form data parse with double quotes (wsaid) - Bug #18317: Additional PHP 8 compatibility fixes (samdark, bizley) - Bug #16831: Fix console Table Widget does not render correctly in combination with ANSI formatting (issidorov, cebe) +- Enh #18285: Enhanced DI container to allow passing parameters by name in constructor (vjik) - Enh #18351: Added option to change default timezone for parsing formats without time part in `yii\validators\DateValidator` (bizley) + 2.0.38 September 14, 2020 ------------------------- diff --git a/framework/di/Container.php b/framework/di/Container.php index 446b87a3bf..57036f1db4 100644 --- a/framework/di/Container.php +++ b/framework/di/Container.php @@ -143,11 +143,14 @@ 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|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 - * ones with the integers that represent their positions in the constructor parameter list. + * @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. Use one of two definitions: + * - Parameters as name-value pairs, for example: `['posts' => PostRepository::class]`. + * - Parameters in the order they appear in the constructor declaration. If you want to skip some parameters, + * you should index the remaining ones with the integers that represent their positions in the constructor + * parameter list. + * Dependencies indexed by name and by position in the same array are not allowed. * @param array $config a list of name-value pairs that will be used to initialize the object properties. * @return object an instance of the requested class. * @throws InvalidConfigException if the class cannot be recognized or correspond to an invalid definition @@ -379,15 +382,23 @@ class Container extends Component /* @var $reflection ReflectionClass */ list($reflection, $dependencies) = $this->getDependencies($class); + $addDependencies = []; if (isset($config['__construct()'])) { - foreach ($config['__construct()'] as $index => $param) { - $dependencies[$index] = $param; - } + $addDependencies = $config['__construct()']; unset($config['__construct()']); } - foreach ($params as $index => $param) { - $dependencies[$index] = $param; + $addDependencies[$index] = $param; + } + + $this->validateDependencies($addDependencies); + + if ($addDependencies && is_int(key($addDependencies))) { + $dependencies = array_values($dependencies); + $dependencies = $this->mergeDependencies($dependencies, $addDependencies); + } else { + $dependencies = $this->mergeDependencies($dependencies, $addDependencies); + $dependencies = array_values($dependencies); } $dependencies = $this->resolveDependencies($dependencies, $reflection); @@ -414,6 +425,47 @@ class Container extends Component return $object; } + /** + * @param array $a + * @param array $b + * @return array + */ + private function mergeDependencies($a, $b) + { + foreach ($b as $index => $dependency) { + $a[$index] = $dependency; + } + return $a; + } + + /** + * @param array $parameters + * @throws InvalidConfigException + */ + private function validateDependencies($parameters) + { + $hasStringParameter = false; + $hasIntParameter = false; + foreach ($parameters as $index => $parameter) { + if (is_string($index)) { + $hasStringParameter = true; + if ($hasIntParameter) { + break; + } + } else { + $hasIntParameter = true; + if ($hasStringParameter) { + break; + } + } + } + if ($hasIntParameter && $hasStringParameter) { + throw new InvalidConfigException( + 'Dependencies indexed by name and by position in the same array are not allowed.' + ); + } + } + /** * Merges the user-specified constructor parameters with the ones registered via [[set()]]. * @param string $class class name, interface name or alias name @@ -463,7 +515,7 @@ class Container extends Component } if ($param->isDefaultValueAvailable()) { - $dependencies[] = $param->getDefaultValue(); + $dependencies[$param->getName()] = $param->getDefaultValue(); } else { if (PHP_VERSION_ID >= 80000) { $c = $param->getType(); @@ -472,7 +524,7 @@ class Container extends Component $c = $param->getClass(); $isClass = $c !== null; } - $dependencies[] = Instance::of($isClass ? $c->getName() : null); + $dependencies[$param->getName()] = Instance::of($isClass ? $c->getName() : null); } } } diff --git a/tests/framework/di/ContainerTest.php b/tests/framework/di/ContainerTest.php index 1d6d54b214..5a3565299b 100644 --- a/tests/framework/di/ContainerTest.php +++ b/tests/framework/di/ContainerTest.php @@ -16,6 +16,7 @@ use yiiunit\data\ar\Order; use yiiunit\data\ar\Type; use yiiunit\framework\di\stubs\Bar; use yiiunit\framework\di\stubs\BarSetter; +use yiiunit\framework\di\stubs\Car; use yiiunit\framework\di\stubs\Corge; use yiiunit\framework\di\stubs\Foo; use yiiunit\framework\di\stubs\FooProperty; @@ -172,7 +173,7 @@ class ContainerTest extends TestCase $myFunc = function ($a, NumberValidator $b, $c = 'default') { - return[$a, \get_class($b), $c]; + return [$a, \get_class($b), $c]; }; $result = Yii::$container->invoke($myFunc, ['a']); $this->assertEquals(['a', 'yii\validators\NumberValidator', 'default'], $result); @@ -262,7 +263,8 @@ class ContainerTest extends TestCase 'qux.using.closure' => function () { return new Qux(); }, - 'rollbar', 'baibaratsky\yii\rollbar\Rollbar' + 'rollbar', + 'baibaratsky\yii\rollbar\Rollbar' ]); $container->setDefinitions([]); @@ -278,8 +280,7 @@ class ContainerTest extends TestCase try { $container->get('rollbar'); $this->fail('InvalidConfigException was not thrown'); - } catch(\Exception $e) - { + } catch (\Exception $e) { $this->assertInstanceOf('yii\base\InvalidConfigException', $e); } } @@ -527,4 +528,30 @@ class ContainerTest extends TestCase Yii::$container->set('setLater', new Qux()); Yii::$container->get('test'); } + + /** + * @see https://github.com/yiisoft/yii2/issues/18284 + */ + public function testNamedConstructorParameters() + { + $test = (new Container())->get(Car::className(), [ + 'name' => 'Hello', + 'color' => 'red', + ]); + $this->assertSame('Hello', $test->name); + $this->assertSame('red', $test->color); + } + + /** + * @see https://github.com/yiisoft/yii2/issues/18284 + */ + public function testInvalidConstructorParameters() + { + $this->expectException('yii\base\InvalidConfigException'); + $this->expectExceptionMessage('Dependencies indexed by name and by position in the same array are not allowed.'); + (new Container())->get(Car::className(), [ + 'color' => 'red', + 'Hello', + ]); + } } diff --git a/tests/framework/di/stubs/Car.php b/tests/framework/di/stubs/Car.php new file mode 100644 index 0000000000..dc91d53b93 --- /dev/null +++ b/tests/framework/di/stubs/Car.php @@ -0,0 +1,17 @@ +color = $color; + $this->name = $name; + } +}