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