Commit | Line | Data |
---|---|---|
f0acec37 CW |
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 | protected static $params = []; | |
22 | ||
23 | protected $args = []; | |
24 | ||
e7f6def6 CW |
25 | /** |
26 | * Used for categorizing functions in the UI | |
27 | * | |
28 | * @var string | |
29 | */ | |
30 | protected static $category; | |
31 | ||
32 | const CATEGORY_AGGREGATE = 'aggregate', | |
33 | CATEGORY_COMPARISON = 'comparison', | |
34 | CATEGORY_DATE = 'date', | |
35 | CATEGORY_MATH = 'math', | |
36 | CATEGORY_STRING = 'string'; | |
37 | ||
f0acec37 CW |
38 | /** |
39 | * Parse the argument string into an array of function arguments | |
40 | */ | |
41 | protected function initialize() { | |
42 | $arg = trim(substr($this->expr, strpos($this->expr, '(') + 1, -1)); | |
43 | foreach ($this->getParams() as $param) { | |
44 | $prefix = $this->captureKeyword($param['prefix'], $arg); | |
45 | if ($param['expr'] && isset($prefix) || in_array('', $param['prefix']) || !$param['optional']) { | |
46 | $this->captureExpressions($arg, $param['expr'], $param['must_be'], $param['cant_be']); | |
47 | $this->captureKeyword($param['suffix'], $arg); | |
48 | } | |
49 | } | |
50 | } | |
51 | ||
52 | /** | |
53 | * Shift a keyword off the beginning of the argument string and into the argument array. | |
54 | * | |
55 | * @param array $keywords | |
56 | * Whitelist of keywords | |
57 | * @param string $arg | |
58 | * @return mixed|null | |
59 | */ | |
60 | private function captureKeyword($keywords, &$arg) { | |
61 | foreach (array_filter($keywords) as $key) { | |
62 | if (strpos($arg, $key . ' ') === 0) { | |
63 | $this->args[] = $key; | |
64 | $arg = ltrim(substr($arg, strlen($key))); | |
65 | return $key; | |
66 | } | |
67 | } | |
68 | return NULL; | |
69 | } | |
70 | ||
71 | /** | |
72 | * Shifts 0 or more expressions off the argument string and into the argument array | |
73 | * | |
74 | * @param string $arg | |
75 | * @param int $limit | |
76 | * @param array $mustBe | |
77 | * @param array $cantBe | |
78 | * @throws \API_Exception | |
79 | */ | |
80 | private function captureExpressions(&$arg, $limit, $mustBe, $cantBe) { | |
81 | $captured = 0; | |
82 | $arg = ltrim($arg); | |
83 | while ($arg) { | |
84 | $item = $this->captureExpression($arg); | |
85 | $arg = ltrim(substr($arg, strlen($item))); | |
86 | $expr = SqlExpression::convert($item, FALSE, $mustBe, $cantBe); | |
87 | $this->fields = array_merge($this->fields, $expr->getFields()); | |
88 | if ($captured) { | |
89 | $this->args[] = ','; | |
90 | } | |
91 | $this->args[] = $expr; | |
92 | $captured++; | |
93 | // Keep going if we have a comma indicating another expression follows | |
94 | if ($captured < $limit && substr($arg, 0, 1) === ',') { | |
95 | $arg = ltrim(substr($arg, 1)); | |
96 | } | |
97 | else { | |
98 | return; | |
99 | } | |
100 | } | |
101 | } | |
102 | ||
103 | /** | |
104 | * Scans the beginning of a string for an expression; stops when it hits delimiter | |
105 | * | |
106 | * @param $arg | |
107 | * @return string | |
108 | */ | |
109 | private function captureExpression($arg) { | |
110 | $chars = str_split($arg); | |
111 | $isEscaped = $quote = NULL; | |
112 | $item = ''; | |
113 | $quotes = ['"', "'"]; | |
114 | $brackets = [ | |
115 | ')' => '(', | |
116 | ]; | |
117 | $enclosures = array_fill_keys($brackets, 0); | |
118 | foreach ($chars as $index => $char) { | |
119 | if (!$isEscaped && in_array($char, $quotes, TRUE)) { | |
120 | // Open quotes - we'll ignore everything inside | |
121 | if (!$quote) { | |
122 | $quote = $char; | |
123 | } | |
124 | // Close quotes | |
125 | elseif ($char === $quote) { | |
126 | $quote = NULL; | |
127 | } | |
128 | } | |
129 | if (!$quote) { | |
130 | // Delineates end of expression | |
131 | if (($char == ',' || $char == ' ') && !array_filter($enclosures)) { | |
132 | return $item; | |
133 | } | |
134 | // Open brackets - we'll ignore delineators inside | |
135 | if (isset($enclosures[$char])) { | |
136 | $enclosures[$char]++; | |
137 | } | |
138 | // Close brackets | |
139 | if (isset($brackets[$char]) && $enclosures[$brackets[$char]]) { | |
140 | $enclosures[$brackets[$char]]--; | |
141 | } | |
142 | } | |
143 | $item .= $char; | |
144 | // We are escaping the next char if this is a backslash not preceded by an odd number of backslashes | |
145 | $isEscaped = $char === '\\' && ((strlen($item) - strlen(rtrim($item, '\\'))) % 2); | |
146 | } | |
147 | return $item; | |
148 | } | |
149 | ||
150 | public function render(array $fieldList): string { | |
151 | $output = $this->getName() . '('; | |
152 | foreach ($this->args as $index => $arg) { | |
153 | if ($index && $arg !== ',') { | |
154 | $output .= ' '; | |
155 | } | |
156 | if (is_object($arg)) { | |
157 | $output .= $arg->render($fieldList); | |
158 | } | |
159 | else { | |
160 | $output .= $arg; | |
161 | } | |
162 | } | |
163 | return $output . ')'; | |
164 | } | |
165 | ||
166 | /** | |
167 | * @inheritDoc | |
168 | */ | |
169 | public function getAlias(): string { | |
170 | return $this->alias ?? $this->getName() . ':' . implode('_', $this->fields); | |
171 | } | |
172 | ||
173 | /** | |
174 | * Get the name of this sql function. | |
175 | * @return string | |
176 | */ | |
177 | public static function getName(): string { | |
178 | $className = static::class; | |
179 | return substr($className, strrpos($className, 'SqlFunction') + 11); | |
180 | } | |
181 | ||
182 | /** | |
183 | * Get the param metadata for this sql function. | |
184 | * @return array | |
185 | */ | |
186 | public static function getParams(): array { | |
187 | $params = []; | |
188 | foreach (static::$params as $param) { | |
189 | // Merge in defaults to ensure each param has these properties | |
190 | $params[] = $param + [ | |
191 | 'prefix' => [], | |
192 | 'expr' => 1, | |
193 | 'suffix' => [], | |
194 | 'optional' => FALSE, | |
195 | 'must_be' => [], | |
196 | 'cant_be' => ['SqlWild'], | |
197 | ]; | |
198 | } | |
199 | return $params; | |
200 | } | |
201 | ||
e7f6def6 CW |
202 | /** |
203 | * @return string | |
204 | */ | |
205 | public static function getCategory(): string { | |
206 | return static::$category; | |
207 | } | |
208 | ||
9cae8a07 CW |
209 | /** |
210 | * @return string | |
211 | */ | |
212 | abstract public static function getTitle(): string; | |
213 | ||
f0acec37 | 214 | } |