APIv4 Organize debug sql into a single array
[civicrm-core.git] / Civi / Api4 / Event / Subscriber / PostSelectQuerySubscriber.php
CommitLineData
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
22namespace Civi\Api4\Event\Subscriber;
23
24use Civi\Api4\Event\Events;
25use Civi\Api4\Event\PostSelectQueryEvent;
26use Civi\Api4\Query\Api4SelectQuery;
27use Civi\Api4\Utils\ArrayInsertionUtil;
2929a8fb 28use Civi\Api4\Utils\FormattingUtil;
19b53e5b
C
29use Symfony\Component\EventDispatcher\EventSubscriberInterface;
30
31/**
32 * Changes the results of a select query, doing 1-n joins and unserializing data
33 */
34class 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}