File size: 15,241 Bytes
f8ce8bb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
<?php

/**
 * Matomo - free/libre analytics platform
 *
 * @link    https://matomo.org
 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
 */

namespace Piwik\Plugin;

use Piwik\ArchiveProcessor;
use Piwik\Cache;
use Piwik\CacheId;
use Piwik\Config as PiwikConfig;
use Piwik\Container\StaticContainer;
use Piwik\ErrorHandler;
use Piwik\Log;
use Piwik\Piwik;

/**
 * The base class that should be extended by plugins that compute their own
 * analytics data.
 *
 * Descendants should implement the {@link aggregateDayReport()} and {@link aggregateMultipleReports()}
 * methods.
 *
 * Both of these methods should persist analytics data using the {@link \Piwik\ArchiveProcessor}
 * instance returned by {@link getProcessor()}. The {@link aggregateDayReport()} method should
 * compute analytics data using the {@link \Piwik\DataAccess\LogAggregator} instance
 * returned by {@link getLogAggregator()}.
 *
 * ### Examples
 *
 * **Extending Archiver**
 *
 *     class MyArchiver extends Archiver
 *     {
 *         public function aggregateDayReport()
 *         {
 *             $logAggregator = $this->getLogAggregator();
 *
 *             $data = $logAggregator->queryVisitsByDimension(...);
 *
 *             $dataTable = new DataTable();
 *             $dataTable->addRowsFromSimpleArray($data);
 *
 *             $archiveProcessor = $this->getProcessor();
 *             $archiveProcessor->insertBlobRecords('MyPlugin_myReport', $dataTable->getSerialized(500));
 *         }
 *
 *         public function aggregateMultipleReports()
 *         {
 *             $archiveProcessor = $this->getProcessor();
 *             $archiveProcessor->aggregateDataTableRecords('MyPlugin_myReport', 500);
 *         }
 *     }
 *
 * @api
 */
class Archiver
{
    public static $ARCHIVE_DEPENDENT = true;

    /**
     * @var \Piwik\ArchiveProcessor
     */
    private $processor;

    /**
     * @var bool
     */
    private $enabled;

    /**
     * @var mixed
     */
    protected $maximumRows;

    /**
     * Used if a plugin has RecordBuilders but no Archiver subclass.
     *
     * @var string|null
     */
    private $pluginName = null;

    /**
     * @param ArchiveProcessor $processor The ArchiveProcessor instance to use when persisting archive
     *                                    data.
     */
    public function __construct(ArchiveProcessor $processor, ?string $pluginName = null)
    {
        $this->maximumRows = PiwikConfig::getInstance()->General['datatable_archiving_maximum_rows_standard'];
        $this->processor = $processor;
        $this->enabled = true;
        $this->pluginName = $pluginName;
    }

    private function getPluginName(): string
    {
        return $this->pluginName ?: Piwik::getPluginNameOfMatomoClass(get_class($this));
    }

    /**
     * @return ArchiveProcessor\RecordBuilder[]
     * @throws \DI\DependencyException
     * @throws \DI\NotFoundException
     */
    private function getRecordBuilders(string $pluginName): array
    {
        $transientCache = Cache::getTransientCache();
        $cacheKey = CacheId::siteAware('Archiver.RecordBuilders') . '.' . $pluginName;

        $recordBuilders = $transientCache->fetch($cacheKey);
        if ($recordBuilders === false) {
            $recordBuilderClasses = $this->getAllRecordBuilderClasses();

            // only select RecordBuilders for the selected plugin
            $recordBuilderClasses = array_filter($recordBuilderClasses, function ($className) use ($pluginName) {
                return Piwik::getPluginNameOfMatomoClass($className) == $pluginName;
            });

            $recordBuilders = array_map(function ($className) {
                return StaticContainer::getContainer()->make($className);
            }, $recordBuilderClasses);

            /**
             * Triggered to add new RecordBuilders that cannot be picked up automatically by the platform.
             * If you define RecordBuilders that take a parameter, for example, an ID to an entity your plugin
             * manages, use this event to add instances of that RecordBuilder to the global list.
             *
             * **Example**
             *
             *     public function addRecordBuilders(&$recordBuilders)
             *     {
             *         $recordBuilders[] = new MyParameterizedRecordBuilder($idOfThingToArchiveFor);
             *     }
             *
             * @param ArchiveProcessor\RecordBuilder[] $recordBuilders An array of RecordBuilder instances
             * @api
             */
            Piwik::postEvent('Archiver.addRecordBuilders', [&$recordBuilders], false, [$pluginName]);

            $transientCache->save($cacheKey, $recordBuilders);
        }

        /**
         * Triggered to filter / restrict reports.
         *
         * **Example**
         *
         *     public function filterRecordBuilders(&$recordBuilders)
         *     {
         *         foreach ($reports as $index => $recordBuilder) {
         *              if ($recordBuilders instanceof AnotherPluginRecordBuilder) {
         *                  unset($reports[$index]);
         *              }
         *         }
         *     }
         *
         * @param ArchiveProcessor\RecordBuilder[] $recordBuilders An array of RecordBuilder instances
         * @api
         */
        Piwik::postEvent('Archiver.filterRecordBuilders', [&$recordBuilders]);

        return $recordBuilders;
    }

