From 9a0ea9b2eb47788d2f17407995f949b647f7a56e Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Wed, 15 Feb 2023 22:58:19 -0800 Subject: [PATCH] APIv4 - Limited support for casting Before ------ * `setCheckPermissions(0)` casts the `0` to `false`. * This is because it's a concrete setter with explict typing. * `setUseTrash(0)` does not. * This is because it's a magic setter with no typing. After ----- * Both `setCheckPermissions(0)` and `setUseTrash(0)` cast the `0` to `false` Technical Details ----------------- I initially drafted in a way where `setUseTrash()` performed exactly the same casting as `setCheckPermissions()`, but there was a countervailing test to assert that `setDebug('debug')` is invalid, and that seemed fair. --- Civi/Api4/Generic/AbstractAction.php | 2 +- Civi/Api4/Utils/ReflectionUtils.php | 32 ++++++++++++++++++++++++++ tests/phpunit/Civi/API/RequestTest.php | 30 ++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/Civi/Api4/Generic/AbstractAction.php b/Civi/Api4/Generic/AbstractAction.php index b20c7d9fcb..9d7f3d77ad 100644 --- a/Civi/Api4/Generic/AbstractAction.php +++ b/Civi/Api4/Generic/AbstractAction.php @@ -227,7 +227,7 @@ abstract class AbstractAction implements \ArrayAccess { return $this->$param; case 'set': - $this->$param = $arguments[0]; + $this->$param = ReflectionUtils::castTypeSoftly($arguments[0], $this->getParamInfo()[$param] ?? []); return $this; } } diff --git a/Civi/Api4/Utils/ReflectionUtils.php b/Civi/Api4/Utils/ReflectionUtils.php index 29e5c4d6c6..9703d2626b 100644 --- a/Civi/Api4/Utils/ReflectionUtils.php +++ b/Civi/Api4/Utils/ReflectionUtils.php @@ -216,4 +216,36 @@ class ReflectionUtils { } } + /** + * Cast the $value to the preferred $type. + * + * This is like PHP's `settype()` but totally not. It only casts in narrow circumstances. + * This reflects an opinion that some casts are better than others. + * + * - Good: 1 => TRUE + * - Bad: 'hello' => TRUE + * + * @param mixed $value + * @param array $paramInfo + * @return mixed + * If the $value is agreeable to casting according to a type-rule from $paramInfo, then + * we return the converted value. Otherwise, leave return the original value. + */ + public static function castTypeSoftly($value, array $paramInfo) { + if (count($paramInfo['type'] ?? []) !== 1) { + // I don't know when or why fields can have multiple types. We're just gone leave-be. + return $value; + } + + switch ($paramInfo['type'][0]) { + case 'bool': + if (in_array($value, [0, 1, '0', '1'], TRUE)) { + return (bool) $value; + } + break; + } + + return $value; + } + } diff --git a/tests/phpunit/Civi/API/RequestTest.php b/tests/phpunit/Civi/API/RequestTest.php index 83d85f7a4a..bd5777509f 100644 --- a/tests/phpunit/Civi/API/RequestTest.php +++ b/tests/phpunit/Civi/API/RequestTest.php @@ -68,4 +68,34 @@ class RequestTest extends \CiviUnitTestCase { Request::create($inEntity, $inAction, ['version' => $inVersion], NULL); } + public function getCastingExamples(): array { + $exs = []; + // We run the same tests on `$checkPermissions` (which has real PHP setter method) + // and `$useTrash` (which has a generic magic method) to show that casting is similar. + $exs[] = ['Contact.delete checkPermissions', 0, FALSE]; + $exs[] = ['Contact.delete checkPermissions', '0', FALSE]; + $exs[] = ['Contact.delete checkPermissions', 1, TRUE]; + $exs[] = ['Contact.delete checkPermissions', '1', TRUE]; + $exs[] = ['Contact.delete useTrash', 0, FALSE]; + $exs[] = ['Contact.delete useTrash', '0', FALSE]; + $exs[] = ['Contact.delete useTrash', 1, TRUE]; + $exs[] = ['Contact.delete useTrash', '1', TRUE]; + return $exs; + } + + /** + * @param $entityActionField + * @param $inputValue + * @param $expectValue + * @dataProvider getCastingExamples + */ + public function testCasting(string $entityActionField, $inputValue, $expectValue): void { + [$entity, $action, $field] = preg_split('/[ \.]/', $entityActionField); + $request = Request::create($entity, $action, ['version' => 4, $field => $inputValue]); + $getter = 'get' . ucfirst($field); + $actualValue = call_user_func([$request, $getter]); + $this->assertEquals(gettype($actualValue), gettype($expectValue)); + $this->assertTrue($actualValue === $expectValue); + } + } -- 2.25.1