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;
33 const CATEGORY_AGGREGATE
= 'aggregate',
34 CATEGORY_COMPARISON
= 'comparison',
35 CATEGORY_DATE
= 'date',
36 CATEGORY_MATH
= 'math',
37 CATEGORY_STRING
= 'string';
40 * Parse the argument string into an array of function arguments
42 protected function initialize() {
43 $arg = trim(substr($this->expr
, strpos($this->expr
, '(') +
1, -1));
44 foreach ($this->getParams() as $idx => $param) {
47 $prefix = $this->captureKeyword([$param['name']], $arg);
49 if (!$prefix && isset($param['api_default'])) {
51 'prefix' => [$param['name']],
52 'expr' => array_map([parent
::class, 'convert'], $param['api_default']['expr']),
57 if (!$prefix && !$param['optional']) {
58 throw new \
API_Exception("Missing {$param['name']} for SQL function " . static::getName());
61 elseif ($param['flag_before']) {
62 $prefix = $this->captureKeyword(array_keys($param['flag_before']), $arg);
65 'prefix' => (array) $prefix,
69 if ($param['max_expr'] && (!$param['name'] ||
$param['name'] === $prefix)) {
70 $exprs = $this->captureExpressions($arg, $param['must_be']);
71 if (count($exprs) < $param['min_expr'] ||
count($exprs) > $param['max_expr']) {
72 throw new \
API_Exception('Incorrect number of arguments for SQL function ' . static::getName());
74 $this->args
[$idx]['expr'] = $exprs;
76 $this->args
[$idx]['suffix'] = (array) $this->captureKeyword(array_keys($param['flag_after']), $arg);
82 * Change $dataType according to output of function
84 * @see \Civi\Api4\Utils\FormattingUtil::formatOutputValues
85 * @param string $value
86 * @param string $dataType
89 public function formatOutputValue($value, &$dataType) {
90 if (static::$dataType) {
91 $dataType = static::$dataType;
97 * Shift a keyword off the beginning of the argument string and return it.
99 * @param array $keywords
100 * Whitelist of keywords
104 private function captureKeyword($keywords, &$arg) {
105 foreach ($keywords as $key) {
106 if (strpos($arg, $key . ' ') === 0) {
107 $arg = ltrim(substr($arg, strlen($key)));
115 * Shifts 0 or more expressions off the argument string and returns them
118 * @param array $mustBe
120 * @throws \API_Exception
122 private function captureExpressions(&$arg, $mustBe) {
126 $item = $this->captureExpression($arg);
127 $arg = ltrim(substr($arg, strlen($item)));
128 $expr = SqlExpression
::convert($item, FALSE, $mustBe);
129 $this->fields
= array_merge($this->fields
, $expr->getFields());
131 // Keep going if we have a comma indicating another expression follows
132 if (substr($arg, 0, 1) === ',') {
133 $arg = ltrim(substr($arg, 1));
143 * Scans the beginning of a string for an expression; stops when it hits delimiter
148 private function captureExpression($arg) {
149 $isEscaped = $quote = NULL;
151 $quotes = ['"', "'"];
155 $enclosures = array_fill_keys($brackets, 0);
156 foreach (str_split($arg) as $char) {
157 if (!$isEscaped && in_array($char, $quotes, TRUE)) {
158 // Open quotes - we'll ignore everything inside
163 elseif ($char === $quote) {
168 // Delineates end of expression
169 if (($char == ',' ||
$char == ' ') && !array_filter($enclosures)) {
172 // Open brackets - we'll ignore delineators inside
173 if (isset($enclosures[$char])) {
174 $enclosures[$char]++
;
177 if (isset($brackets[$char]) && $enclosures[$brackets[$char]]) {
178 $enclosures[$brackets[$char]]--;
182 // We are escaping the next char if this is a backslash not preceded by an odd number of backslashes
183 $isEscaped = $char === '\\' && ((strlen($item) - strlen(rtrim($item, '\\'))) %
2);
189 * Render the expression for insertion into the sql query
191 * @param array $fieldList
194 public function render(array $fieldList): string {
196 foreach ($this->args
as $arg) {
197 $rendered = $this->renderArg($arg, $fieldList);
198 if (strlen($rendered)) {
199 $output .= (strlen($output) ?
' ' : '') . $rendered;
202 return $this->getName() . '(' . $output . ')';
207 * @param array $fieldList
210 private function renderArg($arg, $fieldList): string {
211 $rendered = implode(' ', $arg['prefix']);
212 foreach ($arg['expr'] ??
[] as $idx => $expr) {
213 if (strlen($rendered) ||
$idx) {
214 $rendered .= $idx ?
', ' : ' ';
216 $rendered .= $expr->render($fieldList);
218 if ($arg['suffix']) {
219 $rendered .= (strlen($rendered) ?
' ' : '') . implode(' ', $arg['suffix']);
227 public function getAlias(): string {
228 return $this->alias ??
$this->getName() . ':' . implode('_', $this->fields
);
232 * Get the name of this sql function.
235 public static function getName(): string {
236 $className = static::class;
237 return substr($className, strrpos($className, 'SqlFunction') +
11);
241 * Get the param metadata for this sql function.
244 final public static function getParams(): array {
246 foreach (static::params() as $param) {
247 // Merge in defaults to ensure each param has these properties
248 $params[] = $param +
[
255 'must_be' => ['SqlField', 'SqlFunction', 'SqlString', 'SqlNumber', 'SqlNull'],
256 'api_default' => NULL,
262 abstract protected static function params(): array;
265 * Get the arguments passed to this sql function instance.
268 public function getArgs(): array {
275 public static function getCategory(): string {
276 return static::$category;
282 abstract public static function getTitle(): string;
287 abstract public static function getDescription(): string;