PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 1.3.1
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v1.3.1
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.php
matomo / app / core Last commit date
API 6 years ago Access 6 years ago Application 6 years ago Archive 6 years ago ArchiveProcessor 6 years ago Archiver 6 years ago AssetManager 6 years ago Auth 6 years ago Category 6 years ago CliMulti 6 years ago Columns 6 years ago Composer 6 years ago Concurrency 6 years ago Config 6 years ago Container 6 years ago CronArchive 6 years ago DataAccess 5 years ago DataFiles 6 years ago DataTable 6 years ago Db 6 years ago DeviceDetector 5 years ago Email 6 years ago Exception 6 years ago Http 6 years ago Intl 6 years ago Mail 6 years ago Measurable 6 years ago Menu 6 years ago Metrics 6 years ago Notification 6 years ago Period 6 years ago Plugin 6 years ago ProfessionalServices 6 years ago Report 6 years ago ReportRenderer 6 years ago Scheduler 6 years ago Segment 6 years ago Session 6 years ago Settings 6 years ago Tracker 5 years ago Translation 6 years ago UpdateCheck 6 years ago Updater 6 years ago Updates 6 years ago Validators 6 years ago View 6 years ago ViewDataTable 6 years ago Visualization 6 years ago Widget 6 years ago .htaccess 6 years ago Access.php 6 years ago Archive.php 6 years ago ArchiveProcessor.php 6 years ago AssetManager.php 6 years ago Auth.php 6 years ago BaseFactory.php 6 years ago Cache.php 6 years ago CacheId.php 6 years ago CliMulti.php 6 years ago Common.php 6 years ago Config.php 6 years ago Console.php 6 years ago Context.php 6 years ago Cookie.php 5 years ago CronArchive.php 5 years ago DataArray.php 6 years ago DataTable.php 6 years ago Date.php 6 years ago Db.php 6 years ago DbHelper.php 6 years ago Development.php 6 years ago DeviceDetectorFactory.php 6 years ago ErrorHandler.php 6 years ago EventDispatcher.php 6 years ago ExceptionHandler.php 6 years ago FileIntegrity.php 6 years ago Filechecks.php 6 years ago Filesystem.php 6 years ago FrontController.php 6 years ago Http.php 6 years ago IP.php 6 years ago Log.php 6 years ago LogDeleter.php 6 years ago Mail.php 6 years ago Metrics.php 6 years ago MetricsFormatter.php 6 years ago Nonce.php 5 years ago Notification.php 6 years ago NumberFormatter.php 6 years ago Option.php 5 years ago Period.php 6 years ago Piwik.php 6 years ago Plugin.php 6 years ago Profiler.php 6 years ago ProxyHeaders.php 6 years ago ProxyHttp.php 6 years ago QuickForm2.php 6 years ago RankingQuery.php 6 years ago Registry.php 6 years ago ReportRenderer.php 6 years ago ScheduledTask.php 6 years ago Segment.php 6 years ago Sequence.php 6 years ago Session.php 6 years ago SettingsPiwik.php 6 years ago SettingsServer.php 6 years ago Singleton.php 6 years ago Site.php 6 years ago TCPDF.php 6 years ago TaskScheduler.php 6 years ago Theme.php 6 years ago Timer.php 6 years ago Tracker.php 6 years ago Translate.php 6 years ago Twig.php 6 years ago Unzip.php 6 years ago UpdateCheck.php 6 years ago Updater.php 6 years ago Updates.php 6 years ago Url.php 6 years ago UrlHelper.php 6 years ago Version.php 5 years ago View.php 6 years ago bootstrap.php 6 years ago dispatch.php 6 years ago testMinimumPhpVersion.php 6 years ago
ArchiveProcessor.php
672 lines
1 <?php
2 /**
3 * Piwik - free/libre analytics platform
4 *
5 * @link https://matomo.org
6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7 *
8 */
9 namespace Piwik;
10
11 use Exception;
12 use Piwik\Archive\DataTableFactory;
13 use Piwik\ArchiveProcessor\Parameters;
14 use Piwik\ArchiveProcessor\Rules;
15 use Piwik\DataAccess\ArchiveWriter;
16 use Piwik\DataAccess\LogAggregator;
17 use Piwik\DataTable\Manager;
18 use Piwik\DataTable\Map;
19 use Piwik\DataTable\Row;
20 use Piwik\Segment\SegmentExpression;
21
22 /**
23 * Used by {@link Piwik\Plugin\Archiver} instances to insert and aggregate archive data.
24 *
25 * ### See also
26 *
27 * - **{@link Piwik\Plugin\Archiver}** - to learn how plugins should implement their own analytics
28 * aggregation logic.
29 * - **{@link Piwik\DataAccess\LogAggregator}** - to learn how plugins can perform data aggregation
30 * across Piwik's log tables.
31 *
32 * ### Examples
33 *
34 * **Inserting numeric data**
35 *
36 * // function in an Archiver descendant
37 * public function aggregateDayReport()
38 * {
39 * $archiveProcessor = $this->getProcessor();
40 *
41 * $myFancyMetric = // ... calculate the metric value ...
42 * $archiveProcessor->insertNumericRecord('MyPlugin_myFancyMetric', $myFancyMetric);
43 * }
44 *
45 * **Inserting serialized DataTables**
46 *
47 * // function in an Archiver descendant
48 * public function aggregateDayReport()
49 * {
50 * $archiveProcessor = $this->getProcessor();
51 *
52 * $maxRowsInTable = Config::getInstance()->General['datatable_archiving_maximum_rows_standard'];j
53 *
54 * $dataTable = // ... build by aggregating visits ...
55 * $serializedData = $dataTable->getSerialized($maxRowsInTable, $maxRowsInSubtable = $maxRowsInTable,
56 * $columnToSortBy = Metrics::INDEX_NB_VISITS);
57 *
58 * $archiveProcessor->insertBlobRecords('MyPlugin_myFancyReport', $serializedData);
59 * }
60 *
61 * **Aggregating archive data**
62 *
63 * // function in Archiver descendant
64 * public function aggregateMultipleReports()
65 * {
66 * $archiveProcessor = $this->getProcessor();
67 *
68 * // aggregate a metric
69 * $archiveProcessor->aggregateNumericMetrics('MyPlugin_myFancyMetric');
70 * $archiveProcessor->aggregateNumericMetrics('MyPlugin_mySuperFancyMetric', 'max');
71 *
72 * // aggregate a report
73 * $archiveProcessor->aggregateDataTableRecords('MyPlugin_myFancyReport');
74 * }
75 *
76 */
77 class ArchiveProcessor
78 {
79 /**
80 * @var \Piwik\DataAccess\ArchiveWriter
81 */
82 private $archiveWriter;
83
84 /**
85 * @var \Piwik\DataAccess\LogAggregator
86 */
87 private $logAggregator;
88
89 /**
90 * @var Archive
91 */
92 public $archive = null;
93
94 /**
95 * @var Parameters
96 */
97 private $params;
98
99 /**
100 * @var int
101 */
102 private $numberOfVisits = false;
103
104 private $numberOfVisitsConverted = false;
105
106 public function __construct(Parameters $params, ArchiveWriter $archiveWriter, LogAggregator $logAggregator)
107 {
108 $this->params = $params;
109 $this->logAggregator = $logAggregator;
110 $this->archiveWriter = $archiveWriter;
111 }
112
113 protected function getArchive()
114 {
115 if (empty($this->archive)) {
116 $subPeriods = $this->params->getSubPeriods();
117 $idSites = $this->params->getIdSites();
118 $this->archive = Archive::factory($this->params->getSegment(), $subPeriods, $idSites);
119 }
120
121 return $this->archive;
122 }
123
124 public function setNumberOfVisits($visits, $visitsConverted)
125 {
126 $this->numberOfVisits = $visits;
127 $this->numberOfVisitsConverted = $visitsConverted;
128 }
129
130 /**
131 * Returns the {@link Parameters} object containing the site, period and segment we're archiving
132 * data for.
133 *
134 * @return Parameters
135 * @api
136 */
137 public function getParams()
138 {
139 return $this->params;
140 }
141
142 /**
143 * Returns a `{@link Piwik\DataAccess\LogAggregator}` instance for the site, period and segment this
144 * ArchiveProcessor will insert archive data for.
145 *
146 * @return LogAggregator
147 * @api
148 */
149 public function getLogAggregator()
150 {
151 return $this->logAggregator;
152 }
153
154 /**
155 * Array of (column name before => column name renamed) of the columns for which sum operation is invalid.
156 * These columns will be renamed as per this mapping.
157 * @var array
158 */
159 protected static $columnsToRenameAfterAggregation = array(
160 Metrics::INDEX_NB_UNIQ_VISITORS => Metrics::INDEX_SUM_DAILY_NB_UNIQ_VISITORS,
161 Metrics::INDEX_NB_USERS => Metrics::INDEX_SUM_DAILY_NB_USERS,
162 );
163
164 /**
165 * Sums records for every subperiod of the current period and inserts the result as the record
166 * for this period.
167 *
168 * DataTables are summed recursively so subtables will be summed as well.
169 *
170 * @param string|array $recordNames Name(s) of the report we are aggregating, eg, `'Referrers_type'`.
171 * @param int $maximumRowsInDataTableLevelZero Maximum number of rows allowed in the top level DataTable.
172 * @param int $maximumRowsInSubDataTable Maximum number of rows allowed in each subtable.
173 * @param string $columnToSortByBeforeTruncation The name of the column to sort by before truncating a DataTable.
174 * @param array $columnsAggregationOperation Operations for aggregating columns, see {@link Row::sumRow()}.
175 * @param array $columnsToRenameAfterAggregation Columns mapped to new names for columns that must change names
176 * when summed because they cannot be summed, eg,
177 * `array('nb_uniq_visitors' => 'sum_daily_nb_uniq_visitors')`.
178 * @param bool|array $countRowsRecursive if set to true, will calculate the recursive rows count for all record names
179 * which makes it slower. If you only need it for some records pass an array of
180 * recordNames that defines for which ones you need a recursive row count.
181 * @return array Returns the row counts of each aggregated report before truncation, eg,
182 *
183 * array(
184 * 'report1' => array('level0' => $report1->getRowsCount,
185 * 'recursive' => $report1->getRowsCountRecursive()),
186 * 'report2' => array('level0' => $report2->getRowsCount,
187 * 'recursive' => $report2->getRowsCountRecursive()),
188 * ...
189 * )
190 * @api
191 */
192 public function aggregateDataTableRecords($recordNames,
193 $maximumRowsInDataTableLevelZero = null,
194 $maximumRowsInSubDataTable = null,
195 $columnToSortByBeforeTruncation = null,
196 &$columnsAggregationOperation = null,
197 $columnsToRenameAfterAggregation = null,
198 $countRowsRecursive = true)
199 {
200 if (!is_array($recordNames)) {
201 $recordNames = array($recordNames);
202 }
203
204 $nameToCount = array();
205 foreach ($recordNames as $recordName) {
206 $latestUsedTableId = Manager::getInstance()->getMostRecentTableId();
207
208 $table = $this->aggregateDataTableRecord($recordName, $columnsAggregationOperation, $columnsToRenameAfterAggregation);
209
210 $nameToCount[$recordName]['level0'] = $table->getRowsCount();
211 if ($countRowsRecursive === true || (is_array($countRowsRecursive) && in_array($recordName, $countRowsRecursive))) {
212 $nameToCount[$recordName]['recursive'] = $table->getRowsCountRecursive();
213 }
214
215 $blob = $table->getSerialized($maximumRowsInDataTableLevelZero, $maximumRowsInSubDataTable, $columnToSortByBeforeTruncation);
216 Common::destroy($table);
217 $this->insertBlobRecord($recordName, $blob);
218
219 unset($blob);
220 DataTable\Manager::getInstance()->deleteAll($latestUsedTableId);
221 }
222
223 return $nameToCount;
224 }
225
226 /**
227 * Aggregates one or more metrics for every subperiod of the current period and inserts the results
228 * as metrics for the current period.
229 *
230 * @param array|string $columns Array of metric names to aggregate.
231 * @param bool|string $operationToApply The operation to apply to the metric. Either `'sum'`, `'max'` or `'min'`.
232 * @return array|int Returns the array of aggregate values. If only one metric was aggregated,
233 * the aggregate value will be returned as is, not in an array.
234 * For example, if `array('nb_visits', 'nb_hits')` is supplied for `$columns`,
235 *
236 * array(
237 * 'nb_visits' => 3040,
238 * 'nb_hits' => 405
239 * )
240 *
241 * could be returned. If `array('nb_visits')` or `'nb_visits'` is used for `$columns`,
242 * then `3040` would be returned.
243 * @api
244 */
245 public function aggregateNumericMetrics($columns, $operationToApply = false)
246 {
247 $metrics = $this->getAggregatedNumericMetrics($columns, $operationToApply);
248
249 foreach ($metrics as $column => $value) {
250 $value = Common::forceDotAsSeparatorForDecimalPoint($value);
251 $this->archiveWriter->insertRecord($column, $value);
252 }
253 // if asked for only one field to sum
254 if (count($metrics) == 1) {
255 return reset($metrics);
256 }
257
258 // returns the array of records once summed
259 return $metrics;
260 }
261
262 public function getNumberOfVisits()
263 {
264 if ($this->numberOfVisits === false) {
265 throw new Exception("visits should have been set here");
266 }
267 return $this->numberOfVisits;
268 }
269
270 public function getNumberOfVisitsConverted()
271 {
272 return $this->numberOfVisitsConverted;
273 }
274
275 /**
276 * Caches multiple numeric records in the archive for this processor's site, period
277 * and segment.
278 *
279 * @param array $numericRecords A name-value mapping of numeric values that should be
280 * archived, eg,
281 *
282 * array('Referrers_distinctKeywords' => 23, 'Referrers_distinctCampaigns' => 234)
283 * @api
284 */
285 public function insertNumericRecords($numericRecords)
286 {
287 foreach ($numericRecords as $name => $value) {
288 $this->insertNumericRecord($name, $value);
289 }
290 }
291
292 /**
293 * Caches a single numeric record in the archive for this processor's site, period and
294 * segment.
295 *
296 * Numeric values are not inserted if they equal `0`.
297 *
298 * @param string $name The name of the numeric value, eg, `'Referrers_distinctKeywords'`.
299 * @param float $value The numeric value.
300 * @api
301 */
302 public function insertNumericRecord($name, $value)
303 {
304 $value = round($value, 2);
305 $value = Common::forceDotAsSeparatorForDecimalPoint($value);
306
307 $this->archiveWriter->insertRecord($name, $value);
308 }
309
310 /**
311 * Caches one or more blob records in the archive for this processor's site, period
312 * and segment.
313 *
314 * @param string $name The name of the record, eg, 'Referrers_type'.
315 * @param string|array $values A blob string or an array of blob strings. If an array
316 * is used, the first element in the array will be inserted
317 * with the `$name` name. The others will be inserted with
318 * `$name . '_' . $index` as the record name (where $index is
319 * the index of the blob record in `$values`).
320 * @api
321 */
322 public function insertBlobRecord($name, $values)
323 {
324 $this->archiveWriter->insertBlobRecord($name, $values);
325 }
326
327 /**
328 * This method selects all DataTables that have the name $name over the period.
329 * All these DataTables are then added together, and the resulting DataTable is returned.
330 *
331 * @param string $name
332 * @param array $columnsAggregationOperation Operations for aggregating columns, @see Row::sumRow()
333 * @param array $columnsToRenameAfterAggregation columns in the array (old name, new name) to be renamed as the sum operation is not valid on them (eg. nb_uniq_visitors->sum_daily_nb_uniq_visitors)
334 * @return DataTable
335 */
336 protected function aggregateDataTableRecord($name, $columnsAggregationOperation = null, $columnsToRenameAfterAggregation = null)
337 {
338 try {
339 ErrorHandler::pushFatalErrorBreadcrumb(__CLASS__, ['name' => $name]);
340
341 // By default we shall aggregate all sub-tables.
342 $dataTable = $this->getArchive()->getDataTableExpanded($name, $idSubTable = null, $depth = null, $addMetadataSubtableId = false);
343
344 $columnsRenamed = false;
345
346 if ($dataTable instanceof Map) {
347 $columnsRenamed = true;
348 // see https://github.com/piwik/piwik/issues/4377
349 $self = $this;
350 $dataTable->filter(function ($table) use ($self, $columnsToRenameAfterAggregation) {
351
352 if ($self->areColumnsNotAlreadyRenamed($table)) {
353 /**
354 * This makes archiving and range dates a lot faster. Imagine we archive a week, then we will
355 * rename all columns of each 7 day archives. Afterwards we know the columns will be replaced in a
356 * week archive. When generating month archives, which uses mostly week archives, we do not have
357 * to replace those columns for the week archives again since we can be sure they were already
358 * replaced. Same when aggregating year and range archives. This can save up 10% or more when
359 * aggregating Month, Year and Range archives.
360 */
361 $self->renameColumnsAfterAggregation($table, $columnsToRenameAfterAggregation);
362 }
363 });
364 }
365
366 $dataTable = $this->getAggregatedDataTableMap($dataTable, $columnsAggregationOperation);
367
368 if (!$columnsRenamed) {
369 $this->renameColumnsAfterAggregation($dataTable, $columnsToRenameAfterAggregation);
370 }
371 } finally {
372 ErrorHandler::popFatalErrorBreadcrumb();
373 }
374
375 return $dataTable;
376 }
377
378 /**
379 * Note: public only for use in closure in PHP 5.3.
380 *
381 * @param $table
382 * @return \Piwik\Period
383 */
384 public function areColumnsNotAlreadyRenamed($table)
385 {
386 $period = $table->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX);
387
388 return !$period || $period->getLabel() === 'day';
389 }
390
391 protected function getOperationForColumns($columns, $defaultOperation)
392 {
393 $operationForColumn = array();
394 foreach ($columns as $name) {
395 $operation = $defaultOperation;
396 if (empty($operation)) {
397 $operation = $this->guessOperationForColumn($name);
398 }
399 $operationForColumn[$name] = $operation;
400 }
401 return $operationForColumn;
402 }
403
404 protected function enrichWithUniqueVisitorsMetric(Row $row)
405 {
406 if ($row->getColumn('nb_uniq_visitors') === false
407 && $row->getColumn('nb_users') === false
408 ) {
409 return;
410 }
411
412 $periodLabel = $this->getParams()->getPeriod()->getLabel();
413
414 if (!SettingsPiwik::isUniqueVisitorsEnabled($periodLabel)) {
415 $row->deleteColumn('nb_uniq_visitors');
416 $row->deleteColumn('nb_users');
417 return;
418 }
419
420 $sites = $this->getIdSitesToComputeNbUniques();
421
422 if (count($sites) > 1 && Rules::shouldSkipUniqueVisitorsCalculationForMultipleSites()) {
423 if ($periodLabel != 'day') {
424 // for day we still keep the aggregated metric but for other periods we remove it as it becomes to
425 // inaccurate
426 $row->deleteColumn('nb_uniq_visitors');
427 $row->deleteColumn('nb_users');
428 }
429 return;
430 }
431
432 if (empty($sites)) {
433 // a plugin disabled running below query by removing all sites.
434 $row->deleteColumn('nb_uniq_visitors');
435 $row->deleteColumn('nb_users');
436 return;
437 }
438
439 if (count($sites) === 1) {
440 $uniqueVisitorsMetric = Metrics::INDEX_NB_UNIQ_VISITORS;
441 } else {
442 if (!SettingsPiwik::isSameFingerprintAcrossWebsites()) {
443 throw new Exception("Processing unique visitors across websites is enabled for this instance,
444 but to process this metric you must first set enable_fingerprinting_across_websites=1
445 in the config file, under the [Tracker] section.");
446 }
447 $uniqueVisitorsMetric = Metrics::INDEX_NB_UNIQ_FINGERPRINTS;
448 }
449
450 $metrics = array(
451 Metrics::INDEX_NB_USERS,
452 $uniqueVisitorsMetric
453 );
454
455 $uniques = $this->computeNbUniques($metrics, $sites);
456
457 // see edge case as described in https://github.com/piwik/piwik/issues/9357 where uniq_visitors might be higher
458 // than visits because we archive / process it after nb_visits. Between archiving nb_visits and nb_uniq_visitors
459 // there could have been a new visit leading to a higher nb_unique_visitors than nb_visits which is not possible
460 // by definition. In this case we simply use the visits metric instead of unique visitors metric.
461 $visits = $row->getColumn('nb_visits');
462 if ($visits !== false && $uniques[$uniqueVisitorsMetric] !== false) {
463 $uniques[$uniqueVisitorsMetric] = min($uniques[$uniqueVisitorsMetric], $visits);
464 }
465
466 $row->setColumn('nb_uniq_visitors', $uniques[$uniqueVisitorsMetric]);
467 $row->setColumn('nb_users', $uniques[Metrics::INDEX_NB_USERS]);
468 }
469
470 protected function guessOperationForColumn($column)
471 {
472 if (strpos($column, 'max_') === 0) {
473 return 'max';
474 }
475 if (strpos($column, 'min_') === 0) {
476 return 'min';
477 }
478 return 'sum';
479 }
480
481 private function getIdSitesToComputeNbUniques()
482 {
483 $params = $this->getParams();
484 $sites = array($params->getSite()->getId());
485
486 /**
487 * Triggered to change which site ids should be looked at when processing unique visitors and users.
488 *
489 * @param array &$idSites An array with one idSite. This site is being archived currently. To cancel the query
490 * you can change this value to an empty array. To include other sites in the query you
491 * can add more idSites to this list of idSites.
492 * @param Period $period The period that is being requested to be archived.
493 * @param Segment $segment The segment that is request to be archived.
494 */
495 Piwik::postEvent('ArchiveProcessor.ComputeNbUniques.getIdSites', array(&$sites, $params->getPeriod(), $params->getSegment()));
496
497 return $sites;
498 }
499
500 /**
501 * Processes number of unique visitors for the given period
502 *
503 * This is the only Period metric (ie. week/month/year/range) that we process from the logs directly,
504 * since unique visitors cannot be summed like other metrics.
505 *
506 * @param array $metrics Metrics Ids for which to aggregates count of values
507 * @param int[] $sites A list of idSites that should be included
508 * @return array|null An array of metrics, where the key is metricid and the value is the metric value or null if
509 * the query was cancelled and not executed.
510 */
511 protected function computeNbUniques($metrics, $sites)
512 {
513 $logAggregator = $this->getLogAggregator();
514 $sitesBackup = $logAggregator->getSites();
515
516 $logAggregator->setSites($sites);
517 try {
518 $query = $logAggregator->queryVisitsByDimension(array(), false, array(), $metrics);
519 } finally {
520 $logAggregator->setSites($sitesBackup);
521 }
522 $data = $query->fetch();
523 return $data;
524 }
525
526 /**
527 * If the DataTable is a Map, sums all DataTable in the map and return the DataTable.
528 *
529 *
530 * @param $data DataTable|DataTable\Map
531 * @param $columnsToRenameAfterAggregation array
532 * @return DataTable
533 */
534 protected function getAggregatedDataTableMap($data, $columnsAggregationOperation)
535 {
536 $table = new DataTable();
537
538 if (!empty($columnsAggregationOperation)) {
539 $table->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, $columnsAggregationOperation);
540 }
541
542 if ($data instanceof DataTable\Map) {
543 // as $date => $tableToSum
544 $this->aggregatedDataTableMapsAsOne($data, $table);
545 } else {
546 $table->addDataTable($data);
547 }
548
549 return $table;
550 }
551
552 /**
553 * Aggregates the DataTable\Map into the destination $aggregated
554 * @param $map
555 * @param $aggregated
556 */
557 protected function aggregatedDataTableMapsAsOne(Map $map, DataTable $aggregated)
558 {
559 foreach ($map->getDataTables() as $tableToAggregate) {
560 if ($tableToAggregate instanceof Map) {
561 $this->aggregatedDataTableMapsAsOne($tableToAggregate, $aggregated);
562 } else {
563 $aggregated->addDataTable($tableToAggregate);
564 }
565 }
566 }
567
568 /**
569 * Note: public only for use in closure in PHP 5.3.
570 */
571 public function renameColumnsAfterAggregation(DataTable $table, $columnsToRenameAfterAggregation = null)
572 {
573 // Rename columns after aggregation
574 if (is_null($columnsToRenameAfterAggregation)) {
575 $columnsToRenameAfterAggregation = self::$columnsToRenameAfterAggregation;
576 }
577
578 if (empty($columnsToRenameAfterAggregation)) {
579 return;
580 }
581
582 foreach ($table->getRows() as $row) {
583 foreach ($columnsToRenameAfterAggregation as $oldName => $newName) {
584 $row->renameColumn($oldName, $newName);
585 }
586
587 $subTable = $row->getSubtable();
588 if ($subTable) {
589 $this->renameColumnsAfterAggregation($subTable, $columnsToRenameAfterAggregation);
590 }
591 }
592 }
593
594 protected function getAggregatedNumericMetrics($columns, $operationToApply)
595 {
596 if (!is_array($columns)) {
597 $columns = array($columns);
598 }
599
600 $operationForColumn = $this->getOperationForColumns($columns, $operationToApply);
601
602 $dataTable = $this->getArchive()->getDataTableFromNumeric($columns);
603
604 $results = $this->getAggregatedDataTableMap($dataTable, $operationForColumn);
605 if ($results->getRowsCount() > 1) {
606 throw new Exception("A DataTable is an unexpected state:" . var_export($results, true));
607 }
608
609 $rowMetrics = $results->getFirstRow();
610 if ($rowMetrics === false) {
611 $rowMetrics = new Row;
612 }
613 $this->enrichWithUniqueVisitorsMetric($rowMetrics);
614 $this->renameColumnsAfterAggregation($results, self::$columnsToRenameAfterAggregation);
615
616 $metrics = $rowMetrics->getColumns();
617
618 foreach ($columns as $name) {
619 if (!isset($metrics[$name])) {
620 $metrics[$name] = 0;
621 }
622 }
623
624 return $metrics;
625 }
626
627 /**
628 * Initiate archiving for a plugin during an ongoing archiving. The plugin can be another
629 * plugin or the same plugin.
630 *
631 * This method should be called during archiving when one plugin uses the report of another
632 * plugin with a segment. It will ensure reports for that segment & plugin will be archived
633 * without initiating archiving for every plugin with that segment (which would be a performance
634 * killer).
635 *
636 * @param string $plugin
637 * @param string $segment
638 */
639 public function processDependentArchive($plugin, $segment)
640 {
641 $params = $this->getParams();
642 if (!$params->isRootArchiveRequest()) { // prevent all recursion
643 return;
644 }
645
646 $idSites = [$params->getSite()->getId()];
647
648 $newSegment = Segment::combine($params->getSegment()->getString(), SegmentExpression::AND_DELIMITER, $segment);
649 if ($newSegment === $segment && $params->getRequestedPlugin() === $plugin) { // being processed now
650 return;
651 }
652
653 $newSegment = new Segment($newSegment, $idSites);
654 if (ArchiveProcessor\Rules::isSegmentPreProcessed($idSites, $newSegment)) {
655 // will be processed anyway
656 return;
657 }
658
659 $parameters = new ArchiveProcessor\Parameters($params->getSite(), $params->getPeriod(), $newSegment);
660 $parameters->onlyArchiveRequestedPlugin();
661 $parameters->setIsRootArchiveRequest(false);
662
663 $archiveLoader = new ArchiveProcessor\Loader($parameters);
664 $archiveLoader->prepareArchive($plugin);
665 }
666
667 public function getArchiveWriter()
668 {
669 return $this->archiveWriter;
670 }
671 }
672