mirror of
https://github.com/yiisoft/yii2.git
synced 2025-08-17 07:51:12 +08:00
array union in controllers
! fix yii\web\Controller::bindActionParams behaviour for union types o some refactoring in the process
This commit is contained in:
@ -133,41 +133,15 @@ class Controller extends \yii\base\Controller
|
|||||||
$name = $param->getName();
|
$name = $param->getName();
|
||||||
if (array_key_exists($name, $params)) {
|
if (array_key_exists($name, $params)) {
|
||||||
$isValid = true;
|
$isValid = true;
|
||||||
$isArray = ($type = $param->getType()) instanceof \ReflectionNamedType && $type->getName() === 'array';
|
$type = $param->getType();
|
||||||
if ($isArray) {
|
if ($type instanceof \ReflectionNamedType) {
|
||||||
$params[$name] = (array)$params[$name];
|
[$result, $isValid] = $this->filterSingleTypeActionParam($params[$name], $type);
|
||||||
} elseif (is_array($params[$name])) {
|
$params[$name] = $result;
|
||||||
$isValid = false;
|
} elseif (class_exists('\ReflectionUnionType') && $type instanceof \ReflectionUnionType) {
|
||||||
} elseif (
|
[$result, $isValid] = $this->filterUnionTypeActionParam($params[$name], $type);
|
||||||
PHP_VERSION_ID >= 70000
|
$params[$name] = $result;
|
||||||
&& ($type = $param->getType()) !== null
|
|
||||||
&& method_exists($type, 'isBuiltin')
|
|
||||||
&& $type->isBuiltin()
|
|
||||||
&& ($params[$name] !== null || !$type->allowsNull())
|
|
||||||
) {
|
|
||||||
$typeName = PHP_VERSION_ID >= 70100 ? $type->getName() : (string)$type;
|
|
||||||
|
|
||||||
if ($params[$name] === '' && $type->allowsNull()) {
|
|
||||||
if ($typeName !== 'string') { // for old string behavior compatibility
|
|
||||||
$params[$name] = null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch ($typeName) {
|
|
||||||
case 'int':
|
|
||||||
$params[$name] = filter_var($params[$name], FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
|
|
||||||
break;
|
|
||||||
case 'float':
|
|
||||||
$params[$name] = filter_var($params[$name], FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE);
|
|
||||||
break;
|
|
||||||
case 'bool':
|
|
||||||
$params[$name] = filter_var($params[$name], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if ($params[$name] === null) {
|
|
||||||
$isValid = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$isValid) {
|
if (!$isValid) {
|
||||||
throw new BadRequestHttpException(
|
throw new BadRequestHttpException(
|
||||||
Yii::t('yii', 'Invalid data received for parameter "{param}".', ['param' => $name])
|
Yii::t('yii', 'Invalid data received for parameter "{param}".', ['param' => $name])
|
||||||
@ -211,6 +185,123 @@ class Controller extends \yii\base\Controller
|
|||||||
return $args;
|
return $args;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The logic for [[bindActionParam]] to validate whether a given parameter matches the action's typing
|
||||||
|
* if the function parameter has a single named type.
|
||||||
|
* @param mixed $param The parameter value.
|
||||||
|
* @param \ReflectionNamedType $type
|
||||||
|
* @return array{0: mixed, 1: bool} The resulting parameter value and a boolean indicating whether the value is valid.
|
||||||
|
*/
|
||||||
|
private function filterSingleTypeActionParam($param, $type)
|
||||||
|
{
|
||||||
|
$isArray = $type->getName() === 'array';
|
||||||
|
if ($isArray) {
|
||||||
|
return [(array)$param, true];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($param)) {
|
||||||
|
return [$param, false];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID >= 70000
|
||||||
|
&& method_exists($type, 'isBuiltin')
|
||||||
|
&& $type->isBuiltin()
|
||||||
|
&& ($param !== null || !$type->allowsNull())) {
|
||||||
|
$typeName = PHP_VERSION_ID >= 70100 ? $type->getName() : (string)$type;
|
||||||
|
if ($param === '' && $type->allowsNull()) {
|
||||||
|
if ($typeName !== 'string') { // for old string behavior compatibility
|
||||||
|
return [null, true];
|
||||||
|
}
|
||||||
|
return ['', true];
|
||||||
|
}
|
||||||
|
|
||||||
|
$filterResult = $this->filterParamByType($param, $typeName);
|
||||||
|
return [$filterResult, $filterResult !== null];
|
||||||
|
}
|
||||||
|
return [$param, true];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The logic for [[bindActionParam]] to validate whether a given parameter matches the action's typing
|
||||||
|
* if the function parameter has a union type.
|
||||||
|
* @param mixed $param The parameter value.
|
||||||
|
* @param \ReflectionUnionType $type
|
||||||
|
* @return array{0: mixed, 1: bool} The resulting parameter value and a boolean indicating whether the value is valid.
|
||||||
|
*/
|
||||||
|
private function filterUnionTypeActionParam($param, $type)
|
||||||
|
{
|
||||||
|
$types = $type->getTypes();
|
||||||
|
if ($param === '' && $type->allowsNull()) {
|
||||||
|
// check if type can be string for old string behavior compatibility
|
||||||
|
foreach ($types as $partialType) {
|
||||||
|
if ($partialType === null
|
||||||
|
|| !method_exists($partialType, 'isBuiltin')
|
||||||
|
|| !$partialType->isBuiltin()
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$typeName = PHP_VERSION_ID >= 70100 ? $partialType->getName() : (string)$partialType;
|
||||||
|
if ($typeName === 'string') {
|
||||||
|
return ['', true];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [null, true];
|
||||||
|
}
|
||||||
|
// if we found a built-in type but didn't return out, its validation failed
|
||||||
|
$foundBuiltinType = false;
|
||||||
|
// we save returning out an array or string for later because other types should take precedence
|
||||||
|
$canBeArray = false;
|
||||||
|
$canBeString = false;
|
||||||
|
foreach ($types as $partialType) {
|
||||||
|
if ($partialType === null
|
||||||
|
|| !method_exists($partialType, 'isBuiltin')
|
||||||
|
|| !$partialType->isBuiltin()
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$foundBuiltinType = true;
|
||||||
|
$typeName = PHP_VERSION_ID >= 70100 ? $partialType->getName() : (string)$partialType;
|
||||||
|
$canBeArray |= $typeName === 'array';
|
||||||
|
$canBeString |= $typeName === 'string';
|
||||||
|
if (is_array($param)) {
|
||||||
|
if ($canBeArray) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filterResult = $this->filterParamByType($param, $typeName);
|
||||||
|
if ($filterResult !== null) {
|
||||||
|
return [$filterResult, true];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!is_array($param) && $canBeString) {
|
||||||
|
return [$param, true];
|
||||||
|
}
|
||||||
|
if ($canBeArray) {
|
||||||
|
return [(array)$param, true];
|
||||||
|
}
|
||||||
|
return [$param, $canBeString || !$foundBuiltinType];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the according filter_var logic for teh given type.
|
||||||
|
* @param string $param The value to filter.
|
||||||
|
* @param string $typeName The type name.
|
||||||
|
* @return mixed|null The resulting value, or null if validation failed or the type can't be validated.
|
||||||
|
*/
|
||||||
|
private function filterParamByType(string $param, string $typeName) {
|
||||||
|
switch ($typeName) {
|
||||||
|
case 'int':
|
||||||
|
return filter_var($param, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
|
||||||
|
case 'float':
|
||||||
|
return filter_var($param, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE);
|
||||||
|
case 'bool':
|
||||||
|
return filter_var($param, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
|
@ -10,6 +10,7 @@ namespace yiiunit\framework\web;
|
|||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Yii;
|
use Yii;
|
||||||
use yii\base\InlineAction;
|
use yii\base\InlineAction;
|
||||||
|
use yii\web\BadRequestHttpException;
|
||||||
use yii\web\NotFoundHttpException;
|
use yii\web\NotFoundHttpException;
|
||||||
use yii\web\Response;
|
use yii\web\Response;
|
||||||
use yii\web\ServerErrorHttpException;
|
use yii\web\ServerErrorHttpException;
|
||||||
@ -329,5 +330,52 @@ class ControllerTest extends TestCase
|
|||||||
$args = $this->controller->bindActionParams($injectionAction, $params);
|
$args = $this->controller->bindActionParams($injectionAction, $params);
|
||||||
$this->assertSame('test', $args[0]);
|
$this->assertSame('test', $args[0]);
|
||||||
$this->assertSame(1, $args[1]);
|
$this->assertSame(1, $args[1]);
|
||||||
|
|
||||||
|
// test that a value PHP parsed to a string but that should be an int becomes one
|
||||||
|
$params = ['arg' => 'test', 'second' => '1'];
|
||||||
|
|
||||||
|
$args = $this->controller->bindActionParams($injectionAction, $params);
|
||||||
|
$this->assertSame('test', $args[0]);
|
||||||
|
$this->assertSame(1, $args[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUnionBindingActionParamsWithArray()
|
||||||
|
{
|
||||||
|
if (PHP_VERSION_ID < 80000) {
|
||||||
|
$this->markTestSkipped('Can not be tested on PHP < 8.0');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Use the PHP80 controller for this test
|
||||||
|
$this->controller = new FakePhp80Controller('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('array-or-int', $this->controller, 'actionArrayOrInt');
|
||||||
|
$params = ['foo' => 1];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$args = $this->controller->bindActionParams($injectionAction, $params);
|
||||||
|
$this->assertSame(1, $args[0]);
|
||||||
|
} catch (BadRequestHttpException $e) {
|
||||||
|
$this->fail('Failed to bind int param for array|int union type!');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$paramsArray = ['foo' => [1, 2, 3, 4]];
|
||||||
|
$args = $this->controller->bindActionParams($injectionAction, $paramsArray);
|
||||||
|
$this->assertSame([1, 2, 3, 4], $args[0]);
|
||||||
|
} catch (BadRequestHttpException $e) {
|
||||||
|
$this->fail('Failed to bind array param for array|int union type!');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,4 +17,9 @@ class FakePhp80Controller extends Controller
|
|||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function actionArrayOrInt(array|int $foo)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user