Fix #17722: Add action injection support

This commit is contained in:
Sam
2020-06-12 09:06:18 +02:00
committed by GitHub
parent c365f472cd
commit 4ea484ca68
10 changed files with 391 additions and 9 deletions

View File

@ -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, 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)

View File

@ -523,4 +523,34 @@ class Controller extends Component implements ViewContextInterface
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);
}
}
}

View File

@ -182,19 +182,35 @@ class Controller extends \yii\base\Controller
$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]);
$name = $param->getName();
$key = null;
if (array_key_exists($i, $params)) {
$key = $i;
} elseif (array_key_exists($name, $params)) {
$key = $name;
}
if (!isset($args[$i])) {
if ($param->isDefaultValueAvailable()) {
$args[$i] = $param->getDefaultValue();
} else {
$missing[] = $param->getName();
if ($key !== null) {
if ($param->isArray()) {
$params[$key] = $params[$key] === '' ? [] : preg_split('/\s*,\s*/', $params[$key]);
}
$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);
} catch (\yii\base\Exception $e) {
throw new Exception($e->getMessage());
}
} elseif ($param->isDefaultValueAvailable()) {
$args[] = $actionParams[$i] = $param->getDefaultValue();
} else {
$missing[] = $name;
}
}
@ -202,6 +218,11 @@ class Controller extends \yii\base\Controller
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;
}

View File

@ -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;
@ -125,6 +127,7 @@ class Controller extends \yii\base\Controller
$args = [];
$missing = [];
$actionParams = [];
$requestedParams = [];
foreach ($method->getParameters() as $param) {
$name = $param->getName();
if (array_key_exists($name, $params)) {
@ -162,6 +165,12 @@ class Controller extends \yii\base\Controller
}
$args[] = $actionParams[$name] = $params[$name];
unset($params[$name]);
} elseif (PHP_VERSION_ID >= 70100 && ($type = $param->getType()) !== null && !$type->isBuiltin()) {
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 {
@ -177,6 +186,11 @@ class Controller extends \yii\base\Controller
$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;
}