    private function filterRecordBuildersByRequestedRecords(array $recordBuilders, array $requestedReports): array
    {
        // No record builders might be provided if the plugin does not (yet) provide any
        if (empty($recordBuilders)) {
            return $recordBuilders;
        }

        if (!empty($requestedReports)) {
            $recordBuilders = array_filter($recordBuilders, function (ArchiveProcessor\RecordBuilder $builder) use ($requestedReports) {
                return $builder->isBuilderForAtLeastOneOf($this->processor, $requestedReports);
            });
        }

        if (0 === count($recordBuilders)) {
            Log::debug(
                'Archiver: No record builders found for requested records %s',
                implode(',', $this->processor->getParams()->getArchiveOnlyReportAsArray())
            );
        }

        return $recordBuilders;
    }

    /**
     * @ignore
     */
    final public function callAggregateDayReport()
    {
        try {
            ErrorHandler::pushFatalErrorBreadcrumb(static::class);

            $pluginName = $this->getPluginName();

            if (Manager::getInstance()->isPluginLoaded($pluginName)) {
                $allRecordBuilders = $this->getRecordBuilders($pluginName);
                $recordBuilders = $this->filterRecordBuildersByRequestedRecords($allRecordBuilders, $this->processor->getParams()->getArchiveOnlyReportAsArray());

                foreach ($recordBuilders as $recordBuilder) {
                    if (!$recordBuilder->isEnabled($this->getProcessor())) {
                        continue;
                    }

                    $originalQueryHint = $this->getProcessor()->getLogAggregator()->getQueryOriginHint();
                    $newQueryHint = $originalQueryHint . ' ' . $recordBuilder->getQueryOriginHint();
                    try {
                        $this->getProcessor()->getLogAggregator()->setQueryOriginHint($newQueryHint);
                        $recordBuilder->buildFromLogs($this->getProcessor());
                    } finally {
                        $this->getProcessor()->getLogAggregator()->setQueryOriginHint($originalQueryHint);
                    }
                }
            }

            $this->aggregateDayReport();

            $this->processDependentArchivesForPlugins();
        } finally {
            ErrorHandler::popFatalErrorBreadcrumb();
        }
    }

    /**
     * @ignore
     */
    final public function callAggregateMultipleReports()
    {
        try {
            ErrorHandler::pushFatalErrorBreadcrumb(static::class);

            $pluginName = $this->getPluginName();

            if (Manager::getInstance()->isPluginLoaded($pluginName)) {
                $allRecordBuilders = $this->getRecordBuilders($pluginName);
                $recordBuilders = $this->filterRecordBuildersByRequestedRecords($allRecordBuilders, $this->processor->getParams()->getArchiveOnlyReportAsArray());

                foreach ($recordBuilders as $recordBuilder) {
                    if (!$recordBuilder->isEnabled($this->getProcessor())) {
                        continue;
                    }

                    $originalQueryHint = $this->getProcessor()->getLogAggregator()->getQueryOriginHint();
                    $newQueryHint = $originalQueryHint . ' ' . $recordBuilder->getQueryOriginHint();
                    try {
                        $this->getProcessor()->getLogAggregator()->setQueryOriginHint($newQueryHint);
                        $recordBuilder->buildForNonDayPeriod($this->getProcessor());
                    } finally {
                        $this->getProcessor()->getLogAggregator()->setQueryOriginHint($originalQueryHint);
                    }
                }
            }

            $this->aggregateMultipleReports();

            $this->processDependentArchivesForPlugins();
        } finally {
            ErrorHandler::popFatalErrorBreadcrumb();
        }
    }

    /**
     * Archives data for a day period.
     *
     * Implementations of this method should do more computation intensive activities such
     * as aggregating data across log tables. Since this method only deals w/ data logged for a day,
     * aggregating individual log table rows isn't a problem. Doing this for any larger period,
     * however, would cause performance degradation.
     *
     * Aggregate log table rows using a {@link Piwik\DataAccess\LogAggregator} instance. Get a
     * {@link Piwik\DataAccess\LogAggregator} instance using the {@link getLogAggregator()} method.
     */
    public function aggregateDayReport()
    {
        // empty
    }

