array union in controllers

! fix yii\web\Controller::bindActionParams behaviour for union types
o some refactoring in the process
This commit is contained in:
Chris Reichel
2025-04-10 10:34:06 +02:00
parent 2fad37b597
commit a67bfc1274
3 changed files with 178 additions and 34 deletions

View File

@ -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}
*/