c7ee3ad71338ec8e6dd1576c774fac3120cdda9c
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
{
27 * Used for categorizing functions in the UI
31 protected static $category;
34 * Data type output by this function
38 protected static $dataType;
40 const CATEGORY_AGGREGATE
= 'aggregate',
41 CATEGORY_COMPARISON
= 'comparison',
42 CATEGORY_DATE
= 'date',
43 CATEGORY_MATH
= 'math',
44 CATEGORY_STRING
= 'string';
47 * Parse the argument string into an array of function arguments
49 protected function initialize() {
50 $arg = trim(substr($this->expr
, strpos($this->expr
, '(') +
1, -1));
51 foreach ($this->getParams() as $idx => $param) {
53 if ($param['prefix']) {
54 $prefix = $this->captureKeyword([$param['prefix']], $arg);
56 if (!$prefix && isset($param['api_default'])) {
58 'prefix' => $param['api_default']['prefix'] ??
[$param['prefix']],
59 'expr' => array_map([parent
::class, 'convert'], $param['api_default']['expr']),
60 'suffix' => $param['api_default']['suffix'] ??
[],
64 if (!$prefix && !$param['optional']) {
65 throw new \
API_Exception("Missing {$param['prefix']} for SQL function " . static::getName());
68 elseif ($param['flag_before']) {
69 $prefix = $this->captureKeyword(array_keys($param['flag_before']), $arg);
72 'prefix' => (array) $prefix,
76 if ($param['max_expr'] && (!$param['prefix'] ||
$param['prefix'] === $prefix)) {
77 $exprs = $this->captureExpressions($arg, $param['must_be'], $param['cant_be']);
78 if (count($exprs) < $param['min_expr'] ||
count($exprs) > $param['max_expr']) {
79 throw new \
API_Exception('Incorrect number of arguments for SQL function ' . static::getName());
81 $this->args
[$idx]['expr'] = $exprs;
83 $this->args
[$idx]['suffix'] = (array) $this->captureKeyword(array_keys($param['flag_after']), $arg);
89 * Change $dataType according to output of function
91 * @see \Civi\Api4\Utils\FormattingUtil::formatOutputValues
92 * @param string $value
93 * @param string $dataType
96 public function formatOutputValue($value, &$dataType) {
97 if (static::$dataType) {
98 $dataType = static::$dataType;
104 * Shift a keyword off the beginning of the argument string and return it.
106 * @param array $keywords
107 * Whitelist of keywords
111 private function captureKeyword($keywords, &$arg) {
112 foreach ($keywords as $key) {
113 if (strpos($arg, $key . ' ') === 0) {
114 $arg = ltrim(substr($arg, strlen($key)));
122 * Shifts 0 or more expressions off the argument string and returns them
125 * @param array $mustBe
126 * @param array $cantBe
128 * @throws \API_Exception
130 private function captureExpressions(&$arg, $mustBe, $cantBe) {
134 $item = $this->captureExpression($arg);
135 $arg = ltrim(substr($arg, strlen($item)));
136 $expr = SqlExpression
::convert($item, FALSE, $mustBe, $cantBe);
137 $this->fields
= array_merge($this->fields
, $expr->getFields());
139 // Keep going if we have a comma indicating another expression follows
140 if (substr($arg, 0, 1) === ',') {
141 $arg = ltrim(substr($arg, 1));
151 * Scans the beginning of a string for an expression; stops when it hits delimiter
156 private function captureExpression($arg) {
157 $chars = str_split($arg);
158 $isEscaped = $quote = NULL;
160 $quotes = ['"', "'"];
164 $enclosures = array_fill_keys($brackets, 0);
165 foreach ($chars as $index => $char) {
166 if (!$isEscaped && in_array($char, $quotes, TRUE)) {
167 // Open quotes - we'll ignore everything inside
172 elseif ($char === $quote) {
177 // Delineates end of expression
178 if (($char == ',' ||
$char == ' ') && !array_filter($enclosures)) {
181 // Open brackets - we'll ignore delineators inside
182 if (isset($enclosures[$char])) {
183 $enclosures[$char]++
;
186 if (isset($brackets[$char]) && $enclosures[$brackets[$char]]) {
187 $enclosures[$brackets[$char]]--;
191 // We are escaping the next char if this is a backslash not preceded by an odd number of backslashes
192 $isEscaped = $char === '\\' && ((strlen($item) - strlen(rtrim($item, '\\'))) %
2);
198 * Render the expression for insertion into the sql query
200 * @param array $fieldList
203 public function render(array $fieldList): string {
205 $params = $this->getParams();
206 foreach ($this->args
as $index => $arg) {
207 $rendered = $this->renderArg($arg, $params[$index], $fieldList);
208 if (strlen($rendered)) {
209 $output .= (strlen($output) ?
' ' : '') . $rendered;
212 return $this->getName() . '(' . $output . ')';
217 * @param array $param
218 * @param array $fieldList
221 private function renderArg($arg, $param, $fieldList): string {
222 $rendered = implode(' ', $arg['prefix']);
223 foreach ($arg['expr'] ??
[] as $idx => $expr) {
224 if (strlen($rendered) ||
$idx) {
225 $rendered .= $idx ?
', ' : ' ';
227 $rendered .= $expr->render($fieldList);
229 if ($arg['suffix']) {
230 $rendered .= (strlen($rendered) ?
' ' : '') . implode(' ', $arg['suffix']);
238 public function getAlias(): string {
239 return $this->alias ??
$this->getName() . ':' . implode('_', $this->fields
);
243 * Get the name of this sql function.
246 public static function getName(): string {
247 $className = static::class;
248 return substr($className, strrpos($className, 'SqlFunction') +
11);
252 * Get the param metadata for this sql function.
255 final public static function getParams(): array {
257 foreach (static::params() as $param) {
258 // Merge in defaults to ensure each param has these properties
259 $params[] = $param +
[
267 'cant_be' => ['SqlWild'],
268 'api_default' => NULL,
274 abstract protected static function params(): array;
277 * Get the arguments passed to this sql function instance.
280 public function getArgs(): array {
287 public static function getCategory(): string {
288 return static::$category;
292 * @return string|NULL
294 public static function getDataType():?
string {
295 return static::$dataType;
301 abstract public static function getTitle(): string;