Commit | Line | Data |
---|---|---|
19b53e5b C |
1 | <?php |
2 | ||
380f3545 TO |
3 | /* |
4 | +--------------------------------------------------------------------+ | |
41498ac5 | 5 | | Copyright CiviCRM LLC. All rights reserved. | |
380f3545 | 6 | | | |
41498ac5 TO |
7 | | This work is published under the GNU AGPLv3 license with some | |
8 | | permitted exceptions and without any warranty. For full license | | |
9 | | and copyright information, see https://civicrm.org/licensing | | |
380f3545 TO |
10 | +--------------------------------------------------------------------+ |
11 | */ | |
12 | ||
13 | /** | |
14 | * | |
15 | * @package CRM | |
ca5cec67 | 16 | * @copyright CiviCRM LLC https://civicrm.org/licensing |
380f3545 TO |
17 | * $Id$ |
18 | * | |
19 | */ | |
20 | ||
21 | ||
19b53e5b C |
22 | namespace Civi\Api4\Event\Subscriber; |
23 | ||
24 | use Civi\Api4\Event\Events; | |
25 | use Civi\Api4\Event\PostSelectQueryEvent; | |
26 | use Civi\Api4\Query\Api4SelectQuery; | |
27 | use Civi\Api4\Utils\ArrayInsertionUtil; | |
2929a8fb | 28 | use Civi\Api4\Utils\FormattingUtil; |
19b53e5b C |
29 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; |
30 | ||
31 | /** | |
32 | * Changes the results of a select query, doing 1-n joins and unserializing data | |
33 | */ | |
34 | class PostSelectQuerySubscriber implements EventSubscriberInterface { | |
35 | ||
36 | /** | |
e15f9453 | 37 | * @inheritDoc |
19b53e5b C |
38 | */ |
39 | public static function getSubscribedEvents() { | |
40 | return [ | |
41 | Events::POST_SELECT_QUERY => 'onPostQuery', | |
42 | ]; | |
43 | } | |
44 | ||
45 | /** | |
46 | * @param \Civi\Api4\Event\PostSelectQueryEvent $event | |
47 | */ | |
48 | public function onPostQuery(PostSelectQueryEvent $event) { | |
49 | $results = $event->getResults(); | |
50 | $event->setResults($this->postRun($results, $event->getQuery())); | |
51 | } | |
52 | ||
53 | /** | |
54 | * @param array $results | |
55 | * @param \Civi\Api4\Query\Api4SelectQuery $query | |
56 | * | |
57 | * @return array | |
58 | */ | |
59 | protected function postRun(array $results, Api4SelectQuery $query) { | |
60 | if (empty($results)) { | |
61 | return $results; | |
62 | } | |
63 | ||
2929a8fb | 64 | FormattingUtil::formatOutputValues($results, $query->getApiFieldSpec(), $query->getEntity()); |
19b53e5b C |
65 | |
66 | // Group the selects to avoid queries for each field | |
67 | $groupedSelects = $this->getNtoManyJoinSelects($query); | |
68 | foreach ($groupedSelects as $finalAlias => $selects) { | |
69 | $joinPath = $query->getPathJoinTypes($selects[0]); | |
70 | $selects = $this->formatSelects($finalAlias, $selects, $query); | |
71 | $joinResults = $this->getJoinResults($query, $finalAlias, $selects); | |
72 | $this->formatJoinResults($joinResults, $query, $finalAlias); | |
73 | ||
74 | // Insert join results into original result | |
75 | foreach ($results as &$primaryResult) { | |
76 | $baseId = $primaryResult['id']; | |
77 | $filtered = array_filter($joinResults, function ($res) use ($baseId) { | |
aec2fb31 | 78 | return ($res['_base_id'] == $baseId); |
19b53e5b C |
79 | }); |
80 | $filtered = array_values($filtered); | |
81 | ArrayInsertionUtil::insert($primaryResult, $joinPath, $filtered); | |
82 | } | |
83 | } | |
84 | ||
85 | return array_values($results); | |
86 | } | |
87 | ||
88 | /** | |
89 | * @param array $joinResults | |
90 | * @param \Civi\Api4\Query\Api4SelectQuery $query | |
91 | * @param string $alias | |
92 | */ | |
93 | private function formatJoinResults(&$joinResults, $query, $alias) { | |
94 | $join = $query->getJoinedTable($alias); | |
95 | $fields = []; | |
96 | foreach ($join->getEntityFields() as $field) { | |
97 | $name = explode('.', $field->getName()); | |
98 | $fields[array_pop($name)] = $field->toArray(); | |
99 | } | |
100 | if ($fields) { | |
2929a8fb | 101 | FormattingUtil::formatOutputValues($joinResults, $fields, $join->getEntity()); |
19b53e5b C |
102 | } |
103 | } | |
104 | ||
105 | /** | |
106 | * Find only those joins that need to be handled by a separate query and weren't done in the main query. | |
107 | * | |
108 | * @param \Civi\Api4\Query\Api4SelectQuery $query | |
109 | * | |
110 | * @return array | |
111 | */ | |
112 | private function getNtoManyJoinSelects(Api4SelectQuery $query) { | |
113 | $fkAliases = $query->getFkSelectAliases(); | |
114 | $joinedDotSelects = array_filter( | |
115 | $query->getSelect(), | |
116 | function ($select) use ($fkAliases, $query) { | |
117 | return isset($fkAliases[$select]) && array_filter($query->getPathJoinTypes($select)); | |
118 | } | |
119 | ); | |
120 | ||
121 | $selects = []; | |
122 | // group related selects by alias so they can be executed in one query | |
123 | foreach ($joinedDotSelects as $select) { | |
124 | $parts = explode('.', $select); | |
125 | $finalAlias = $parts[count($parts) - 2]; | |
126 | $selects[$finalAlias][] = $select; | |
127 | } | |
128 | ||
129 | // sort by depth, e.g. email selects should be done before email.location | |
130 | uasort($selects, function ($a, $b) { | |
131 | $aFirst = $a[0]; | |
132 | $bFirst = $b[0]; | |
133 | return substr_count($aFirst, '.') > substr_count($bFirst, '.'); | |
134 | }); | |
135 | ||
136 | return $selects; | |
137 | } | |
138 | ||
139 | /** | |
140 | * @param array $selects | |
141 | * @param $serializationType | |
142 | * @param \Civi\Api4\Query\Api4SelectQuery $query | |
143 | * | |
144 | * @return array | |
145 | */ | |
146 | private function getResultsForSerializedField( | |
147 | array $selects, | |
148 | $serializationType, | |
149 | Api4SelectQuery $query | |
150 | ) { | |
151 | // Get the alias (Selects are grouped and all target the same table) | |
152 | $sampleField = current($selects); | |
153 | $alias = strstr($sampleField, '.', TRUE); | |
154 | ||
155 | // Fetch the results with the serialized field | |
156 | $selects['serialized'] = $query::MAIN_TABLE_ALIAS . '.' . $alias; | |
157 | $serializedResults = $this->runWithNewSelects($selects, $query); | |
158 | $newResults = []; | |
159 | ||
160 | // Create a new results array, with a separate entry for each option value | |
161 | foreach ($serializedResults as $result) { | |
162 | $optionValues = \CRM_Core_DAO::unSerializeField( | |
163 | $result['serialized'], | |
164 | $serializationType | |
165 | ); | |
166 | unset($result['serialized']); | |
167 | foreach ($optionValues as $value) { | |
168 | $newResults[] = array_merge($result, ['value' => $value]); | |
169 | } | |
170 | } | |
171 | ||
172 | $optionValueValues = array_unique(array_column($newResults, 'value')); | |
173 | $optionValues = $this->getOptionValuesFromValues( | |
174 | $selects, | |
175 | $query, | |
176 | $optionValueValues | |
177 | ); | |
178 | $valueField = $alias . '.value'; | |
179 | ||
180 | // Index by value | |
181 | foreach ($optionValues as $key => $subResult) { | |
182 | $optionValues[$subResult['value']] = $subResult; | |
183 | unset($subResult[$key]); | |
184 | ||
185 | // Exclude 'value' if not in original selects | |
186 | if (!in_array($valueField, $selects)) { | |
187 | unset($optionValues[$subResult['value']]['value']); | |
188 | } | |
189 | } | |
190 | ||
191 | // Replace serialized with the sub-select results | |
192 | foreach ($newResults as &$result) { | |
193 | $result = array_merge($result, $optionValues[$result['value']]); | |
194 | unset($result['value']); | |
195 | } | |
196 | ||
197 | return $newResults; | |
198 | } | |
199 | ||
200 | /** | |
201 | * Prepares selects for the subquery to fetch join results | |
202 | * | |
203 | * @param string $alias | |
204 | * @param array $selects | |
205 | * @param \Civi\Api4\Query\Api4SelectQuery $query | |
206 | * | |
207 | * @return array | |
208 | */ | |
209 | private function formatSelects($alias, $selects, Api4SelectQuery $query) { | |
210 | $mainAlias = $query::MAIN_TABLE_ALIAS; | |
211 | $selectFields = []; | |
212 | ||
213 | foreach ($selects as $select) { | |
214 | $selectAlias = $query->getFkSelectAliases()[$select]; | |
215 | $fieldAlias = substr($select, strrpos($select, '.') + 1); | |
216 | $selectFields[$fieldAlias] = $selectAlias; | |
217 | } | |
218 | ||
219 | $firstSelect = $selects[0]; | |
220 | $pathParts = explode('.', $firstSelect); | |
221 | $numParts = count($pathParts); | |
222 | $parentAlias = $numParts > 2 ? $pathParts[$numParts - 3] : $mainAlias; | |
223 | ||
224 | $selectFields['id'] = sprintf('%s.id', $alias); | |
225 | $selectFields['_parent_id'] = $parentAlias . '.id'; | |
226 | $selectFields['_base_id'] = $mainAlias . '.id'; | |
227 | ||
228 | return $selectFields; | |
229 | } | |
230 | ||
231 | /** | |
232 | * @param array $selects | |
233 | * @param \Civi\Api4\Query\Api4SelectQuery $query | |
234 | * | |
235 | * @return array | |
236 | */ | |
237 | private function runWithNewSelects(array $selects, Api4SelectQuery $query) { | |
238 | $aliasedSelects = array_map(function ($field, $alias) { | |
239 | return sprintf('%s as "%s"', $field, $alias); | |
240 | }, $selects, array_keys($selects)); | |
241 | ||
242 | $newSelect = sprintf('SELECT DISTINCT %s', implode(", ", $aliasedSelects)); | |
b65fa6dc CW |
243 | $sql = $query->getQuery()->toSQL(); |
244 | // Replace the "SELECT" clause | |
245 | $sql = $newSelect . substr($sql, strpos($sql, "\nFROM")); | |
246 | ||
247 | if (is_array($query->debugOutput)) { | |
0f7babcc | 248 | $query->debugOutput['sql'][] = $sql; |
b65fa6dc | 249 | } |
19b53e5b C |
250 | |
251 | $relatedResults = []; | |
252 | $resultDAO = \CRM_Core_DAO::executeQuery($sql); | |
253 | while ($resultDAO->fetch()) { | |
254 | $relatedResult = []; | |
255 | foreach ($selects as $alias => $column) { | |
256 | $returnName = $alias; | |
257 | $alias = str_replace('.', '_', $alias); | |
258 | if (property_exists($resultDAO, $alias)) { | |
259 | $relatedResult[$returnName] = $resultDAO->$alias; | |
260 | } | |
261 | }; | |
262 | $relatedResults[] = $relatedResult; | |
263 | } | |
264 | ||
265 | return $relatedResults; | |
266 | } | |
267 | ||
268 | /** | |
269 | * @param \Civi\Api4\Query\Api4SelectQuery $query | |
270 | * @param $alias | |
271 | * @param $selects | |
272 | * @return array | |
273 | */ | |
274 | protected function getJoinResults(Api4SelectQuery $query, $alias, $selects) { | |
275 | $apiFieldSpec = $query->getApiFieldSpec(); | |
276 | if (!empty($apiFieldSpec[$alias]['serialize'])) { | |
277 | $type = $apiFieldSpec[$alias]['serialize']; | |
278 | $joinResults = $this->getResultsForSerializedField($selects, $type, $query); | |
279 | } | |
280 | else { | |
281 | $joinResults = $this->runWithNewSelects($selects, $query); | |
282 | } | |
283 | ||
284 | // Remove results with no matching entries | |
285 | $joinResults = array_filter($joinResults, function ($result) { | |
286 | return !empty($result['id']); | |
287 | }); | |
288 | ||
289 | return $joinResults; | |
290 | } | |
291 | ||
292 | /** | |
293 | * Get all the option_value values required in the query | |
294 | * | |
295 | * @param array $selects | |
296 | * @param \Civi\Api4\Query\Api4SelectQuery $query | |
297 | * @param array $values | |
298 | * | |
299 | * @return array | |
300 | */ | |
301 | private function getOptionValuesFromValues( | |
302 | array $selects, | |
303 | Api4SelectQuery $query, | |
304 | array $values | |
305 | ) { | |
306 | $sampleField = current($selects); | |
307 | $alias = strstr($sampleField, '.', TRUE); | |
308 | ||
309 | // Get the option value table that was joined | |
310 | $relatedTable = NULL; | |
311 | foreach ($query->getJoinedTables() as $joinedTable) { | |
312 | if ($joinedTable->getAlias() === $alias) { | |
313 | $relatedTable = $joinedTable; | |
314 | } | |
315 | } | |
316 | ||
317 | // We only want subselects related to the joined table | |
318 | $subSelects = array_filter($selects, function ($select) use ($alias) { | |
319 | return strpos($select, $alias) === 0; | |
320 | }); | |
321 | ||
322 | // Fetch all related option_value entries | |
323 | $valueField = $alias . '.value'; | |
324 | $subSelects[] = $valueField; | |
325 | $tableName = $relatedTable->getTargetTable(); | |
326 | $conditions = $relatedTable->getExtraJoinConditions(); | |
327 | $conditions[] = $valueField . ' IN ("' . implode('", "', $values) . '")'; | |
328 | $subQuery = new \CRM_Utils_SQL_Select($tableName . ' ' . $alias); | |
329 | $subQuery->where($conditions); | |
330 | $subQuery->select($subSelects); | |
331 | $subResults = $subQuery->execute()->fetchAll(); | |
332 | ||
333 | return $subResults; | |
334 | } | |
335 | ||
336 | } |