Browse Source

Several improvements to DI container

- Refactored invoke into separate resolve method (reusability)
- Added support for associative params array
- Added PHPUnit group doc to DI tests.
- Improved the tests for better coverage, and fixed a bug discovered by the new tests.

close #10811
tags/2.0.7
Sam Mousa 9 years ago committed by Carsten Brandt
parent
commit
8ea4c660da
  1. 2
      framework/CHANGELOG.md
  2. 109
      framework/di/Container.php
  3. 44
      tests/framework/di/ContainerTest.php
  4. 1
      tests/framework/di/InstanceTest.php
  5. 1
      tests/framework/di/ServiceLocatorTest.php

2
framework/CHANGELOG.md

@ -94,7 +94,7 @@ Yii Framework 2 Change Log
- Enh #9733: Added Unprocessable Entity HTTP Exception (janfrs) - Enh #9733: Added Unprocessable Entity HTTP Exception (janfrs)
- Enh #9762: Added `JsonResponseFormatter::$encodeOptions` and `::$prettyPrint` for better JSON output formatting (cebe) - Enh #9762: Added `JsonResponseFormatter::$encodeOptions` and `::$prettyPrint` for better JSON output formatting (cebe)
- Enh #9783: jQuery inputmask dependency updated to `~3.2.2` (samdark) - Enh #9783: jQuery inputmask dependency updated to `~3.2.2` (samdark)
- Enh #9785: Added ability to invoke callback with dependency resolution to DI container (mdmunir) - Enh #9785: Added ability to invoke callback with dependency resolution to DI container (mdmunir, SamMousa)
- Enh #9869: Allow path alias for SQLite database files in DSN config (ASlatius) - Enh #9869: Allow path alias for SQLite database files in DSN config (ASlatius)
- Enh #9901: Default `Cache.SerializerPermissions` configuration option for `HTMLPurifier` is set to `0775` (klimov-paul) - Enh #9901: Default `Cache.SerializerPermissions` configuration option for `HTMLPurifier` is set to `0775` (klimov-paul)
- Enh #10056: Allowed any callable to be passed to `ActionColumn::$urlCreator` (freezy-sk) - Enh #10056: Allowed any callable to be passed to `ActionColumn::$urlCreator` (freezy-sk)

109
framework/di/Container.php

@ -8,8 +8,10 @@
namespace yii\di; namespace yii\di;
use ReflectionClass; use ReflectionClass;
use Yii;
use yii\base\Component; use yii\base\Component;
use yii\base\InvalidConfigException; use yii\base\InvalidConfigException;
use yii\helpers\ArrayHelper;
/** /**
* Container implements a [dependency injection](http://en.wikipedia.org/wiki/Dependency_injection) container. * Container implements a [dependency injection](http://en.wikipedia.org/wiki/Dependency_injection) container.
@ -456,50 +458,93 @@ class Container extends Component
} }
/** /**
* Invoke callback with resolved dependecies parameters. * Invoke a callback with resolving dependencies in parameters.
*
* This methods allows invoking a callback and let type hinted parameter names to be
* resolved as objects of the Container. It additionally allow calling function using named parameters.
*
* For example, the following callback may be invoked using the Container to resolve the formatter dependency:
*
* ```php
* $formatString = function($string, \yii\i18n\Formatter $formatter) {
* // ...
* }
* Yii::$container->invoke($formatString, ['string' => 'Hello World!']);
* ```
*
* This will pass the string `'Hello World!'` as the first param, and a formatter instance created
* by the DI container as the second param to the callable.
*
* @param callable $callback callable to be invoked. * @param callable $callback callable to be invoked.
* @param array $params callback paramater. * @param array $params The array of parameters for the function.
* This can be either a list of parameters, or an associative array representing named function parameters.
* @return mixed the callback return value. * @return mixed the callback return value.
* @throws InvalidConfigException if a dependency cannot be resolved or if a dependency cannot be fulfilled. * @throws InvalidConfigException if a dependency cannot be resolved or if a dependency cannot be fulfilled.
* @since 2.0.7 * @since 2.0.7
*/ */
public function invoke($callback, $params = []) public function invoke(callable $callback, $params = [])
{ {
if (is_callable($callback)) { if (is_callable($callback)) {
if (is_array($callback)) { return call_user_func_array($callback, $this->resolveCallableDependencies($callback, $params));
$reflection = new \ReflectionMethod($callback[0], $callback[1]); } else {
} else { return call_user_func_array($callback, $params);
$reflection = new \ReflectionFunction($callback); }
} }
$args = []; /**
foreach ($reflection->getParameters() as $param) { * Resolve dependencies for a function.
$name = $param->getName(); *
if (($class = $param->getClass()) !== null) { * This method can be used to implement similar functionality as provided by [[invoke()]] in other
$className = $class->getName(); * components.
if (isset($params[0]) && $params[0] instanceof $className) { *
$args[] = array_shift($params); * @param callable $callback callable to be invoked.
} elseif (\Yii::$app->has($name) && ($obj = \Yii::$app->get($name)) instanceof $className) { * @param array $params The array of parameters for the function, can be either numeric or associative.
$args[] = $obj; * @return array The resolved dependencies.
} else { * @throws InvalidConfigException if a dependency cannot be resolved or if a dependency cannot be fulfilled.
$args[] = $this->get($className); * @since 2.0.7
} */
} elseif (count($params)) { public function resolveCallableDependencies(callable $callback, $params = [])
{
if (is_array($callback)) {
$reflection = new \ReflectionMethod($callback[0], $callback[1]);
} else {
$reflection = new \ReflectionFunction($callback);
}
$args = [];
$associative = ArrayHelper::isAssociative($params);
foreach ($reflection->getParameters() as $param) {
$name = $param->getName();
if (($class = $param->getClass()) !== null) {
$className = $class->getName();
if ($associative && isset($params[$name]) && $params[$name] instanceof $className) {
$args[] = $params[$name];
unset($params[$name]);
} elseif (!$associative && isset($params[0]) && $params[0] instanceof $className) {
$args[] = array_shift($params); $args[] = array_shift($params);
} elseif ($param->isDefaultValueAvailable()) { } elseif (Yii::$app->has($name) && ($obj = Yii::$app->get($name)) instanceof $className) {
$args[] = $param->getDefaultValue(); $args[] = $obj;
} elseif (!$param->isOptional()) { } else {
$funcName = $reflection->getName(); $args[] = $this->get($className);
throw new InvalidConfigException("Missing required parameter \"$name\" when calling \"$funcName\".");
} }
} elseif ($associative && isset($params[$name])) {
$args[] = $params[$name];
unset($params[$name]);
} elseif (!$associative && count($params)) {
$args[] = array_shift($params);
} elseif ($param->isDefaultValueAvailable()) {
$args[] = $param->getDefaultValue();
} elseif (!$param->isOptional()) {
$funcName = $reflection->getName();
throw new InvalidConfigException("Missing required parameter \"$name\" when calling \"$funcName\".");
} }
}
foreach ($params as $value) { foreach ($params as $value) {
$args[] = $value; $args[] = $value;
}
return call_user_func_array($callback, $args);
} else {
return call_user_func_array($callback, $params);
} }
return $args;
} }
} }

