3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
12 namespace Civi\Api4\Query
;
15 * Base class for all Sql functions.
17 * @package Civi\Api4\Query
19 abstract class SqlFunction
extends SqlExpression
{
24 protected static $params = [];
32 * Used for categorizing functions in the UI
36 protected static $category;
38 const CATEGORY_AGGREGATE
= 'aggregate',
39 CATEGORY_COMPARISON
= 'comparison',
40 CATEGORY_DATE
= 'date',
41 CATEGORY_MATH
= 'math',
42 CATEGORY_STRING
= 'string';
45 * Parse the argument string into an array of function arguments
47 protected function initialize() {
48 $arg = trim(substr($this->expr
, strpos($this->expr
, '(') +
1, -1));
49 foreach ($this->getParams() as $idx => $param) {
50 $prefix = $this->captureKeyword($param['prefix'], $arg);
56 if ($param['expr'] && isset($prefix) ||
in_array('', $param['prefix']) ||
!$param['optional']) {
57 $this->args
[$idx]['expr'] = $this->captureExpressions($arg, $param['expr'], $param['must_be'], $param['cant_be']);
58 $this->args
[$idx]['suffix'] = $this->captureKeyword($param['suffix'], $arg);
64 * Shift a keyword off the beginning of the argument string and return it.
66 * @param array $keywords
67 * Whitelist of keywords
71 private function captureKeyword($keywords, &$arg) {
72 foreach (array_filter($keywords) as $key) {
73 if (strpos($arg, $key . ' ') === 0) {
74 $arg = ltrim(substr($arg, strlen($key)));
82 * Shifts 0 or more expressions off the argument string and returns them
86 * @param array $mustBe
87 * @param array $cantBe
89 * @throws \API_Exception
91 private function captureExpressions(&$arg, $limit, $mustBe, $cantBe) {
95 $item = $this->captureExpression($arg);
96 $arg = ltrim(substr($arg, strlen($item)));
97 $expr = SqlExpression
::convert($item, FALSE, $mustBe, $cantBe);
98 $this->fields
= array_merge($this->fields
, $expr->getFields());
100 // Keep going if we have a comma indicating another expression follows
101 if (count($captured) < $limit && substr($arg, 0, 1) === ',') {
102 $arg = ltrim(substr($arg, 1));
112 * Scans the beginning of a string for an expression; stops when it hits delimiter
117 private function captureExpression($arg) {
118 $chars = str_split($arg);
119 $isEscaped = $quote = NULL;
121 $quotes = ['"', "'"];
125 $enclosures = array_fill_keys($brackets, 0);
126 foreach ($chars as $index => $char) {
127 if (!$isEscaped && in_array($char, $quotes, TRUE)) {
128 // Open quotes - we'll ignore everything inside
133 elseif ($char === $quote) {
138 // Delineates end of expression
139 if (($char == ',' ||
$char == ' ') && !array_filter($enclosures)) {
142 // Open brackets - we'll ignore delineators inside
143 if (isset($enclosures[$char])) {
144 $enclosures[$char]++
;
147 if (isset($brackets[$char]) && $enclosures[$brackets[$char]]) {
148 $enclosures[$brackets[$char]]--;
152 // We are escaping the next char if this is a backslash not preceded by an odd number of backslashes
153 $isEscaped = $char === '\\' && ((strlen($item) - strlen(rtrim($item, '\\'))) %
2);
159 * Render the expression for insertion into the sql query
161 * @param array $fieldList
164 public function render(array $fieldList): string {
166 $params = $this->getParams();
167 foreach ($this->args
as $index => $arg) {
168 $rendered = $this->renderArg($arg, $params[$index], $fieldList);
169 if (strlen($rendered)) {
170 $output .= (strlen($output) ?
' ' : '') . $rendered;
173 return $this->getName() . '(' . $output . ')';
178 * @param array $param
179 * @param array $fieldList
182 private function renderArg($arg, $param, $fieldList): string {
183 // Supply api_default
184 if (!isset($arg['prefix']) && !isset($arg['suffix']) && empty($arg['expr']) && !empty($param['api_default'])) {
186 'prefix' => $param['api_default']['prefix'] ??
reset($param['prefix']),
187 'expr' => array_map([parent
::class, 'convert'], $param['api_default']['expr'] ??
[]),
188 'suffix' => $param['api_default']['suffix'] ??
reset($param['suffix']),
191 $rendered = $arg['prefix'] ??
'';
192 foreach ($arg['expr'] ??
[] as $idx => $expr) {
193 if (strlen($rendered) ||
$idx) {
194 $rendered .= $idx ?
', ' : ' ';
196 $rendered .= $expr->render($fieldList);
198 if (isset($arg['suffix'])) {
199 $rendered .= (strlen($rendered) ?
' ' : '') . $arg['suffix'];
207 public function getAlias(): string {
208 return $this->alias ??
$this->getName() . ':' . implode('_', $this->fields
);
212 * Get the name of this sql function.
215 public static function getName(): string {
216 $className = static::class;
217 return substr($className, strrpos($className, 'SqlFunction') +
11);
221 * Get the param metadata for this sql function.
224 public static function getParams(): array {
226 foreach (static::$params as $param) {
227 // Merge in defaults to ensure each param has these properties
228 $params[] = $param +
[
234 'cant_be' => ['SqlWild'],
235 'api_default' => NULL,
242 * Get the arguments passed to this sql function instance.
245 public function getArgs(): array {
252 public static function getCategory(): string {
253 return static::$category;
259 abstract public static function getTitle(): string;