    /**
     * Archives data for a non-day period.
     *
     * Implementations of this method should only aggregate existing reports of subperiods of the
     * current period. For example, it is more efficient to aggregate reports for each day of a
     * week than to aggregate each log entry of the week.
     *
     * Use {@link Piwik\ArchiveProcessor::aggregateNumericMetrics()} and {@link Piwik\ArchiveProcessor::aggregateDataTableRecords()}
     * to aggregate archived reports. Get the {@link Piwik\ArchiveProcessor} instance using the {@link getProcessor()}
     * method.
     */
    public function aggregateMultipleReports()
    {
        // empty
    }

    /**
     * Returns a {@link Piwik\ArchiveProcessor} instance that can be used to insert archive data for
     * the period, segment and site we are archiving data for.
     *
     * @return \Piwik\ArchiveProcessor
     * @api
     */
    protected function getProcessor()
    {
        return $this->processor;
    }

    /**
     * Returns a {@link Piwik\DataAccess\LogAggregator} instance that can be used to aggregate log table rows
     * for this period, segment and site.
     *
     * @return \Piwik\DataAccess\LogAggregator
     * @api
     */
    protected function getLogAggregator()
    {
        return $this->getProcessor()->getLogAggregator();
    }

    public function disable()
    {
        $this->enabled = false;
    }

    /**
     * Whether this Archiver should be used or not.
     *
     * @return bool
     */
    public function isEnabled()
    {
        return $this->enabled;
    }

    /**
     * By overwriting this method and returning true, a plugin archiver can force the archiving to run even when there
     * was no visit for the website/date/period/segment combination
     * (by default, archivers are skipped when there is no visit).
     *
     * @return bool
     */
    public static function shouldRunEvenWhenNoVisits()
    {
        return false;
    }

    /**
     * Returns a list of segments that should be pre-archived along with the segment currently being archived.
     * The segments in this list will be added to the current segment via an AND condition and archiving
     * for the current plugin will be launched. This process will not recurse further.
     *
     * If your plugin's API appends conditions to the requested segment when fetching data, you will want to
     * use this method to make sure those segments get pre-archived. Otherwise, if browser archiving is disabled,
     * the modified segments will appear to have no data.
     *
     * To archive another plugin, use an array instead of a string segment, for example:
     *
     * ```
     * ['plugin' => 'VisitsSummary', 'segment' => '...']
     * ```
     *
     * See the Goals and VisitFrequency plugins for examples.
     *
     * @return array
     * @api
     */
    public function getDependentSegmentsToArchive(): array
    {
        return [];
    }

    protected function isRequestedReport(string $reportName)
    {
        $requestedReport = $this->getProcessor()->getParams()->getArchiveOnlyReport();

        return empty($requestedReport) || $requestedReport == $reportName;
    }

    private function processDependentArchivesForPlugins()
    {
        if (!self::$ARCHIVE_DEPENDENT) {
            return;
        }

        $dependentSegments = $this->getDependentSegmentsToArchive();
        foreach ($dependentSegments as $dependentSegment) {
            $plugin = $this->getPluginName();
            $segment = $dependentSegment;

            if (is_array($dependentSegment)) {
                $plugin = $dependentSegment['plugin'] ?? $plugin;
                $segment = $dependentSegment['segment'];
            }

            $this->getProcessor()->processDependentArchive($plugin, $segment);
        }
    }

    private static function getDefaultConstructibleClasses(array $classes): array
    {
        return array_filter($classes, function ($className) {
            return (new \ReflectionClass($className))->getConstructor()->getNumberOfRequiredParameters() == 0;
        });
    }

    private static function getAllRecordBuilderClasses(): array
    {
        $transientCache = Cache::getTransientCache();
        $cacheKey = CacheId::siteAware('RecordBuilders.allRecordBuilders');

        $recordBuilderClasses = $transientCache->fetch($cacheKey);
        if ($recordBuilderClasses === false) {
            $recordBuilderClasses = Manager::getInstance()->findMultipleComponents('RecordBuilders', ArchiveProcessor\RecordBuilder::class);
            $recordBuilderClasses = self::getDefaultConstructibleClasses($recordBuilderClasses);

            $transientCache->save($cacheKey, $recordBuilderClasses);
        }
        return $recordBuilderClasses;
    }

    public static function doesPluginHaveRecordBuilders(string $pluginName): bool
    {
        $recordBuilders = self::getAllRecordBuilderClasses();
        foreach ($recordBuilders as $builder) {
            if ($pluginName === Piwik::getPluginNameOfMatomoClass($builder)) {
                return true;
            }
        }
        return false;
    }
}