From a128748cd7a6ffa923fc5e12e4c584a380fd4d79 Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Wed, 6 May 2020 16:39:39 +0200 Subject: [PATCH 01/24] Fixes #17722 --- framework/web/Controller.php | 19 +++++++++++++++++++ tests/framework/web/ControllerTest.php | 13 +++++++++++++ tests/framework/web/FakeController.php | 7 +++++++ 3 files changed, 39 insertions(+) diff --git a/framework/web/Controller.php b/framework/web/Controller.php index 5492df62958..31eba9474ec 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -125,6 +125,7 @@ public function bindActionParams($action, $params) $args = []; $missing = []; $actionParams = []; + $requestedParams = []; foreach ($method->getParameters() as $param) { $name = $param->getName(); if (array_key_exists($name, $params)) { @@ -162,6 +163,19 @@ public function bindActionParams($action, $params) } $args[] = $actionParams[$name] = $params[$name]; unset($params[$name]); + } elseif (($type = $param->getType()) !== null && !$type->isBuiltin()) { + // Since it is not a builtin type it must be DI injection. + $typeName = PHP_VERSION_ID >= 70100 ? $type->getName() : (string) $type; + if (($component = $this->module->get($name, false)) instanceof $typeName) { + $args[] = $component; + $requestedParams[$name] = "Component: $name"; + } elseif (($service = \Yii::$container->get($name)) instanceof $typeName) { + $args[] = $service; + $requestedParams[$name] = "DI: $name"; + } elseif ($type->allowsNull()) { + $args[] = null; + $requestedParams[$name] = "Unavailable service: $name"; + } } elseif ($param->isDefaultValueAvailable()) { $args[] = $actionParams[$name] = $param->getDefaultValue(); } else { @@ -177,6 +191,11 @@ public function bindActionParams($action, $params) $this->actionParams = $actionParams; + // We use a different array here, specifically one that doesn't contain service instances but descriptions instead. + if (\Yii::$app->requestedParams === null) { + \Yii::$app->requestedParams = array_merge($actionParams, $requestedParams); + } + return $args; } diff --git a/tests/framework/web/ControllerTest.php b/tests/framework/web/ControllerTest.php index 2aff74e6ce8..b7fbefae059 100644 --- a/tests/framework/web/ControllerTest.php +++ b/tests/framework/web/ControllerTest.php @@ -17,6 +17,8 @@ */ class ControllerTest extends TestCase { + /** @var FakeController */ + private $controller; public function testBindActionParams() { $aksi1 = new InlineAction('aksi1', $this->controller, 'actionAksi1'); @@ -32,6 +34,17 @@ public function testBindActionParams() $this->assertEquals('avaliable', $other); } + public function testInjectedActionParams() + { + $injectionAction = new InlineAction('injection', $this->controller, 'actionInjection'); + $params = ['between' => 'test', 'after' => 'another', 'before' => 'test']; + $args = $this->controller->bindActionParams($injectionAction, $params); + $this->assertEquals($params['before'], $args[0]); + $this->assertEquals(\Yii::$app->request, $args[1]); + $this->assertEquals($params['between'], $args[2]); + $this->assertInstanceOf(Post::class, $args[3]); + $this->assertEquals($params['after'], $args[4]); + } /** * @see https://github.com/yiisoft/yii2/issues/17701 */ diff --git a/tests/framework/web/FakeController.php b/tests/framework/web/FakeController.php index 572d169e5b9..6a5ca57a81d 100644 --- a/tests/framework/web/FakeController.php +++ b/tests/framework/web/FakeController.php @@ -8,6 +8,8 @@ namespace yiiunit\framework\web; use yii\web\Controller; +use yii\web\Request; +use yii\web\Response; /** * @author Misbahul D Munir @@ -20,4 +22,9 @@ class FakeController extends Controller public function actionAksi1($fromGet, $other = 'default') { } + + public function actionInjection($before, Request $request, Post $post, $between, $after) + { + + } } From 27058543a7952c409723d3c3315daec6fe9f899e Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Wed, 6 May 2020 16:51:57 +0200 Subject: [PATCH 02/24] Use class name when retrieving from DI --- framework/web/Controller.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/web/Controller.php b/framework/web/Controller.php index 31eba9474ec..a2e813f2a71 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -169,9 +169,9 @@ public function bindActionParams($action, $params) if (($component = $this->module->get($name, false)) instanceof $typeName) { $args[] = $component; $requestedParams[$name] = "Component: $name"; - } elseif (($service = \Yii::$container->get($name)) instanceof $typeName) { + } elseif (($service = \Yii::$container->get($typeName)) instanceof $typeName) { $args[] = $service; - $requestedParams[$name] = "DI: $name"; + $requestedParams[$name] = "DI: $typeName"; } elseif ($type->allowsNull()) { $args[] = null; $requestedParams[$name] = "Unavailable service: $name"; From 166541163718b19ece5185423feb5b27f9bc4305 Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Wed, 6 May 2020 16:57:57 +0200 Subject: [PATCH 03/24] Use className() for PHP5.4 support --- tests/framework/web/ControllerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/framework/web/ControllerTest.php b/tests/framework/web/ControllerTest.php index b7fbefae059..64f14cb0694 100644 --- a/tests/framework/web/ControllerTest.php +++ b/tests/framework/web/ControllerTest.php @@ -42,7 +42,7 @@ public function testInjectedActionParams() $this->assertEquals($params['before'], $args[0]); $this->assertEquals(\Yii::$app->request, $args[1]); $this->assertEquals($params['between'], $args[2]); - $this->assertInstanceOf(Post::class, $args[3]); + $this->assertInstanceOf(Post::className(), $args[3]); $this->assertEquals($params['after'], $args[4]); } /** From fd78e31cb9c40337888203e7cda3e370384f26bf Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Wed, 6 May 2020 17:04:06 +0200 Subject: [PATCH 04/24] Use different class to test DI injection --- tests/framework/web/ControllerTest.php | 3 ++- tests/framework/web/FakeController.php | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/framework/web/ControllerTest.php b/tests/framework/web/ControllerTest.php index 64f14cb0694..cd3f8b4790c 100644 --- a/tests/framework/web/ControllerTest.php +++ b/tests/framework/web/ControllerTest.php @@ -10,6 +10,7 @@ use Yii; use yii\base\InlineAction; use yii\web\Response; +use yiiunit\framework\web\stubs\VendorImage; use yiiunit\TestCase; /** @@ -42,7 +43,7 @@ public function testInjectedActionParams() $this->assertEquals($params['before'], $args[0]); $this->assertEquals(\Yii::$app->request, $args[1]); $this->assertEquals($params['between'], $args[2]); - $this->assertInstanceOf(Post::className(), $args[3]); + $this->assertInstanceOf(VendorImage::className(), $args[3]); $this->assertEquals($params['after'], $args[4]); } /** diff --git a/tests/framework/web/FakeController.php b/tests/framework/web/FakeController.php index 6a5ca57a81d..61d6a0ed176 100644 --- a/tests/framework/web/FakeController.php +++ b/tests/framework/web/FakeController.php @@ -9,7 +9,7 @@ use yii\web\Controller; use yii\web\Request; -use yii\web\Response; +use yiiunit\framework\web\stubs\VendorImage; /** * @author Misbahul D Munir @@ -23,7 +23,7 @@ public function actionAksi1($fromGet, $other = 'default') { } - public function actionInjection($before, Request $request, Post $post, $between, $after) + public function actionInjection($before, Request $request, VendorImage $vendorImage, $between, $after) { } From 6118917801eecc57b01ff051de8f20e153bbc14f Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Wed, 6 May 2020 17:16:57 +0200 Subject: [PATCH 05/24] Added test for optional and nullable injections. Improved descriptions --- framework/web/Controller.php | 4 +-- tests/framework/web/ControllerTest.php | 35 +++++++++++++++++++++- tests/framework/web/FakeController.php | 2 +- tests/framework/web/FakePhp7Controller.php | 6 ++++ 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/framework/web/Controller.php b/framework/web/Controller.php index a2e813f2a71..74354e1eef1 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -168,10 +168,10 @@ public function bindActionParams($action, $params) $typeName = PHP_VERSION_ID >= 70100 ? $type->getName() : (string) $type; if (($component = $this->module->get($name, false)) instanceof $typeName) { $args[] = $component; - $requestedParams[$name] = "Component: $name"; + $requestedParams[$name] = "Component: " . get_class($component) . " \$$name"; } elseif (($service = \Yii::$container->get($typeName)) instanceof $typeName) { $args[] = $service; - $requestedParams[$name] = "DI: $typeName"; + $requestedParams[$name] = "DI: $typeName \$$name"; } elseif ($type->allowsNull()) { $args[] = null; $requestedParams[$name] = "Unavailable service: $name"; diff --git a/tests/framework/web/ControllerTest.php b/tests/framework/web/ControllerTest.php index cd3f8b4790c..d031bbbd6e2 100644 --- a/tests/framework/web/ControllerTest.php +++ b/tests/framework/web/ControllerTest.php @@ -35,6 +35,35 @@ public function testBindActionParams() $this->assertEquals('avaliable', $other); } + public function testNullableInjectedActionParams() + { + if (PHP_VERSION_ID < 70100) { + $this->markTestSkipped('Can not be tested on PHP < 7.1'); + return; + } + + // Use the PHP7 controller for this test + $this->controller = new FakePhp7Controller('fake', new \yii\web\Application([ + 'id' => 'app', + 'basePath' => __DIR__, + + 'components' => [ + 'request' => [ + 'cookieValidationKey' => 'wefJDF8sfdsfSDefwqdxj9oq', + 'scriptFile' => __DIR__ . '/index.php', + 'scriptUrl' => '/index.php', + ], + ], + ])); + $this->mockWebApplication(['controller' => $this->controller]); + + $injectionAction = new InlineAction('injection', $this->controller, 'actionInjection'); + $params = []; + $args = $this->controller->bindActionParams($injectionAction, $params); + $this->assertEquals(\Yii::$app->request, $args[0]); + $this->assertNull($args[1]); + } + public function testInjectedActionParams() { $injectionAction = new InlineAction('injection', $this->controller, 'actionInjection'); @@ -42,9 +71,13 @@ public function testInjectedActionParams() $args = $this->controller->bindActionParams($injectionAction, $params); $this->assertEquals($params['before'], $args[0]); $this->assertEquals(\Yii::$app->request, $args[1]); + $this->assertEquals('Component: yii\web\request $request', \Yii::$app->requestedParams['request']); $this->assertEquals($params['between'], $args[2]); $this->assertInstanceOf(VendorImage::className(), $args[3]); - $this->assertEquals($params['after'], $args[4]); + $this->assertEquals('DI: yiiunit\framework\web\stubs\VendorImage $vendorImage', \Yii::$app->requestedParams['vendorImage']); + $this->assertNull($args[4]); + $this->assertEquals('Unavailable service: $post', \Yii::$app->requestedParams['post']); + $this->assertEquals($params['after'], $args[5]); } /** * @see https://github.com/yiisoft/yii2/issues/17701 diff --git a/tests/framework/web/FakeController.php b/tests/framework/web/FakeController.php index 61d6a0ed176..d98d53b1266 100644 --- a/tests/framework/web/FakeController.php +++ b/tests/framework/web/FakeController.php @@ -23,7 +23,7 @@ public function actionAksi1($fromGet, $other = 'default') { } - public function actionInjection($before, Request $request, VendorImage $vendorImage, $between, $after) + public function actionInjection($before, Request $request, $between, VendorImage $vendorImage, Post $post = null, $after) { } diff --git a/tests/framework/web/FakePhp7Controller.php b/tests/framework/web/FakePhp7Controller.php index 68a35170788..ee5dabadeed 100644 --- a/tests/framework/web/FakePhp7Controller.php +++ b/tests/framework/web/FakePhp7Controller.php @@ -8,6 +8,8 @@ namespace yiiunit\framework\web; use yii\web\Controller; +use yii\web\Request; +use yiiunit\framework\web\stubs\VendorImage; /** * @author Brandon Kelly @@ -20,4 +22,8 @@ class FakePhp7Controller extends Controller public function actionAksi1(int $foo, float $bar = null, bool $true, bool $false) { } + + public function actionInjection(?Request $request, ?Post $post) + { + } } From 03b7c845ca25745785bb4ff5ef5d2cbc760bcc74 Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Wed, 6 May 2020 17:28:37 +0200 Subject: [PATCH 06/24] Injection using the DI container will now only work for classes with a definition in the container --- framework/web/Controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/web/Controller.php b/framework/web/Controller.php index 74354e1eef1..c4552e475d3 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -169,7 +169,7 @@ public function bindActionParams($action, $params) if (($component = $this->module->get($name, false)) instanceof $typeName) { $args[] = $component; $requestedParams[$name] = "Component: " . get_class($component) . " \$$name"; - } elseif (($service = \Yii::$container->get($typeName)) instanceof $typeName) { + } elseif (\Yii::$container->has($typeName) && ($service = \Yii::$container->get($typeName)) instanceof $typeName) { $args[] = $service; $requestedParams[$name] = "DI: $typeName \$$name"; } elseif ($type->allowsNull()) { From 8279e32fafa3f3994123a1845155bd4a6cd70f30 Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Wed, 6 May 2020 17:46:09 +0200 Subject: [PATCH 07/24] Don't use action injection php < 7 --- framework/web/Controller.php | 2 +- tests/framework/web/ControllerTest.php | 19 +++++++++++++++++++ tests/framework/web/FakeController.php | 5 ----- tests/framework/web/FakePhp7Controller.php | 7 ++++++- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/framework/web/Controller.php b/framework/web/Controller.php index c4552e475d3..e401509dc38 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -163,7 +163,7 @@ public function bindActionParams($action, $params) } $args[] = $actionParams[$name] = $params[$name]; unset($params[$name]); - } elseif (($type = $param->getType()) !== null && !$type->isBuiltin()) { + } elseif (PHP_VERSION_ID >= 70000 && ($type = $param->getType()) !== null && !$type->isBuiltin()) { // Since it is not a builtin type it must be DI injection. $typeName = PHP_VERSION_ID >= 70100 ? $type->getName() : (string) $type; if (($component = $this->module->get($name, false)) instanceof $typeName) { diff --git a/tests/framework/web/ControllerTest.php b/tests/framework/web/ControllerTest.php index d031bbbd6e2..3b1613bf03d 100644 --- a/tests/framework/web/ControllerTest.php +++ b/tests/framework/web/ControllerTest.php @@ -66,6 +66,25 @@ public function testNullableInjectedActionParams() public function testInjectedActionParams() { + if (PHP_VERSION_ID < 70000) { + $this->markTestSkipped('Can not be tested on PHP < 7.0'); + return; + } + // Use the PHP7 controller for this test + $this->controller = new FakePhp7Controller('fake', new \yii\web\Application([ + 'id' => 'app', + 'basePath' => __DIR__, + + 'components' => [ + 'request' => [ + 'cookieValidationKey' => 'wefJDF8sfdsfSDefwqdxj9oq', + 'scriptFile' => __DIR__ . '/index.php', + 'scriptUrl' => '/index.php', + ], + ], + ])); + $this->mockWebApplication(['controller' => $this->controller]); + $injectionAction = new InlineAction('injection', $this->controller, 'actionInjection'); $params = ['between' => 'test', 'after' => 'another', 'before' => 'test']; $args = $this->controller->bindActionParams($injectionAction, $params); diff --git a/tests/framework/web/FakeController.php b/tests/framework/web/FakeController.php index d98d53b1266..7b7d1a30be9 100644 --- a/tests/framework/web/FakeController.php +++ b/tests/framework/web/FakeController.php @@ -22,9 +22,4 @@ class FakeController extends Controller public function actionAksi1($fromGet, $other = 'default') { } - - public function actionInjection($before, Request $request, $between, VendorImage $vendorImage, Post $post = null, $after) - { - - } } diff --git a/tests/framework/web/FakePhp7Controller.php b/tests/framework/web/FakePhp7Controller.php index ee5dabadeed..a5939963ea5 100644 --- a/tests/framework/web/FakePhp7Controller.php +++ b/tests/framework/web/FakePhp7Controller.php @@ -23,7 +23,12 @@ public function actionAksi1(int $foo, float $bar = null, bool $true, bool $false { } - public function actionInjection(?Request $request, ?Post $post) + public function actionInjection($before, Request $request, $between, VendorImage $vendorImage, Post $post = null, $after) + { + + } + + public function actionNullableInjection(?Request $request, ?Post $post) { } } From 4734e7e083d952651d649083e9373ae75ee510b4 Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Wed, 6 May 2020 18:03:35 +0200 Subject: [PATCH 08/24] Disable action injection for php7.0 as well --- framework/web/Controller.php | 4 ++-- tests/framework/web/ControllerTest.php | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/framework/web/Controller.php b/framework/web/Controller.php index e401509dc38..dcd6b7639fe 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -163,9 +163,9 @@ public function bindActionParams($action, $params) } $args[] = $actionParams[$name] = $params[$name]; unset($params[$name]); - } elseif (PHP_VERSION_ID >= 70000 && ($type = $param->getType()) !== null && !$type->isBuiltin()) { + } elseif (PHP_VERSION_ID >= 70100 && ($type = $param->getType()) !== null && !$type->isBuiltin()) { // Since it is not a builtin type it must be DI injection. - $typeName = PHP_VERSION_ID >= 70100 ? $type->getName() : (string) $type; + $typeName = $type->getName(); if (($component = $this->module->get($name, false)) instanceof $typeName) { $args[] = $component; $requestedParams[$name] = "Component: " . get_class($component) . " \$$name"; diff --git a/tests/framework/web/ControllerTest.php b/tests/framework/web/ControllerTest.php index 3b1613bf03d..9617b27338d 100644 --- a/tests/framework/web/ControllerTest.php +++ b/tests/framework/web/ControllerTest.php @@ -57,7 +57,7 @@ public function testNullableInjectedActionParams() ])); $this->mockWebApplication(['controller' => $this->controller]); - $injectionAction = new InlineAction('injection', $this->controller, 'actionInjection'); + $injectionAction = new InlineAction('injection', $this->controller, 'actionNullableInjection'); $params = []; $args = $this->controller->bindActionParams($injectionAction, $params); $this->assertEquals(\Yii::$app->request, $args[0]); @@ -66,8 +66,8 @@ public function testNullableInjectedActionParams() public function testInjectedActionParams() { - if (PHP_VERSION_ID < 70000) { - $this->markTestSkipped('Can not be tested on PHP < 7.0'); + if (PHP_VERSION_ID < 70100) { + $this->markTestSkipped('Can not be tested on PHP < 7.1'); return; } // Use the PHP7 controller for this test From 3f1f4726e6a555b2c03087c2127b142d655613b3 Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Wed, 6 May 2020 18:27:26 +0200 Subject: [PATCH 09/24] Introduce php7.1 test controller --- tests/framework/web/ControllerTest.php | 8 +++--- tests/framework/web/FakePhp71Controller.php | 30 +++++++++++++++++++++ tests/framework/web/FakePhp7Controller.php | 11 -------- 3 files changed, 34 insertions(+), 15 deletions(-) create mode 100644 tests/framework/web/FakePhp71Controller.php diff --git a/tests/framework/web/ControllerTest.php b/tests/framework/web/ControllerTest.php index 9617b27338d..1302de94263 100644 --- a/tests/framework/web/ControllerTest.php +++ b/tests/framework/web/ControllerTest.php @@ -42,8 +42,8 @@ public function testNullableInjectedActionParams() return; } - // Use the PHP7 controller for this test - $this->controller = new FakePhp7Controller('fake', new \yii\web\Application([ + // Use the PHP71 controller for this test + $this->controller = new FakePhp71Controller('fake', new \yii\web\Application([ 'id' => 'app', 'basePath' => __DIR__, @@ -70,8 +70,8 @@ public function testInjectedActionParams() $this->markTestSkipped('Can not be tested on PHP < 7.1'); return; } - // Use the PHP7 controller for this test - $this->controller = new FakePhp7Controller('fake', new \yii\web\Application([ + // Use the PHP71 controller for this test + $this->controller = new FakePhp71Controller('fake', new \yii\web\Application([ 'id' => 'app', 'basePath' => __DIR__, diff --git a/tests/framework/web/FakePhp71Controller.php b/tests/framework/web/FakePhp71Controller.php new file mode 100644 index 00000000000..a46f6a710eb --- /dev/null +++ b/tests/framework/web/FakePhp71Controller.php @@ -0,0 +1,30 @@ + + * @since 2.0.35 + */ +class FakePhp71Controller extends Controller +{ + public $enableCsrfValidation = false; + + public function actionInjection($before, Request $request, $between, VendorImage $vendorImage, Post $post = null, $after) + { + + } + + public function actionNullableInjection(?Request $request, ?Post $post) + { + } +} diff --git a/tests/framework/web/FakePhp7Controller.php b/tests/framework/web/FakePhp7Controller.php index a5939963ea5..68a35170788 100644 --- a/tests/framework/web/FakePhp7Controller.php +++ b/tests/framework/web/FakePhp7Controller.php @@ -8,8 +8,6 @@ namespace yiiunit\framework\web; use yii\web\Controller; -use yii\web\Request; -use yiiunit\framework\web\stubs\VendorImage; /** * @author Brandon Kelly @@ -22,13 +20,4 @@ class FakePhp7Controller extends Controller public function actionAksi1(int $foo, float $bar = null, bool $true, bool $false) { } - - public function actionInjection($before, Request $request, $between, VendorImage $vendorImage, Post $post = null, $after) - { - - } - - public function actionNullableInjection(?Request $request, ?Post $post) - { - } } From 208276ca3c4acb267259e4abcd6e55cbf0ce3e4f Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Wed, 6 May 2020 18:36:51 +0200 Subject: [PATCH 10/24] Fixed assertion --- tests/framework/web/ControllerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/framework/web/ControllerTest.php b/tests/framework/web/ControllerTest.php index 1302de94263..640624c9dbc 100644 --- a/tests/framework/web/ControllerTest.php +++ b/tests/framework/web/ControllerTest.php @@ -90,7 +90,7 @@ public function testInjectedActionParams() $args = $this->controller->bindActionParams($injectionAction, $params); $this->assertEquals($params['before'], $args[0]); $this->assertEquals(\Yii::$app->request, $args[1]); - $this->assertEquals('Component: yii\web\request $request', \Yii::$app->requestedParams['request']); + $this->assertEquals('Component: yii\web\Request $request', \Yii::$app->requestedParams['request']); $this->assertEquals($params['between'], $args[2]); $this->assertInstanceOf(VendorImage::className(), $args[3]); $this->assertEquals('DI: yiiunit\framework\web\stubs\VendorImage $vendorImage', \Yii::$app->requestedParams['vendorImage']); From 44bbbefafb0ca3875b2b8a3eae46758e7155e396 Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Thu, 7 May 2020 09:41:02 +0200 Subject: [PATCH 11/24] Added more tests for unknown / missing DI definitions --- framework/web/Controller.php | 2 + tests/framework/web/ControllerTest.php | 61 ++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/framework/web/Controller.php b/framework/web/Controller.php index dcd6b7639fe..8f020cc33f1 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -175,6 +175,8 @@ public function bindActionParams($action, $params) } elseif ($type->allowsNull()) { $args[] = null; $requestedParams[$name] = "Unavailable service: $name"; + } else { + throw new ServerErrorHttpException('Could not load required service: ' . $name); } } elseif ($param->isDefaultValueAvailable()) { $args[] = $actionParams[$name] = $param->getDefaultValue(); diff --git a/tests/framework/web/ControllerTest.php b/tests/framework/web/ControllerTest.php index 640624c9dbc..a894fb1df4d 100644 --- a/tests/framework/web/ControllerTest.php +++ b/tests/framework/web/ControllerTest.php @@ -10,6 +10,7 @@ use Yii; use yii\base\InlineAction; use yii\web\Response; +use yii\web\ServerErrorHttpException; use yiiunit\framework\web\stubs\VendorImage; use yiiunit\TestCase; @@ -64,6 +65,65 @@ public function testNullableInjectedActionParams() $this->assertNull($args[1]); } + public function testInjectionContainerException() + { + if (PHP_VERSION_ID < 70100) { + $this->markTestSkipped('Can not be tested on PHP < 7.1'); + return; + } + // Use the PHP71 controller for this test + $this->controller = new FakePhp71Controller('fake', new \yii\web\Application([ + 'id' => 'app', + 'basePath' => __DIR__, + + 'components' => [ + 'request' => [ + 'cookieValidationKey' => 'wefJDF8sfdsfSDefwqdxj9oq', + 'scriptFile' => __DIR__ . '/index.php', + 'scriptUrl' => '/index.php', + ], + ], + ])); + $this->mockWebApplication(['controller' => $this->controller]); + + $injectionAction = new InlineAction('injection', $this->controller, 'actionInjection'); + $params = ['between' => 'test', 'after' => 'another', 'before' => 'test']; + \Yii::$container->set(VendorImage::className(), function() { throw new \RuntimeException('uh oh'); }); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('uh oh'); + $this->controller->bindActionParams($injectionAction, $params); + } + + public function testUnknownInjection() + { + if (PHP_VERSION_ID < 70100) { + $this->markTestSkipped('Can not be tested on PHP < 7.1'); + return; + } + // Use the PHP71 controller for this test + $this->controller = new FakePhp71Controller('fake', new \yii\web\Application([ + 'id' => 'app', + 'basePath' => __DIR__, + + 'components' => [ + 'request' => [ + 'cookieValidationKey' => 'wefJDF8sfdsfSDefwqdxj9oq', + 'scriptFile' => __DIR__ . '/index.php', + 'scriptUrl' => '/index.php', + ], + ], + ])); + $this->mockWebApplication(['controller' => $this->controller]); + + $injectionAction = new InlineAction('injection', $this->controller, 'actionInjection'); + $params = ['between' => 'test', 'after' => 'another', 'before' => 'test']; + \Yii::$container->clear(VendorImage::className()); + $this->expectException(ServerErrorHttpException::class); + $this->expectExceptionMessage('Could not load required service: vendorImage'); + $this->controller->bindActionParams($injectionAction, $params); + } + public function testInjectedActionParams() { if (PHP_VERSION_ID < 70100) { @@ -87,6 +147,7 @@ public function testInjectedActionParams() $injectionAction = new InlineAction('injection', $this->controller, 'actionInjection'); $params = ['between' => 'test', 'after' => 'another', 'before' => 'test']; + \Yii::$container->set(VendorImage::className(), VendorImage::className()); $args = $this->controller->bindActionParams($injectionAction, $params); $this->assertEquals($params['before'], $args[0]); $this->assertEquals(\Yii::$app->request, $args[1]); From 6a361af590302e360a028fde6ed7b030f1a31aa9 Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Thu, 7 May 2020 10:16:39 +0200 Subject: [PATCH 12/24] Fixed tests --- tests/framework/web/ControllerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/framework/web/ControllerTest.php b/tests/framework/web/ControllerTest.php index a894fb1df4d..b93704f2b26 100644 --- a/tests/framework/web/ControllerTest.php +++ b/tests/framework/web/ControllerTest.php @@ -156,7 +156,7 @@ public function testInjectedActionParams() $this->assertInstanceOf(VendorImage::className(), $args[3]); $this->assertEquals('DI: yiiunit\framework\web\stubs\VendorImage $vendorImage', \Yii::$app->requestedParams['vendorImage']); $this->assertNull($args[4]); - $this->assertEquals('Unavailable service: $post', \Yii::$app->requestedParams['post']); + $this->assertEquals('Unavailable service: post', \Yii::$app->requestedParams['post']); $this->assertEquals($params['after'], $args[5]); } /** From a8a81b705c88736ce1c8aa93a3f09f4d08d112bd Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Thu, 7 May 2020 10:25:47 +0200 Subject: [PATCH 13/24] Fixed php 5.4 syntax error --- tests/framework/web/ControllerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/framework/web/ControllerTest.php b/tests/framework/web/ControllerTest.php index b93704f2b26..fc1e4217e78 100644 --- a/tests/framework/web/ControllerTest.php +++ b/tests/framework/web/ControllerTest.php @@ -90,7 +90,7 @@ public function testInjectionContainerException() $params = ['between' => 'test', 'after' => 'another', 'before' => 'test']; \Yii::$container->set(VendorImage::className(), function() { throw new \RuntimeException('uh oh'); }); - $this->expectException(\RuntimeException::class); + $this->expectException('\RuntimeException'); $this->expectExceptionMessage('uh oh'); $this->controller->bindActionParams($injectionAction, $params); } From 3fa769c035a309d01d2b48ff563c8e1e1acc974e Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Thu, 7 May 2020 10:37:26 +0200 Subject: [PATCH 14/24] Another 5.4 fix --- tests/framework/web/ControllerTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/framework/web/ControllerTest.php b/tests/framework/web/ControllerTest.php index fc1e4217e78..3e7cfa64271 100644 --- a/tests/framework/web/ControllerTest.php +++ b/tests/framework/web/ControllerTest.php @@ -7,6 +7,7 @@ namespace yiiunit\framework\web; +use RuntimeException; use Yii; use yii\base\InlineAction; use yii\web\Response; @@ -90,7 +91,7 @@ public function testInjectionContainerException() $params = ['between' => 'test', 'after' => 'another', 'before' => 'test']; \Yii::$container->set(VendorImage::className(), function() { throw new \RuntimeException('uh oh'); }); - $this->expectException('\RuntimeException'); + $this->expectException(get_class(new RuntimeException())); $this->expectExceptionMessage('uh oh'); $this->controller->bindActionParams($injectionAction, $params); } @@ -119,7 +120,7 @@ public function testUnknownInjection() $injectionAction = new InlineAction('injection', $this->controller, 'actionInjection'); $params = ['between' => 'test', 'after' => 'another', 'before' => 'test']; \Yii::$container->clear(VendorImage::className()); - $this->expectException(ServerErrorHttpException::class); + $this->expectException(get_class(new ServerErrorHttpException())); $this->expectExceptionMessage('Could not load required service: vendorImage'); $this->controller->bindActionParams($injectionAction, $params); } From f359ef9c1c61b500c8ddf38f601e387e8dbe13e6 Mon Sep 17 00:00:00 2001 From: Sam Mousa Date: Thu, 7 May 2020 12:59:49 +0200 Subject: [PATCH 15/24] Refactor to reduce npath complexity --- framework/web/Controller.php | 44 ++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/framework/web/Controller.php b/framework/web/Controller.php index 8f020cc33f1..dc938c53d38 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -103,6 +103,35 @@ public function asXml($data) return $response; } + /** + * @param \ReflectionType $type The reflected type of the action parameter + * @param string $name The name of the parameter + * @param array &$args The array of arguments for the action, this function may append items to it + * @param array &$requestedParams The array with reqested params, this function may write specific keys to it + * @throws ServerErrorHttpException Thrown when we cannot load a required service + * @throws \yii\base\InvalidConfigException Thrown when there is an error in the DI configuration + * @throws \yii\di\NotInstantiableException Thrown when a definition cannot be resolved to a concrete class + * (for example an interface type hint) without a proper definition in the container. + * @since 2.0.35 + */ + private function bindInjectedParams(\ReflectionType $type, $name, &$args, &$requestedParams) + { + // Since it is not a builtin type it must be DI injection. + $typeName = $type->getName(); + if (($component = $this->module->get($name, false)) instanceof $typeName) { + $args[] = $component; + $requestedParams[$name] = "Component: " . get_class($component) . " \$$name"; + } elseif (\Yii::$container->has($typeName) && ($service = \Yii::$container->get($typeName)) instanceof $typeName) { + $args[] = $service; + $requestedParams[$name] = "DI: $typeName \$$name"; + } elseif ($type->allowsNull()) { + $args[] = null; + $requestedParams[$name] = "Unavailable service: $name"; + } else { + throw new ServerErrorHttpException('Could not load required service: ' . $name); + } + } + /** * Binds the parameters to the action. * This method is invoked by [[\yii\base\Action]] when it begins to run with the given parameters. @@ -164,20 +193,7 @@ public function bindActionParams($action, $params) $args[] = $actionParams[$name] = $params[$name]; unset($params[$name]); } elseif (PHP_VERSION_ID >= 70100 && ($type = $param->getType()) !== null && !$type->isBuiltin()) { - // Since it is not a builtin type it must be DI injection. - $typeName = $type->getName(); - if (($component = $this->module->get($name, false)) instanceof $typeName) { - $args[] = $component; - $requestedParams[$name] = "Component: " . get_class($component) . " \$$name"; - } elseif (\Yii::$container->has($typeName) && ($service = \Yii::$container->get($typeName)) instanceof $typeName) { - $args[] = $service; - $requestedParams[$name] = "DI: $typeName \$$name"; - } elseif ($type->allowsNull()) { - $args[] = null; - $requestedParams[$name] = "Unavailable service: $name"; - } else { - throw new ServerErrorHttpException('Could not load required service: ' . $name); - } + $this->bindInjectedParams($type, $name, $args, $requestedParams); } elseif ($param->isDefaultValueAvailable()) { $args[] = $actionParams[$name] = $param->getDefaultValue(); } else { From f2df145bb81772f36191c27879197630ece0006c Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sun, 31 May 2020 14:56:32 +0300 Subject: [PATCH 16/24] Update version, remove unused imports --- framework/web/Controller.php | 2 +- tests/framework/web/FakeController.php | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/framework/web/Controller.php b/framework/web/Controller.php index dc938c53d38..15f1dc1e8a4 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -112,7 +112,7 @@ public function asXml($data) * @throws \yii\base\InvalidConfigException Thrown when there is an error in the DI configuration * @throws \yii\di\NotInstantiableException Thrown when a definition cannot be resolved to a concrete class * (for example an interface type hint) without a proper definition in the container. - * @since 2.0.35 + * @since 2.0.36 */ private function bindInjectedParams(\ReflectionType $type, $name, &$args, &$requestedParams) { diff --git a/tests/framework/web/FakeController.php b/tests/framework/web/FakeController.php index 7b7d1a30be9..572d169e5b9 100644 --- a/tests/framework/web/FakeController.php +++ b/tests/framework/web/FakeController.php @@ -8,8 +8,6 @@ namespace yiiunit\framework\web; use yii\web\Controller; -use yii\web\Request; -use yiiunit\framework\web\stubs\VendorImage; /** * @author Misbahul D Munir From 16d5ee36e83a10f1eaea6d75c2cc8027c030db53 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sun, 31 May 2020 14:57:06 +0300 Subject: [PATCH 17/24] Update version --- tests/framework/web/FakePhp71Controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/framework/web/FakePhp71Controller.php b/tests/framework/web/FakePhp71Controller.php index a46f6a710eb..9c67256f520 100644 --- a/tests/framework/web/FakePhp71Controller.php +++ b/tests/framework/web/FakePhp71Controller.php @@ -13,7 +13,7 @@ /** * @author Sam Mousa - * @since 2.0.35 + * @since 2.0.36 */ class FakePhp71Controller extends Controller { From 76d0d03ef78e9b8ace3e5a01599ad352510f780c Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sun, 31 May 2020 15:40:10 +0300 Subject: [PATCH 18/24] Fix phpdoc --- framework/web/Controller.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/framework/web/Controller.php b/framework/web/Controller.php index 15f1dc1e8a4..160eb0edea5 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -104,12 +104,13 @@ public function asXml($data) } /** - * @param \ReflectionType $type The reflected type of the action parameter - * @param string $name The name of the parameter - * @param array &$args The array of arguments for the action, this function may append items to it - * @param array &$requestedParams The array with reqested params, this function may write specific keys to it - * @throws ServerErrorHttpException Thrown when we cannot load a required service - * @throws \yii\base\InvalidConfigException Thrown when there is an error in the DI configuration + * Fills parameters based on types and names in action method signature. + * @param \ReflectionType $type The reflected type of the action parameter. + * @param string $name The name of the parameter. + * @param array &$args The array of arguments for the action, this function may append items to it. + * @param array &$requestedParams The array with requested params, this function may write specific keys to it. + * @throws ServerErrorHttpException Thrown when we cannot load a required service. + * @throws \yii\base\InvalidConfigException Thrown when there is an error in the DI configuration. * @throws \yii\di\NotInstantiableException Thrown when a definition cannot be resolved to a concrete class * (for example an interface type hint) without a proper definition in the container. * @since 2.0.36 From cc2c2459648bbd6a4e4f7e5cbb44718c86887a41 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sun, 31 May 2020 15:44:22 +0300 Subject: [PATCH 19/24] Add info about the feature to the guide --- docs/guide/concept-di-container.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/guide/concept-di-container.md b/docs/guide/concept-di-container.md index aa05bb83339..3e59468b75b 100644 --- a/docs/guide/concept-di-container.md +++ b/docs/guide/concept-di-container.md @@ -375,6 +375,24 @@ cannot be instantiated. This is because you need to tell the DI container how to Now if you access the controller again, an instance of `app\components\BookingService` will be created and injected as the 3rd parameter to the controller's constructor. +Since Yii 2.0.36 when using PHP 7 action injection is available: + +```php +namespace app\controllers; + +use yii\web\Controller; +use app\components\BookingInterface; + +class HotelController extends Controller +{ + public function actionBook($id, BookingInterface $bookingService) + { + $result = $bookingService->book($id); + // ... + } +} +``` + Advanced Practical Usage --------------- From e0737d93828a4a05e88067f7665bafcdcea407be Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sun, 31 May 2020 16:04:16 +0300 Subject: [PATCH 20/24] Add CHANGELOG --- framework/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index e4b4008d89b..2895aee521e 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -9,6 +9,7 @@ Yii Framework 2 Change Log - Bug #18026: Fix `ArrayHelper::getValue()` did not work with `ArrayAccess` objects (mikk150) - Enh #18048: Use `Instance::ensure()` to set `User::$accessChecker` (lav45) - Bug #18051: Fix missing support for custom validation method in EachValidator (bizley) +- Enh #17722: Add action injection support (SamMousa) 2.0.35 May 02, 2020 From 5f6a782a34851d19961645ceaa8f11a229c00ee5 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 10 Jun 2020 16:21:19 +0300 Subject: [PATCH 21/24] Implement console action injection --- framework/base/Controller.php | 30 +++++++++++++++++++++++++ framework/console/Controller.php | 34 +++++++++++++++++++--------- framework/web/Controller.php | 38 ++++++-------------------------- 3 files changed, 61 insertions(+), 41 deletions(-) diff --git a/framework/base/Controller.php b/framework/base/Controller.php index 9921cfec6ee..d934810d2f9 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -523,4 +523,34 @@ public function findLayoutFile($view) return $path; } + + /** + * Fills parameters based on types and names in action method signature. + * @param \ReflectionType $type The reflected type of the action parameter. + * @param string $name The name of the parameter. + * @param array &$args The array of arguments for the action, this function may append items to it. + * @param array &$requestedParams The array with requested params, this function may write specific keys to it. + * @throws ErrorException when we cannot load a required service. + * @throws \yii\base\InvalidConfigException Thrown when there is an error in the DI configuration. + * @throws \yii\di\NotInstantiableException Thrown when a definition cannot be resolved to a concrete class + * (for example an interface type hint) without a proper definition in the container. + * @since 2.0.36 + */ + final protected function bindInjectedParams(\ReflectionType $type, $name, &$args, &$requestedParams) + { + // Since it is not a builtin type it must be DI injection. + $typeName = $type->getName(); + if (($component = $this->module->get($name, false)) instanceof $typeName) { + $args[] = $component; + $requestedParams[$name] = "Component: " . get_class($component) . " \$$name"; + } elseif (\Yii::$container->has($typeName) && ($service = \Yii::$container->get($typeName)) instanceof $typeName) { + $args[] = $service; + $requestedParams[$name] = "DI: $typeName \$$name"; + } elseif ($type->allowsNull()) { + $args[] = null; + $requestedParams[$name] = "Unavailable service: $name"; + } else { + throw new Exception('Could not load required service: ' . $name); + } + } } diff --git a/framework/console/Controller.php b/framework/console/Controller.php index 7bacd62bd9c..340e0ab0149 100644 --- a/framework/console/Controller.php +++ b/framework/console/Controller.php @@ -182,19 +182,28 @@ public function bindActionParams($action, $params) $method = new \ReflectionMethod($action, 'run'); } - $args = array_values($params); - + $args = []; $missing = []; + $actionParams = []; + $requestedParams = []; foreach ($method->getParameters() as $i => $param) { - if ($param->isArray() && isset($args[$i])) { - $args[$i] = $args[$i] === '' ? [] : preg_split('/\s*,\s*/', $args[$i]); - } - if (!isset($args[$i])) { - if ($param->isDefaultValueAvailable()) { - $args[$i] = $param->getDefaultValue(); - } else { - $missing[] = $param->getName(); + $name = $param->getName(); + if (array_key_exists($i, $params)) { + if ($param->isArray()) { + $params[$i] = $params[$i] === '' ? [] : preg_split('/\s*,\s*/', $params[$i]); } + $args[] = $actionParams[$i] = $params[$i]; + unset($params[$i]); + } elseif (PHP_VERSION_ID >= 70100 && ($type = $param->getType()) !== null && !$type->isBuiltin()) { + try { + $this->bindInjectedParams($type, $name, $args, $requestedParams); + } catch (\yii\base\Exception $e) { + throw new Exception($e->getMessage()); + } + } elseif ($param->isDefaultValueAvailable()) { + $args[] = $actionParams[$i] = $param->getDefaultValue(); + } else { + $missing[] = $name; } } @@ -202,6 +211,11 @@ public function bindActionParams($action, $params) throw new Exception(Yii::t('yii', 'Missing required arguments: {params}', ['params' => implode(', ', $missing)])); } + // We use a different array here, specifically one that doesn't contain service instances but descriptions instead. + if (\Yii::$app->requestedParams === null) { + \Yii::$app->requestedParams = array_merge($actionParams, $requestedParams); + } + return $args; } diff --git a/framework/web/Controller.php b/framework/web/Controller.php index 160eb0edea5..0175f0f5e5e 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -8,6 +8,8 @@ namespace yii\web; use Yii; +use yii\base\ErrorException; +use yii\base\Exception; use yii\base\InlineAction; use yii\helpers\Url; @@ -103,36 +105,6 @@ public function asXml($data) return $response; } - /** - * Fills parameters based on types and names in action method signature. - * @param \ReflectionType $type The reflected type of the action parameter. - * @param string $name The name of the parameter. - * @param array &$args The array of arguments for the action, this function may append items to it. - * @param array &$requestedParams The array with requested params, this function may write specific keys to it. - * @throws ServerErrorHttpException Thrown when we cannot load a required service. - * @throws \yii\base\InvalidConfigException Thrown when there is an error in the DI configuration. - * @throws \yii\di\NotInstantiableException Thrown when a definition cannot be resolved to a concrete class - * (for example an interface type hint) without a proper definition in the container. - * @since 2.0.36 - */ - private function bindInjectedParams(\ReflectionType $type, $name, &$args, &$requestedParams) - { - // Since it is not a builtin type it must be DI injection. - $typeName = $type->getName(); - if (($component = $this->module->get($name, false)) instanceof $typeName) { - $args[] = $component; - $requestedParams[$name] = "Component: " . get_class($component) . " \$$name"; - } elseif (\Yii::$container->has($typeName) && ($service = \Yii::$container->get($typeName)) instanceof $typeName) { - $args[] = $service; - $requestedParams[$name] = "DI: $typeName \$$name"; - } elseif ($type->allowsNull()) { - $args[] = null; - $requestedParams[$name] = "Unavailable service: $name"; - } else { - throw new ServerErrorHttpException('Could not load required service: ' . $name); - } - } - /** * Binds the parameters to the action. * This method is invoked by [[\yii\base\Action]] when it begins to run with the given parameters. @@ -194,7 +166,11 @@ public function bindActionParams($action, $params) $args[] = $actionParams[$name] = $params[$name]; unset($params[$name]); } elseif (PHP_VERSION_ID >= 70100 && ($type = $param->getType()) !== null && !$type->isBuiltin()) { - $this->bindInjectedParams($type, $name, $args, $requestedParams); + try { + $this->bindInjectedParams($type, $name, $args, $requestedParams); + } catch (Exception $e) { + throw new ServerErrorHttpException($e->getMessage(), 0, $e); + } } elseif ($param->isDefaultValueAvailable()) { $args[] = $actionParams[$name] = $param->getDefaultValue(); } else { From cff428cee1592366f143d58ccc42a3bafc5d8575 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 10 Jun 2020 17:13:58 +0300 Subject: [PATCH 22/24] Add tests for console controller, implement named params binding --- framework/console/Controller.php | 13 ++- tests/framework/console/ControllerTest.php | 100 ++++++++++++++++++ .../framework/console/FakePhp71Controller.php | 24 +++++ .../framework/console/stubs/DummyService.php | 16 +++ 4 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 tests/framework/console/FakePhp71Controller.php create mode 100644 tests/framework/console/stubs/DummyService.php diff --git a/framework/console/Controller.php b/framework/console/Controller.php index 340e0ab0149..aa5021aeb76 100644 --- a/framework/console/Controller.php +++ b/framework/console/Controller.php @@ -188,12 +188,19 @@ public function bindActionParams($action, $params) $requestedParams = []; foreach ($method->getParameters() as $i => $param) { $name = $param->getName(); + $key = null; if (array_key_exists($i, $params)) { + $key = $i; + } elseif (array_key_exists($name, $params)) { + $key = $name; + } + + if ($key !== null) { if ($param->isArray()) { - $params[$i] = $params[$i] === '' ? [] : preg_split('/\s*,\s*/', $params[$i]); + $params[$key] = $params[$key] === '' ? [] : preg_split('/\s*,\s*/', $params[$key]); } - $args[] = $actionParams[$i] = $params[$i]; - unset($params[$i]); + $args[] = $actionParams[$key] = $params[$key]; + unset($params[$key]); } elseif (PHP_VERSION_ID >= 70100 && ($type = $param->getType()) !== null && !$type->isBuiltin()) { try { $this->bindInjectedParams($type, $name, $args, $requestedParams); diff --git a/tests/framework/console/ControllerTest.php b/tests/framework/console/ControllerTest.php index 6220a5357c4..01092790189 100644 --- a/tests/framework/console/ControllerTest.php +++ b/tests/framework/console/ControllerTest.php @@ -7,8 +7,13 @@ namespace yiiunit\framework\console; +use RuntimeException; +use yii\console\Exception; +use yiiunit\framework\console\stubs\DummyService; use Yii; +use yii\base\InlineAction; use yii\base\Module; +use yii\console\Application; use yii\console\Request; use yii\helpers\Console; use yiiunit\TestCase; @@ -18,6 +23,9 @@ */ class ControllerTest extends TestCase { + /** @var FakeController */ + private $controller; + protected function setUp() { parent::setUp(); @@ -76,6 +84,98 @@ public function testBindActionParams() $result = $controller->runAction('aksi3', $params); } + public function testNullableInjectedActionParams() + { + if (PHP_VERSION_ID < 70100) { + $this->markTestSkipped('Can not be tested on PHP < 7.1'); + return; + } + + // Use the PHP71 controller for this test + $this->controller = new FakePhp71Controller('fake', new Application([ + 'id' => 'app', + 'basePath' => __DIR__, + ])); + $this->mockApplication(['controller' => $this->controller]); + + $injectionAction = new InlineAction('injection', $this->controller, 'actionNullableInjection'); + $params = []; + $args = $this->controller->bindActionParams($injectionAction, $params); + $this->assertEquals(\Yii::$app->request, $args[0]); + $this->assertNull($args[1]); + } + + public function testInjectionContainerException() + { + if (PHP_VERSION_ID < 70100) { + $this->markTestSkipped('Can not be tested on PHP < 7.1'); + return; + } + // Use the PHP71 controller for this test + $this->controller = new FakePhp71Controller('fake', new Application([ + 'id' => 'app', + 'basePath' => __DIR__, + ])); + $this->mockApplication(['controller' => $this->controller]); + + $injectionAction = new InlineAction('injection', $this->controller, 'actionInjection'); + $params = ['between' => 'test', 'after' => 'another', 'before' => 'test']; + \Yii::$container->set(DummyService::className(), function() { throw new \RuntimeException('uh oh'); }); + + $this->expectException(get_class(new RuntimeException())); + $this->expectExceptionMessage('uh oh'); + $this->controller->bindActionParams($injectionAction, $params); + } + + public function testUnknownInjection() + { + if (PHP_VERSION_ID < 70100) { + $this->markTestSkipped('Can not be tested on PHP < 7.1'); + return; + } + // Use the PHP71 controller for this test + $this->controller = new FakePhp71Controller('fake', new Application([ + 'id' => 'app', + 'basePath' => __DIR__, + ])); + $this->mockApplication(['controller' => $this->controller]); + + $injectionAction = new InlineAction('injection', $this->controller, 'actionInjection'); + $params = ['between' => 'test', 'after' => 'another', 'before' => 'test']; + \Yii::$container->clear(DummyService::className()); + $this->expectException(get_class(new Exception())); + $this->expectExceptionMessage('Could not load required service: dummyService'); + $this->controller->bindActionParams($injectionAction, $params); + } + + public function testInjectedActionParams() + { + if (PHP_VERSION_ID < 70100) { + $this->markTestSkipped('Can not be tested on PHP < 7.1'); + return; + } + // Use the PHP71 controller for this test + $this->controller = new FakePhp71Controller('fake', new Application([ + 'id' => 'app', + 'basePath' => __DIR__, + ])); + $this->mockApplication(['controller' => $this->controller]); + + $injectionAction = new InlineAction('injection', $this->controller, 'actionInjection'); + $params = ['between' => 'test', 'after' => 'another', 'before' => 'test']; + \Yii::$container->set(DummyService::className(), DummyService::className()); + $args = $this->controller->bindActionParams($injectionAction, $params); + $this->assertEquals($params['before'], $args[0]); + $this->assertEquals(\Yii::$app->request, $args[1]); + $this->assertEquals('Component: yii\console\Request $request', \Yii::$app->requestedParams['request']); + $this->assertEquals($params['between'], $args[2]); + $this->assertInstanceOf(DummyService::className(), $args[3]); + $this->assertEquals('DI: yiiunit\framework\console\stubs\DummyService $dummyService', \Yii::$app->requestedParams['dummyService']); + $this->assertNull($args[4]); + $this->assertEquals('Unavailable service: post', \Yii::$app->requestedParams['post']); + $this->assertEquals($params['after'], $args[5]); + } + public function assertResponseStatus($status, $response) { $this->assertInstanceOf('yii\console\Response', $response); diff --git a/tests/framework/console/FakePhp71Controller.php b/tests/framework/console/FakePhp71Controller.php new file mode 100644 index 00000000000..9b5ff601f76 --- /dev/null +++ b/tests/framework/console/FakePhp71Controller.php @@ -0,0 +1,24 @@ + Date: Wed, 10 Jun 2020 17:15:47 +0300 Subject: [PATCH 23/24] Mention console controllers in docs --- docs/guide/concept-di-container.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/concept-di-container.md b/docs/guide/concept-di-container.md index 3e59468b75b..cbb050a4d2e 100644 --- a/docs/guide/concept-di-container.md +++ b/docs/guide/concept-di-container.md @@ -375,7 +375,7 @@ cannot be instantiated. This is because you need to tell the DI container how to Now if you access the controller again, an instance of `app\components\BookingService` will be created and injected as the 3rd parameter to the controller's constructor. -Since Yii 2.0.36 when using PHP 7 action injection is available: +Since Yii 2.0.36 when using PHP 7 action injection is available for both web and console controllers: ```php namespace app\controllers; From 5bb82f4c35b95babff2c858439afd2e47eea1c15 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 12 Jun 2020 10:05:51 +0300 Subject: [PATCH 24/24] Update CHANGELOG.md --- framework/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 1853f325e8e..a6fc510d8f8 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -9,7 +9,7 @@ Yii Framework 2 Change Log - Bug #18026: Fix `ArrayHelper::getValue()` did not work with `ArrayAccess` objects (mikk150) - Enh #18048: Use `Instance::ensure()` to set `User::$accessChecker` (lav45) - Bug #18051: Fix missing support for custom validation method in EachValidator (bizley) -- Enh #17722: Add action injection support (SamMousa) +- Enh #17722: Add action injection support (SamMousa, samdark) - Bug #18041: Fix RBAC migration for MSSQL (darkdef) - Bug #18081: Fix for PDO_DBLIB/MSSQL. Set flag ANSI_NULL_DFLT_ON to ON for current connect to DB (darkdef) - Bug #13828: Fix retrieving inserted data for a primary key of type uniqueidentifier for SQL Server 2005 or later (darkdef)