| /** | |
| * 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\DataTable; | |
| use Closure; | |
| use Piwik\Common; | |
| use Piwik\DataTable; | |
| use Piwik\DataTable\Renderer\Html; | |
| /** | |
| * Stores an array of {@link DataTable}s indexed by one type of {@link DataTable} metadata (such as site ID | |
| * or period). | |
| * | |
| * DataTable Maps are returned on all queries that involve multiple sites and/or multiple | |
| * periods. The Maps will contain a {@link DataTable} for each site and period combination. | |
| * | |
| * The Map implements some {@link DataTable} such as {@link queueFilter()} and {@link getRowsCount}. | |
| * | |
| * | |
| * @api | |
| */ | |
| class Map implements DataTableInterface | |
| { | |
| /** | |
| * Array containing the DataTable within this Set | |
| * | |
| * @var DataTable[] | |
| */ | |
| protected $array = array(); | |
| /** | |
| * @see self::getKeyName() | |
| * @var string | |
| */ | |
| protected $keyName = 'defaultKeyName'; | |
| protected $isBuiltWithoutArchives = true; | |
| /** | |
| * Returns a string description of the data used to index the DataTables. | |
| * | |
| * This label is used by DataTable Renderers (it becomes a column name or the XML description tag). | |
| * | |
| * @return string eg, `'idSite'`, `'period'` | |
| */ | |
| public function getKeyName() | |
| { | |
| return $this->keyName; | |
| } | |
| /** | |
| * Set the name of they metadata used to index {@link DataTable}s. See {@link getKeyName()}. | |
| * | |
| * @param string $name | |
| */ | |
| public function setKeyName($name) | |
| { | |
| $this->keyName = $name; | |
| } | |
| /** | |
| * Returns the number of {@link DataTable}s in this DataTable\Map. | |
| * | |
| * @return int | |
| */ | |
| public function getRowsCount() | |
| { | |
| return count($this->getDataTables()); | |
| } | |
| /** | |
| * Queue a filter to {@link DataTable} child of contained by this instance. | |
| * | |
| * See {@link Piwik\DataTable::queueFilter()} for more information.. | |
| * | |
| * @param string|Closure $className Filter name, eg. `'Limit'` or a Closure. | |
| * @param array $parameters Filter parameters, eg. `array(50, 10)`. | |
| */ | |
| public function queueFilter($className, $parameters = array()) | |
| { | |
| foreach ($this->getDataTables() as $table) { | |
| $table->queueFilter($className, $parameters); | |
| } | |
| } | |
| /** | |
| * Apply the filters previously queued to each DataTable contained by this DataTable\Map. | |
| */ | |
| public function applyQueuedFilters() | |
| { | |
| foreach ($this->getDataTables() as $table) { | |
| $table->applyQueuedFilters(); | |
| } | |
| } | |
| /** | |
| * Apply a filter to all tables contained by this instance. | |
| * | |
| * @param string|Closure $className Name of filter class or a Closure. | |
| * @param array $parameters Parameters to pass to the filter. | |
| */ | |
| public function filter($className, $parameters = array()) | |
| { | |
| foreach ($this->getDataTables() as $table) { | |
| $table->filter($className, $parameters); | |
| } | |
| } | |
| /** | |
| * Apply a callback to all tables contained by this instance and tables with the same key in $otherTables. | |
| * | |
| * This method is used to iterate over multiple DataTable\Map's concurrently. | |
| * | |
| * $filter will be called with multiple DataTable instances, the first is the instance contained in | |
| * this Map instance. The rest are the corresponding instances found in $otherTables. The position of | |
| * the parameter in $filter corresponds with the position in $otherTables. | |
| * | |
| * If a key exists in this instance but not in one of the otherTables, $filter will be invoked with null | |
| * for that parameter. | |
| * | |
| * @param (Map|null)[] $otherTables Other tables to invoke $filter with. | |
| * @param callable $filter A function like `function (DataTable $thisTable, $otherTable1, $otherTable2, ...) {}`. | |
| * @return mixed[] The return value of each `multiFilter()` call made on child tables, indexed by the keys in this Map instance. | |
| */ | |
| public function multiFilter($otherTables, $filter) | |
| { | |
| $result = []; | |
| foreach ($this->getDataTables() as $key => $childTable) { | |
| $otherChildTables = array_map(function ($otherTable) use ($key) { | |
| return !empty($otherTable) && $otherTable->hasTable($key) ? $otherTable->getTable($key) : null; | |
| }, $otherTables); | |
| $result[$key] = $childTable->multiFilter($otherChildTables, $filter); | |
| } | |
| return $result; | |
| } | |
| /** | |
| * Apply a filter to all subtables contained by this instance. | |
| * | |
| * @param string|Closure $className Name of filter class or a Closure. | |
| * @param array $parameters Parameters to pass to the filter. | |
| */ | |
| public function filterSubtables($className, $parameters = array()) | |
| { | |
| foreach ($this->getDataTables() as $table) { | |
| $table->filterSubtables($className, $parameters); | |
| } | |
| } | |
| /** | |
| * Apply a queued filter to all subtables contained by this instance. | |
| * | |
| * @param string|Closure $className Name of filter class or a Closure. | |
| * @param array $parameters Parameters to pass to the filter. | |
| */ | |
| public function queueFilterSubtables($className, $parameters = array()) | |
| { | |
| foreach ($this->getDataTables() as $table) { | |
| $table->queueFilterSubtables($className, $parameters); | |
| } | |
| } | |
| /** | |
| * Returns the array of DataTables contained by this class. | |
| * | |
| * @return DataTable[]|Map[] | |
| */ | |
| public function getDataTables() | |
| { | |
| return $this->array; | |
| } | |
| /** | |
| * Returns the table with the specific label. | |
| * | |
| * @param string $label | |
| * @return DataTable|Map | |
| */ | |
| public function getTable($label) | |
| { | |
| return $this->array[$label]; | |
| } | |
| /** | |
| * @param string $label | |
| * @return bool | |
| */ | |
| public function hasTable($label) | |
| { | |
| return isset($this->array[$label]); | |
| } | |
| /** | |
| * Returns the first element in the Map's array. | |
| * | |
| * @return DataTable|Map|false | |
| */ | |
| public function getFirstRow() | |
| { | |
| return reset($this->array); | |
| } | |
| /** | |
| * Returns the last element in the Map's array. | |
| * | |
| * @return DataTable|Map|false | |
| */ | |
| public function getLastRow() | |
| { | |
| return end($this->array); | |
| } | |
| /** | |
| * Adds a new {@link DataTable} or Map instance to this DataTable\Map. | |
| * | |
| * @param DataTable|Map $table | |
| * @param string $label Label used to index this table in the array. | |
| */ | |
| public function addTable($table, $label) | |
| { | |
| $this->array[$label] = $table; | |
| } | |
| public function getRowFromIdSubDataTable($idSubtable) | |
| { | |
| $dataTables = $this->getDataTables(); | |
| // find first datatable containing data | |
| foreach ($dataTables as $subTable) { | |
| $subTableRow = $subTable->getRowFromIdSubDataTable($idSubtable); | |
| if (!empty($subTableRow)) { | |
| return $subTableRow; | |
| } | |
| } | |
| return null; | |
| } | |
| /** | |
| * Returns a string output of this DataTable\Map (applying the default renderer to every {@link DataTable} | |
| * of this DataTable\Map). | |
| * | |
| * @return string | |
| */ | |
| public function __toString() | |
| { | |
| $renderer = new Html(); | |
| $renderer->setTable($this); | |
| return (string)$renderer; | |
| } | |
| /** | |
| * See {@link DataTable::enableRecursiveSort()}. | |
| */ | |
| public function enableRecursiveSort() | |
| { | |
| foreach ($this->getDataTables() as $table) { | |
| $table->enableRecursiveSort(); | |
| } | |
| } | |
| /** | |
| * See {@link DataTable::disableFilter()}. | |
| */ | |
| public function disableFilter($className) | |
| { | |
| foreach ($this->getDataTables() as $table) { | |
| $table->disableFilter($className); | |
| } | |
| } | |
| /** | |
| * @ignore | |
| */ | |
| public function disableRecursiveFilters() | |
| { | |
| foreach ($this->getDataTables() as $table) { | |
| $table->disableRecursiveFilters(); | |
| } | |
| } | |
| /** | |
| * @ignore | |
| */ | |
| public function enableRecursiveFilters() | |
| { | |
| foreach ($this->getDataTables() as $table) { | |
| $table->enableRecursiveFilters(); | |
| } | |
| } | |
| /** | |
| * Renames the given column in each contained {@link DataTable}. | |
| * | |
| * See {@link DataTable::renameColumn()}. | |
| * | |
| * @param string $oldName | |
| * @param string $newName | |
| */ | |
| public function renameColumn($oldName, $newName) | |
| { | |
| foreach ($this->getDataTables() as $table) { | |
| $table->renameColumn($oldName, $newName); | |
| } | |
| } | |
| /** | |
| * Deletes the specified columns in each contained {@link DataTable}. | |
| * | |
| * See {@link DataTable::deleteColumns()}. | |
| * | |
| * @param list<string|int> $columns The columns to delete. | |
| * @param bool $deleteRecursiveInSubtables This param is currently not used. | |
| */ | |
| public function deleteColumns($columns, $deleteRecursiveInSubtables = false) | |
| { | |
| foreach ($this->getDataTables() as $table) { | |
| $table->deleteColumns($columns); | |
| } | |
| } | |
| /** | |
| * Deletes a table from the array of DataTables. | |
| * | |
| * @param string $id The label associated with {@link DataTable}. | |
| */ | |
| public function deleteRow($id) | |
| { | |
| unset($this->array[$id]); | |
| } | |
| /** | |
| * Deletes the given column in every contained {@link DataTable}. | |
| * | |
| * @param string|int $name | |
| * @see DataTable::deleteColumn | |
| */ | |
| public function deleteColumn($name) | |
| { | |
| foreach ($this->getDataTables() as $table) { | |
| $table->deleteColumn($name); | |
| } | |
| } | |
| /** | |
| * Returns the array containing all column values in all contained {@link DataTable}s for the requested column. | |
| * | |
| * @param string $name The column name. | |
| * @return array | |
| */ | |
| public function getColumn($name) | |
| { | |
| $values = array(); | |
| foreach ($this->getDataTables() as $table) { | |
| $moreValues = $table->getColumn($name); | |
| foreach ($moreValues as &$value) { | |
| $values[] = $value; | |
| } | |
| } | |
| return $values; | |
| } | |
| /** | |
| * Merges the rows of every child {@link DataTable} into a new one and | |
| * returns it. This function will also set the label of the merged rows | |
| * to the label of the {@link DataTable} they were originally from. | |
| * | |
| * The result of this function is determined by the type of DataTable | |
| * this instance holds. If this DataTable\Map instance holds an array | |
| * of DataTables, this function will transform it from: | |
| * | |
| * Label 0: | |
| * DataTable(row1) | |
| * Label 1: | |
| * DataTable(row2) | |
| * | |
| * to: | |
| * | |
| * DataTable(row1[label = 'Label 0'], row2[label = 'Label 1']) | |
| * | |
| * If this instance holds an array of DataTable\Maps, this function will | |
| * transform it from: | |
| * | |
| * Outer Label 0: // the outer DataTable\Map | |
| * Inner Label 0: // one of the inner DataTable\Maps | |
| * DataTable(row1) | |
| * Inner Label 1: | |
| * DataTable(row2) | |
| * Outer Label 1: | |
| * Inner Label 0: | |
| * DataTable(row3) | |
| * Inner Label 1: | |
| * DataTable(row4) | |
| * | |
| * to: | |
| * | |
| * Inner Label 0: | |
| * DataTable(row1[label = 'Outer Label 0'], row3[label = 'Outer Label 1']) | |
| * Inner Label 1: | |
| * DataTable(row2[label = 'Outer Label 0'], row4[label = 'Outer Label 1']) | |
| * | |
| * If this instance holds an array of DataTable\Maps, the | |
| * metadata of the first child is used as the metadata of the result. | |
| * | |
| * This function can be used, for example, to smoosh IndexedBySite archive | |
| * query results into one DataTable w/ different rows differentiated by site ID. | |
| * | |
| * Note: This DataTable/Map will be destroyed and will be no longer usable after the tables have been merged into | |
| * the new dataTable to reduce memory usage. Destroying all DataTables within the Map also seems to fix a | |
| * Segmentation Fault that occurred in the AllWebsitesDashboard when having > 16k sites. | |
| * | |
| * @return DataTable|Map | |
| */ | |
| public function mergeChildren() | |
| { | |
| $firstChild = reset($this->array); | |
| if ($firstChild instanceof Map) { | |
| $result = $firstChild->getEmptyClone(); | |
| /** @var Map $subDataTableMap */ | |
| foreach ($this->getDataTables() as $label => $subDataTableMap) { | |
| foreach ($subDataTableMap->getDataTables() as $innerLabel => $subTable) { | |
| if (!isset($result->array[$innerLabel])) { | |
| $dataTable = new DataTable(); | |
| $dataTable->setMetadataValues($subTable->getAllTableMetadata()); | |
| $result->addTable($dataTable, $innerLabel); | |
| } | |
| $this->copyRowsAndSetLabel($result->array[$innerLabel], $subTable, $label); | |
| } | |
| } | |
| } else { | |
| $result = new DataTable(); | |
| foreach ($this->getDataTables() as $label => $subTable) { | |
| $this->copyRowsAndSetLabel($result, $subTable, $label); | |
| Common::destroy($subTable); | |
| } | |
| $this->array = array(); | |
| } | |
| return $result; | |
| } | |
| /** | |
| * Utility function used by mergeChildren. Copies the rows from one table, | |
| * sets their 'label' columns to a value and adds them to another table. | |
| * | |
| * @param DataTable $toTable The table to copy rows to. | |
| * @param DataTable $fromTable The table to copy rows from. | |
| * @param string $label The value to set the 'label' column of every copied row. | |
| */ | |
| private function copyRowsAndSetLabel($toTable, $fromTable, $label) | |
| { | |
| foreach ($fromTable->getRows() as $fromRow) { | |
| $oldColumns = $fromRow->getColumns(); | |
| unset($oldColumns['label']); | |
| $columns = array_merge(array('label' => $label), $oldColumns); | |
| $row = new Row(array( | |
| Row::COLUMNS => $columns, | |
| Row::METADATA => $fromRow->getMetadata(), | |
| Row::DATATABLE_ASSOCIATED => $fromRow->getIdSubDataTable(), | |
| )); | |
| $toTable->addRow($row); | |
| } | |
| } | |
| /** | |
| * Sums a DataTable to all the tables in this array. | |
| * | |
| * _Note: Will only add `$tableToSum` if the childTable has some rows._ | |
| * | |
| * See {@link Piwik\DataTable::addDataTable()}. | |
| */ | |
| public function addDataTable(DataTable $tableToSum) | |
| { | |
| foreach ($this->getDataTables() as $childTable) { | |
| $childTable->addDataTable($tableToSum); | |
| } | |
| } | |
| /** | |
| * Returns a new DataTable\Map w/ child tables that have had their | |
| * subtables merged. | |
| * | |
| * See {@link DataTable::mergeSubtables()}. | |
| * | |
| * @return Map | |
| */ | |
| public function mergeSubtables() | |
| { | |
| $result = $this->getEmptyClone(); | |
| foreach ($this->getDataTables() as $label => $childTable) { | |
| $result->addTable($childTable->mergeSubtables(), $label); | |
| } | |
| return $result; | |
| } | |
| /** | |
| * Returns a new DataTable\Map w/o any child DataTables, but with | |
| * the same key name as this instance. | |
| * | |
| * @return Map | |
| */ | |
| public function getEmptyClone() | |
| { | |
| $dataTableMap = new Map(); | |
| $dataTableMap->setKeyName($this->getKeyName()); | |
| return $dataTableMap; | |
| } | |
| /** | |
| * Returns the intersection of children's metadata arrays (what they all have in common). | |
| * | |
| * @param string $name The metadata name. | |
| * @return mixed | |
| */ | |
| public function getMetadataIntersectArray($name) | |
| { | |
| $data = array(); | |
| foreach ($this->getDataTables() as $childTable) { | |
| $childData = $childTable->getMetadata($name); | |
| if (is_array($childData)) { | |
| $data = array_intersect($data, $childData); | |
| } | |
| } | |
| return array_values($data); | |
| } | |
| /** | |
| * Delete row metadata by name in every row. | |
| * | |
| * @param $name | |
| * @param bool $deleteRecursiveInSubtables | |
| */ | |
| public function deleteRowsMetadata($name, $deleteRecursiveInSubtables = false) | |
| { | |
| foreach ($this->getDataTables() as $table) { | |
| $table->deleteRowsMetadata($name, $deleteRecursiveInSubtables); | |
| } | |
| } | |
| /** | |
| * See {@link DataTable::getColumns()}. | |
| * | |
| * @return array | |
| */ | |
| public function getColumns() | |
| { | |
| foreach ($this->getDataTables() as $childTable) { | |
| if ($childTable->getRowsCount() > 0) { | |
| return $childTable->getColumns(); | |
| } | |
| } | |
| return array(); | |
| } | |
| public function setAsBuiltWithoutArchives(bool $flag): void | |
| { | |
| $this->isBuiltWithoutArchives = $flag; | |
| } | |
| public function wasBuiltWithoutArchives(): bool | |
| { | |
| return $this->isBuiltWithoutArchives; | |
| } | |
| } | |