diff --git a/docs/guide/concept-di-container.md b/docs/guide/concept-di-container.md index 426abba..57b35e1 100644 --- a/docs/guide/concept-di-container.md +++ b/docs/guide/concept-di-container.md @@ -116,6 +116,20 @@ $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. +### Controller action injection + +Controller action injection is a special type of DI where dependecies are resolved per action which is useful for +keeping dependencies number low in MVC controllers. + +```php +public function actionSend($email, EmailValidator $validator) +{ + if ($validator->validate($email)) { + // ... send email + } +} +``` + Registering Dependencies ------------------------ diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 2a3e0ee..0c62493 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -20,6 +20,7 @@ Yii Framework 2 Change Log - 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) - Enh #2106: Added Unprocessable Entity HTTP Exception (janfrs) +- Enh #9476: Added DI injection via controller action method signature (mdmunir) - Enh #9635: Added default CSS class for `\yii\grid\ActionColumn` header (arogachev, dynasource) - Enh #9711: Added `yii\widgets\LinkPager::$pageCssClass` that allows to set default page class (ShNURoK42) diff --git a/framework/console/Controller.php b/framework/console/Controller.php index 51ecc04..6dc3575 100644 --- a/framework/console/Controller.php +++ b/framework/console/Controller.php @@ -109,14 +109,25 @@ class Controller extends \yii\base\Controller $method = new \ReflectionMethod($action, 'run'); } - $args = array_values($params); + $params = array_values($params); + $args = []; $missing = []; foreach ($method->getParameters() as $i => $param) { - if ($param->isArray() && isset($args[$i])) { - $args[$i] = preg_split('/\s*,\s*/', $args[$i]); + if (($class = $param->getClass()) !== null) { + $name = $param->getName(); + $className = $class->getName(); + if (Yii::$app->has($name) && ($obj = Yii::$app->get($name)) instanceof $className) { + $args[$i] = $obj; + } else { + $args[$i] = Yii::$container->get($className); + } + continue; } - if (!isset($args[$i])) { + $value = array_shift($params); + if (isset($value)) { + $args[$i] = $param->isArray() ? preg_split('/\s*,\s*/', $value) : $value; + } else { if ($param->isDefaultValueAvailable()) { $args[$i] = $param->getDefaultValue(); } else { diff --git a/framework/web/Controller.php b/framework/web/Controller.php index b3721a7..9795b97 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -71,7 +71,14 @@ class Controller extends \yii\base\Controller $actionParams = []; foreach ($method->getParameters() as $param) { $name = $param->getName(); - if (array_key_exists($name, $params)) { + if (($class = $param->getClass()) !== null) { + $className = $class->getName(); + 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()) { $args[] = $actionParams[$name] = (array) $params[$name]; } elseif (!is_array($params[$name])) { diff --git a/tests/framework/console/ControllerTest.php b/tests/framework/console/ControllerTest.php new file mode 100644 index 0000000..c988dd2 --- /dev/null +++ b/tests/framework/console/ControllerTest.php @@ -0,0 +1,86 @@ +mockApplication([ + 'components' => [ + 'barBelongApp' => [ + 'class' => Bar::className(), + 'foo' => 'belong_app' + ], + 'quxApp' => [ + 'class' => OtherQux::className(), + 'b' => 'belong_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']; + list($bar, $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('default', $other); + + $params = []; + list($barBelongApp, $qux) = $controller->run('aksi2', $params); + $this->assertTrue($barBelongApp instanceof Bar); + $this->assertEquals($barBelongApp, Yii::$app->barBelongApp); + $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']; + $result = $controller->runAction('aksi4', $params); + $this->assertEquals(['independent', 'other_qux', ['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 = ['avaliable']; + $message = Yii::t('yii', 'Missing required arguments: {params}', ['params' => implode(', ', ['missing'])]); + $this->setExpectedException('yii\console\Exception', $message); + $result = $controller->runAction('aksi7', $params); + } +} diff --git a/tests/framework/console/FakeController.php b/tests/framework/console/FakeController.php new file mode 100644 index 0000000..b12ef40 --- /dev/null +++ b/tests/framework/console/FakeController.php @@ -0,0 +1,56 @@ + + * @since 2.0 + */ +class FakeController extends Controller +{ + + public function actionAksi1(Bar $bar, $fromParam, $other = 'default') + { + return[$bar, $fromParam, $other]; + } + + public function actionAksi2(Bar $barBelongApp, QuxInterface $qux) + { + return[$barBelongApp, $qux]; + } + + public function actionAksi3(QuxInterface $quxApp) + { + 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) + { + + } +} diff --git a/tests/framework/web/ControllerTest.php b/tests/framework/web/ControllerTest.php new file mode 100644 index 0000000..20930b0 --- /dev/null +++ b/tests/framework/web/ControllerTest.php @@ -0,0 +1,81 @@ +mockApplication([ + 'components'=>[ + 'barBelongApp'=>[ + 'class'=> Bar::className(), + 'foo'=>'belong_app' + ], + 'quxApp'=>[ + 'class' => OtherQux::className(), + 'b' => 'belong_app' + ] + ] + ]); + + $controller = new FakeController('fake', Yii::$app); + $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']; + + 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('default', $other); + + list($barBelongApp, $qux) = $controller->bindActionParams($aksi2, $params); + $this->assertTrue($barBelongApp instanceof Bar); + $this->assertEquals($barBelongApp, Yii::$app->barBelongApp); + $this->assertEquals('belong_app', $barBelongApp->foo); + $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); + } +} diff --git a/tests/framework/web/FakeController.php b/tests/framework/web/FakeController.php new file mode 100644 index 0000000..83da669 --- /dev/null +++ b/tests/framework/web/FakeController.php @@ -0,0 +1,49 @@ + + * @since 2.0 + */ +class FakeController extends Controller +{ + public $enableCsrfValidation = false; + + public function actionAksi1(Bar $bar, $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')]; + } +} diff --git a/tests/framework/web/stubs/Bar.php b/tests/framework/web/stubs/Bar.php new file mode 100644 index 0000000..22f62ae --- /dev/null +++ b/tests/framework/web/stubs/Bar.php @@ -0,0 +1,19 @@ + + * @since 2.0 + */ +class Bar extends Object +{ + public $foo; +} diff --git a/tests/framework/web/stubs/OtherQux.php b/tests/framework/web/stubs/OtherQux.php new file mode 100644 index 0000000..f83838d --- /dev/null +++ b/tests/framework/web/stubs/OtherQux.php @@ -0,0 +1,24 @@ + + * @since 2.0 + */ +class OtherQux extends Object implements QuxInterface +{ + public $b; + public function quxMethod() + { + return 'other_qux'; + } +}