mirror of
https://github.com/yiisoft/yii2.git
synced 2025-08-16 15:21:13 +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();
|
||||
if (array_key_exists($name, $params)) {
|
||||
$isValid = true;
|
||||
$isArray = ($type = $param->getType()) instanceof \ReflectionNamedType && $type->getName() === 'array';
|
||||
if ($isArray) {
|
||||
$params[$name] = (array)$params[$name];
|
||||
} elseif (is_array($params[$name])) {
|
||||
$isValid = false;
|
||||
} elseif (
|
||||
PHP_VERSION_ID >= 70000
|
||||
&& ($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;
|
||||
}
|
||||
}
|
||||
$type = $param->getType();
|
||||
if ($type instanceof \ReflectionNamedType) {
|
||||
[$result, $isValid] = $this->filterSingleTypeActionParam($params[$name], $type);
|
||||
$params[$name] = $result;
|
||||
} elseif (class_exists('\ReflectionUnionType') && $type instanceof \ReflectionUnionType) {
|
||||
[$result, $isValid] = $this->filterUnionTypeActionParam($params[$name], $type);
|
||||
$params[$name] = $result;
|
||||
}
|
||||
|
||||
if (!$isValid) {
|
||||
throw new BadRequestHttpException(
|
||||
Yii::t('yii', 'Invalid data received for parameter "{param}".', ['param' => $name])
|
||||
@ -211,6 +185,123 @@ class Controller extends \yii\base\Controller
|
||||
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}
|
||||
*/
|
||||
|
@ -10,6 +10,7 @@ namespace yiiunit\framework\web;
|
||||
use RuntimeException;
|
||||
use Yii;
|
||||
use yii\base\InlineAction;
|
||||
use yii\web\BadRequestHttpException;
|
||||
use yii\web\NotFoundHttpException;
|
||||
use yii\web\Response;
|
||||
use yii\web\ServerErrorHttpException;
|
||||
@ -329,5 +330,52 @@ class ControllerTest extends TestCase
|
||||
$args = $this->controller->bindActionParams($injectionAction, $params);
|
||||
$this->assertSame('test', $args[0]);
|
||||
$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