1<?php
2/**
3 * Matomo - 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 */
9namespace Piwik\DataTable;
10
11use Closure;
12use Piwik\Common;
13use Piwik\DataTable;
14use Piwik\DataTable\Renderer\Console;
15use Piwik\DataTable\Renderer\Html;
16
17/**
18 * Stores an array of {@link DataTable}s indexed by one type of {@link DataTable} metadata (such as site ID
19 * or period).
20 *
21 * DataTable Maps are returned on all queries that involve multiple sites and/or multiple
22 * periods. The Maps will contain a {@link DataTable} for each site and period combination.
23 *
24 * The Map implements some {@link DataTable} such as {@link queueFilter()} and {@link getRowsCount}.
25 *
26 *
27 * @api
28 */
29class Map implements DataTableInterface
30{
31    /**
32     * Array containing the DataTable within this Set
33     *
34     * @var DataTable[]
35     */
36    protected $array = array();
37
38    /**
39     * @see self::getKeyName()
40     * @var string
41     */
42    protected $keyName = 'defaultKeyName';
43
44    /**
45     * Returns a string description of the data used to index the DataTables.
46     *
47     * This label is used by DataTable Renderers (it becomes a column name or the XML description tag).
48     *
49     * @return string eg, `'idSite'`, `'period'`
50     */
51    public function getKeyName()
52    {
53        return $this->keyName;
54    }
55
56    /**
57     * Set the name of they metadata used to index {@link DataTable}s. See {@link getKeyName()}.
58     *
59     * @param string $name
60     */
61    public function setKeyName($name)
62    {
63        $this->keyName = $name;
64    }
65
66    /**
67     * Returns the number of {@link DataTable}s in this DataTable\Map.
68     *
69     * @return int
70     */
71    public function getRowsCount()
72    {
73        return count($this->getDataTables());
74    }
75
76    /**
77     * Queue a filter to {@link DataTable} child of contained by this instance.
78     *
79     * See {@link Piwik\DataTable::queueFilter()} for more information..
80     *
81     * @param string|Closure $className Filter name, eg. `'Limit'` or a Closure.
82     * @param array $parameters Filter parameters, eg. `array(50, 10)`.
83     */
84    public function queueFilter($className, $parameters = array())
85    {
86        foreach ($this->getDataTables() as $table) {
87            $table->queueFilter($className, $parameters);
88        }
89    }
90
91    /**
92     * Apply the filters previously queued to each DataTable contained by this DataTable\Map.
93     */
94    public function applyQueuedFilters()
95    {
96        foreach ($this->getDataTables() as $table) {
97            $table->applyQueuedFilters();
98        }
99    }
100
101    /**
102     * Apply a filter to all tables contained by this instance.
103     *
104     * @param string|Closure $className Name of filter class or a Closure.
105     * @param array $parameters Parameters to pass to the filter.
106     */
107    public function filter($className, $parameters = array())
108    {
109        foreach ($this->getDataTables() as $table) {
110            $table->filter($className, $parameters);
111        }
112    }
113
114    /**
115     * Apply a filter to all subtables contained by this instance.
116     *
117     * @param string|Closure $className Name of filter class or a Closure.
118     * @param array $parameters Parameters to pass to the filter.
119     */
120    public function filterSubtables($className, $parameters = array())
121    {
122        foreach ($this->getDataTables() as $table) {
123            $table->filterSubtables($className, $parameters);
124        }
125    }
126
127    /**
128     * Apply a queued filter to all subtables contained by this instance.
129     *
130     * @param string|Closure $className Name of filter class or a Closure.
131     * @param array $parameters Parameters to pass to the filter.
132     */
133    public function queueFilterSubtables($className, $parameters = array())
134    {
135        foreach ($this->getDataTables() as $table) {
136            $table->queueFilterSubtables($className, $parameters);
137        }
138    }
139
140    /**
141     * Returns the array of DataTables contained by this class.
142     *
143     * @return DataTable[]|Map[]
144     */
145    public function getDataTables()
146    {
147        return $this->array;
148    }
149
150    /**
151     * Returns the table with the specific label.
152     *
153     * @param string $label
154     * @return DataTable|Map
155     */
156    public function getTable($label)
157    {
158        return $this->array[$label];
159    }
160
161    /**
162     * @param string $label
163     * @return bool
164     */
165    public function hasTable($label)
166    {
167        return isset($this->array[$label]);
168    }
169
170    /**
171     * Returns the first element in the Map's array.
172     *
173     * @return DataTable|Map|false
174     */
175    public function getFirstRow()
176    {
177        return reset($this->array);
178    }
179
180    /**
181     * Returns the last element in the Map's array.
182     *
183     * @return DataTable|Map|false
184     */
185    public function getLastRow()
186    {
187        return end($this->array);
188    }
189
190    /**
191     * Adds a new {@link DataTable} or Map instance to this DataTable\Map.
192     *
193     * @param DataTable|Map $table
194     * @param string $label Label used to index this table in the array.
195     */
196    public function addTable($table, $label)
197    {
198        $this->array[$label] = $table;
199    }
200
201    public function getRowFromIdSubDataTable($idSubtable)
202    {
203        $dataTables = $this->getDataTables();
204
205        // find first datatable containing data
206        foreach ($dataTables as $subTable) {
207            $subTableRow = $subTable->getRowFromIdSubDataTable($idSubtable);
208
209            if (!empty($subTableRow)) {
210                return $subTableRow;
211            }
212        }
213
214        return null;
215    }
216
217    /**
218     * Returns a string output of this DataTable\Map (applying the default renderer to every {@link DataTable}
219     * of this DataTable\Map).
220     *
221     * @return string
222     */
223    public function __toString()
224    {
225        $renderer = new Html();
226        $renderer->setTable($this);
227        return (string)$renderer;
228    }
229
230    /**
231     * See {@link DataTable::enableRecursiveSort()}.
232     */
233    public function enableRecursiveSort()
234    {
235        foreach ($this->getDataTables() as $table) {
236            $table->enableRecursiveSort();
237        }
238    }
239
240    /**
241     * See {@link DataTable::disableFilter()}.
242     */
243    public function disableFilter($className)
244    {
245        foreach ($this->getDataTables() as $table) {
246            $table->disableFilter($className);
247        }
248    }
249
250    /**
251     * @ignore
252     */
253    public function disableRecursiveFilters()
254    {
255        foreach ($this->getDataTables() as $table) {
256            $table->disableRecursiveFilters();
257        }
258    }
259
260    /**
261     * @ignore
262     */
263    public function enableRecursiveFilters()
264    {
265        foreach ($this->getDataTables() as $table) {
266            $table->enableRecursiveFilters();
267        }
268    }
269
270    /**
271     * Renames the given column in each contained {@link DataTable}.
272     *
273     * See {@link DataTable::renameColumn()}.
274     *
275     * @param string $oldName
276     * @param string $newName
277     */
278    public function renameColumn($oldName, $newName)
279    {
280        foreach ($this->getDataTables() as $table) {
281            $table->renameColumn($oldName, $newName);
282        }
283    }
284
285    /**
286     * Deletes the specified columns in each contained {@link DataTable}.
287     *
288     * See {@link DataTable::deleteColumns()}.
289     *
290     * @param array $columns The columns to delete.
291     * @param bool $deleteRecursiveInSubtables This param is currently not used.
292     */
293    public function deleteColumns($columns, $deleteRecursiveInSubtables = false)
294    {
295        foreach ($this->getDataTables() as $table) {
296            $table->deleteColumns($columns);
297        }
298    }
299
300    /**
301     * Deletes a table from the array of DataTables.
302     *
303     * @param string $id The label associated with {@link DataTable}.
304     */
305    public function deleteRow($id)
306    {
307        unset($this->array[$id]);
308    }
309
310    /**
311     * Deletes the given column in every contained {@link DataTable}.
312     *
313     * @see DataTable::deleteColumn
314     * @param string $name
315     */
316    public function deleteColumn($name)
317    {
318        foreach ($this->getDataTables() as $table) {
319            $table->deleteColumn($name);
320        }
321    }
322
323    /**
324     * Returns the array containing all column values in all contained {@link DataTable}s for the requested column.
325     *
326     * @param string $name The column name.
327     * @return array
328     */
329    public function getColumn($name)
330    {
331        $values = array();
332
333        foreach ($this->getDataTables() as $table) {
334            $moreValues = $table->getColumn($name);
335            foreach ($moreValues as &$value) {
336                $values[] = $value;
337            }
338        }
339
340        return $values;
341    }
342
343    /**
344     * Merges the rows of every child {@link DataTable} into a new one and
345     * returns it. This function will also set the label of the merged rows
346     * to the label of the {@link DataTable} they were originally from.
347     *
348     * The result of this function is determined by the type of DataTable
349     * this instance holds. If this DataTable\Map instance holds an array
350     * of DataTables, this function will transform it from:
351     *
352     *     Label 0:
353     *       DataTable(row1)
354     *     Label 1:
355     *       DataTable(row2)
356     *
357     * to:
358     *
359     *     DataTable(row1[label = 'Label 0'], row2[label = 'Label 1'])
360     *
361     * If this instance holds an array of DataTable\Maps, this function will
362     * transform it from:
363     *
364     *     Outer Label 0:            // the outer DataTable\Map
365     *       Inner Label 0:            // one of the inner DataTable\Maps
366     *         DataTable(row1)
367     *       Inner Label 1:
368     *         DataTable(row2)
369     *     Outer Label 1:
370     *       Inner Label 0:
371     *         DataTable(row3)
372     *       Inner Label 1:
373     *         DataTable(row4)
374     *
375     * to:
376     *
377     *     Inner Label 0:
378     *       DataTable(row1[label = 'Outer Label 0'], row3[label = 'Outer Label 1'])
379     *     Inner Label 1:
380     *       DataTable(row2[label = 'Outer Label 0'], row4[label = 'Outer Label 1'])
381     *
382     * If this instance holds an array of DataTable\Maps, the
383     * metadata of the first child is used as the metadata of the result.
384     *
385     * This function can be used, for example, to smoosh IndexedBySite archive
386     * query results into one DataTable w/ different rows differentiated by site ID.
387     *
388     * Note: This DataTable/Map will be destroyed and will be no longer usable after the tables have been merged into
389     *       the new dataTable to reduce memory usage. Destroying all DataTables witihn the Map also seems to fix a
390     *       Segmentation Fault that occurred in the AllWebsitesDashboard when having > 16k sites.
391     *
392     * @return DataTable|Map
393     */
394    public function mergeChildren()
395    {
396        $firstChild = reset($this->array);
397
398        if ($firstChild instanceof Map) {
399            $result = $firstChild->getEmptyClone();
400
401            /** @var $subDataTableMap Map */
402            foreach ($this->getDataTables() as $label => $subDataTableMap) {
403                foreach ($subDataTableMap->getDataTables() as $innerLabel => $subTable) {
404                    if (!isset($result->array[$innerLabel])) {
405                        $dataTable = new DataTable();
406                        $dataTable->setMetadataValues($subTable->getAllTableMetadata());
407
408                        $result->addTable($dataTable, $innerLabel);
409                    }
410
411                    $this->copyRowsAndSetLabel($result->array[$innerLabel], $subTable, $label);
412                }
413            }
414        } else {
415            $result = new DataTable();
416
417            foreach ($this->getDataTables() as $label => $subTable) {
418                $this->copyRowsAndSetLabel($result, $subTable, $label);
419                Common::destroy($subTable);
420            }
421
422            $this->array = array();
423        }
424
425        return $result;
426    }
427
428    /**
429     * Utility function used by mergeChildren. Copies the rows from one table,
430     * sets their 'label' columns to a value and adds them to another table.
431     *
432     * @param DataTable $toTable The table to copy rows to.
433     * @param DataTable $fromTable The table to copy rows from.
434     * @param string $label The value to set the 'label' column of every copied row.
435     */
436    private function copyRowsAndSetLabel($toTable, $fromTable, $label)
437    {
438        foreach ($fromTable->getRows() as $fromRow) {
439            $oldColumns = $fromRow->getColumns();
440            unset($oldColumns['label']);
441
442            $columns = array_merge(array('label' => $label), $oldColumns);
443            $row = new Row(array(
444                                Row::COLUMNS              => $columns,
445                                Row::METADATA             => $fromRow->getMetadata(),
446                                Row::DATATABLE_ASSOCIATED => $fromRow->getIdSubDataTable()
447                           ));
448            $toTable->addRow($row);
449        }
450    }
451
452    /**
453     * Sums a DataTable to all the tables in this array.
454     *
455     * _Note: Will only add `$tableToSum` if the childTable has some rows._
456     *
457     * See {@link Piwik\DataTable::addDataTable()}.
458     *
459     * @param DataTable $tableToSum
460     */
461    public function addDataTable(DataTable $tableToSum)
462    {
463        foreach ($this->getDataTables() as $childTable) {
464            $childTable->addDataTable($tableToSum);
465        }
466    }
467
468    /**
469     * Returns a new DataTable\Map w/ child tables that have had their
470     * subtables merged.
471     *
472     * See {@link DataTable::mergeSubtables()}.
473     *
474     * @return Map
475     */
476    public function mergeSubtables()
477    {
478        $result = $this->getEmptyClone();
479        foreach ($this->getDataTables() as $label => $childTable) {
480            $result->addTable($childTable->mergeSubtables(), $label);
481        }
482        return $result;
483    }
484
485    /**
486     * Returns a new DataTable\Map w/o any child DataTables, but with
487     * the same key name as this instance.
488     *
489     * @return Map
490     */
491    public function getEmptyClone()
492    {
493        $dataTableMap = new Map;
494        $dataTableMap->setKeyName($this->getKeyName());
495        return $dataTableMap;
496    }
497
498    /**
499     * Returns the intersection of children's metadata arrays (what they all have in common).
500     *
501     * @param string $name The metadata name.
502     * @return mixed
503     */
504    public function getMetadataIntersectArray($name)
505    {
506        $data = array();
507        foreach ($this->getDataTables() as $childTable) {
508            $childData = $childTable->getMetadata($name);
509            if (is_array($childData)) {
510                $data = array_intersect($data, $childData);
511            }
512        }
513        return array_values($data);
514    }
515
516    /**
517     * Delete row metadata by name in every row.
518     *
519     * @param       $name
520     * @param bool $deleteRecursiveInSubtables
521     */
522    public function deleteRowsMetadata($name, $deleteRecursiveInSubtables = false)
523    {
524        foreach ($this->getDataTables() as $table) {
525            $table->deleteRowsMetadata($name, $deleteRecursiveInSubtables);
526        }
527    }
528
529    /**
530     * See {@link DataTable::getColumns()}.
531     *
532     * @return array
533     */
534    public function getColumns()
535    {
536        foreach ($this->getDataTables() as $childTable) {
537            if ($childTable->getRowsCount() > 0) {
538                return $childTable->getColumns();
539            }
540        }
541        return array();
542    }
543}
544