Merge pull request #23912 from mlutfy/reportVars
[civicrm-core.git] / CRM / Utils / Type.php
index 01d833e72b829a9a2035e10cd81edab9588c1e33..9eb9702f016abcb03a221cfc8d02be3334d303bd 100644 (file)
@@ -334,11 +334,10 @@ class CRM_Utils_Type {
 
       default:
         throw new CRM_Core_Exception(
-          $type . " is not a recognised (camel cased) data type."
+          $type . " is not a recognized (camel cased) data type."
         );
     }
 
-    // @todo Use exceptions instead of CRM_Core_Error::fatal().
     if ($abort) {
       $data = htmlentities($data);
 
@@ -473,6 +472,101 @@ class CRM_Utils_Type {
     return NULL;
   }
 
+  /**
+   * Validate that a value matches a PHP type.
+   *
+   * Note that, at a micro-level, this is probably slower than using real PHP type-checking, but it doesn't seem bad.
+   * (In light benchmarking of ~1000 validations on an i3-10100, there is no obvious effect on the execution-time.)
+   * Should be fast enough for validating business entities.
+   *
+   * Example usage: 'validatePhpType(123, 'int|double');`
+   *
+   * @param mixed $value
+   * @param string|string[] $types
+   *   The list of acceptable PHP types and/or classnames.
+   *   Either an array or a string (with '|' delimiters).
+   *   Note that 'null' is a distinct type.
+   *   Ex: 'int'
+   *   Ex: 'Countable|null'
+   *   Ex: 'string|bool'
+   *   Ex: 'string|false'
+   * @param bool $isStrict
+   *   If data is likely to come from another text medium, then you may want to
+   *   allow (say) numbers and string-like-numbers to be used interchangably.
+   *
+   *   With $isStrict=TRUE, the string "123" does not match type "int". The int 456 does not match type "double". etc.
+   *
+   *   With $isStrict=FALSE, the string "123" will match types "string", "int", and "double".
+   * @return bool
+   */
+  public static function validatePhpType($value, $types, bool $isStrict = TRUE) {
+    if (is_string($types)) {
+      $types = preg_split('/ *\| */', $types);
+    }
+
+    $checkTypeStrict = function($type, $value) {
+      static $aliases = ['integer' => 'int', 'boolean' => 'bool', 'float' => 'double', 'NULL' => 'null'];
+      switch ($type) {
+        case 'mixed':
+          return TRUE;
+
+        case 'false':
+        case 'FALSE':
+        case 'true':
+        case 'TRUE':
+          $expectBool = mb_strtolower($type) === 'true';
+          return $value === $expectBool;
+      }
+      $realType = gettype($value);
+      if (($aliases[$realType] ?? $realType) === ($aliases[$type] ?? $type)) {
+        return TRUE;
+      }
+      if ($realType === 'object' && $value instanceof $type) {
+        return TRUE;
+      }
+      return FALSE;
+    };
+    $checkTypeRelaxed = function($type, $value) use ($checkTypeStrict) {
+      switch ($type) {
+        case 'string':
+          return is_string($value) || is_int($value) || is_float($value);
+
+        case 'bool':
+        case 'boolean':
+          return is_bool($value) || CRM_Utils_Rule::integer($value);
+
+        case 'int':
+        case' integer':
+          return CRM_Utils_Rule::integer($value);
+
+        case 'float':
+        case 'double':
+          return CRM_Utils_Rule::numeric($value);
+
+        default:
+          return $checkTypeStrict($type, $value);
+      }
+    };
+    $checkType = $isStrict ? $checkTypeStrict : $checkTypeRelaxed;
+
+    foreach ($types as $type) {
+      $isTypedArray = substr($type, -2, 2) === '[]';
+      if (!$isTypedArray && $checkType($type, $value)) {
+        return TRUE;
+      }
+      if ($isTypedArray && is_array($value)) {
+        $baseType = substr($type, 0, -2);
+        foreach ($value as $vItem) {
+          if (!\CRM_Utils_Type::validatePhpType($vItem, [$baseType], $isStrict)) {
+            continue 2;
+          }
+        }
+        return TRUE;
+      }
+    }
+    return FALSE;
+  }
+
   /**
    * Preg_replace_callback for mysqlOrderByFieldFunction escape.
    *
@@ -512,7 +606,7 @@ class CRM_Utils_Type {
   }
 
   /**
-   * Get list of avaliable Data Types for Option Groups
+   * Get list of available Data Types for Option Groups
    *
    * @return array
    */