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 / Plugin / Archiver.php
matomo / app / core / Plugin Last commit date
ConsoleCommand 2 years ago Dimension 3 months ago API.php 6 months ago AggregatedMetric.php 2 years ago ArchivedMetric.php 1 year ago Archiver.php 6 months ago Categories.php 2 years ago ComponentFactory.php 3 months ago ComputedMetric.php 1 year ago ConsoleCommand.php 3 months ago Controller.php 3 months ago ControllerAdmin.php 3 months ago Dependency.php 1 year ago LogTablesProvider.php 2 years ago Manager.php 3 months ago Menu.php 3 months ago MetadataLoader.php 1 year ago Metric.php 3 months ago PluginException.php 1 year ago ProcessedMetric.php 3 months ago ReleaseChannels.php 3 months ago Report.php 3 months ago ReportsProvider.php 2 years ago RequestProcessors.php 4 months ago Segment.php 3 months ago SettingsProvider.php 3 months ago Tasks.php 3 months ago ThemeStyles.php 6 months ago ViewDataTable.php 3 months ago Visualization.php 1 year ago WidgetsProvider.php 3 months ago
Archiver.php
382 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\Plugin;
10
11 use Piwik\ArchiveProcessor;
12 use Piwik\Cache;
13 use Piwik\CacheId;
14 use Piwik\Config as PiwikConfig;
15 use Piwik\Container\StaticContainer;
16 use Piwik\ErrorHandler;
17 use Piwik\Log;
18 use Piwik\Piwik;
19 /**
20 * The base class that should be extended by plugins that compute their own
21 * analytics data.
22 *
23 * Descendants should implement the {@link aggregateDayReport()} and {@link aggregateMultipleReports()}
24 * methods.
25 *
26 * Both of these methods should persist analytics data using the {@link \Piwik\ArchiveProcessor}
27 * instance returned by {@link getProcessor()}. The {@link aggregateDayReport()} method should
28 * compute analytics data using the {@link \Piwik\DataAccess\LogAggregator} instance
29 * returned by {@link getLogAggregator()}.
30 *
31 * ### Examples
32 *
33 * **Extending Archiver**
34 *
35 * class MyArchiver extends Archiver
36 * {
37 * public function aggregateDayReport()
38 * {
39 * $logAggregator = $this->getLogAggregator();
40 *
41 * $data = $logAggregator->queryVisitsByDimension(...);
42 *
43 * $dataTable = new DataTable();
44 * $dataTable->addRowsFromSimpleArray($data);
45 *
46 * $archiveProcessor = $this->getProcessor();
47 * $archiveProcessor->insertBlobRecords('MyPlugin_myReport', $dataTable->getSerialized(500));
48 * }
49 *
50 * public function aggregateMultipleReports()
51 * {
52 * $archiveProcessor = $this->getProcessor();
53 * $archiveProcessor->aggregateDataTableRecords('MyPlugin_myReport', 500);
54 * }
55 * }
56 *
57 * @api
58 */
59 class Archiver
60 {
61 public static $ARCHIVE_DEPENDENT = \true;
62 /**
63 * @var \Piwik\ArchiveProcessor
64 */
65 private $processor;
66 /**
67 * @var bool
68 */
69 private $enabled;
70 /**
71 * @var mixed
72 */
73 protected $maximumRows;
74 /**
75 * Used if a plugin has RecordBuilders but no Archiver subclass.
76 *
77 * @var string|null
78 */
79 private $pluginName = null;
80 /**
81 * Constructor.
82 *
83 * @param ArchiveProcessor $processor The ArchiveProcessor instance to use when persisting archive
84 * data.
85 */
86 public function __construct(ArchiveProcessor $processor, ?string $pluginName = null)
87 {
88 $this->maximumRows = PiwikConfig::getInstance()->General['datatable_archiving_maximum_rows_standard'];
89 $this->processor = $processor;
90 $this->enabled = \true;
91 $this->pluginName = $pluginName;
92 }
93 private function getPluginName() : string
94 {
95 return $this->pluginName ?: Piwik::getPluginNameOfMatomoClass(get_class($this));
96 }
97 /**
98 * @return ArchiveProcessor\RecordBuilder[]
99 * @throws \DI\DependencyException
100 * @throws \DI\NotFoundException
101 */
102 private function getRecordBuilders(string $pluginName) : array
103 {
104 $transientCache = Cache::getTransientCache();
105 $cacheKey = CacheId::siteAware('Archiver.RecordBuilders') . '.' . $pluginName;
106 $recordBuilders = $transientCache->fetch($cacheKey);
107 if ($recordBuilders === \false) {
108 $recordBuilderClasses = $this->getAllRecordBuilderClasses();
109 // only select RecordBuilders for the selected plugin
110 $recordBuilderClasses = array_filter($recordBuilderClasses, function ($className) use($pluginName) {
111 return Piwik::getPluginNameOfMatomoClass($className) == $pluginName;
112 });
113 $recordBuilders = array_map(function ($className) {
114 return StaticContainer::getContainer()->make($className);
115 }, $recordBuilderClasses);
116 /**
117 * Triggered to add new RecordBuilders that cannot be picked up automatically by the platform.
118 * If you define RecordBuilders that take a parameter, for example, an ID to an entity your plugin
119 * manages, use this event to add instances of that RecordBuilder to the global list.
120 *
121 * **Example**
122 *
123 * public function addRecordBuilders(&$recordBuilders)
124 * {
125 * $recordBuilders[] = new MyParameterizedRecordBuilder($idOfThingToArchiveFor);
126 * }
127 *
128 * @param ArchiveProcessor\RecordBuilder[] $recordBuilders An array of RecordBuilder instances
129 * @api
130 */
131 Piwik::postEvent('Archiver.addRecordBuilders', [&$recordBuilders], \false, [$pluginName]);
132 $transientCache->save($cacheKey, $recordBuilders);
133 }
134 /**
135 * Triggered to filter / restrict reports.
136 *
137 * **Example**
138 *
139 * public function filterRecordBuilders(&$recordBuilders)
140 * {
141 * foreach ($reports as $index => $recordBuilder) {
142 * if ($recordBuilders instanceof AnotherPluginRecordBuilder) {
143 * unset($reports[$index]);
144 * }
145 * }
146 * }
147 *
148 * @param ArchiveProcessor\RecordBuilder[] $recordBuilders An array of RecordBuilder instances
149 * @api
150 */
151 Piwik::postEvent('Archiver.filterRecordBuilders', [&$recordBuilders]);
152 return $recordBuilders;
153 }
154 private function filterRecordBuildersByRequestedRecords(array $recordBuilders, array $requestedReports) : array
155 {
156 // No record builders might be provided if the plugin does not (yet) provide any
157 if (empty($recordBuilders)) {
158 return $recordBuilders;
159 }
160 if (!empty($requestedReports)) {
161 $recordBuilders = array_filter($recordBuilders, function (ArchiveProcessor\RecordBuilder $builder) use($requestedReports) {
162 return $builder->isBuilderForAtLeastOneOf($this->processor, $requestedReports);
163 });
164 }
165 if (0 === count($recordBuilders)) {
166 Log::debug('Archiver: No record builders found for requested records %s', implode(',', $this->processor->getParams()->getArchiveOnlyReportAsArray()));
167 }
168 return $recordBuilders;
169 }
170 /**
171 * @ignore
172 */
173 public final function callAggregateDayReport()
174 {
175 try {
176 ErrorHandler::pushFatalErrorBreadcrumb(static::class);
177 $pluginName = $this->getPluginName();
178 if (\Piwik\Plugin\Manager::getInstance()->isPluginLoaded($pluginName)) {
179 $allRecordBuilders = $this->getRecordBuilders($pluginName);
180 $recordBuilders = $this->filterRecordBuildersByRequestedRecords($allRecordBuilders, $this->processor->getParams()->getArchiveOnlyReportAsArray());
181 foreach ($recordBuilders as $recordBuilder) {
182 if (!$recordBuilder->isEnabled($this->getProcessor())) {
183 continue;
184 }
185 $originalQueryHint = $this->getProcessor()->getLogAggregator()->getQueryOriginHint();
186 $newQueryHint = $originalQueryHint . ' ' . $recordBuilder->getQueryOriginHint();
187 try {
188 $this->getProcessor()->getLogAggregator()->setQueryOriginHint($newQueryHint);
189 $recordBuilder->buildFromLogs($this->getProcessor());
190 } finally {
191 $this->getProcessor()->getLogAggregator()->setQueryOriginHint($originalQueryHint);
192 }
193 }
194 }
195 $this->aggregateDayReport();
196 $this->processDependentArchivesForPlugins();
197 } finally {
198 ErrorHandler::popFatalErrorBreadcrumb();
199 }
200 }
201 /**
202 * @ignore
203 */
204 public final function callAggregateMultipleReports()
205 {
206 try {
207 ErrorHandler::pushFatalErrorBreadcrumb(static::class);
208 $pluginName = $this->getPluginName();
209 if (\Piwik\Plugin\Manager::getInstance()->isPluginLoaded($pluginName)) {
210 $allRecordBuilders = $this->getRecordBuilders($pluginName);
211 $recordBuilders = $this->filterRecordBuildersByRequestedRecords($allRecordBuilders, $this->processor->getParams()->getArchiveOnlyReportAsArray());
212 foreach ($recordBuilders as $recordBuilder) {
213 if (!$recordBuilder->isEnabled($this->getProcessor())) {
214 continue;
215 }
216 $originalQueryHint = $this->getProcessor()->getLogAggregator()->getQueryOriginHint();
217 $newQueryHint = $originalQueryHint . ' ' . $recordBuilder->getQueryOriginHint();
218 try {
219 $this->getProcessor()->getLogAggregator()->setQueryOriginHint($newQueryHint);
220 $recordBuilder->buildForNonDayPeriod($this->getProcessor());
221 } finally {
222 $this->getProcessor()->getLogAggregator()->setQueryOriginHint($originalQueryHint);
223 }
224 }
225 }
226 $this->aggregateMultipleReports();
227 $this->processDependentArchivesForPlugins();
228 } finally {
229 ErrorHandler::popFatalErrorBreadcrumb();
230 }
231 }
232 /**
233 * Archives data for a day period.
234 *
235 * Implementations of this method should do more computation intensive activities such
236 * as aggregating data across log tables. Since this method only deals w/ data logged for a day,
237 * aggregating individual log table rows isn't a problem. Doing this for any larger period,
238 * however, would cause performance degradation.
239 *
240 * Aggregate log table rows using a {@link Piwik\DataAccess\LogAggregator} instance. Get a
241 * {@link Piwik\DataAccess\LogAggregator} instance using the {@link getLogAggregator()} method.
242 */
243 public function aggregateDayReport()
244 {
245 // empty
246 }
247 /**
248 * Archives data for a non-day period.
249 *
250 * Implementations of this method should only aggregate existing reports of subperiods of the
251 * current period. For example, it is more efficient to aggregate reports for each day of a
252 * week than to aggregate each log entry of the week.
253 *
254 * Use {@link Piwik\ArchiveProcessor::aggregateNumericMetrics()} and {@link Piwik\ArchiveProcessor::aggregateDataTableRecords()}
255 * to aggregate archived reports. Get the {@link Piwik\ArchiveProcessor} instance using the {@link getProcessor()}
256 * method.
257 */
258 public function aggregateMultipleReports()
259 {
260 // empty
261 }
262 /**
263 * Returns a {@link Piwik\ArchiveProcessor} instance that can be used to insert archive data for
264 * the period, segment and site we are archiving data for.
265 *
266 * @return \Piwik\ArchiveProcessor
267 * @api
268 */
269 protected function getProcessor()
270 {
271 return $this->processor;
272 }
273 /**
274 * Returns a {@link Piwik\DataAccess\LogAggregator} instance that can be used to aggregate log table rows
275 * for this period, segment and site.
276 *
277 * @return \Piwik\DataAccess\LogAggregator
278 * @api
279 */
280 protected function getLogAggregator()
281 {
282 return $this->getProcessor()->getLogAggregator();
283 }
284 public function disable()
285 {
286 $this->enabled = \false;
287 }
288 /**
289 * Whether this Archiver should be used or not.
290 *
291 * @return bool
292 */
293 public function isEnabled()
294 {
295 return $this->enabled;
296 }
297 /**
298 * By overwriting this method and returning true, a plugin archiver can force the archiving to run even when there
299 * was no visit for the website/date/period/segment combination
300 * (by default, archivers are skipped when there is no visit).
301 *
302 * @return bool
303 */
304 public static function shouldRunEvenWhenNoVisits()
305 {
306 return \false;
307 }
308 /**
309 * Returns a list of segments that should be pre-archived along with the segment currently being archived.
310 * The segments in this list will be added to the current segment via an AND condition and archiving
311 * for the current plugin will be launched. This process will not recurse further.
312 *
313 * If your plugin's API appends conditions to the requested segment when fetching data, you will want to
314 * use this method to make sure those segments get pre-archived. Otherwise, if browser archiving is disabled,
315 * the modified segments will appear to have no data.
316 *
317 * To archive another plugin, use an array instead of a string segment, for example:
318 *
319 * ```
320 * ['plugin' => 'VisitsSummary', 'segment' => '...']
321 * ```
322 *
323 * See the Goals and VisitFrequency plugins for examples.
324 *
325 * @return array
326 * @api
327 */
328 public function getDependentSegmentsToArchive() : array
329 {
330 return [];
331 }
332 protected function isRequestedReport(string $reportName)
333 {
334 $requestedReport = $this->getProcessor()->getParams()->getArchiveOnlyReport();
335 return empty($requestedReport) || $requestedReport == $reportName;
336 }
337 private function processDependentArchivesForPlugins()
338 {
339 if (!self::$ARCHIVE_DEPENDENT) {
340 return;
341 }
342 $dependentSegments = $this->getDependentSegmentsToArchive();
343 foreach ($dependentSegments as $dependentSegment) {
344 $plugin = $this->getPluginName();
345 $segment = $dependentSegment;
346 if (is_array($dependentSegment)) {
347 $plugin = $dependentSegment['plugin'] ?? $plugin;
348 $segment = $dependentSegment['segment'];
349 }
350 $this->getProcessor()->processDependentArchive($plugin, $segment);
351 }
352 }
353 private static function getDefaultConstructibleClasses(array $classes) : array
354 {
355 return array_filter($classes, function ($className) {
356 return (new \ReflectionClass($className))->getConstructor()->getNumberOfRequiredParameters() == 0;
357 });
358 }
359 private static function getAllRecordBuilderClasses() : array
360 {
361 $transientCache = Cache::getTransientCache();
362 $cacheKey = CacheId::siteAware('RecordBuilders.allRecordBuilders');
363 $recordBuilderClasses = $transientCache->fetch($cacheKey);
364 if ($recordBuilderClasses === \false) {
365 $recordBuilderClasses = \Piwik\Plugin\Manager::getInstance()->findMultipleComponents('RecordBuilders', ArchiveProcessor\RecordBuilder::class);
366 $recordBuilderClasses = self::getDefaultConstructibleClasses($recordBuilderClasses);
367 $transientCache->save($cacheKey, $recordBuilderClasses);
368 }
369 return $recordBuilderClasses;
370 }
371 public static function doesPluginHaveRecordBuilders(string $pluginName) : bool
372 {
373 $recordBuilders = self::getAllRecordBuilderClasses();
374 foreach ($recordBuilders as $builder) {
375 if ($pluginName === Piwik::getPluginNameOfMatomoClass($builder)) {
376 return \true;
377 }
378 }
379 return \false;
380 }
381 }
382