APIv4 - Add support for sql equations
[civicrm-core.git] / Civi / Api4 / Query / SqlFunction.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
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 +--------------------------------------------------------------------+
10 */
11
12 namespace Civi\Api4\Query;
13
14 /**
15 * Base class for all Sql functions.
16 *
17 * @package Civi\Api4\Query
18 */
19 abstract class SqlFunction extends SqlExpression {
20
21 /**
22 * @var array[]
23 */
24 protected $args = [];
25
26 /**
27 * Used for categorizing functions in the UI
28 *
29 * @var string
30 */
31 protected static $category;
32
33 const CATEGORY_AGGREGATE = 'aggregate',
34 CATEGORY_COMPARISON = 'comparison',
35 CATEGORY_DATE = 'date',
36 CATEGORY_MATH = 'math',
37 CATEGORY_STRING = 'string';
38
39 /**
40 * Parse the argument string into an array of function arguments
41 */
42 protected function initialize() {
43 $arg = trim(substr($this->expr, strpos($this->expr, '(') + 1, -1));
44 foreach ($this->getParams() as $idx => $param) {
45 $prefix = NULL;
46 if ($param['name']) {
47 $prefix = $this->captureKeyword([$param['name']], $arg);
48 // Supply api_default
49 if (!$prefix && isset($param['api_default'])) {
50 $this->args[$idx] = [
51 'prefix' => [$param['name']],
52 'expr' => array_map([parent::class, 'convert'], $param['api_default']['expr']),
53 'suffix' => [],
54 ];
55 continue;
56 }
57 if (!$prefix && !$param['optional']) {
58 throw new \API_Exception("Missing {$param['name']} for SQL function " . static::getName());
59 }
60 }
61 elseif ($param['flag_before']) {
62 $prefix = $this->captureKeyword(array_keys($param['flag_before']), $arg);
63 }
64 $this->args[$idx] = [
65 'prefix' => (array) $prefix,
66 'expr' => [],
67 'suffix' => [],
68 ];
69 if ($param['max_expr'] && (!$param['name'] || $param['name'] === $prefix)) {
70 $exprs = $this->captureExpressions($arg, $param['must_be'], TRUE);
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());
73 }
74 $this->args[$idx]['expr'] = $exprs;
75
76 $this->args[$idx]['suffix'] = (array) $this->captureKeyword(array_keys($param['flag_after']), $arg);
77 }
78 }
79 }
80
81 /**
82 * Change $dataType according to output of function
83 *
84 * @see \Civi\Api4\Utils\FormattingUtil::formatOutputValues
85 * @param string $value
86 * @param string $dataType
87 * @return string
88 */
89 public function formatOutputValue($value, &$dataType) {
90 if (static::$dataType) {
91 $dataType = static::$dataType;
92 }
93 return $value;
94 }
95
96 /**
97 * Render the expression for insertion into the sql query
98 *
99 * @param array $fieldList
100 * @return string
101 */
102 public function render(array $fieldList): string {
103 $output = '';
104 foreach ($this->args as $arg) {
105 $rendered = $this->renderArg($arg, $fieldList);
106 if (strlen($rendered)) {
107 $output .= (strlen($output) ? ' ' : '') . $rendered;
108 }
109 }
110 return $this->getName() . '(' . $output . ')';
111 }
112
113 /**
114 * @param array $arg
115 * @param array $fieldList
116 * @return string
117 */
118 private function renderArg($arg, $fieldList): string {
119 $rendered = implode(' ', $arg['prefix']);
120 foreach ($arg['expr'] ?? [] as $idx => $expr) {
121 if (strlen($rendered) || $idx) {
122 $rendered .= $idx ? ', ' : ' ';
123 }
124 $rendered .= $expr->render($fieldList);
125 }
126 if ($arg['suffix']) {
127 $rendered .= (strlen($rendered) ? ' ' : '') . implode(' ', $arg['suffix']);
128 }
129 return $rendered;
130 }
131
132 /**
133 * @inheritDoc
134 */
135 public function getAlias(): string {
136 return $this->alias ?? $this->getName() . ':' . implode('_', $this->fields);
137 }
138
139 /**
140 * Get the name of this sql function.
141 * @return string
142 */
143 public static function getName(): string {
144 $className = static::class;
145 return substr($className, strrpos($className, 'SqlFunction') + 11);
146 }
147
148 /**
149 * Get the param metadata for this sql function.
150 * @return array
151 */
152 final public static function getParams(): array {
153 $params = [];
154 foreach (static::params() as $param) {
155 // Merge in defaults to ensure each param has these properties
156 $params[] = $param + [
157 'name' => NULL,
158 'min_expr' => 1,
159 'max_expr' => 1,
160 'flag_before' => [],
161 'flag_after' => [],
162 'optional' => FALSE,
163 'must_be' => ['SqlField', 'SqlFunction', 'SqlString', 'SqlNumber', 'SqlNull'],
164 'api_default' => NULL,
165 ];
166 }
167 return $params;
168 }
169
170 abstract protected static function params(): array;
171
172 /**
173 * Get the arguments passed to this sql function instance.
174 * @return array[]
175 */
176 public function getArgs(): array {
177 return $this->args;
178 }
179
180 /**
181 * @return string
182 */
183 public static function getCategory(): string {
184 return static::$category;
185 }
186
187 /**
188 * @return string
189 */
190 abstract public static function getTitle(): string;
191
192 /**
193 * @return string
194 */
195 abstract public static function getDescription(): string;
196
197 }