Revert DI injection via controller action method signature

reverts b7020065c and related commits.
reverts implementation of #9476

For reasons, see discussion https://github.com/yiisoft/yii2/issues/9476#issuecomment-179749039
This commit is contained in:
Carsten Brandt
2016-02-06 05:17:33 +01:00
parent e8a7a8a0f4
commit 42b8569846
12 changed files with 28 additions and 335 deletions

View File

@ -114,22 +114,6 @@ $foo = $container->get('Foo');
このようにすれば、`Foo` クラスを構成しようとする人は、`Foo` がどのように構築されるかを気にする必要はもうなくなります。 このようにすれば、`Foo` クラスを構成しようとする人は、`Foo` がどのように構築されるかを気にする必要はもうなくなります。
### コントローラ・アクション・インジェクション <span id="controller-action-injection"></span>
コントローラ・アクション・インジェクションは、メソッド・シグニチャの型ヒントを使って依存が宣言される特殊な DI です。
依存は、実行時に、アクションが実際に呼ばれるときに解決されます。
この場合、必要になるかも知れない依存を前もって構成する必要がありませんので、MVC のコントローラを軽量に保つのに役立ちます。
```php
public function actionSend($email, EmailValidator $validator)
{
if ($validator->validate($email)) {
// ... メールを送信
}
}
```
依存関係の登録 <span id="registering-dependencies"></span> 依存関係の登録 <span id="registering-dependencies"></span>
-------------- --------------

View File

@ -111,20 +111,6 @@ $foo = $container->get('Foo');
Теперь тот, кто будет настраивать класс `Foo`, не обязан знать, как этот класс устроен. Теперь тот, кто будет настраивать класс `Foo`, не обязан знать, как этот класс устроен.
### Внедрение зависимости через действие контроллера <span id="controller-action-injection"></span>
Внедрение зависимости через действие контроллера - это специальный тип внедрения зависимостей, использующий указание типов параметров, принимаемых методом, исполняемый во время выполнения кода действия. Этот тип внедрения зависимостей помогает сделать контроллеры MVC *тонкими* и *легкими*, так как нет необходимости настраивать все возможные зависимости контроллера заранее.
```php
public function actionSend($email, EmailValidator $validator)
{
if ($validator->validate($email)) {
// ... отправка email
}
}
```
Регистрация зависимостей <span id="registering-dependencies"></span> Регистрация зависимостей <span id="registering-dependencies"></span>
------------------------ ------------------------

View File

@ -117,23 +117,6 @@ $foo = $container->get('Foo');
By doing so, the person who wants to configure the `Foo` class no longer needs to be aware of how it is built. By doing so, the person who wants to configure the `Foo` class no longer needs to be aware of how it is built.
### Controller action injection <span id="controller-action-injection"></span>
Controller action injection is a special type of DI where dependecies are declared using the type hints of
method signature and resolved in the runtime when the action is acturally called. It is useful for keeping
the MVC controllers slim and light-weighted since it doesn't require you to configure all the possible dependencies
of the controller beforehand.
```php
public function actionSend($email, EmailValidator $validator)
{
if ($validator->validate($email)) {
// ... send email
}
}
```
Registering Dependencies <span id="registering-dependencies"></span> Registering Dependencies <span id="registering-dependencies"></span>
------------------------ ------------------------

View File