44
tests/framework/di/ContainerTest.php

@ -20,6 +20,7 @@ use yii\validators\NumberValidator;
/** /**
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
* @group di
*/ */
class ContainerTest extends TestCase class ContainerTest extends TestCase
{ {
@ -169,4 +170,47 @@ class ContainerTest extends TestCase
$this->assertEquals($response, Yii::$app->response); $this->assertEquals($response, Yii::$app->response);
} }
public function testAssociativeInvoke()
{
$this->mockApplication([
'components' => [
'qux' => [
'class' => 'yiiunit\framework\di\stubs\Qux',
'a' => 'belongApp',
],
'qux2' => [
'class' => 'yiiunit\framework\di\stubs\Qux',
'a' => 'belongAppQux2',
],
]
]);
$closure = function($a, $x = 5, $b) {
return $a > $b;
};
$this->assertFalse(Yii::$container->invoke($closure, ['b' => 5, 'a' => 1]));
$this->assertTrue(Yii::$container->invoke($closure, ['b' => 1, 'a' => 5]));
}
public function testResolveCallableDependencies()
{
$this->mockApplication([
'components' => [
'qux' => [
'class' => 'yiiunit\framework\di\stubs\Qux',
'a' => 'belongApp',
],
'qux2' => [
'class' => 'yiiunit\framework\di\stubs\Qux',
'a' => 'belongAppQux2',
],
]
]);
$closure = function($a, $b) {
return $a > $b;
};
$this->assertEquals([1, 5], Yii::$container->resolveCallableDependencies($closure, ['b' => 5, 'a' => 1]));
$this->assertEquals([1, 5], Yii::$container->resolveCallableDependencies($closure, ['a' => 1, 'b' => 5]));
$this->assertEquals([1, 5], Yii::$container->resolveCallableDependencies($closure, [1, 5]));
}
} }

1
tests/framework/di/InstanceTest.php

@ -17,6 +17,7 @@ use yiiunit\TestCase;
/** /**
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
* @group di
*/ */
class InstanceTest extends TestCase class InstanceTest extends TestCase
{ {

1
tests/framework/di/ServiceLocatorTest.php

@ -28,6 +28,7 @@ class TestClass extends Object
/** /**
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
* @group di
*/ */
class ServiceLocatorTest extends TestCase class ServiceLocatorTest extends TestCase
{ {

Loading…
Cancel
Save