PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.8.2
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.8.2
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 5 months ago Loader.php 4 months ago LoaderLock.php 1 year ago Parameters.php 7 months ago PluginsArchiver.php 1 year ago PluginsArchiverException.php 2 years ago Record.php 3 months ago RecordBuilder.php 3 months ago Rules.php 3 months ago
RecordBuilder.php
272 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\ArchiveProcessor;
12 use Piwik\Common;
13 use Piwik\DataTable;
14 use Piwik\Piwik;
15 /**
16 * Inherit from this class to define archiving logic for one or more records.
17 */
18 abstract class RecordBuilder
19 {
20 /**
21 * @var int|null
22 */
23 protected $maxRowsInTable;
24 /**
25 * @var int|null
26 */
27 protected $maxRowsInSubtable;
28 /**
29 * @var string|int|null
30 */
31 protected $columnToSortByBeforeTruncation;
32 /**
33 * @var array|null
34 */
35 protected $columnAggregationOps;
36 /**
37 * @var array<string|int,string|int>|null
38 */
39 protected $columnToRenameAfterAggregation;
40 /**
41 * @param array|null $columnAggregationOps
42 * @param array<string|int,string|int>|null $columnToRenameAfterAggregation
43 */
44 public function __construct(?int $maxRowsInTable = null, ?int $maxRowsInSubtable = null, ?string $columnToSortByBeforeTruncation = null, ?array $columnAggregationOps = null, ?array $columnToRenameAfterAggregation = null)
45 {
46 $this->maxRowsInTable = $maxRowsInTable;
47 $this->maxRowsInSubtable = $maxRowsInSubtable;
48 $this->columnToSortByBeforeTruncation = $columnToSortByBeforeTruncation;
49 $this->columnAggregationOps = $columnAggregationOps;
50 $this->columnToRenameAfterAggregation = $columnToRenameAfterAggregation;
51 }
52 public function isEnabled(ArchiveProcessor $archiveProcessor) : bool
53 {
54 return \true;
55 }
56 /**
57 * Uses the protected `aggregate()` function to build records by aggregating log table data directly, then
58 * inserts them as archive data.
59 *
60 */
61 public function buildFromLogs(ArchiveProcessor $archiveProcessor) : void
62 {
63 if (!$this->isEnabled($archiveProcessor)) {
64 return;
65 }
66 $recordsBuilt = $this->getRecordMetadata($archiveProcessor);
67 $recordMetadataByName = [];
68 foreach ($recordsBuilt as $recordMetadata) {
69 if (!$recordMetadata instanceof \Piwik\ArchiveProcessor\Record) {
70 continue;
71 }
72 $recordMetadataByName[$recordMetadata->getName()] = $recordMetadata;
73 }
74 $numericRecords = [];
75 $records = $this->aggregate($archiveProcessor);
76 foreach ($records as $recordName => $recordValue) {
77 if (empty($recordMetadataByName[$recordName])) {
78 if ($recordValue instanceof DataTable) {
79 Common::destroy($recordValue);
80 }
81 continue;
82 }
83 if ($recordValue instanceof DataTable) {
84 $record = $recordMetadataByName[$recordName];
85 $maxRowsInTable = $record->getMaxRowsInTable() ?? $this->maxRowsInTable;
86 $maxRowsInSubtable = $record->getMaxRowsInSubtable() ?? $this->maxRowsInSubtable;
87 $columnToSortByBeforeTruncation = $record->getColumnToSortByBeforeTruncation() ?? $this->columnToSortByBeforeTruncation;
88 $this->insertBlobRecord($archiveProcessor, $recordName, $recordValue, $maxRowsInTable, $maxRowsInSubtable, $columnToSortByBeforeTruncation);
89 Common::destroy($recordValue);
90 } else {
91 // collect numeric records so we can insert them all at once
92 $numericRecords[$recordName] = $recordValue;
93 }
94 }
95 unset($records);
96 if (!empty($numericRecords)) {
97 $archiveProcessor->insertNumericRecords($numericRecords);
98 }
99 }
100 /**
101 * Builds records for non-day periods by aggregating day records together, then inserts
102 * them as archive data.
103 *
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 $aggregatedCounts = [];
120 // make sure if there are requested numeric records that depend on blob records, that the blob records will be archived first
121 foreach ($numericRecords as $record) {
122 if (empty($record->getCountOfRecordName()) || !in_array($record->getName(), $requestedReports)) {
123 continue;
124 }
125 $dependentRecordName = $record->getCountOfRecordName();
126 if (!in_array($dependentRecordName, $requestedReports)) {
127 $requestedReports[] = $dependentRecordName;
128 }
129 // we need to aggregate the blob record to get the count, so even if it's found, we must re-aggregate it
130 // TODO: this could potentially be optimized away, but it would be non-trivial given the current ArchiveProcessor API
131 $indexInFoundRecords = array_search($dependentRecordName, $foundRequestedReports);
132 if ($indexInFoundRecords !== \false) {
133 unset($foundRequestedReports[$indexInFoundRecords]);
134 }
135 }
136 foreach ($blobRecords as $record) {
137 if (!empty($requestedReports) && (!in_array($record->getName(), $requestedReports) || in_array($record->getName(), $foundRequestedReports))) {
138 continue;
139 }
140 $maxRowsInTable = $record->getMaxRowsInTable() ?? $this->maxRowsInTable;
141 $maxRowsInSubtable = $record->getMaxRowsInSubtable() ?? $this->maxRowsInSubtable;
142 $columnToSortByBeforeTruncation = $record->getColumnToSortByBeforeTruncation() ?? $this->columnToSortByBeforeTruncation;
143 $columnToRenameAfterAggregation = $record->getColumnToRenameAfterAggregation() ?? $this->columnToRenameAfterAggregation;
144 $columnAggregationOps = $record->getBlobColumnAggregationOps() ?? $this->columnAggregationOps;
145 // only do recursive row counts if there is a numeric record that depends on it
146 $countRecursiveRows = $countLeafRows = [];
147 foreach ($numericRecords as $numeric) {
148 if ($numeric->getCountOfRecordName() == $record->getName()) {
149 if ($numeric->getCountOfRecordNameIsRecursive()) {
150 $countRecursiveRows[] = $numeric->getCountOfRecordName();
151 }
152 if ($numeric->getCountOfRecordNameIsForLeafs()) {
153 $countLeafRows[] = $numeric->getCountOfRecordName();
154 }
155 }
156 }
157 $counts = $archiveProcessor->aggregateDataTableRecords($record->getName(), $maxRowsInTable, $maxRowsInSubtable, $columnToSortByBeforeTruncation, $columnAggregationOps, $columnToRenameAfterAggregation, $countRecursiveRows, $countLeafRows);
158 $aggregatedCounts = array_merge($aggregatedCounts, $counts);
159 }
160 if (!empty($numericRecords)) {
161 // handle metrics that are aggregated using metric values from child periods
162 $autoAggregateMetrics = array_filter($numericRecords, function (\Piwik\ArchiveProcessor\Record $r) {
163 return empty($r->getCountOfRecordName());
164 });
165 $autoAggregateMetrics = array_map(function (\Piwik\ArchiveProcessor\Record $r) {
166 return $r->getName();
167 }, $autoAggregateMetrics);
168 if (!empty($requestedReports)) {
169 $autoAggregateMetrics = array_filter($autoAggregateMetrics, function ($name) use($requestedReports, $foundRequestedReports) {
170 return in_array($name, $requestedReports) && !in_array($name, $foundRequestedReports);
171 });
172 }
173 $autoAggregateMetrics = array_values($autoAggregateMetrics);
174 if (!empty($autoAggregateMetrics)) {
175 $archiveProcessor->aggregateNumericMetrics($autoAggregateMetrics, $this->columnAggregationOps);
176 }
177 // handle metrics that are set to counts of blob records
178 $recordCountMetricValues = [];
179 $recordCountMetrics = array_filter($numericRecords, function (\Piwik\ArchiveProcessor\Record $r) {
180 return !empty($r->getCountOfRecordName());
181 });
182 foreach ($recordCountMetrics as $record) {
183 $dependentRecordName = $record->getCountOfRecordName();
184 if (empty($aggregatedCounts[$dependentRecordName])) {
185 continue;
186 // dependent record not archived, so skip this metric
187 }
188 $count = $aggregatedCounts[$dependentRecordName];
189 if ($record->getCountOfRecordNameIsForLeafs()) {
190 $recordCountMetricValues[$record->getName()] = $count['leafs'];
191 } elseif ($record->getCountOfRecordNameIsRecursive()) {
192 $recordCountMetricValues[$record->getName()] = $count['recursive'];
193 } else {
194 $recordCountMetricValues[$record->getName()] = $count['level0'];
195 }
196 $transform = $record->getMultiplePeriodTransform();
197 if (!empty($transform)) {
198 $recordCountMetricValues[$record->getName()] = $transform($recordCountMetricValues[$record->getName()], $count);
199 }
200 }
201 if (!empty($recordCountMetricValues)) {
202 $archiveProcessor->insertNumericRecords($recordCountMetricValues);
203 }
204 }
205 }
206 /**
207 * Returns metadata for records primarily used when aggregating over non-day periods. Every numeric/blob
208 * record your RecordBuilder creates should have an associated piece of record metadata.
209 *
210 * @return Record[]
211 */
212 public abstract function getRecordMetadata(ArchiveProcessor $archiveProcessor) : array;
213 /**
214 * Derived classes should define this method to aggregate log data for a single day and return the records
215 * to store indexed by record names.
216 *
217 * @return array<string, DataTable|int|float|string> Record values indexed by their record name, eg, `['MyPlugin_MyRecord' => new DataTable()]`
218 */
219 protected abstract function aggregate(ArchiveProcessor $archiveProcessor) : array;
220 protected function insertBlobRecord(ArchiveProcessor $archiveProcessor, string $recordName, DataTable $record, ?int $maxRowsInTable, ?int $maxRowsInSubtable, ?string $columnToSortByBeforeTruncation) : void
221 {
222 $serialized = $record->getSerialized($maxRowsInTable ?: $this->maxRowsInTable, $maxRowsInSubtable ?: $this->maxRowsInSubtable, $columnToSortByBeforeTruncation ?: $this->columnToSortByBeforeTruncation);
223 $archiveProcessor->insertBlobRecord($recordName, $serialized);
224 unset($serialized);
225 }
226 public function getMaxRowsInTable() : ?int
227 {
228 return $this->maxRowsInTable;
229 }
230 public function getMaxRowsInSubtable() : ?int
231 {
232 return $this->maxRowsInSubtable;
233 }
234 public function getColumnToSortByBeforeTruncation() : ?string
235 {
236 return $this->columnToSortByBeforeTruncation;
237 }
238 public function getPluginName() : string
239 {
240 return Piwik::getPluginNameOfMatomoClass(get_class($this));
241 }
242 /**
243 * Returns an extra hint for LogAggregator to add to log aggregation SQL. Can be overridden if you'd
244 * like the origin hint to have more information.
245 *
246 */
247 public function getQueryOriginHint() : string
248 {
249 $recordBuilderName = get_class($this);
250 $recordBuilderName = explode('\\', $recordBuilderName);
251 return end($recordBuilderName);
252 }
253 /**
254 * Returns true if at least one of the given reports is handled by this RecordBuilder instance
255 * when invoked with the given ArchiveProcessor.
256 *
257 * @param ArchiveProcessor $archiveProcessor Archiving parameters, like idSite, can influence the list of
258 * all records a RecordBuilder produces, so it is required here.
259 * @param string[] $requestedReports The list of requested reports to check for.
260 */
261 public function isBuilderForAtLeastOneOf(ArchiveProcessor $archiveProcessor, array $requestedReports) : bool
262 {
263 $recordMetadata = $this->getRecordMetadata($archiveProcessor);
264 foreach ($recordMetadata as $record) {
265 if (in_array($record->getName(), $requestedReports)) {
266 return \true;
267 }
268 }
269 return \false;
270 }
271 }
272