@ -79,7 +79,6 @@ Yii Framework 2 Change Log
- Enh #9412: `yii\web\Response::sendHeaders()` does now set the status header last which negates certain magic PHP behavior regarding the `header()` function (nd4c, kidol) - Enh #9412: `yii\web\Response::sendHeaders()` does now set the status header last which negates certain magic PHP behavior regarding the `header()` function (nd4c, kidol)
- Enh #9443: Added `unsigned()` to `ColumnSchemaBuilder` (samdark) - Enh #9443: Added `unsigned()` to `ColumnSchemaBuilder` (samdark)
- Enh #9465: ./yii migrate/create now generates code based on migration name and --fields (pana1990) - Enh #9465: ./yii migrate/create now generates code based on migration name and --fields (pana1990)
- Enh #9476: Added DI injection via controller action method signature (mdmunir)
- Enh #9573: Added `yii\rbac\ManagerInterface::getUserIdsByRole()` and implementations (samdark) - Enh #9573: Added `yii\rbac\ManagerInterface::getUserIdsByRole()` and implementations (samdark)
- Enh #9635: Added default CSS class for `\yii\grid\ActionColumn` header (arogachev, dynasource) - Enh #9635: Added default CSS class for `\yii\grid\ActionColumn` header (arogachev, dynasource)
- Enh #9643: Added migrations for DB cache (mdmunir) - Enh #9643: Added migrations for DB cache (mdmunir)

View File

