Merge pull request #17035 from civicrm/5.25
[civicrm-core.git] / Civi / Api4 / Query / SqlExpression.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 SqlColumn, SqlString, SqlBool, and SqlFunction classes.
16 *
17 * These are used to validate and format sql expressions in Api4 select queries.
18 *
19 * @package Civi\Api4\Query
20 */
21 abstract class SqlExpression {
22
23 /**
24 * @var array
25 */
26 protected $fields = [];
27
28 /**
29 * The SELECT alias (if null it will be calculated by getAlias)
30 * @var string|null
31 */
32 protected $alias;
33
34 /**
35 * The raw expression, minus the alias.
36 * @var string
37 */
38 protected $expr = '';
39
40 /**
41 * SqlFunction constructor.
42 * @param string $expr
43 * @param string|null $alias
44 */
45 public function __construct(string $expr, $alias = NULL) {
46 $this->expr = $expr;
47 $this->alias = $alias;
48 $this->initialize();
49 }
50
51 abstract protected function initialize();
52
53 /**
54 * Converts a string to a SqlExpression object.
55 *
56 * E.g. the expression "SUM(foo)" would return a SqlFunctionSUM object.
57 *
58 * @param string $expression
59 * @param bool $parseAlias
60 * @param array $mustBe
61 * @param array $cantBe
62 * @return SqlExpression
63 * @throws \API_Exception
64 */
65 public static function convert(string $expression, $parseAlias = FALSE, $mustBe = [], $cantBe = ['SqlWild']) {
66 $as = $parseAlias ? strrpos($expression, ' AS ') : FALSE;
67 $expr = $as ? substr($expression, 0, $as) : $expression;
68 $alias = $as ? \CRM_Utils_String::munge(substr($expression, $as + 4)) : NULL;
69 $bracketPos = strpos($expr, '(');
70 $firstChar = substr($expr, 0, 1);
71 $lastChar = substr($expr, -1);
72 // If there are brackets but not the first character, we have a function
73 if ($bracketPos && $lastChar === ')') {
74 $fnName = substr($expr, 0, $bracketPos);
75 if ($fnName !== strtoupper($fnName)) {
76 throw new \API_Exception('Sql function must be uppercase.');
77 }
78 $className = 'SqlFunction' . $fnName;
79 }
80 // String expression
81 elseif ($firstChar === $lastChar && in_array($firstChar, ['"', "'"], TRUE)) {
82 $className = 'SqlString';
83 }
84 elseif ($expr === 'NULL') {
85 $className = 'SqlNull';
86 }
87 elseif ($expr === '*') {
88 $className = 'SqlWild';
89 }
90 elseif (is_numeric($expr)) {
91 $className = 'SqlNumber';
92 }
93 // If none of the above, assume it's a field name
94 else {
95 $className = 'SqlField';
96 }
97 $className = __NAMESPACE__ . '\\' . $className;
98 if (!class_exists($className)) {
99 throw new \API_Exception('Unable to parse sql expression: ' . $expression);
100 }
101 $sqlExpression = new $className($expr, $alias);
102 foreach ($cantBe as $cant) {
103 if (is_a($sqlExpression, __NAMESPACE__ . '\\' . $cant)) {
104 throw new \API_Exception('Illegal sql expression.');
105 }
106 }
107 if ($mustBe) {
108 foreach ($mustBe as $must) {
109 if (is_a($sqlExpression, __NAMESPACE__ . '\\' . $must)) {
110 return $sqlExpression;
111 }
112 }
113 throw new \API_Exception('Illegal sql expression.');
114 }
115 return $sqlExpression;
116 }
117
118 /**
119 * Returns the field names of all sql columns that are arguments to this expression.
120 *
121 * @return array
122 */
123 public function getFields(): array {
124 return $this->fields;
125 }
126
127 /**
128 * Renders expression to a sql string, replacing field names with column names.
129 *
130 * @param array $fieldList
131 * @return string
132 */
133 abstract public function render(array $fieldList): string;
134
135 /**
136 * @return string
137 */
138 public function getExpr(): string {
139 return $this->expr;
140 }
141
142 /**
143 * Returns the alias to use for SELECT AS.
144 *
145 * @return string
146 */
147 public function getAlias(): string {
148 return $this->alias ?? $this->fields[0] ?? \CRM_Utils_String::munge($this->expr);
149 }
150
151 }