PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.12.1
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.12.1
5.12.1 5.12.0 5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / ArchiveProcessor / RecordBuilder.php
matomo / app / core / ArchiveProcessor Last commit date
ArchivingStatus.php 6 months ago BlobTableAggregator.php 2 weeks ago Loader.php 2 weeks ago LoaderLock.php 2 weeks ago Parameters.php 2 weeks ago PluginsArchiver.php 2 weeks ago PluginsArchiverException.php 2 years ago Record.php 2 weeks ago RecordBuilder.php 2 weeks ago Rules.php 2 weeks ago
RecordBuilder.php
530 lines
1 <?php
2
3 /**
4 * Matomo - free/libre analytics platform
5 *
6 * @link https://matomo.org
7 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8 */
9 namespace Piwik\ArchiveProcessor;
10
11 use Piwik\Archive;
12 use Piwik\ArchiveProcessor;
13 use Piwik\Common;
14 use Piwik\DataTable;
15 use Piwik\DataTable\Row;
16 use Piwik\Piwik;
17 /**
18 * Inherit from this class to define archiving logic for one or more records.
19 */
20 abstract class RecordBuilder
21 {
22 /**
23 * @var int|null
24 */
25 protected $maxRowsInTable;
26 /**
27 * @var int|null
28 */
29 protected $maxRowsInSubtable;
30 /**
31 * @var string|int|null
32 */
33 protected $columnToSortByBeforeTruncation;
34 /**
35 * @var array|null
36 */
37 protected $columnAggregationOps;
38 /**
39 * @var array<string|int,string|int>|null
40 */
41 protected $columnToRenameAfterAggregation;
42 /**
43 * @param array|null $columnAggregationOps
44 * @param array<string|int,string|int>|null $columnToRenameAfterAggregation
45 */
46 public function __construct(?int $maxRowsInTable = null, ?int $maxRowsInSubtable = null, ?string $columnToSortByBeforeTruncation = null, ?array $columnAggregationOps = null, ?array $columnToRenameAfterAggregation = null)
47 {
48 $this->maxRowsInTable = $maxRowsInTable;
49 $this->maxRowsInSubtable = $maxRowsInSubtable;
50 $this->columnToSortByBeforeTruncation = $columnToSortByBeforeTruncation;
51 $this->columnAggregationOps = $columnAggregationOps;
52 $this->columnToRenameAfterAggregation = $columnToRenameAfterAggregation;
53 }
54 public function isEnabled(ArchiveProcessor $archiveProcessor) : bool
55 {
56 return \true;
57 }
58 /**
59 * Uses the protected `aggregate()` function to build records by aggregating log table data directly, then
60 * inserts them as archive data.
61 */
62 public function buildFromLogs(ArchiveProcessor $archiveProcessor) : void
63 {
64 if (!$this->isEnabled($archiveProcessor)) {
65 return;
66 }
67 $recordsBuilt = $this->getRecordMetadata($archiveProcessor);
68 $recordMetadataByName = [];
69 foreach ($recordsBuilt as $recordMetadata) {
70 if (!$recordMetadata instanceof \Piwik\ArchiveProcessor\Record) {
71 continue;
72 }
73 $recordMetadataByName[$recordMetadata->getName()] = $recordMetadata;
74 }
75 $numericRecords = [];
76 $records = $this->aggregate($archiveProcessor);
77 foreach ($records as $recordName => $recordValue) {
78 if (empty($recordMetadataByName[$recordName])) {
79 if ($recordValue instanceof DataTable) {
80 Common::destroy($recordValue);
81 }
82 continue;
83 }
84 if ($recordValue instanceof DataTable) {
85 $record = $recordMetadataByName[$recordName];
86 $maxRowsInTable = $record->getMaxRowsInTable() ?? $this->maxRowsInTable;
87 $maxRowsInSubtable = $record->getMaxRowsInSubtable() ?? $this->maxRowsInSubtable;
88 $columnToSortByBeforeTruncation = $record->getColumnToSortByBeforeTruncation() ?? $this->columnToSortByBeforeTruncation;
89 $this->insertBlobRecord($archiveProcessor, $recordName, $recordValue, $maxRowsInTable, $maxRowsInSubtable, $columnToSortByBeforeTruncation);
90 Common::destroy($recordValue);
91 } else {
92 // collect numeric records so we can insert them all at once
93 $numericRecords[$recordName] = $recordValue;
94 }
95 }
96 unset($records);
97 if (!empty($numericRecords)) {
98 $archiveProcessor->insertNumericRecords($numericRecords);
99 }
100 }
101 /**
102 * Builds records for non-day periods by aggregating day records together, then inserts
103 * them as archive data.
104 */
105 public function buildForNonDayPeriod(ArchiveProcessor $archiveProcessor) : void
106 {
107 if (!$this->isEnabled($archiveProcessor)) {
108 return;
109 }
110 $requestedReports = $archiveProcessor->getParams()->getArchiveOnlyReportAsArray();
111 $foundRequestedReports = $archiveProcessor->getParams()->getFoundRequestedReports();
112 $recordsBuilt = $this->getRecordMetadata($archiveProcessor);
113 $numericRecords = array_filter($recordsBuilt, function (\Piwik\ArchiveProcessor\Record $r) {
114 return $r->getType() == \Piwik\ArchiveProcessor\Record::TYPE_NUMERIC;
115 });
116 $blobRecords = array_filter($recordsBuilt, function (\Piwik\ArchiveProcessor\Record $r) {
117 return $r->getType() == \Piwik\ArchiveProcessor\Record::TYPE_BLOB;
118 });
119 $blobRecordsByName = [];
120 foreach ($blobRecords as $blobRecord) {
121 $blobRecordsByName[$blobRecord->getName()] = $blobRecord;
122 }
123 $aggregatedCounts = [];
124 foreach ($blobRecords as $record) {
125 $flatRecordName = $record->getBuiltFromFlatRecord();
126 if (empty($flatRecordName) || !in_array($flatRecordName, $requestedReports)) {
127 continue;
128 }
129 // If the flat record is requested directly, also force aggregation of the corresponding
130 // hierarchical record so the API can still read the expected hierarchical blob.
131 if (!in_array($record->getName(), $requestedReports)) {
132 $requestedReports[] = $record->getName();
133 }
134 // We are about to rebuild this record from flat data, so treat it as not-found and
135 // make sure it is re-aggregated even if a previous archive row exists.
136 $indexInFoundRecords = array_search($record->getName(), $foundRequestedReports);
137 if ($indexInFoundRecords !== \false) {
138 unset($foundRequestedReports[$indexInFoundRecords]);
139 }
140 }
141 // make sure if there are requested numeric records that depend on blob records, that the blob records will be archived first
142 foreach ($numericRecords as $record) {
143 if (empty($record->getCountOfRecordName()) || !in_array($record->getName(), $requestedReports)) {
144 continue;
145 }
146 $dependentRecordName = $record->getCountOfRecordName();
147 if (!in_array($dependentRecordName, $requestedReports)) {
148 $requestedReports[] = $dependentRecordName;
149 }
150 // we need to aggregate the blob record to get the count, so even if it's found, we must re-aggregate it
151 // TODO: this could potentially be optimized away, but it would be non-trivial given the current ArchiveProcessor API
152 $indexInFoundRecords = array_search($dependentRecordName, $foundRequestedReports);
153 if ($indexInFoundRecords !== \false) {
154 unset($foundRequestedReports[$indexInFoundRecords]);
155 }
156 }
157 $processedFlatRecords = [];
158 foreach ($blobRecords as $record) {
159 if (!empty($requestedReports) && (!in_array($record->getName(), $requestedReports) || in_array($record->getName(), $foundRequestedReports))) {
160 continue;
161 }
162 if (isset($processedFlatRecords[$record->getName()])) {
163 continue;
164 }
165 $maxRowsInTable = $record->getMaxRowsInTable() ?? $this->maxRowsInTable;
166 $maxRowsInSubtable = $record->getMaxRowsInSubtable() ?? $this->maxRowsInSubtable;
167 $columnToSortByBeforeTruncation = $record->getColumnToSortByBeforeTruncation() ?? $this->columnToSortByBeforeTruncation;
168 $columnToRenameAfterAggregation = $record->getColumnToRenameAfterAggregation() ?? $this->columnToRenameAfterAggregation;
169 $columnAggregationOps = $record->getBlobColumnAggregationOps() ?? $this->columnAggregationOps;
170 if ($this->aggregateBuiltFromFlatRecordForNonDay($archiveProcessor, $record, $blobRecordsByName, $columnAggregationOps, $columnToRenameAfterAggregation, $columnToSortByBeforeTruncation, $processedFlatRecords)) {
171 continue;
172 }
173 // only do recursive row counts if there is a numeric record that depends on it
174 $countRecursiveRows = $countLeafRows = [];
175 foreach ($numericRecords as $numeric) {
176 if ($numeric->getCountOfRecordName() == $record->getName()) {
177 if ($numeric->getCountOfRecordNameIsRecursive()) {
178 $countRecursiveRows[] = $numeric->getCountOfRecordName();
179 }
180 if ($numeric->getCountOfRecordNameIsForLeafs()) {
181 $countLeafRows[] = $numeric->getCountOfRecordName();
182 }
183 }
184 }
185 $recordTransform = $record->getAggregatedRecordTransform();
186 $postAggregationTransform = $recordTransform === null ? null : function (DataTable $table) use($recordTransform, $archiveProcessor, $record) : void {
187 $recordTransform($table, $archiveProcessor, $record);
188 };
189 $counts = $archiveProcessor->aggregateDataTableRecords($record->getName(), $maxRowsInTable, $maxRowsInSubtable, $columnToSortByBeforeTruncation, $columnAggregationOps, $columnToRenameAfterAggregation, $countRecursiveRows, $countLeafRows, $postAggregationTransform);
190 $aggregatedCounts = array_merge($aggregatedCounts, $counts);
191 }
192 if (!empty($numericRecords)) {
193 // handle metrics that are aggregated using metric values from child periods
194 $autoAggregateMetrics = array_filter($numericRecords, function (\Piwik\ArchiveProcessor\Record $r) {
195 return empty($r->getCountOfRecordName());
196 });
197 $autoAggregateMetrics = array_map(function (\Piwik\ArchiveProcessor\Record $r) {
198 return $r->getName();
199 }, $autoAggregateMetrics);
200 if (!empty($requestedReports)) {
201 $autoAggregateMetrics = array_filter($autoAggregateMetrics, function ($name) use($requestedReports, $foundRequestedReports) {
202 return in_array($name, $requestedReports) && !in_array($name, $foundRequestedReports);
203 });
204 }
205 $autoAggregateMetrics = array_values($autoAggregateMetrics);
206 if (!empty($autoAggregateMetrics)) {
207 $archiveProcessor->aggregateNumericMetrics($autoAggregateMetrics, $this->columnAggregationOps);
208 }
209 // handle metrics that are set to counts of blob records
210 $recordCountMetricValues = [];
211 $recordCountMetrics = array_filter($numericRecords, function (\Piwik\ArchiveProcessor\Record $r) {
212 return !empty($r->getCountOfRecordName());
213 });
214 foreach ($recordCountMetrics as $record) {
215 $dependentRecordName = $record->getCountOfRecordName();
216 if (empty($aggregatedCounts[$dependentRecordName])) {
217 continue;
218 // dependent record not archived, so skip this metric
219 }
220 $count = $aggregatedCounts[$dependentRecordName];
221 if ($record->getCountOfRecordNameIsForLeafs()) {
222 $recordCountMetricValues[$record->getName()] = $count['leafs'];
223 } elseif ($record->getCountOfRecordNameIsRecursive()) {
224 $recordCountMetricValues[$record->getName()] = $count['recursive'];
225 } else {
226 $recordCountMetricValues[$record->getName()] = $count['level0'];
227 }
228 $transform = $record->getMultiplePeriodTransform();
229 if (!empty($transform)) {
230 $recordCountMetricValues[$record->getName()] = $transform($recordCountMetricValues[$record->getName()], $count);
231 }
232 }
233 if (!empty($recordCountMetricValues)) {
234 $archiveProcessor->insertNumericRecords($recordCountMetricValues);
235 }
236 }
237 }
238 protected function aggregateBuiltFromFlatRecordForNonDay(ArchiveProcessor $archiveProcessor, \Piwik\ArchiveProcessor\Record $hierarchicalRecord, array $blobRecordsByName, ?array $columnAggregationOps, ?array $columnToRenameAfterAggregation, ?string $columnToSortByBeforeTruncation, array &$processedFlatRecords) : bool
239 {
240 $flatRecordName = $hierarchicalRecord->getBuiltFromFlatRecord();
241 if (empty($flatRecordName)) {
242 return \false;
243 }
244 $flatToHierarchyPathCallback = $hierarchicalRecord->getFlatToHierarchyPathCallback();
245 if (!is_callable($flatToHierarchyPathCallback)) {
246 return \false;
247 }
248 $flatRecord = $blobRecordsByName[$flatRecordName] ?? null;
249 if (empty($flatRecord)) {
250 return \false;
251 }
252 $flatColumnAggregationOps = $flatRecord->getBlobColumnAggregationOps() ?? $this->columnAggregationOps;
253 $flatColumnToRenameAfterAggregation = $flatRecord->getColumnToRenameAfterAggregation() ?? $this->columnToRenameAfterAggregation;
254 $flatColumnToSortByBeforeTruncation = $flatRecord->getColumnToSortByBeforeTruncation() ?? $this->columnToSortByBeforeTruncation;
255 $flatMaxRowsInTable = $flatRecord->getMaxRowsInTable() ?? $this->maxRowsInTable;
256 [$flatTable, $hasFlatSourceData, $periodsWithFlatRecord] = $this->aggregateRootDataTableFromBlobs($archiveProcessor, $flatRecordName, $flatColumnAggregationOps, $flatColumnToRenameAfterAggregation);
257 $allSubperiodKeys = $this->getAllSubperiodKeys($archiveProcessor);
258 $periodsWithoutFlatRecord = array_diff_key($allSubperiodKeys, $periodsWithFlatRecord);
259 $hasLegacyFallbackData = \false;
260 $legacyReducerCallback = $hierarchicalRecord->getLegacyHierarchyToFlatReducerCallback();
261 if (!empty($periodsWithoutFlatRecord) && is_callable($legacyReducerCallback)) {
262 $hasLegacyFallbackData = $this->aggregateLegacyHierarchyPeriodsIntoFlatTable($archiveProcessor, $hierarchicalRecord->getName(), $flatTable, $legacyReducerCallback, $hierarchicalRecord, $columnAggregationOps, $columnToRenameAfterAggregation, $periodsWithoutFlatRecord);
263 }
264 if (!$hasFlatSourceData && !$hasLegacyFallbackData) {
265 Common::destroy($flatTable);
266 return \false;
267 }
268 $flatTransform = $flatRecord->getAggregatedRecordTransform();
269 if (null !== $flatTransform) {
270 $flatTransform($flatTable, $archiveProcessor, $flatRecord);
271 }
272 $flatSerialized = $flatTable->getSerialized($flatMaxRowsInTable, null, $flatColumnToSortByBeforeTruncation);
273 $archiveProcessor->insertBlobRecord($flatRecordName, $flatSerialized);
274 unset($flatSerialized);
275 $processedFlatRecords[$flatRecordName] = \true;
276 $hierarchicalTable = $this->buildHierarchicalTableFromFlatTableAndConsumeRows($flatTable, $columnAggregationOps, function (Row $flatRow) use($flatToHierarchyPathCallback, $archiveProcessor, $hierarchicalRecord) {
277 return call_user_func($flatToHierarchyPathCallback, $flatRow, $archiveProcessor, $hierarchicalRecord);
278 });
279 $this->beforeInsertBuiltFromFlatHierarchyRecord($archiveProcessor, $hierarchicalRecord, $hierarchicalTable, $flatTable);
280 $hierarchicalTransform = $hierarchicalRecord->getAggregatedRecordTransform();
281 if (null !== $hierarchicalTransform) {
282 $hierarchicalTransform($hierarchicalTable, $archiveProcessor, $hierarchicalRecord);
283 }
284 $hierarchicalSerialized = $hierarchicalTable->getSerialized(null, null, $columnToSortByBeforeTruncation);
285 $archiveProcessor->insertBlobRecord($hierarchicalRecord->getName(), $hierarchicalSerialized);
286 unset($hierarchicalSerialized);
287 Common::destroy($hierarchicalTable);
288 Common::destroy($flatTable);
289 return \true;
290 }
291 protected function aggregateLegacyHierarchyPeriodsIntoFlatTable(ArchiveProcessor $archiveProcessor, string $recordName, DataTable $flatTable, callable $legacyReducerCallback, \Piwik\ArchiveProcessor\Record $hierarchicalRecord, ?array $columnsAggregationOperation, ?array $columnsToRenameAfterAggregation, ?array $periodsToInclude) : bool
292 {
293 $currentPeriod = null;
294 $currentPeriodRows = [];
295 $hasRows = \false;
296 foreach ($this->querySingleBlobRows($archiveProcessor, $recordName) as $archiveDataRow) {
297 $period = $archiveDataRow['date1'] . ',' . $archiveDataRow['date2'];
298 if ($periodsToInclude !== null && !isset($periodsToInclude[$period])) {
299 continue;
300 }
301 if ($currentPeriod !== null && $period !== $currentPeriod) {
302 $hasRows = $this->reduceLegacyHierarchyPeriodRowsIntoFlatTable($currentPeriodRows, $recordName, $flatTable, $legacyReducerCallback, $archiveProcessor, $hierarchicalRecord, $columnsAggregationOperation, $columnsToRenameAfterAggregation) || $hasRows;
303 $currentPeriodRows = [];
304 }
305 $currentPeriod = $period;
306 $currentPeriodRows[] = $archiveDataRow;
307 }
308 if (!empty($currentPeriodRows)) {
309 $hasRows = $this->reduceLegacyHierarchyPeriodRowsIntoFlatTable($currentPeriodRows, $recordName, $flatTable, $legacyReducerCallback, $archiveProcessor, $hierarchicalRecord, $columnsAggregationOperation, $columnsToRenameAfterAggregation) || $hasRows;
310 }
311 return $hasRows;
312 }
313 protected function reduceLegacyHierarchyPeriodRowsIntoFlatTable(array $periodRows, string $recordName, DataTable $flatTable, callable $legacyReducerCallback, ArchiveProcessor $archiveProcessor, \Piwik\ArchiveProcessor\Record $hierarchicalRecord, ?array $columnsAggregationOperation, ?array $columnsToRenameAfterAggregation) : bool
314 {
315 [$legacyHierarchicalTable, $hasRows] = \Piwik\ArchiveProcessor\BlobTableAggregator::aggregateBlobRows($periodRows, $recordName, $columnsAggregationOperation, function (DataTable $table) use($archiveProcessor, $columnsToRenameAfterAggregation) : void {
316 $archiveProcessor->renameColumnsAfterAggregation($table, $columnsToRenameAfterAggregation);
317 });
318 if ($hasRows) {
319 call_user_func($legacyReducerCallback, $legacyHierarchicalTable, $flatTable, $archiveProcessor, $hierarchicalRecord);
320 }
321 Common::destroy($legacyHierarchicalTable);
322 return $hasRows;
323 }
324 /**
325 * Hook executed after the hierarchy table has been rebuilt from the flat table and before
326 * the hierarchical blob record is serialized and inserted.
327 *
328 * Intended for plugin-specific finalization (for example, metadata or column cleanup) when
329 * using setBuiltFromFlatRecord(). The flat table has already been serialized at this point
330 * and may have been fully consumed while rebuilding the hierarchy.
331 */
332 protected function beforeInsertBuiltFromFlatHierarchyRecord(ArchiveProcessor $archiveProcessor, \Piwik\ArchiveProcessor\Record $hierarchicalRecord, DataTable $hierarchicalTable, DataTable $flatTable) : void
333 {
334 }
335 protected function buildHierarchicalTableFromFlatTable(DataTable $flatTable, ?array $columnAggregationOps, callable $flatToHierarchyPathCallback, array $defaultHierarchyRowColumns = []) : DataTable
336 {
337 $hierarchicalTable = new DataTable();
338 if (!empty($columnAggregationOps)) {
339 $hierarchicalTable->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, $columnAggregationOps);
340 }
341 foreach ($flatTable->getRows() as $flatRow) {
342 if ($flatRow->isSummaryRow()) {
343 if ($this->isSummaryRowEmpty($flatRow)) {
344 continue;
345 }
346 $summaryRow = $hierarchicalTable->getRowFromId(DataTable::ID_SUMMARY_ROW);
347 if ($summaryRow === \false) {
348 $summaryRow = clone $flatRow;
349 $summaryRow->setIsSummaryRow();
350 $hierarchicalTable->addSummaryRow($summaryRow);
351 continue;
352 }
353 $this->sumRowIntoDestination($flatRow, $summaryRow, $columnAggregationOps);
354 continue;
355 }
356 $path = call_user_func($flatToHierarchyPathCallback, $flatRow);
357 if (!is_array($path) || empty($path)) {
358 continue;
359 }
360 [$destinationRow, $level] = $hierarchicalTable->walkPath($path, $defaultHierarchyRowColumns, 0);
361 if (!$destinationRow instanceof Row) {
362 continue;
363 }
364 $this->sumRowIntoDestination($flatRow, $destinationRow, $columnAggregationOps);
365 }
366 return $hierarchicalTable;
367 }
368 protected function buildHierarchicalTableFromFlatTableAndConsumeRows(DataTable $flatTable, ?array $columnAggregationOps, callable $flatToHierarchyPathCallback, array $defaultHierarchyRowColumns = []) : DataTable
369 {
370 $hierarchicalTable = new DataTable();
371 if (!empty($columnAggregationOps)) {
372 $hierarchicalTable->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, $columnAggregationOps);
373 }
374 while (($flatRow = $flatTable->shiftRow()) instanceof Row) {
375 $path = call_user_func($flatToHierarchyPathCallback, $flatRow);
376 if (is_array($path) && !empty($path)) {
377 [$destinationRow, $level] = $hierarchicalTable->walkPath($path, $defaultHierarchyRowColumns, 0);
378 if ($destinationRow instanceof Row) {
379 $this->sumRowIntoDestination($flatRow, $destinationRow, $columnAggregationOps);
380 }
381 }
382 Common::destroy($flatRow);
383 }
384 $summaryRow = $flatTable->getSummaryRow();
385 if ($summaryRow instanceof Row && !$this->isSummaryRowEmpty($summaryRow)) {
386 $destinationSummaryRow = $hierarchicalTable->getRowFromId(DataTable::ID_SUMMARY_ROW);
387 if ($destinationSummaryRow === \false) {
388 $destinationSummaryRow = clone $summaryRow;
389 $destinationSummaryRow->setIsSummaryRow();
390 $hierarchicalTable->addSummaryRow($destinationSummaryRow);
391 } else {
392 $this->sumRowIntoDestination($summaryRow, $destinationSummaryRow, $columnAggregationOps);
393 }
394 }
395 $flatTable->deleteRow(DataTable::ID_SUMMARY_ROW);
396 Common::destroy($summaryRow);
397 return $hierarchicalTable;
398 }
399 protected function sumRowIntoDestination(Row $source, Row $destination, ?array $columnAggregationOps) : void
400 {
401 $sourceCopy = clone $source;
402 // Preserve original column representation (eg "0.0620" strings) when
403 // destination does not have a value yet. This keeps day flat-first
404 // output consistent with legacy day archiving.
405 foreach ($sourceCopy->getColumns() as $columnName => $columnValue) {
406 if ($columnName === 'label') {
407 continue;
408 }
409 if ($destination->getColumn($columnName) !== \false) {
410 continue;
411 }
412 $destination->setColumn($columnName, $columnValue);
413 $sourceCopy->deleteColumn($columnName);
414 }
415 $destination->sumRow($sourceCopy, \true, $columnAggregationOps ?? []);
416 }
417 protected function isSummaryRowEmpty(Row $summaryRow) : bool
418 {
419 foreach ($summaryRow->getColumns() as $name => $value) {
420 if ($name === 'label') {
421 continue;
422 }
423 if (!empty($value)) {
424 return \false;
425 }
426 }
427 return \true;
428 }
429 /**
430 * Aggregates a root blob record while discovering periods that contain the root record in a single pass.
431 *
432 * @return array{0: DataTable, 1: bool, 2: array<string, bool>}
433 */
434 protected function aggregateRootDataTableFromBlobs(ArchiveProcessor $archiveProcessor, string $recordName, ?array $columnsAggregationOperation, ?array $columnsToRenameAfterAggregation) : array
435 {
436 $periodsWithRootRecord = [];
437 [$result, $hasRows] = \Piwik\ArchiveProcessor\BlobTableAggregator::aggregateBlobRows($this->querySingleBlobRows($archiveProcessor, $recordName), $recordName, $columnsAggregationOperation, function (DataTable $table) use($archiveProcessor, $columnsToRenameAfterAggregation) : void {
438 $archiveProcessor->renameColumnsAfterAggregation($table, $columnsToRenameAfterAggregation);
439 }, function (array $archiveDataRow) use(&$periodsWithRootRecord, $recordName) : bool {
440 $period = $archiveDataRow['date1'] . ',' . $archiveDataRow['date2'];
441 if ($archiveDataRow['name'] === $recordName) {
442 $periodsWithRootRecord[$period] = \true;
443 return \true;
444 }
445 return isset($periodsWithRootRecord[$period]);
446 });
447 return [$result, $hasRows, $periodsWithRootRecord];
448 }
449 protected function querySingleBlobRows(ArchiveProcessor $archiveProcessor, string $recordName) : iterable
450 {
451 $archive = Archive::factory($archiveProcessor->getParams()->getSegment(), $archiveProcessor->getParams()->getPeriod()->getSubperiods(), [$archiveProcessor->getParams()->getSite()->getId()]);
452 if (!method_exists($archive, 'querySingleBlob')) {
453 return [];
454 }
455 return $archive->querySingleBlob($recordName);
456 }
457 protected function getAllSubperiodKeys(ArchiveProcessor $archiveProcessor) : array
458 {
459 $result = [];
460 foreach ($archiveProcessor->getParams()->getPeriod()->getSubperiods() as $period) {
461 $result[$period->getDateStart()->toString() . ',' . $period->getDateEnd()->toString()] = \true;
462 }
463 return $result;
464 }
465 /**
466 * Returns metadata for records primarily used when aggregating over non-day periods. Every numeric/blob
467 * record your RecordBuilder creates should have an associated piece of record metadata.
468 *
469 * @return Record[]
470 */
471 public abstract function getRecordMetadata(ArchiveProcessor $archiveProcessor) : array;
472 /**
473 * Derived classes should define this method to aggregate log data for a single day and return the records
474 * to store indexed by record names.
475 *
476 * @return array<string, DataTable|int|float|string> Record values indexed by their record name, eg, `['MyPlugin_MyRecord' => new DataTable()]`
477 */
478 protected abstract function aggregate(ArchiveProcessor $archiveProcessor) : array;
479 protected function insertBlobRecord(ArchiveProcessor $archiveProcessor, string $recordName, DataTable $record, ?int $maxRowsInTable, ?int $maxRowsInSubtable, ?string $columnToSortByBeforeTruncation) : void
480 {
481 $serialized = $record->getSerialized($maxRowsInTable ?? $this->maxRowsInTable, $maxRowsInSubtable ?? $this->maxRowsInSubtable, $columnToSortByBeforeTruncation ?? $this->columnToSortByBeforeTruncation);
482 $archiveProcessor->insertBlobRecord($recordName, $serialized);
483 unset($serialized);
484 }
485 public function getMaxRowsInTable() : ?int
486 {
487 return $this->maxRowsInTable;
488 }
489 public function getMaxRowsInSubtable() : ?int
490 {
491 return $this->maxRowsInSubtable;
492 }
493 public function getColumnToSortByBeforeTruncation() : ?string
494 {
495 return $this->columnToSortByBeforeTruncation;
496 }
497 public function getPluginName() : string
498 {
499 return Piwik::getPluginNameOfMatomoClass(get_class($this));
500 }
501 /**
502 * Returns an extra hint for LogAggregator to add to log aggregation SQL. Can be overridden if you'd
503 * like the origin hint to have more information.
504 */
505 public function getQueryOriginHint() : string
506 {
507 $recordBuilderName = get_class($this);
508 $recordBuilderName = explode('\\', $recordBuilderName);
509 return end($recordBuilderName);
510 }
511 /**
512 * Returns true if at least one of the given reports is handled by this RecordBuilder instance
513 * when invoked with the given ArchiveProcessor.
514 *
515 * @param ArchiveProcessor $archiveProcessor Archiving parameters, like idSite, can influence the list of
516 * all records a RecordBuilder produces, so it is required here.
517 * @param string[] $requestedReports The list of requested reports to check for.
518 */
519 public function isBuilderForAtLeastOneOf(ArchiveProcessor $archiveProcessor, array $requestedReports) : bool
520 {
521 $recordMetadata = $this->getRecordMetadata($archiveProcessor);
522 foreach ($recordMetadata as $record) {
523 if (in_array($record->getName(), $requestedReports)) {
524 return \true;
525 }
526 }
527 return \false;
528 }
529 }
530