@ -122,27 +122,16 @@ class Controller extends \yii\base\Controller
$method = new \ReflectionMethod($action, 'run'); $method = new \ReflectionMethod($action, 'run');
} }
$params = array_values($params); $args = array_values($params);
$args = [];
$missing = []; $missing = [];
foreach ($method->getParameters() as $param) { foreach ($method->getParameters() as $i => $param) {
if (($class = $param->getClass()) !== null) { if ($param->isArray() && isset($args[$i])) {
$name = $param->getName(); $args[$i] = preg_split('/\s*,\s*/', $args[$i]);
$className = $class->getName();
if (Yii::$app->has($name) && ($obj = Yii::$app->get($name)) instanceof $className) {
$args[] = $obj;
} else {
$args[] = Yii::$container->get($className);
} }
continue; if (!isset($args[$i])) {
}
$value = array_shift($params);
if (isset($value)) {
$args[] = $param->isArray() ? preg_split('/\s*,\s*/', $value) : $value;
} else {
if ($param->isDefaultValueAvailable()) { if ($param->isDefaultValueAvailable()) {
$args[] = $param->getDefaultValue(); $args[$i] = $param->getDefaultValue();
} else { } else {
$missing[] = $param->getName(); $missing[] = $param->getName();
} }
@ -153,9 +142,6 @@ class Controller extends \yii\base\Controller
throw new Exception(Yii::t('yii', 'Missing required arguments: {params}', ['params' => implode(', ', $missing)])); throw new Exception(Yii::t('yii', 'Missing required arguments: {params}', ['params' => implode(', ', $missing)]));
} }
foreach ($params as $value) {
$args[] = $value;
}
return $args; return $args;
} }
@ -418,9 +404,6 @@ class Controller extends \yii\base\Controller
/** @var \ReflectionParameter $reflection */ /** @var \ReflectionParameter $reflection */
foreach ($method->getParameters() as $i => $reflection) { foreach ($method->getParameters() as $i => $reflection) {
$name = $reflection->getName(); $name = $reflection->getName();
if ($reflection->getClass() !== null) {
continue;
}
$tag = isset($params[$i]) ? $params[$i] : ''; $tag = isset($params[$i]) ? $params[$i] : '';
if (preg_match('/^(\S+)\s+(\$\w+\s+)?(.*)/s', $tag, $matches)) { if (preg_match('/^(\S+)\s+(\$\w+\s+)?(.*)/s', $tag, $matches)) {
$type = $matches[1]; $type = $matches[1];

View File

@ -71,20 +71,7 @@ class Controller extends \yii\base\Controller
$actionParams = []; $actionParams = [];
foreach ($method->getParameters() as $param) { foreach ($method->getParameters() as $param) {
$name = $param->getName(); $name = $param->getName();
if (($class = $param->getClass()) !== null) { if (array_key_exists($name, $params)) {
$className = $class->getName();
}
// We only enter the class injection code path if:
// - A class is hinted in the method signature
// - And the param name of hinted class does not exist in existing $params, or the value in existing $params is not an instance of the hinted class
// The latter two checks allow us to manually inject classes via $params while ignoring wrongly injected values (no instances of hinted class).
if ($class !== null && (!array_key_exists($name, $params) || !$params[$name] instanceof $className)) {
if (Yii::$app->has($name) && ($obj = Yii::$app->get($name)) instanceof $className) {
$args[] = $actionParams[$name] = $obj;
} else {
$args[] = $actionParams[$name] = Yii::$container->get($className);
}
} elseif (array_key_exists($name, $params)) {
if ($param->isArray()) { if ($param->isArray()) {
$args[] = $actionParams[$name] = (array) $params[$name]; $args[] = $actionParams[$name] = (array) $params[$name];
} elseif (!is_array($params[$name])) { } elseif (!is_array($params[$name])) {

View File

@ -21,98 +21,28 @@ class ControllerTest extends TestCase
public function testBindActionParams() public function testBindActionParams()
{ {
$this->mockApplication([ $this->mockApplication([]);
'components' => [
'barBelongApp' => [
'class' => Bar::className(),
'foo' => 'belong_app'
],
'quxApp' => [
'class' => OtherQux::className(),
'b' => 'belong_app'
]
]
]);
$controller = new FakeController('fake', Yii::$app); $controller = new FakeController('fake', Yii::$app);
Yii::$container->set('yiiunit\framework\di\stubs\QuxInterface', [
'class' => Qux::className(),
'a' => 'D426'
]);
Yii::$container->set(Bar::className(), [
'foo' => 'independent'
]);
$params = ['from params']; $params = ['from params'];
list($bar, $fromParam, $other) = $controller->run('aksi1', $params); list($fromParam, $other) = $controller->run('aksi1', $params);
$this->assertTrue($bar instanceof Bar);
$this->assertNotEquals($bar, Yii::$app->barBelongApp);
$this->assertEquals('independent', $bar->foo);
$this->assertEquals('from params', $fromParam); $this->assertEquals('from params', $fromParam);
$this->assertEquals('default', $other); $this->assertEquals('default', $other);
$params = []; $params = ['from params', 'notdefault'];
list($barBelongApp, $qux) = $controller->run('aksi2', $params); list($fromParam, $other) = $controller->run('aksi1', $params);
$this->assertTrue($barBelongApp instanceof Bar); $this->assertEquals('from params', $fromParam);
$this->assertEquals($barBelongApp, Yii::$app->barBelongApp); $this->assertEquals('notdefault', $other);
$this->assertEquals('belong_app', $barBelongApp->foo);
$this->assertTrue($qux instanceof Qux);
$this->assertEquals('D426', $qux->a);
$params = [];
list($quxApp) = $controller->run('aksi3', $params);
$this->assertTrue($quxApp instanceof OtherQux);
$this->assertEquals($quxApp, Yii::$app->quxApp);
$this->assertEquals('belong_app', $quxApp->b);
$params = ['d426,mdmunir', 'single']; $params = ['d426,mdmunir', 'single'];
$result = $controller->runAction('aksi4', $params); $result = $controller->runAction('aksi2', $params);
$this->assertEquals(['independent', 'other_qux', ['d426', 'mdmunir'], 'single'], $result); $this->assertEquals([['d426', 'mdmunir'], 'single'], $result);
$params = ['d426'];
$result = $controller->runAction('aksi5', $params);
$this->assertEquals(['d426', 'independent', 'other_qux'], $result);
$params = ['mdmunir'];
$result = $controller->runAction('aksi6', $params);
$this->assertEquals(['mdmunir', false, true], $result);
$params = ['arg1', 'arg2', 'arg3'];
$result = $controller->runAction('aksi8', $params);
$this->assertEquals($params, $result);
$params = ['arg1', 'arg2', 'arg3'];
$result = $controller->runAction('aksi9', $params);
$this->assertEquals(['arg1', 'arg2', Yii::$app->quxApp, 'arg3'], $result);
$params = ['avaliable']; $params = ['avaliable'];
$message = Yii::t('yii', 'Missing required arguments: {params}', ['params' => implode(', ', ['missing'])]); $message = Yii::t('yii', 'Missing required arguments: {params}', ['params' => implode(', ', ['missing'])]);
$this->setExpectedException('yii\console\Exception', $message); $this->setExpectedException('yii\console\Exception', $message);
$result = $controller->runAction('aksi7', $params); $result = $controller->runAction('aksi3', $params);
} }
/**
* Tests if action help does not include class-hinted arguments
* @see https://github.com/yiisoft/yii2/issues/10372
*/
public function testHelp()
{
$this->mockApplication([
'components' => [
'barBelongApp' => [
'class' => Bar::className(),
'foo' => 'belong_app'
],
'quxApp' => [
'class' => OtherQux::className(),
'b' => 'belong_app'
]
]
]);
$controller = new FakeController('fake', Yii::$app);
$this->assertArrayNotHasKey('bar', $controller->getActionArgsHelp($controller->createAction('aksi1')));
}
} }

View File

@ -19,48 +19,17 @@ use yii\validators\EmailValidator;
class FakeController extends Controller class FakeController extends Controller
{ {
public function actionAksi1(Bar $bar, $fromParam, $other = 'default') public function actionAksi1($fromParam, $other = 'default')
{ {
return[$bar, $fromParam, $other]; return[$fromParam, $other];
} }
public function actionAksi2(Bar $barBelongApp, QuxInterface $qux) public function actionAksi2(array $values, $value)
{ {
return[$barBelongApp, $qux]; return [$values, $value];
} }
public function actionAksi3(QuxInterface $quxApp) public function actionAksi3($available, $missing)
{ {
return[$quxApp];
}
public function actionAksi4(Bar $bar, QuxInterface $quxApp, array $values, $value)
{
return [$bar->foo, $quxApp->quxMethod(), $values, $value];
}
public function actionAksi5($q, Bar $bar, QuxInterface $quxApp)
{
return [$q, $bar->foo, $quxApp->quxMethod()];
}
public function actionAksi6($q, EmailValidator $validator)
{
return [$q, $validator->validate($q), $validator->validate('misbahuldmunir@gmail.com')];
}
public function actionAksi7(Bar $bar, $avaliable, $missing)
{
}
public function actionAksi8($arg1, $arg2)
{
return func_get_args();
}
public function actionAksi9($arg1, $arg2, QuxInterface $quxApp)
{
return func_get_args();
} }
} }

View File

@ -22,73 +22,20 @@ class ControllerTest extends TestCase
public function testBindActionParams() public function testBindActionParams()
{ {
$this->mockApplication([ $this->mockApplication();
'components'=>[
'barBelongApp'=>[
'class'=> Bar::className(),
'foo'=>'belong_app'
],
'quxApp'=>[
'class' => OtherQux::className(),
'b' => 'belong_app'
]
]
]);
$controller = new FakeController('fake', Yii::$app); $controller = new FakeController('fake', Yii::$app);
$aksi1 = new InlineAction('aksi1', $controller, 'actionAksi1'); $aksi1 = new InlineAction('aksi1', $controller, 'actionAksi1');
$aksi2 = new InlineAction('aksi2', $controller, 'actionAksi2');
$aksi3 = new InlineAction('aksi3', $controller, 'actionAksi3');
Yii::$container->set('yiiunit\framework\di\stubs\QuxInterface', [
'class' => Qux::className(),
'a' => 'D426'
]);
Yii::$container->set(Bar::className(),[
'foo' => 'independent'
]);
$params = ['fromGet'=>'from query params','q'=>'d426','validator'=>'avaliable']; $params = ['fromGet'=>'from query params','q'=>'d426','validator'=>'avaliable'];
list($fromGet, $other) = $controller->bindActionParams($aksi1, $params);
list($bar, $fromGet, $other) = $controller->bindActionParams($aksi1, $params);
$this->assertTrue($bar instanceof Bar);
$this->assertNotEquals($bar, Yii::$app->barBelongApp);
$this->assertEquals('independent', $bar->foo);
$this->assertEquals('from query params', $fromGet); $this->assertEquals('from query params', $fromGet);
$this->assertEquals('default', $other); $this->assertEquals('default', $other);
list($barBelongApp, $qux) = $controller->bindActionParams($aksi2, $params); $params = ['fromGet'=>'from query params','q'=>'d426','other'=>'avaliable'];
$this->assertTrue($barBelongApp instanceof Bar); list($fromGet, $other) = $controller->bindActionParams($aksi1, $params);
$this->assertEquals($barBelongApp, Yii::$app->barBelongApp); $this->assertEquals('from query params', $fromGet);
$this->assertEquals('belong_app', $barBelongApp->foo); $this->assertEquals('avaliable', $other);
$this->assertTrue($qux instanceof Qux);
$this->assertEquals('D426', $qux->a);
list($quxApp) = $controller->bindActionParams($aksi3, $params);
$this->assertTrue($quxApp instanceof OtherQux);
$this->assertEquals($quxApp, Yii::$app->quxApp);
$this->assertEquals('belong_app', $quxApp->b);
$result = $controller->runAction('aksi4', $params);
$this->assertEquals(['independent', 'other_qux', 'd426'], $result);
$result = $controller->runAction('aksi5', $params);
$this->assertEquals(['d426', 'independent', 'other_qux'], $result);
$result = $controller->runAction('aksi6', $params);
$this->assertEquals(['d426', false, true], $result);
// Manually inject an instance of \StdClass
// In this case we don't want a newly created instance, but use the existing one
$stdClass = new \StdClass;
$stdClass->test = 'dummy';
$result = $controller->runAction('aksi7', array_merge($params, ['validator' => $stdClass]));
$this->assertEquals(['d426', 'dummy'], $result);
// Manually inject a string instead of an instance of \StdClass
// Since this is wrong usage, we expect a new instance of the type hinted \StdClass anyway
$stdClass = 'string';
$result = $controller->runAction('aksi8', array_merge($params, ['validator' => $stdClass]));
$this->assertEquals(['d426', 'object'], $result);
} }
} }

View File

@ -20,40 +20,8 @@ class FakeController extends Controller
{ {
public $enableCsrfValidation = false; public $enableCsrfValidation = false;
public function actionAksi1(Bar $bar, $fromGet, $other = 'default') public function actionAksi1($fromGet, $other = 'default')
{ {
} }
public function actionAksi2(Bar $barBelongApp, QuxInterface $qux)
{
}
public function actionAksi3(QuxInterface $quxApp)
{
}
public function actionAksi4(Bar $bar, QuxInterface $quxApp, $q)
{
return [$bar->foo, $quxApp->quxMethod(), $q];
}
public function actionAksi5($q, Bar $bar, QuxInterface $quxApp)
{
return [$q, $bar->foo, $quxApp->quxMethod()];
}
public function actionAksi6($q, EmailValidator $validator)
{
return [$q, $validator->validate($q), $validator->validate('misbahuldmunir@gmail.com')];
}
public function actionAksi7($q, \StdClass $validator)
{
return [$q, $validator->test];
}
public function actionAksi8($q, \StdClass $validator)
{
return [$q, gettype($validator)];
}
} }

View File

@ -1,19 +0,0 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yiiunit\framework\web\stubs;
use yii\base\Object;
/**
* @author Misbahul D Munir <misbahuldmunir@gmail.com>
* @since 2.0
*/
class Bar extends Object
{
public $foo;
}

View File

@ -1,24 +0,0 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yiiunit\framework\web\stubs;
use yii\base\Object;
use yiiunit\framework\di\stubs\QuxInterface;
/**
* @author Misbahul D Munir <misbahuldmunir@gmail.com>
* @since 2.0
*/
class OtherQux extends Object implements QuxInterface
{
public $b;
public function quxMethod()
{
return 'other_qux';
}
}