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