APIv4 - Add support for sql equations
[civicrm-core.git] / Civi / Api4 / Query / SqlFunction.php
index c6c4f93e4e041edcbb66d682b8477d727f385d4d..fd10f80ccabaff338cf374f723ed79d1eafe458f 100644 (file)
@@ -18,11 +18,6 @@ namespace Civi\Api4\Query;
  */
 abstract class SqlFunction extends SqlExpression {
 
-  /**
-   * @var array
-   */
-  protected static $params = [];
-
   /**
    * @var array[]
    */
@@ -47,112 +42,55 @@ abstract class SqlFunction extends SqlExpression {
   protected function initialize() {
     $arg = trim(substr($this->expr, strpos($this->expr, '(') + 1, -1));
     foreach ($this->getParams() as $idx => $param) {
-      $prefix = $this->captureKeyword($param['prefix'], $arg);
+      $prefix = NULL;
+      if ($param['name']) {
+        $prefix = $this->captureKeyword([$param['name']], $arg);
+        // Supply api_default
+        if (!$prefix && isset($param['api_default'])) {
+          $this->args[$idx] = [
+            'prefix' => [$param['name']],
+            'expr' => array_map([parent::class, 'convert'], $param['api_default']['expr']),
+            'suffix' => [],
+          ];
+          continue;
+        }
+        if (!$prefix && !$param['optional']) {
+          throw new \API_Exception("Missing {$param['name']} for SQL function " . static::getName());
+        }
+      }
+      elseif ($param['flag_before']) {
+        $prefix = $this->captureKeyword(array_keys($param['flag_before']), $arg);
+      }
       $this->args[$idx] = [
-        'prefix' => $prefix,
+        'prefix' => (array) $prefix,
         'expr' => [],
-        'suffix' => NULL,
+        'suffix' => [],
       ];
-      if ($param['expr'] && isset($prefix) || in_array('', $param['prefix']) || !$param['optional']) {
-        $this->args[$idx]['expr'] = $this->captureExpressions($arg, $param['expr'], $param['must_be'], $param['cant_be']);
-        $this->args[$idx]['suffix'] = $this->captureKeyword($param['suffix'], $arg);
-      }
-    }
-  }
+      if ($param['max_expr'] && (!$param['name'] || $param['name'] === $prefix)) {
+        $exprs = $this->captureExpressions($arg, $param['must_be'], TRUE);
+        if (count($exprs) < $param['min_expr'] || count($exprs) > $param['max_expr']) {
+          throw new \API_Exception('Incorrect number of arguments for SQL function ' . static::getName());
+        }
+        $this->args[$idx]['expr'] = $exprs;
 
-  /**
-   * Shift a keyword off the beginning of the argument string and return it.
-   *
-   * @param array $keywords
-   *   Whitelist of keywords
-   * @param string $arg
-   * @return mixed|null
-   */
-  private function captureKeyword($keywords, &$arg) {
-    foreach (array_filter($keywords) as $key) {
-      if (strpos($arg, $key . ' ') === 0) {
-        $arg = ltrim(substr($arg, strlen($key)));
-        return $key;
+        $this->args[$idx]['suffix'] = (array) $this->captureKeyword(array_keys($param['flag_after']), $arg);
       }
     }
-    return NULL;
   }
 
   /**
-   * Shifts 0 or more expressions off the argument string and returns them
+   * Change $dataType according to output of function
    *
-   * @param string $arg
-   * @param int $limit
-   * @param array $mustBe
-   * @param array $cantBe
-   * @return array
-   * @throws \API_Exception
-   */
-  private function captureExpressions(&$arg, $limit, $mustBe, $cantBe) {
-    $captured = [];
-    $arg = ltrim($arg);
-    while ($arg) {
-      $item = $this->captureExpression($arg);
-      $arg = ltrim(substr($arg, strlen($item)));
-      $expr = SqlExpression::convert($item, FALSE, $mustBe, $cantBe);
-      $this->fields = array_merge($this->fields, $expr->getFields());
-      $captured[] = $expr;
-      // Keep going if we have a comma indicating another expression follows
-      if (count($captured) < $limit && substr($arg, 0, 1) === ',') {
-        $arg = ltrim(substr($arg, 1));
-      }
-      else {
-        break;
-      }
-    }
-    return $captured;
-  }
-
-  /**
-   * Scans the beginning of a string for an expression; stops when it hits delimiter
-   *
-   * @param $arg
+   * @see \Civi\Api4\Utils\FormattingUtil::formatOutputValues
+   * @param string $value
+   * @param string $dataType
    * @return string
    */
-  private function captureExpression($arg) {
-    $chars = str_split($arg);
-    $isEscaped = $quote = NULL;
-    $item = '';
-    $quotes = ['"', "'"];
-    $brackets = [
-      ')' => '(',
-    ];
-    $enclosures = array_fill_keys($brackets, 0);
-    foreach ($chars as $index => $char) {
-      if (!$isEscaped && in_array($char, $quotes, TRUE)) {
-        // Open quotes - we'll ignore everything inside
-        if (!$quote) {
-          $quote = $char;
-        }
-        // Close quotes
-        elseif ($char === $quote) {
-          $quote = NULL;
-        }
-      }
-      if (!$quote) {
-        // Delineates end of expression
-        if (($char == ',' || $char == ' ') && !array_filter($enclosures)) {
-          return $item;
-        }
-        // Open brackets - we'll ignore delineators inside
-        if (isset($enclosures[$char])) {
-          $enclosures[$char]++;
-        }
-        // Close brackets
-        if (isset($brackets[$char]) && $enclosures[$brackets[$char]]) {
-          $enclosures[$brackets[$char]]--;
-        }
-      }
-      $item .= $char;
-      // We are escaping the next char if this is a backslash not preceded by an odd number of backslashes
-      $isEscaped = $char === '\\' && ((strlen($item) - strlen(rtrim($item, '\\'))) % 2);
+  public function formatOutputValue($value, &$dataType) {
+    if (static::$dataType) {
+      $dataType = static::$dataType;
     }
-    return $item;
+    return $value;
   }
 
   /**
@@ -163,9 +101,8 @@ abstract class SqlFunction extends SqlExpression {
    */
   public function render(array $fieldList): string {
     $output = '';
-    $params = $this->getParams();
-    foreach ($this->args as $index => $arg) {
-      $rendered = $this->renderArg($arg, $params[$index], $fieldList);
+    foreach ($this->args as $arg) {
+      $rendered = $this->renderArg($arg, $fieldList);
       if (strlen($rendered)) {
         $output .= (strlen($output) ? ' ' : '') . $rendered;
       }
@@ -175,28 +112,19 @@ abstract class SqlFunction extends SqlExpression {
 
   /**
    * @param array $arg
-   * @param array $param
    * @param array $fieldList
    * @return string
    */
-  private function renderArg($arg, $param, $fieldList): string {
-    // Supply api_default
-    if (!isset($arg['prefix']) && !isset($arg['suffix']) && empty($arg['expr']) && !empty($param['api_default'])) {
-      $arg = [
-        'prefix' => $param['api_default']['prefix'] ?? reset($param['prefix']),
-        'expr' => array_map([parent::class, 'convert'], $param['api_default']['expr'] ?? []),
-        'suffix' => $param['api_default']['suffix'] ?? reset($param['suffix']),
-      ];
-    }
-    $rendered = $arg['prefix'] ?? '';
+  private function renderArg($arg, $fieldList): string {
+    $rendered = implode(' ', $arg['prefix']);
     foreach ($arg['expr'] ?? [] as $idx => $expr) {
       if (strlen($rendered) || $idx) {
         $rendered .= $idx ? ', ' : ' ';
       }
       $rendered .= $expr->render($fieldList);
     }
-    if (isset($arg['suffix'])) {
-      $rendered .= (strlen($rendered) ? ' ' : '') . $arg['suffix'];
+    if ($arg['suffix']) {
+      $rendered .= (strlen($rendered) ? ' ' : '') . implode(' ', $arg['suffix']);
     }
     return $rendered;
   }
@@ -221,23 +149,26 @@ abstract class SqlFunction extends SqlExpression {
    * Get the param metadata for this sql function.
    * @return array
    */
-  public static function getParams(): array {
+  final public static function getParams(): array {
     $params = [];
-    foreach (static::$params as $param) {
+    foreach (static::params() as $param) {
       // Merge in defaults to ensure each param has these properties
       $params[] = $param + [
-        'prefix' => [],
-        'expr' => 1,
-        'suffix' => [],
+        'name' => NULL,
+        'min_expr' => 1,
+        'max_expr' => 1,
+        'flag_before' => [],
+        'flag_after' => [],
         'optional' => FALSE,
-        'must_be' => [],
-        'cant_be' => ['SqlWild'],
+        'must_be' => ['SqlField', 'SqlFunction', 'SqlString', 'SqlNumber', 'SqlNull'],
         'api_default' => NULL,
       ];
     }
     return $params;
   }
 
+  abstract protected static function params(): array;
+
   /**
    * Get the arguments passed to this sql function instance.
    * @return array[]
@@ -258,4 +189,9 @@ abstract class SqlFunction extends SqlExpression {
    */
   abstract public static function getTitle(): string;
 
+  /**
+   * @return string
+   */
+  abstract public static function getDescription(): string;
+
 }