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. 4
      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

4
framework/CHANGELOG.md

@ -94,7 +94,7 @@ Yii Framework 2 Change Log
- Enh #9733: Added Unprocessable Entity HTTP Exception (janfrs)
- 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 #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 #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)
@ -118,7 +118,7 @@ Yii Framework 2 Change Log
- Enh #10535: Allow passing a `yii\db\Expression` to `Query::orderBy()` and `Query::groupBy()` (andrewnester, cebe)
- Enh #10545: `yii\web\XMLResponseFormatter` changed to format models in a proper way (andrewnester)
- Enh #10783: Added migration and unit-tests for `yii\i18n\DbMessageSource` (silverfire)
- Enh #10797: Cleaned up requirements checker CSS (muhammadcahya)
- Enh #10797: Cleaned up requirements checker CSS (muhammadcahya)
- Enh: Added last resort measure for `FileHelper::removeDirectory()` fail to unlink symlinks under Windows (samdark)
- Chg #9369: `Yii::$app->user->can()` now returns `false` instead of erroring in case `authManager` component is not configured (creocoder)
- Chg #9411: `DetailView` now automatically sets container tag ID in case it's not specified (samdark)

109
framework/di/Container.php

@ -8,8 +8,10 @@
namespace yii\di;
use ReflectionClass;
use Yii;
use yii\base\Component;
use yii\base\InvalidConfigException;
use yii\helpers\ArrayHelper;
/**
* 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 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.
* @throws InvalidConfigException if a dependency cannot be resolved or if a dependency cannot be fulfilled.
* @since 2.0.7
*/
public function invoke($callback, $params = [])
public function invoke(callable $callback, $params = [])
{
if (is_callable($callback)) {
if (is_array($callback)) {
$reflection = new \ReflectionMethod($callback[0], $callback[1]);
} else {
$reflection = new \ReflectionFunction($callback);
}
return call_user_func_array($callback, $this->resolveCallableDependencies($callback, $params));
} else {
return call_user_func_array($callback, $params);
}
}
$args = [];
foreach ($reflection->getParameters() as $param) {
$name = $param->getName();
if (($class = $param->getClass()) !== null) {
$className = $class->getName();
if (isset($params[0]) && $params[0] instanceof $className) {
$args[] = array_shift($params);
} elseif (\Yii::$app->has($name) && ($obj = \Yii::$app->get($name)) instanceof $className) {
$args[] = $obj;
} else {
$args[] = $this->get($className);
}
} elseif (count($params)) {
/**
* Resolve dependencies for a function.
*
* This method can be used to implement similar functionality as provided by [[invoke()]] in other
* components.
*
* @param callable $callback callable to be invoked.
* @param array $params The array of parameters for the function, can be either numeric or associative.
* @return array The resolved dependencies.
* @throws InvalidConfigException if a dependency cannot be resolved or if a dependency cannot be fulfilled.
* @since 2.0.7
*/
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);
} elseif ($param->isDefaultValueAvailable()) {
$args[] = $param->getDefaultValue();
} elseif (!$param->isOptional()) {
$funcName = $reflection->getName();
throw new InvalidConfigException("Missing required parameter \"$name\" when calling \"$funcName\".");
} elseif (Yii::$app->has($name) && ($obj = Yii::$app->get($name)) instanceof $className) {
$args[] = $obj;
} else {
$args[] = $this->get($className);
}
} 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) {
$args[] = $value;
}
return call_user_func_array($callback, $args);
} else {
return call_user_func_array($callback, $params);
foreach ($params as $value) {
$args[] = $value;
}
return $args;
}
}

44
tests/framework/di/ContainerTest.php

@ -20,6 +20,7 @@ use yii\validators\NumberValidator;
/**
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
* @group di
*/
class ContainerTest extends TestCase
{
@ -169,4 +170,47 @@ class ContainerTest extends TestCase
$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>
* @since 2.0
* @group di
*/
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>
* @since 2.0
* @group di
*/
class ServiceLocatorTest extends TestCase
{

Loading…
Cancel
Save