forked from rebillar/site-accueil-insa
2055 lines
70 KiB
PHP
2055 lines
70 KiB
PHP
<?php
|
|
/**
|
|
* Matomo - free/libre analytics platform
|
|
*
|
|
* @link https://matomo.org
|
|
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
|
*
|
|
*/
|
|
|
|
namespace Piwik;
|
|
|
|
use Closure;
|
|
use Exception;
|
|
use Piwik\DataTable\DataTableInterface;
|
|
use Piwik\DataTable\Manager;
|
|
use Piwik\DataTable\Renderer\Html;
|
|
use Piwik\DataTable\Row;
|
|
use Piwik\DataTable\Row\DataTableSummaryRow;
|
|
use Piwik\DataTable\Simple;
|
|
use ReflectionClass;
|
|
|
|
/**
|
|
* @see Common::destroy()
|
|
*/
|
|
require_once PIWIK_INCLUDE_PATH . '/core/Common.php';
|
|
require_once PIWIK_INCLUDE_PATH . "/core/DataTable/Bridges.php";
|
|
|
|
/**
|
|
* The primary data structure used to store analytics data in Piwik.
|
|
*
|
|
* <a name="class-desc-the-basics"></a>
|
|
* ### The Basics
|
|
*
|
|
* DataTables consist of rows and each row consists of columns. A column value can be
|
|
* a numeric, a string or an array.
|
|
*
|
|
* Every row has an ID. The ID is either the index of the row or {@link ID_SUMMARY_ROW}.
|
|
*
|
|
* DataTables are hierarchical data structures. Each row can also contain an additional
|
|
* nested sub-DataTable (commonly referred to as a 'subtable').
|
|
*
|
|
* Both DataTables and DataTable rows can hold **metadata**. _DataTable metadata_ is information
|
|
* regarding all the data, such as the site or period that the data is for. _Row metadata_
|
|
* is information regarding that row, such as a browser logo or website URL.
|
|
*
|
|
* Finally, all DataTables contain a special _summary_ row. This row, if it exists, is
|
|
* always at the end of the DataTable.
|
|
*
|
|
* ### Populating DataTables
|
|
*
|
|
* Data can be added to DataTables in three different ways. You can either:
|
|
*
|
|
* 1. create rows one by one and add them through {@link addRow()} then truncate if desired,
|
|
* 2. create an array of DataTable\Row instances or an array of arrays and add them using
|
|
* {@link addRowsFromArray()} or {@link addRowsFromSimpleArray()}
|
|
* then truncate if desired,
|
|
* 3. or set the maximum number of allowed rows (with {@link setMaximumAllowedRows()})
|
|
* and add rows one by one.
|
|
*
|
|
* If you want to eventually truncate your data (standard practice for all Piwik plugins),
|
|
* the third method is the most memory efficient. It is, unfortunately, not always possible
|
|
* to use since it requires that the data be sorted before adding.
|
|
*
|
|
* ### Manipulating DataTables
|
|
*
|
|
* There are two ways to manipulate a DataTable. You can either:
|
|
*
|
|
* 1. manually iterate through each row and manipulate the data,
|
|
* 2. or you can use predefined filters.
|
|
*
|
|
* A filter is a class that has a 'filter' method which will manipulate a DataTable in
|
|
* some way. There are several predefined Filters that allow you to do common things,
|
|
* such as,
|
|
*
|
|
* - add a new column to each row,
|
|
* - add new metadata to each row,
|
|
* - modify an existing column value for each row,
|
|
* - sort an entire DataTable,
|
|
* - and more.
|
|
*
|
|
* Using these filters instead of writing your own code will increase code clarity and
|
|
* reduce code redundancy. Additionally, filters have the advantage that they can be
|
|
* applied to DataTable\Map instances. So you can visit every DataTable in a {@link DataTable\Map}
|
|
* without having to write a recursive visiting function.
|
|
*
|
|
* All predefined filters exist in the **Piwik\DataTable\BaseFilter** namespace.
|
|
*
|
|
* _Note: For convenience, [anonymous functions](http://www.php.net/manual/en/functions.anonymous.php)
|
|
* can be used as DataTable filters._
|
|
*
|
|
* ### Applying Filters
|
|
*
|
|
* Filters can be applied now (via {@link filter()}), or they can be applied later (via
|
|
* {@link queueFilter()}).
|
|
*
|
|
* Filters that sort rows or manipulate the number of rows should be applied right away.
|
|
* Non-essential, presentation filters should be queued.
|
|
*
|
|
* ### Learn more
|
|
*
|
|
* - See **{@link ArchiveProcessor}** to learn how DataTables are persisted.
|
|
*
|
|
* ### Examples
|
|
*
|
|
* **Populating a DataTable**
|
|
*
|
|
* // adding one row at a time
|
|
* $dataTable = new DataTable();
|
|
* $dataTable->addRow(new Row(array(
|
|
* Row::COLUMNS => array('label' => 'thing1', 'nb_visits' => 1, 'nb_actions' => 1),
|
|
* Row::METADATA => array('url' => 'http://thing1.com')
|
|
* )));
|
|
* $dataTable->addRow(new Row(array(
|
|
* Row::COLUMNS => array('label' => 'thing2', 'nb_visits' => 2, 'nb_actions' => 2),
|
|
* Row::METADATA => array('url' => 'http://thing2.com')
|
|
* )));
|
|
*
|
|
* // using an array of rows
|
|
* $dataTable = new DataTable();
|
|
* $dataTable->addRowsFromArray(array(
|
|
* array(
|
|
* Row::COLUMNS => array('label' => 'thing1', 'nb_visits' => 1, 'nb_actions' => 1),
|
|
* Row::METADATA => array('url' => 'http://thing1.com')
|
|
* ),
|
|
* array(
|
|
* Row::COLUMNS => array('label' => 'thing2', 'nb_visits' => 2, 'nb_actions' => 2),
|
|
* Row::METADATA => array('url' => 'http://thing2.com')
|
|
* )
|
|
* ));
|
|
*
|
|
* // using a "simple" array
|
|
* $dataTable->addRowsFromSimpleArray(array(
|
|
* array('label' => 'thing1', 'nb_visits' => 1, 'nb_actions' => 1),
|
|
* array('label' => 'thing2', 'nb_visits' => 2, 'nb_actions' => 2)
|
|
* ));
|
|
*
|
|
* **Getting & setting metadata**
|
|
*
|
|
* $dataTable = \Piwik\Plugins\Referrers\API::getInstance()->getSearchEngines($idSite = 1, $period = 'day', $date = '2007-07-24');
|
|
* $oldPeriod = $dataTable->metadata['period'];
|
|
* $dataTable->metadata['period'] = Period\Factory::build('week', Date::factory('2013-10-18'));
|
|
*
|
|
* **Serializing & unserializing**
|
|
*
|
|
* $maxRowsInTable = Config::getInstance()->General['datatable_archiving_maximum_rows_standard'];j
|
|
*
|
|
* $dataTable = // ... build by aggregating visits ...
|
|
* $serializedData = $dataTable->getSerialized($maxRowsInTable, $maxRowsInSubtable = $maxRowsInTable,
|
|
* $columnToSortBy = Metrics::INDEX_NB_VISITS);
|
|
*
|
|
* $serializedDataTable = $serializedData[0];
|
|
* $serailizedSubTable = $serializedData[$idSubtable];
|
|
*
|
|
* **Filtering for an API method**
|
|
*
|
|
* public function getMyReport($idSite, $period, $date, $segment = false, $expanded = false)
|
|
* {
|
|
* $dataTable = Archive::createDataTableFromArchive('MyPlugin_MyReport', $idSite, $period, $date, $segment, $expanded);
|
|
* $dataTable->filter('Sort', array(Metrics::INDEX_NB_VISITS, 'desc', $naturalSort = false, $expanded));
|
|
* $dataTable->queueFilter('ColumnCallbackAddMetadata', array('label', 'url', __NAMESPACE__ . '\getUrlFromLabelForMyReport'));
|
|
* return $dataTable;
|
|
* }
|
|
*
|
|
*
|
|
* @api
|
|
*/
|
|
class DataTable implements DataTableInterface, \IteratorAggregate, \ArrayAccess
|
|
{
|
|
const MAX_DEPTH_DEFAULT = 15;
|
|
|
|
/** Name for metadata that describes when a report was archived. */
|
|
const ARCHIVED_DATE_METADATA_NAME = 'ts_archived';
|
|
|
|
/** Name for metadata that describes which columns are empty and should not be shown. */
|
|
const EMPTY_COLUMNS_METADATA_NAME = 'empty_column';
|
|
|
|
/** Name for metadata that describes the number of rows that existed before the Limit filter was applied. */
|
|
const TOTAL_ROWS_BEFORE_LIMIT_METADATA_NAME = 'total_rows_before_limit';
|
|
|
|
/**
|
|
* Name for metadata that describes how individual columns should be aggregated when {@link addDataTable()}
|
|
* or {@link Piwik\DataTable\Row::sumRow()} is called.
|
|
*
|
|
* This metadata value must be an array that maps column names with valid operations. Valid aggregation operations are:
|
|
*
|
|
* - `'skip'`: do nothing
|
|
* - `'max'`: does `max($column1, $column2)`
|
|
* - `'min'`: does `min($column1, $column2)`
|
|
* - `'sum'`: does `$column1 + $column2`
|
|
*
|
|
* See {@link addDataTable()} and {@link DataTable\Row::sumRow()} for more information.
|
|
*/
|
|
const COLUMN_AGGREGATION_OPS_METADATA_NAME = 'column_aggregation_ops';
|
|
|
|
/**
|
|
* Name for metadata that stores array of generic filters that should not be run on the table.
|
|
*/
|
|
const GENERIC_FILTERS_TO_DISABLE_METADATA_NAME = 'generic_filters_to_disable';
|
|
|
|
/** The ID of the Summary Row. */
|
|
const ID_SUMMARY_ROW = -1;
|
|
|
|
/**
|
|
* The ID of the special metadata row. This row only exists in the serialized row data and stores the datatable metadata.
|
|
*
|
|
* This allows us to save datatable metadata in archive data.
|
|
*/
|
|
const ID_ARCHIVED_METADATA_ROW = -3;
|
|
|
|
/** The original label of the Summary Row. */
|
|
const LABEL_SUMMARY_ROW = -1;
|
|
const LABEL_TOTALS_ROW = -2;
|
|
const LABEL_ARCHIVED_METADATA_ROW = '__datatable_metadata__';
|
|
|
|
/**
|
|
* Name for metadata that contains extra {@link Piwik\Plugin\ProcessedMetric}s for a DataTable.
|
|
* These metrics will be added in addition to the ones specified in the table's associated
|
|
* {@link Piwik\Plugin\Report} class.
|
|
*/
|
|
const EXTRA_PROCESSED_METRICS_METADATA_NAME = 'extra_processed_metrics';
|
|
|
|
/**
|
|
* Maximum nesting level.
|
|
*/
|
|
private static $maximumDepthLevelAllowed = self::MAX_DEPTH_DEFAULT;
|
|
|
|
/**
|
|
* Array of Row
|
|
*
|
|
* @var Row[]
|
|
*/
|
|
protected $rows = array();
|
|
|
|
/**
|
|
* Id assigned to the DataTable, used to lookup the table using the DataTable_Manager
|
|
*
|
|
* @var int
|
|
*/
|
|
protected $currentId;
|
|
|
|
/**
|
|
* Current depth level of this data table
|
|
* 0 is the parent data table
|
|
*
|
|
* @var int
|
|
*/
|
|
protected $depthLevel = 0;
|
|
|
|
/**
|
|
* This flag is set to false once we modify the table in a way that outdates the index
|
|
*
|
|
* @var bool
|
|
*/
|
|
protected $indexNotUpToDate = true;
|
|
|
|
/**
|
|
* This flag sets the index to be rebuild whenever a new row is added,
|
|
* as opposed to re-building the full index when getRowFromLabel is called.
|
|
* This is to optimize and not rebuild the full Index in the case where we
|
|
* add row, getRowFromLabel, addRow, getRowFromLabel thousands of times.
|
|
*
|
|
* @var bool
|
|
*/
|
|
protected $rebuildIndexContinuously = false;
|
|
|
|
/**
|
|
* Column name of last time the table was sorted
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $tableSortedBy = false;
|
|
|
|
/**
|
|
* List of BaseFilter queued to this table
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $queuedFilters = array();
|
|
|
|
/**
|
|
* List of disabled filter names eg 'Limit' or 'Sort'
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $disabledFilters = array();
|
|
|
|
/**
|
|
* We keep track of the number of rows before applying the LIMIT filter that deletes some rows
|
|
*
|
|
* @var int
|
|
*/
|
|
protected $rowsCountBeforeLimitFilter = 0;
|
|
|
|
/**
|
|
* Defaults to false for performance reasons (most of the time we don't need recursive sorting so we save a looping over the dataTable)
|
|
*
|
|
* @var bool
|
|
*/
|
|
protected $enableRecursiveSort = false;
|
|
|
|
/**
|
|
* When the table and all subtables are loaded, this flag will be set to true to ensure filters are applied to all subtables
|
|
*
|
|
* @var bool
|
|
*/
|
|
protected $enableRecursiveFilters = false;
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
protected $rowsIndexByLabel = array();
|
|
|
|
/**
|
|
* @var \Piwik\DataTable\Row
|
|
*/
|
|
protected $summaryRow = null;
|
|
|
|
/**
|
|
* @var \Piwik\DataTable\Row
|
|
*/
|
|
protected $totalsRow = null;
|
|
|
|
/**
|
|
* Table metadata. Read [this](#class-desc-the-basics) to learn more.
|
|
*
|
|
* Any data that describes the data held in the table's rows should go here.
|
|
*
|
|
* Note: this field is protected so derived classes will serialize it.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $metadata = array();
|
|
|
|
/**
|
|
* Maximum number of rows allowed in this datatable (including the summary row).
|
|
* If adding more rows is attempted, the extra rows get summed to the summary row.
|
|
*
|
|
* @var int
|
|
*/
|
|
protected $maximumAllowedRows = 0;
|
|
|
|
/**
|
|
* Constructor. Creates an empty DataTable.
|
|
*/
|
|
public function __construct()
|
|
{
|
|
// registers this instance to the manager
|
|
$this->currentId = Manager::getInstance()->addTable($this);
|
|
}
|
|
|
|
/**
|
|
* Destructor. Makes sure DataTable memory will be cleaned up.
|
|
*/
|
|
public function __destruct()
|
|
{
|
|
static $depth = 0;
|
|
// destruct can be called several times
|
|
if ($depth < self::$maximumDepthLevelAllowed
|
|
&& isset($this->rows)
|
|
) {
|
|
$depth++;
|
|
foreach ($this->rows as $row) {
|
|
Common::destroy($row);
|
|
}
|
|
if (isset($this->summaryRow)) {
|
|
Common::destroy($this->summaryRow);
|
|
}
|
|
unset($this->rows);
|
|
Manager::getInstance()->setTableDeleted($this->currentId);
|
|
$depth--;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clone. Called when cloning the datatable. We need to make sure to create a new datatableId.
|
|
* If we do not increase tableId it can result in segmentation faults when destructing a datatable.
|
|
*/
|
|
public function __clone()
|
|
{
|
|
// registers this instance to the manager
|
|
$this->currentId = Manager::getInstance()->addTable($this);
|
|
}
|
|
|
|
public function setLabelsHaveChanged()
|
|
{
|
|
$this->indexNotUpToDate = true;
|
|
}
|
|
|
|
/**
|
|
* @ignore
|
|
* does not update the summary row!
|
|
*/
|
|
public function setRows($rows)
|
|
{
|
|
unset($this->rows);
|
|
$this->rows = $rows;
|
|
$this->indexNotUpToDate = true;
|
|
}
|
|
|
|
/**
|
|
* Sorts the DataTable rows using the supplied callback function.
|
|
*
|
|
* @param string $functionCallback A comparison callback compatible with {@link usort}.
|
|
* @param string $columnSortedBy The column name `$functionCallback` sorts by. This is stored
|
|
* so we can determine how the DataTable was sorted in the future.
|
|
*/
|
|
public function sort($functionCallback, $columnSortedBy)
|
|
{
|
|
$this->setTableSortedBy($columnSortedBy);
|
|
|
|
usort($this->rows, $functionCallback);
|
|
|
|
if ($this->isSortRecursiveEnabled()) {
|
|
foreach ($this->getRowsWithoutSummaryRow() as $row) {
|
|
$subTable = $row->getSubtable();
|
|
if ($subTable) {
|
|
$subTable->enableRecursiveSort();
|
|
$subTable->sort($functionCallback, $columnSortedBy);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public function setTotalsRow(Row $totalsRow)
|
|
{
|
|
$this->totalsRow = $totalsRow;
|
|
}
|
|
|
|
public function getTotalsRow()
|
|
{
|
|
return $this->totalsRow;
|
|
}
|
|
|
|
public function getSummaryRow()
|
|
{
|
|
return $this->summaryRow;
|
|
}
|
|
|
|
/**
|
|
* Returns the name of the column this table was sorted by (if any).
|
|
*
|
|
* See {@link sort()}.
|
|
*
|
|
* @return false|string The sorted column name or false if none.
|
|
*/
|
|
public function getSortedByColumnName()
|
|
{
|
|
return $this->tableSortedBy;
|
|
}
|
|
|
|
/**
|
|
* Enables recursive sorting. If this method is called {@link sort()} will also sort all
|
|
* subtables.
|
|
*/
|
|
public function enableRecursiveSort()
|
|
{
|
|
$this->enableRecursiveSort = true;
|
|
}
|
|
|
|
/**
|
|
* @ignore
|
|
*/
|
|
public function isSortRecursiveEnabled()
|
|
{
|
|
return $this->enableRecursiveSort === true;
|
|
}
|
|
|
|
/**
|
|
* @ignore
|
|
*/
|
|
public function setTableSortedBy($column)
|
|
{
|
|
$this->indexNotUpToDate = true;
|
|
$this->tableSortedBy = $column;
|
|
}
|
|
|
|
/**
|
|
* Enables recursive filtering. If this method is called then the {@link filter()} method
|
|
* will apply filters to every subtable in addition to this instance.
|
|
*/
|
|
public function enableRecursiveFilters()
|
|
{
|
|
$this->enableRecursiveFilters = true;
|
|
}
|
|
|
|
/**
|
|
* @ignore
|
|
*/
|
|
public function disableRecursiveFilters()
|
|
{
|
|
$this->enableRecursiveFilters = false;
|
|
}
|
|
|
|
/**
|
|
* Applies a filter to this datatable.
|
|
*
|
|
* If {@link enableRecursiveFilters()} was called, the filter will be applied
|
|
* to all subtables as well.
|
|
*
|
|
* @param string|Closure $className Class name, eg. `"Sort"` or "Piwik\DataTable\Filters\Sort"`. If no
|
|
* namespace is supplied, `Piwik\DataTable\BaseFilter` is assumed. This parameter
|
|
* can also be a closure that takes a DataTable as its first parameter.
|
|
* @param array $parameters Array of extra parameters to pass to the filter.
|
|
*/
|
|
public function filter($className, $parameters = array())
|
|
{
|
|
if ($className instanceof \Closure
|
|
|| is_array($className)
|
|
) {
|
|
array_unshift($parameters, $this);
|
|
call_user_func_array($className, $parameters);
|
|
return;
|
|
}
|
|
|
|
if (in_array($className, $this->disabledFilters)) {
|
|
return;
|
|
}
|
|
|
|
if (!class_exists($className, true)) {
|
|
$className = 'Piwik\DataTable\Filter\\' . $className;
|
|
}
|
|
$reflectionObj = new ReflectionClass($className);
|
|
|
|
// the first parameter of a filter is the DataTable
|
|
// we add the current datatable as the parameter
|
|
$parameters = array_merge(array($this), $parameters);
|
|
|
|
$filter = $reflectionObj->newInstanceArgs($parameters);
|
|
|
|
$filter->enableRecursive($this->enableRecursiveFilters);
|
|
|
|
$filter->filter($this);
|
|
}
|
|
|
|
/**
|
|
* Applies a filter to all subtables but not to this datatable.
|
|
*
|
|
* @param string|Closure $className Class name, eg. `"Sort"` or "Piwik\DataTable\Filters\Sort"`. If no
|
|
* namespace is supplied, `Piwik\DataTable\BaseFilter` is assumed. This parameter
|
|
* can also be a closure that takes a DataTable as its first parameter.
|
|
* @param array $parameters Array of extra parameters to pass to the filter.
|
|
*/
|
|
public function filterSubtables($className, $parameters = array())
|
|
{
|
|
foreach ($this->getRowsWithoutSummaryRow() as $row) {
|
|
$subtable = $row->getSubtable();
|
|
if ($subtable) {
|
|
$subtable->filter($className, $parameters);
|
|
$subtable->filterSubtables($className, $parameters);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds a filter and a list of parameters to the list of queued filters of all subtables. These filters will be
|
|
* executed when {@link applyQueuedFilters()} is called.
|
|
*
|
|
* Filters that prettify the column values or don't need the full set of rows should be queued. This
|
|
* way they will be run after the table is truncated which will result in better performance.
|
|
*
|
|
* @param string|Closure $className The class name of the filter, eg. `'Limit'`.
|
|
* @param array $parameters The parameters to give to the filter, eg. `array($offset, $limit)` for the Limit filter.
|
|
*/
|
|
public function queueFilterSubtables($className, $parameters = array())
|
|
{
|
|
foreach ($this->getRowsWithoutSummaryRow() as $row) {
|
|
$subtable = $row->getSubtable();
|
|
if ($subtable) {
|
|
$subtable->queueFilter($className, $parameters);
|
|
$subtable->queueFilterSubtables($className, $parameters);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds a filter and a list of parameters to the list of queued filters. These filters will be
|
|
* executed when {@link applyQueuedFilters()} is called.
|
|
*
|
|
* Filters that prettify the column values or don't need the full set of rows should be queued. This
|
|
* way they will be run after the table is truncated which will result in better performance.
|
|
*
|
|
* @param string|Closure $className The class name of the filter, eg. `'Limit'`.
|
|
* @param array $parameters The parameters to give to the filter, eg. `array($offset, $limit)` for the Limit filter.
|
|
*/
|
|
public function queueFilter($className, $parameters = array())
|
|
{
|
|
if (!is_array($parameters)) {
|
|
$parameters = array($parameters);
|
|
}
|
|
$this->queuedFilters[] = array('className' => $className, 'parameters' => $parameters);
|
|
}
|
|
|
|
/**
|
|
* Disable a specific filter to run on this DataTable in case you have already applied this filter or if you will
|
|
* handle this filter manually by using a custom filter. Be aware if you disable a given filter, that filter won't
|
|
* be ever executed. Even if another filter calls this filter on the DataTable.
|
|
*
|
|
* @param string $className eg 'Limit' or 'Sort'. Passing a `Closure` or an `array($class, $methodName)` is not
|
|
* supported yet. We check for exact match. So if you disable 'Limit' and
|
|
* call `->filter('Limit')` this filter won't be executed. If you call
|
|
* `->filter('Piwik\DataTable\Filter\Limit')` that filter will be executed. See it as a
|
|
* feature.
|
|
* @ignore
|
|
*/
|
|
public function disableFilter($className)
|
|
{
|
|
$this->disabledFilters[] = $className;
|
|
}
|
|
|
|
/**
|
|
* Applies all filters that were previously queued to the table. See {@link queueFilter()}
|
|
* for more information.
|
|
*/
|
|
public function applyQueuedFilters()
|
|
{
|
|
foreach ($this->queuedFilters as $filter) {
|
|
$this->filter($filter['className'], $filter['parameters']);
|
|
}
|
|
$this->clearQueuedFilters();
|
|
}
|
|
|
|
/**
|
|
* Sums a DataTable to this one.
|
|
*
|
|
* This method will sum rows that have the same label. If a row is found in `$tableToSum` whose
|
|
* label is not found in `$this`, the row will be added to `$this`.
|
|
*
|
|
* If the subtables for this table are loaded, they will be summed as well.
|
|
*
|
|
* Rows are summed together by summing individual columns. By default columns are summed by
|
|
* adding one column value to another. Some columns cannot be aggregated this way. In these
|
|
* cases, the {@link COLUMN_AGGREGATION_OPS_METADATA_NAME}
|
|
* metadata can be used to specify a different type of operation.
|
|
*
|
|
* @param \Piwik\DataTable $tableToSum
|
|
* @throws Exception
|
|
*/
|
|
public function addDataTable(DataTable $tableToSum)
|
|
{
|
|
if ($tableToSum instanceof Simple) {
|
|
if ($tableToSum->getRowsCount() > 1) {
|
|
throw new Exception("Did not expect a Simple table with more than one row in addDataTable()");
|
|
}
|
|
$row = $tableToSum->getFirstRow();
|
|
$this->aggregateRowFromSimpleTable($row);
|
|
} else {
|
|
$columnAggregationOps = $this->getMetadata(self::COLUMN_AGGREGATION_OPS_METADATA_NAME);
|
|
foreach ($tableToSum->getRowsWithoutSummaryRow() as $row) {
|
|
$this->aggregateRowWithLabel($row, $columnAggregationOps);
|
|
}
|
|
// we do not use getRows() as this method might get called 100k times when aggregating many datatables and
|
|
// this takes a lot of time.
|
|
$row = $tableToSum->getRowFromId(DataTable::ID_SUMMARY_ROW);
|
|
if ($row) {
|
|
$this->aggregateRow($this->summaryRow, $row, $columnAggregationOps, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the Row whose `'label'` column is equal to `$label`.
|
|
*
|
|
* This method executes in constant time except for the first call which caches row
|
|
* label => row ID mappings.
|
|
*
|
|
* @param string $label `'label'` column value to look for.
|
|
* @return Row|false The row if found, `false` if otherwise.
|
|
*/
|
|
public function getRowFromLabel($label)
|
|
{
|
|
$rowId = $this->getRowIdFromLabel($label);
|
|
if (is_int($rowId) && isset($this->rows[$rowId])) {
|
|
return $this->rows[$rowId];
|
|
}
|
|
if ($rowId == self::ID_SUMMARY_ROW
|
|
&& !empty($this->summaryRow)
|
|
) {
|
|
return $this->summaryRow;
|
|
}
|
|
if (empty($rowId)
|
|
&& !empty($this->totalsRow)
|
|
&& $label == $this->totalsRow->getColumn('label')
|
|
) {
|
|
return $this->totalsRow;
|
|
}
|
|
if ($rowId instanceof Row) {
|
|
return $rowId;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns the row id for the row whose `'label'` column is equal to `$label`.
|
|
*
|
|
* This method executes in constant time except for the first call which caches row
|
|
* label => row ID mappings.
|
|
*
|
|
* @param string $label `'label'` column value to look for.
|
|
* @return int The row ID.
|
|
*/
|
|
public function getRowIdFromLabel($label)
|
|
{
|
|
if ($this->indexNotUpToDate) {
|
|
$this->rebuildIndex();
|
|
}
|
|
|
|
$label = (string) $label;
|
|
|
|
if (!isset($this->rowsIndexByLabel[$label])) {
|
|
// in case label is '-1' and there is no normal row w/ that label. Note: this is for BC since
|
|
// in the past, it was possible to get the summary row by searching for the label '-1'
|
|
if ($label == self::LABEL_SUMMARY_ROW
|
|
&& !is_null($this->summaryRow)
|
|
) {
|
|
return self::ID_SUMMARY_ROW;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return $this->rowsIndexByLabel[$label];
|
|
}
|
|
|
|
/**
|
|
* Returns an empty DataTable with the same metadata and queued filters as `$this` one.
|
|
*
|
|
* @param bool $keepFilters Whether to pass the queued filter list to the new DataTable or not.
|
|
* @return DataTable
|
|
*/
|
|
public function getEmptyClone($keepFilters = true)
|
|
{
|
|
$clone = new DataTable;
|
|
if ($keepFilters) {
|
|
$clone->queuedFilters = $this->queuedFilters;
|
|
}
|
|
$clone->metadata = $this->metadata;
|
|
return $clone;
|
|
}
|
|
|
|
/**
|
|
* Rebuilds the index used to lookup a row by label
|
|
* @internal
|
|
*/
|
|
public function rebuildIndex()
|
|
{
|
|
$this->rowsIndexByLabel = array();
|
|
$this->rebuildIndexContinuously = true;
|
|
|
|
foreach ($this->rows as $id => $row) {
|
|
$label = $row->getColumn('label');
|
|
if ($label !== false) {
|
|
$this->rowsIndexByLabel[$label] = $id;
|
|
}
|
|
}
|
|
|
|
$this->indexNotUpToDate = false;
|
|
}
|
|
|
|
/**
|
|
* Returns a row by ID. The ID is either the index of the row or {@link ID_SUMMARY_ROW}.
|
|
*
|
|
* @param int $id The row ID.
|
|
* @return Row|false The Row or false if not found.
|
|
*/
|
|
public function getRowFromId($id)
|
|
{
|
|
if ($id == self::ID_SUMMARY_ROW
|
|
&& !is_null($this->summaryRow)
|
|
) {
|
|
return $this->summaryRow;
|
|
}
|
|
|
|
if (!isset($this->rows[$id])) {
|
|
return false;
|
|
}
|
|
return $this->rows[$id];
|
|
}
|
|
|
|
/**
|
|
* Returns the row that has a subtable with ID matching `$idSubtable`.
|
|
*
|
|
* @param int $idSubTable The subtable ID.
|
|
* @return Row|false The row or false if not found
|
|
*/
|
|
public function getRowFromIdSubDataTable($idSubTable)
|
|
{
|
|
$idSubTable = (int)$idSubTable;
|
|
foreach ($this->rows as $row) {
|
|
if ($row->getIdSubDataTable() === $idSubTable) {
|
|
return $row;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Adds a row to this table.
|
|
*
|
|
* If {@link setMaximumAllowedRows()} was called and the current row count is
|
|
* at the maximum, the new row will be summed to the summary row. If there is no summary row,
|
|
* this row is set as the summary row.
|
|
*
|
|
* @param Row $row
|
|
* @return Row `$row` or the summary row if we're at the maximum number of rows.
|
|
*/
|
|
public function addRow(Row $row)
|
|
{
|
|
// if there is a upper limit on the number of allowed rows and the table is full,
|
|
// add the new row to the summary row
|
|
if ($this->maximumAllowedRows > 0
|
|
&& $this->getRowsCount() >= $this->maximumAllowedRows - 1
|
|
) {
|
|
if ($this->summaryRow === null) {
|
|
// create the summary row if necessary
|
|
|
|
$columns = array('label' => self::LABEL_SUMMARY_ROW) + $row->getColumns();
|
|
$this->addSummaryRow(new Row(array(Row::COLUMNS => $columns)));
|
|
} else {
|
|
$this->summaryRow->sumRow(
|
|
$row, $enableCopyMetadata = false, $this->getMetadata(self::COLUMN_AGGREGATION_OPS_METADATA_NAME));
|
|
}
|
|
return $this->summaryRow;
|
|
}
|
|
|
|
$this->rows[] = $row;
|
|
if (!$this->indexNotUpToDate
|
|
&& $this->rebuildIndexContinuously
|
|
) {
|
|
$label = $row->getColumn('label');
|
|
if ($label !== false) {
|
|
$this->rowsIndexByLabel[$label] = count($this->rows) - 1;
|
|
}
|
|
}
|
|
return $row;
|
|
}
|
|
|
|
/**
|
|
* Sets the summary row.
|
|
*
|
|
* _Note: A DataTable can have only one summary row._
|
|
*
|
|
* @param Row $row
|
|
* @return Row Returns `$row`.
|
|
*/
|
|
public function addSummaryRow(Row $row)
|
|
{
|
|
$this->summaryRow = $row;
|
|
$row->setIsSummaryRow();
|
|
|
|
// NOTE: the summary row does not go in the index, since it will overwrite rows w/ label == -1
|
|
|
|
return $row;
|
|
}
|
|
|
|
/**
|
|
* Returns the DataTable ID.
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getId()
|
|
{
|
|
return $this->currentId;
|
|
}
|
|
|
|
/**
|
|
* Adds a new row from an array.
|
|
*
|
|
* You can add row metadata with this method.
|
|
*
|
|
* @param array $row eg. `array(Row::COLUMNS => array('visits' => 13, 'test' => 'toto'),
|
|
* Row::METADATA => array('mymetadata' => 'myvalue'))`
|
|
*/
|
|
public function addRowFromArray($row)
|
|
{
|
|
$this->addRowsFromArray(array($row));
|
|
}
|
|
|
|
/**
|
|
* Adds a new row a from an array of column values.
|
|
*
|
|
* Row metadata cannot be added with this method.
|
|
*
|
|
* @param array $row eg. `array('name' => 'google analytics', 'license' => 'commercial')`
|
|
*/
|
|
public function addRowFromSimpleArray($row)
|
|
{
|
|
$this->addRowsFromSimpleArray(array($row));
|
|
}
|
|
|
|
/**
|
|
* Returns the array of Rows.
|
|
* Internal logic in Matomo core should avoid using this method as it is time and memory consuming when being
|
|
* executed thousands of times. The alternative is to use {@link getRowsWithoutSummaryRow()} + get the summary
|
|
* row manually.
|
|
*
|
|
* @return Row[]
|
|
*/
|
|
public function getRows()
|
|
{
|
|
if (is_null($this->summaryRow)) {
|
|
return $this->rows;
|
|
} else {
|
|
return $this->rows + array(self::ID_SUMMARY_ROW => $this->summaryRow);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @ignore
|
|
*/
|
|
public function getRowsWithoutSummaryRow()
|
|
{
|
|
return $this->rows;
|
|
}
|
|
|
|
/**
|
|
* @ignore
|
|
*/
|
|
public function getRowsCountWithoutSummaryRow()
|
|
{
|
|
return count($this->rows);
|
|
}
|
|
|
|
/**
|
|
* Returns an array containing all column values for the requested column.
|
|
*
|
|
* @param string $name The column name.
|
|
* @return array The array of column values.
|
|
*/
|
|
public function getColumn($name)
|
|
{
|
|
$columnValues = array();
|
|
foreach ($this->getRows() as $row) {
|
|
$columnValues[] = $row->getColumn($name);
|
|
}
|
|
return $columnValues;
|
|
}
|
|
|
|
/**
|
|
* Returns an array containing all column values of columns whose name starts with `$name`.
|
|
*
|
|
* @param string $namePrefix The column name prefix.
|
|
* @return array The array of column values.
|
|
*/
|
|
public function getColumnsStartingWith($namePrefix)
|
|
{
|
|
$columnValues = array();
|
|
foreach ($this->getRows() as $row) {
|
|
$columns = $row->getColumns();
|
|
foreach ($columns as $column => $value) {
|
|
if (strpos($column, $namePrefix) === 0) {
|
|
$columnValues[] = $row->getColumn($column);
|
|
}
|
|
}
|
|
}
|
|
return $columnValues;
|
|
}
|
|
|
|
/**
|
|
* Returns the names of every column this DataTable contains. This method will return the
|
|
* columns of the first row with data and will assume they occur in every other row as well.
|
|
*
|
|
*_ Note: If column names still use their in-database INDEX values (@see Metrics), they
|
|
* will be converted to their string name in the array result._
|
|
*
|
|
* @return array Array of string column names.
|
|
*/
|
|
public function getColumns()
|
|
{
|
|
$result = array();
|
|
foreach ($this->getRows() as $row) {
|
|
$columns = $row->getColumns();
|
|
if (!empty($columns)) {
|
|
$result = array_keys($columns);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// make sure column names are not DB index values
|
|
foreach ($result as &$column) {
|
|
if (isset(Metrics::$mappingFromIdToName[$column])) {
|
|
$column = Metrics::$mappingFromIdToName[$column];
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Returns an array containing the requested metadata value of each row.
|
|
*
|
|
* @param string $name The metadata column to return.
|
|
* @return array
|
|
*/
|
|
public function getRowsMetadata($name)
|
|
{
|
|
$metadataValues = array();
|
|
foreach ($this->getRows() as $row) {
|
|
$metadataValues[] = $row->getMetadata($name);
|
|
}
|
|
return $metadataValues;
|
|
}
|
|
|
|
/**
|
|
* Delete row metadata by name in every row.
|
|
*
|
|
* @param $name
|
|
* @param bool $deleteRecursiveInSubtables
|
|
*/
|
|
public function deleteRowsMetadata($name, $deleteRecursiveInSubtables = false)
|
|
{
|
|
foreach ($this->rows as $row) {
|
|
$row->deleteMetadata($name);
|
|
|
|
$subTable = $row->getSubtable();
|
|
if ($subTable) {
|
|
$subTable->deleteRowsMetadata($name, $deleteRecursiveInSubtables);
|
|
}
|
|
}
|
|
if (!is_null($this->summaryRow)) {
|
|
$this->summaryRow->deleteMetadata($name);
|
|
}
|
|
if (!is_null($this->totalsRow)) {
|
|
$this->totalsRow->deleteMetadata($name);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Returns the number of rows in the table including the summary row.
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getRowsCount()
|
|
{
|
|
if (is_null($this->summaryRow)) {
|
|
return count($this->rows);
|
|
} else {
|
|
return count($this->rows) + 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the first row of the DataTable.
|
|
*
|
|
* @return Row|false The first row or `false` if it cannot be found.
|
|
*/
|
|
public function getFirstRow()
|
|
{
|
|
if (count($this->rows) == 0) {
|
|
if (!is_null($this->summaryRow)) {
|
|
return $this->summaryRow;
|
|
}
|
|
return false;
|
|
}
|
|
return reset($this->rows);
|
|
}
|
|
|
|
/**
|
|
* Returns the last row of the DataTable. If there is a summary row, it
|
|
* will always be considered the last row.
|
|
*
|
|
* @return Row|false The last row or `false` if it cannot be found.
|
|
*/
|
|
public function getLastRow()
|
|
{
|
|
if (!is_null($this->summaryRow)) {
|
|
return $this->summaryRow;
|
|
}
|
|
|
|
if (count($this->rows) == 0) {
|
|
return false;
|
|
}
|
|
|
|
return end($this->rows);
|
|
}
|
|
|
|
/**
|
|
* Returns the number of rows in the entire DataTable hierarchy. This is the number of rows in this DataTable
|
|
* summed with the row count of each descendant subtable.
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getRowsCountRecursive()
|
|
{
|
|
$totalCount = 0;
|
|
foreach ($this->rows as $row) {
|
|
$subTable = $row->getSubtable();
|
|
if ($subTable) {
|
|
$count = $subTable->getRowsCountRecursive();
|
|
$totalCount += $count;
|
|
}
|
|
}
|
|
|
|
$totalCount += $this->getRowsCount();
|
|
return $totalCount;
|
|
}
|
|
|
|
/**
|
|
* Delete a column by name in every row. This change is NOT applied recursively to all
|
|
* subtables.
|
|
*
|
|
* @param string $name Column name to delete.
|
|
*/
|
|
public function deleteColumn($name)
|
|
{
|
|
$this->deleteColumns(array($name));
|
|
}
|
|
|
|
public function __sleep()
|
|
{
|
|
return array('rows', 'summaryRow', 'metadata', 'totalsRow');
|
|
}
|
|
|
|
/**
|
|
* Rename a column in every row. This change is applied recursively to all subtables.
|
|
*
|
|
* @param string $oldName Old column name.
|
|
* @param string $newName New column name.
|
|
*/
|
|
public function renameColumn($oldName, $newName)
|
|
{
|
|
foreach ($this->rows as $row) {
|
|
$row->renameColumn($oldName, $newName);
|
|
|
|
$subTable = $row->getSubtable();
|
|
if ($subTable) {
|
|
$subTable->renameColumn($oldName, $newName);
|
|
}
|
|
}
|
|
if (!is_null($this->summaryRow)) {
|
|
$this->summaryRow->renameColumn($oldName, $newName);
|
|
}
|
|
if (!is_null($this->totalsRow)) {
|
|
$this->totalsRow->renameColumn($oldName, $newName);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deletes several columns by name in every row.
|
|
*
|
|
* @param array $names List of column names to delete.
|
|
* @param bool $deleteRecursiveInSubtables Whether to apply this change to all subtables or not.
|
|
*/
|
|
public function deleteColumns($names, $deleteRecursiveInSubtables = false)
|
|
{
|
|
foreach ($this->rows as $row) {
|
|
foreach ($names as $name) {
|
|
$row->deleteColumn($name);
|
|
}
|
|
$subTable = $row->getSubtable();
|
|
if ($subTable) {
|
|
$subTable->deleteColumns($names, $deleteRecursiveInSubtables);
|
|
}
|
|
}
|
|
if (!is_null($this->summaryRow)) {
|
|
foreach ($names as $name) {
|
|
$this->summaryRow->deleteColumn($name);
|
|
}
|
|
}
|
|
if (!is_null($this->totalsRow)) {
|
|
foreach ($names as $name) {
|
|
$this->totalsRow->deleteColumn($name);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deletes a row by ID.
|
|
*
|
|
* @param int $id The row ID.
|
|
* @throws Exception If the row `$id` cannot be found.
|
|
*/
|
|
public function deleteRow($id)
|
|
{
|
|
if ($id === self::ID_SUMMARY_ROW) {
|
|
$this->summaryRow = null;
|
|
return;
|
|
}
|
|
if (!isset($this->rows[$id])) {
|
|
throw new Exception("Trying to delete unknown row with idkey = $id");
|
|
}
|
|
unset($this->rows[$id]);
|
|
}
|
|
|
|
/**
|
|
* Deletes rows from `$offset` to `$offset + $limit`.
|
|
*
|
|
* @param int $offset The offset to start deleting rows from.
|
|
* @param int|null $limit The number of rows to delete. If `null` all rows after the offset
|
|
* will be removed.
|
|
* @return int The number of rows deleted.
|
|
*/
|
|
public function deleteRowsOffset($offset, $limit = null)
|
|
{
|
|
if ($limit === 0) {
|
|
return 0;
|
|
}
|
|
|
|
$count = $this->getRowsCount();
|
|
if ($offset >= $count) {
|
|
return 0;
|
|
}
|
|
|
|
// if we delete until the end, we delete the summary row as well
|
|
if (is_null($limit)
|
|
|| $limit >= $count
|
|
) {
|
|
$this->summaryRow = null;
|
|
}
|
|
|
|
if (is_null($limit)) {
|
|
array_splice($this->rows, $offset);
|
|
} else {
|
|
array_splice($this->rows, $offset, $limit);
|
|
}
|
|
|
|
return $count - $this->getRowsCount();
|
|
}
|
|
|
|
/**
|
|
* Deletes a set of rows by ID.
|
|
*
|
|
* @param array $rowIds The list of row IDs to delete.
|
|
* @throws Exception If a row ID cannot be found.
|
|
*/
|
|
public function deleteRows(array $rowIds)
|
|
{
|
|
foreach ($rowIds as $key) {
|
|
$this->deleteRow($key);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a string representation of this DataTable for convenient viewing.
|
|
*
|
|
* _Note: This uses the **html** DataTable renderer._
|
|
*
|
|
* @return string
|
|
*/
|
|
public function __toString()
|
|
{
|
|
$renderer = new Html();
|
|
$renderer->setTable($this);
|
|
return (string)$renderer;
|
|
}
|
|
|
|
/**
|
|
* Returns true if both DataTable instances are exactly the same.
|
|
*
|
|
* DataTables are equal if they have the same number of rows, if
|
|
* each row has a label that exists in the other table, and if each row
|
|
* is equal to the row in the other table with the same label. The order
|
|
* of rows is not important.
|
|
*
|
|
* @param \Piwik\DataTable $table1
|
|
* @param \Piwik\DataTable $table2
|
|
* @return bool
|
|
*/
|
|
public static function isEqual(DataTable $table1, DataTable $table2)
|
|
{
|
|
$table1->rebuildIndex();
|
|
$table2->rebuildIndex();
|
|
|
|
if ($table1->getRowsCount() != $table2->getRowsCount()) {
|
|
return false;
|
|
}
|
|
|
|
$rows1 = $table1->getRows();
|
|
|
|
foreach ($rows1 as $row1) {
|
|
$row2 = $table2->getRowFromLabel($row1->getColumn('label'));
|
|
if ($row2 === false
|
|
|| !Row::isEqual($row1, $row2)
|
|
) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Serializes an entire DataTable hierarchy and returns the array of serialized DataTables.
|
|
*
|
|
* The first element in the returned array will be the serialized representation of this DataTable.
|
|
* Every subsequent element will be a serialized subtable.
|
|
*
|
|
* This DataTable and subtables can optionally be truncated before being serialized. In most
|
|
* cases where DataTables can become quite large, they should be truncated before being persisted
|
|
* in an archive.
|
|
*
|
|
* The result of this method is intended for use with the {@link ArchiveProcessor::insertBlobRecord()} method.
|
|
*
|
|
* @throws Exception If infinite recursion detected. This will occur if a table's subtable is one of its parent tables.
|
|
* @param int $maximumRowsInDataTable If not null, defines the maximum number of rows allowed in the serialized DataTable.
|
|
* @param int $maximumRowsInSubDataTable If not null, defines the maximum number of rows allowed in serialized subtables.
|
|
* @param string $columnToSortByBeforeTruncation The column to sort by before truncating, eg, `Metrics::INDEX_NB_VISITS`.
|
|
* @param array $aSerializedDataTable Will contain all the output arrays
|
|
* @return array The array of serialized DataTables:
|
|
*
|
|
* array(
|
|
* // this DataTable (the root)
|
|
* 0 => 'eghuighahgaueytae78yaet7yaetae',
|
|
*
|
|
* // a subtable
|
|
* 1 => 'gaegae gh gwrh guiwh uigwhuige',
|
|
*
|
|
* // another subtable
|
|
* 2 => 'gqegJHUIGHEQjkgneqjgnqeugUGEQHGUHQE',
|
|
*
|
|
* // etc.
|
|
* );
|
|
*/
|
|
public function getSerialized($maximumRowsInDataTable = null,
|
|
$maximumRowsInSubDataTable = null,
|
|
$columnToSortByBeforeTruncation = null,
|
|
&$aSerializedDataTable = array())
|
|
{
|
|
static $depth = 0;
|
|
// make sure subtableIds are consecutive from 1 to N
|
|
static $subtableId = 0;
|
|
|
|
if ($depth > self::$maximumDepthLevelAllowed) {
|
|
$depth = 0;
|
|
$subtableId = 0;
|
|
throw new Exception("Maximum recursion level of " . self::$maximumDepthLevelAllowed . " reached. Maybe you have set a DataTable\Row with an associated DataTable belonging already to one of its parent tables?");
|
|
}
|
|
|
|
// gather metadata before filters are called, so their metadata is not stored in serialized form
|
|
$metadata = $this->getAllTableMetadata();
|
|
foreach ($metadata as $key => $value) {
|
|
if (!is_scalar($value) && !is_string($value)) {
|
|
unset($metadata[$key]);
|
|
}
|
|
}
|
|
|
|
if (!is_null($maximumRowsInDataTable)) {
|
|
$this->filter('Truncate',
|
|
array($maximumRowsInDataTable - 1,
|
|
DataTable::LABEL_SUMMARY_ROW,
|
|
$columnToSortByBeforeTruncation,
|
|
$filterRecursive = false)
|
|
);
|
|
}
|
|
|
|
$consecutiveSubtableIds = array();
|
|
$forcedId = $subtableId;
|
|
|
|
// For each row (including the summary row), get the serialized row
|
|
// If it is associated to a sub table, get the serialized table recursively ;
|
|
// but returns all serialized tables and subtable in an array of 1 dimension
|
|
foreach ($this->getRows() as $id => $row) {
|
|
$subTable = $row->getSubtable();
|
|
if ($subTable) {
|
|
$consecutiveSubtableIds[$id] = ++$subtableId;
|
|
$depth++;
|
|
$subTable->getSerialized($maximumRowsInSubDataTable, $maximumRowsInSubDataTable, $columnToSortByBeforeTruncation, $aSerializedDataTable);
|
|
$depth--;
|
|
} else {
|
|
$row->removeSubtable();
|
|
}
|
|
}
|
|
|
|
// if the datatable is the parent we force the Id at 0 (this is part of the specification)
|
|
if ($depth == 0) {
|
|
$forcedId = 0;
|
|
$subtableId = 0;
|
|
}
|
|
|
|
// we then serialize the rows and store them in the serialized dataTable
|
|
$rows = array();
|
|
foreach ($this->rows as $id => $row) {
|
|
if (isset($consecutiveSubtableIds[$id])) {
|
|
$backup = $row->subtableId;
|
|
$row->subtableId = $consecutiveSubtableIds[$id];
|
|
$rows[$id] = $row->export();
|
|
$row->subtableId = $backup;
|
|
} else {
|
|
$rows[$id] = $row->export();
|
|
}
|
|
}
|
|
|
|
if (isset($this->summaryRow)) {
|
|
$id = self::ID_SUMMARY_ROW;
|
|
$row = $this->summaryRow;
|
|
|
|
// duplicating code above so we don't create a new array w/ getRows() above in this function which is
|
|
// used heavily in matomo.
|
|
if (isset($consecutiveSubtableIds[$id])) {
|
|
$backup = $row->subtableId;
|
|
$row->subtableId = $consecutiveSubtableIds[$id];
|
|
$rows[$id] = $row->export();
|
|
$row->subtableId = $backup;
|
|
} else {
|
|
$rows[$id] = $row->export();
|
|
}
|
|
}
|
|
|
|
if (!empty($metadata)) {
|
|
$metadataRow = new Row();
|
|
$metadataRow->setColumns($metadata);
|
|
|
|
// set the label so the row will be indexed correctly internally
|
|
$metadataRow->setColumn('label', self::LABEL_ARCHIVED_METADATA_ROW);
|
|
|
|
$rows[self::ID_ARCHIVED_METADATA_ROW] = $metadataRow->export();
|
|
}
|
|
|
|
$aSerializedDataTable[$forcedId] = serialize($rows);
|
|
unset($rows);
|
|
|
|
return $aSerializedDataTable;
|
|
}
|
|
|
|
private static $previousRowClasses = array('O:39:"Piwik\DataTable\Row\DataTableSummaryRow"', 'O:19:"Piwik\DataTable\Row"', 'O:36:"Piwik_DataTable_Row_DataTableSummary"', 'O:19:"Piwik_DataTable_Row"');
|
|
private static $rowClassToUseForUnserialize = 'O:29:"Piwik_DataTable_SerializedRow"';
|
|
|
|
/**
|
|
* It is faster to unserialize existing serialized Row instances to "Piwik_DataTable_SerializedRow" and access the
|
|
* `$row->c` property than implementing a "__wakeup" method in the Row instance to map the "$row->c" to $row->columns
|
|
* etc. We're talking here about 15% faster reports aggregation in some cases. To be concrete: We have a test where
|
|
* Archiving a year takes 1700 seconds with "__wakeup" and 1400 seconds with this method. Yes, it takes 300 seconds
|
|
* to wake up millions of rows. We should be able to remove this code here end 2015 and use the "__wakeup" way by then.
|
|
* Why? By then most new archives will have only arrays serialized anyway and therefore this mapping is rather an overhead.
|
|
*
|
|
* @param string $serialized
|
|
* @return array
|
|
* @throws Exception In case the unserialize fails
|
|
*/
|
|
private function unserializeRows($serialized)
|
|
{
|
|
$serialized = str_replace(self::$previousRowClasses, self::$rowClassToUseForUnserialize, $serialized);
|
|
$rows = Common::safe_unserialize($serialized, [
|
|
Row::class,
|
|
DataTableSummaryRow::class,
|
|
\Piwik_DataTable_SerializedRow::class
|
|
]);
|
|
|
|
if ($rows === false) {
|
|
throw new Exception("The unserialization has failed!");
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
/**
|
|
* Adds a set of rows from a serialized DataTable string.
|
|
*
|
|
* See {@link serialize()}.
|
|
*
|
|
* _Note: This function will successfully load DataTables serialized by Piwik 1.X._
|
|
*
|
|
* @param string $serialized A string with the format of a string in the array returned by
|
|
* {@link serialize()}.
|
|
* @throws Exception if `$serialized` is invalid.
|
|
*/
|
|
public function addRowsFromSerializedArray($serialized)
|
|
{
|
|
$rows = $this->unserializeRows($serialized);
|
|
|
|
if (array_key_exists(self::ID_SUMMARY_ROW, $rows)) {
|
|
if (is_array($rows[self::ID_SUMMARY_ROW])) {
|
|
$this->summaryRow = new Row($rows[self::ID_SUMMARY_ROW]);
|
|
$this->summaryRow->setIsSummaryRow();
|
|
} elseif (isset($rows[self::ID_SUMMARY_ROW]->c)) {
|
|
$this->summaryRow = new Row($rows[self::ID_SUMMARY_ROW]->c); // Pre Piwik 2.13
|
|
$this->summaryRow->setIsSummaryRow();
|
|
}
|
|
unset($rows[self::ID_SUMMARY_ROW]);
|
|
}
|
|
|
|
if (array_key_exists(self::ID_ARCHIVED_METADATA_ROW, $rows)) {
|
|
$metadata = $rows[self::ID_ARCHIVED_METADATA_ROW][Row::COLUMNS];
|
|
unset($metadata['label']);
|
|
$this->setAllTableMetadata($metadata);
|
|
unset($rows[self::ID_ARCHIVED_METADATA_ROW]);
|
|
}
|
|
|
|
foreach ($rows as $id => $row) {
|
|
if (isset($row->c)) {
|
|
$this->addRow(new Row($row->c)); // Pre Piwik 2.13
|
|
} else {
|
|
$this->addRow(new Row($row));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds multiple rows from an array.
|
|
*
|
|
* You can add row metadata with this method.
|
|
*
|
|
* @param array $array Array with the following structure
|
|
*
|
|
* array(
|
|
* // row1
|
|
* array(
|
|
* Row::COLUMNS => array( col1_name => value1, col2_name => value2, ...),
|
|
* Row::METADATA => array( metadata1_name => value1, ...), // see Row
|
|
* ),
|
|
* // row2
|
|
* array( ... ),
|
|
* )
|
|
*/
|
|
public function addRowsFromArray($array)
|
|
{
|
|
foreach ($array as $id => $row) {
|
|
if (is_array($row)) {
|
|
$row = new Row($row);
|
|
}
|
|
|
|
if ($id == self::ID_SUMMARY_ROW) {
|
|
$this->summaryRow = $row;
|
|
$this->summaryRow->setIsSummaryRow();
|
|
} else {
|
|
$this->addRow($row);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds multiple rows from an array containing arrays of column values.
|
|
*
|
|
* Row metadata cannot be added with this method.
|
|
*
|
|
* @param array $array Array with the following structure:
|
|
*
|
|
* array(
|
|
* array( col1_name => valueA, col2_name => valueC, ...),
|
|
* array( col1_name => valueB, col2_name => valueD, ...),
|
|
* )
|
|
* @throws Exception if `$array` is in an incorrect format.
|
|
*/
|
|
public function addRowsFromSimpleArray($array)
|
|
{
|
|
if (count($array) === 0) {
|
|
return;
|
|
}
|
|
|
|
$exceptionText = " Data structure returned is not convertible in the requested format." .
|
|
" Try to call this method with the parameters '&format=original&serialize=1'" .
|
|
"; you will get the original php data structure serialized." .
|
|
" The data structure looks like this: \n \$data = %s; ";
|
|
|
|
// first pass to see if the array has the structure
|
|
// array(col1_name => val1, col2_name => val2, etc.)
|
|
// with val* that are never arrays (only strings/numbers/bool/etc.)
|
|
// if we detect such a "simple" data structure we convert it to a row with the correct columns' names
|
|
$thisIsNotThatSimple = false;
|
|
|
|
foreach ($array as $columnValue) {
|
|
if (is_array($columnValue) || is_object($columnValue)) {
|
|
$thisIsNotThatSimple = true;
|
|
break;
|
|
}
|
|
}
|
|
if ($thisIsNotThatSimple === false) {
|
|
// case when the array is indexed by the default numeric index
|
|
if (array_keys($array) === array_keys(array_fill(0, count($array), true))) {
|
|
foreach ($array as $row) {
|
|
$this->addRow(new Row(array(Row::COLUMNS => array($row))));
|
|
}
|
|
} else {
|
|
$this->addRow(new Row(array(Row::COLUMNS => $array)));
|
|
}
|
|
// we have converted our simple array to one single row
|
|
// => we exit the method as the job is now finished
|
|
return;
|
|
}
|
|
|
|
foreach ($array as $key => $row) {
|
|
// stuff that looks like a line
|
|
if (is_array($row)) {
|
|
/**
|
|
* We make sure we can convert this PHP array without losing information.
|
|
* We are able to convert only simple php array (no strings keys, no sub arrays, etc.)
|
|
*
|
|
*/
|
|
|
|
// if the key is a string it means that some information was contained in this key.
|
|
// it cannot be lost during the conversion. Because we are not able to handle properly
|
|
// this key, we throw an explicit exception.
|
|
if (is_string($key)) {
|
|
// we define an exception we may throw if at one point we notice that we cannot handle the data structure
|
|
throw new Exception(sprintf($exceptionText, var_export($array, true)));
|
|
}
|
|
// if any of the sub elements of row is an array we cannot handle this data structure...
|
|
foreach ($row as $subRow) {
|
|
if (is_array($subRow)) {
|
|
throw new Exception(sprintf($exceptionText, var_export($array, true)));
|
|
}
|
|
}
|
|
$row = new Row(array(Row::COLUMNS => $row));
|
|
} // other (string, numbers...) => we build a line from this value
|
|
else {
|
|
$row = new Row(array(Row::COLUMNS => array($key => $row)));
|
|
}
|
|
$this->addRow($row);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rewrites the input `$array`
|
|
*
|
|
* array (
|
|
* LABEL => array(col1 => X, col2 => Y),
|
|
* LABEL2 => array(col1 => X, col2 => Y),
|
|
* )
|
|
*
|
|
* to a DataTable with rows that look like:
|
|
*
|
|
* array (
|
|
* array( Row::COLUMNS => array('label' => LABEL, col1 => X, col2 => Y)),
|
|
* array( Row::COLUMNS => array('label' => LABEL2, col1 => X, col2 => Y)),
|
|
* )
|
|
*
|
|
* Will also convert arrays like:
|
|
*
|
|
* array (
|
|
* LABEL => X,
|
|
* LABEL2 => Y,
|
|
* )
|
|
*
|
|
* to:
|
|
*
|
|
* array (
|
|
* array( Row::COLUMNS => array('label' => LABEL, 'value' => X)),
|
|
* array( Row::COLUMNS => array('label' => LABEL2, 'value' => Y)),
|
|
* )
|
|
*
|
|
* @param array $array Indexed array, two formats supported, see above.
|
|
* @param array|null $subtablePerLabel An array mapping label values with DataTable instances to associate as a subtable.
|
|
* @return \Piwik\DataTable
|
|
*/
|
|
public static function makeFromIndexedArray($array, $subtablePerLabel = null)
|
|
{
|
|
$table = new DataTable();
|
|
foreach ($array as $label => $row) {
|
|
$cleanRow = array();
|
|
|
|
// Support the case of an $array of single values
|
|
if (!is_array($row)) {
|
|
$row = array('value' => $row);
|
|
}
|
|
// Put the 'label' column first
|
|
$cleanRow[Row::COLUMNS] = array('label' => $label) + $row;
|
|
// Assign subtable if specified
|
|
if (isset($subtablePerLabel[$label])) {
|
|
$cleanRow[Row::DATATABLE_ASSOCIATED] = $subtablePerLabel[$label];
|
|
}
|
|
|
|
if ($label === RankingQuery::LABEL_SUMMARY_ROW) {
|
|
$table->addSummaryRow(new Row($cleanRow));
|
|
} else {
|
|
$table->addRow(new Row($cleanRow));
|
|
}
|
|
}
|
|
return $table;
|
|
}
|
|
|
|
/**
|
|
* Sets the maximum depth level to at least a certain value. If the current value is
|
|
* greater than `$atLeastLevel`, the maximum nesting level is not changed.
|
|
*
|
|
* The maximum depth level determines the maximum number of subtable levels in the
|
|
* DataTable tree. For example, if it is set to `2`, this DataTable is allowed to
|
|
* have subtables, but the subtables are not.
|
|
*
|
|
* @param int $atLeastLevel
|
|
*/
|
|
public static function setMaximumDepthLevelAllowedAtLeast($atLeastLevel)
|
|
{
|
|
self::$maximumDepthLevelAllowed = max($atLeastLevel, self::$maximumDepthLevelAllowed);
|
|
if (self::$maximumDepthLevelAllowed < 1) {
|
|
self::$maximumDepthLevelAllowed = 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns metadata by name.
|
|
*
|
|
* @param string $name The metadata name.
|
|
* @return mixed|false The metadata value or `false` if it cannot be found.
|
|
*/
|
|
public function getMetadata($name)
|
|
{
|
|
if (!isset($this->metadata[$name])) {
|
|
return false;
|
|
}
|
|
return $this->metadata[$name];
|
|
}
|
|
|
|
/**
|
|
* Sets a metadata value by name.
|
|
*
|
|
* @param string $name The metadata name.
|
|
* @param mixed $value
|
|
*/
|
|
public function setMetadata($name, $value)
|
|
{
|
|
$this->metadata[$name] = $value;
|
|
}
|
|
|
|
/**
|
|
* Deletes a metadata property by name.
|
|
*
|
|
* @param bool|string $name The metadata name (omit to delete all metadata)
|
|
* @return bool True if the requested metadata was deleted
|
|
*/
|
|
public function deleteMetadata($name = false) : bool
|
|
{
|
|
if ($name === false) {
|
|
$this->metadata = [];
|
|
return true;
|
|
}
|
|
if (!isset($this->metadata[$name])) {
|
|
return false;
|
|
}
|
|
unset($this->metadata[$name]);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Returns all table metadata.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getAllTableMetadata()
|
|
{
|
|
return $this->metadata;
|
|
}
|
|
|
|
/**
|
|
* Sets several metadata values by name.
|
|
*
|
|
* @param array $values Array mapping metadata names with metadata values.
|
|
*/
|
|
public function setMetadataValues($values)
|
|
{
|
|
foreach ($values as $name => $value) {
|
|
$this->metadata[$name] = $value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets metadata, erasing existing values.
|
|
*
|
|
* @param array $values Array mapping metadata names with metadata values.
|
|
*/
|
|
public function setAllTableMetadata($metadata)
|
|
{
|
|
$this->metadata = $metadata;
|
|
}
|
|
|
|
/**
|
|
* Sets the maximum number of rows allowed in this datatable (including the summary
|
|
* row). If adding more then the allowed number of rows is attempted, the extra
|
|
* rows are summed to the summary row.
|
|
*
|
|
* @param int $maximumAllowedRows If `0`, the maximum number of rows is unset.
|
|
*/
|
|
public function setMaximumAllowedRows($maximumAllowedRows)
|
|
{
|
|
$this->maximumAllowedRows = $maximumAllowedRows;
|
|
}
|
|
|
|
/**
|
|
* Traverses a DataTable tree using an array of labels and returns the row
|
|
* it finds or `false` if it cannot find one. The number of path segments that
|
|
* were successfully walked is also returned.
|
|
*
|
|
* If `$missingRowColumns` is supplied, the specified path is created. When
|
|
* a subtable is encountered w/o the required label, a new row is created
|
|
* with the label, and a new subtable is added to the row.
|
|
*
|
|
* Read [http://en.wikipedia.org/wiki/Tree_(data_structure)#Traversal_methods](http://en.wikipedia.org/wiki/Tree_(data_structure)#Traversal_methods)
|
|
* for more information about tree walking.
|
|
*
|
|
* @param array $path The path to walk. An array of label values. The first element
|
|
* refers to a row in this DataTable, the second in a subtable of
|
|
* the first row, the third a subtable of the second row, etc.
|
|
* @param array|bool $missingRowColumns The default columns to use when creating new rows.
|
|
* If this parameter is supplied, new rows will be
|
|
* created for path labels that cannot be found.
|
|
* @param int $maxSubtableRows The maximum number of allowed rows in new subtables. New
|
|
* subtables are only created if `$missingRowColumns` is provided.
|
|
* @return array First element is the found row or `false`. Second element is
|
|
* the number of path segments walked. If a row is found, this
|
|
* will be == to `count($path)`. Otherwise, it will be the index
|
|
* of the path segment that we could not find.
|
|
*/
|
|
public function walkPath($path, $missingRowColumns = false, $maxSubtableRows = 0)
|
|
{
|
|
$pathLength = count($path);
|
|
|
|
$table = $this;
|
|
$next = false;
|
|
for ($i = 0; $i < $pathLength; ++$i) {
|
|
$segment = $path[$i];
|
|
|
|
$next = $table->getRowFromLabel($segment);
|
|
if ($next === false) {
|
|
// if there is no table to advance to, and we're not adding missing rows, return false
|
|
if ($missingRowColumns === false) {
|
|
return array(false, $i);
|
|
} else {
|
|
// if we're adding missing rows, add a new row
|
|
|
|
$row = new DataTableSummaryRow();
|
|
$row->setColumns(array('label' => $segment) + $missingRowColumns);
|
|
|
|
$next = $table->addRow($row);
|
|
|
|
if ($next !== $row) {
|
|
// if the row wasn't added, the table is full
|
|
|
|
// Summary row, has no metadata
|
|
$next->deleteMetadata();
|
|
return array($next, $i);
|
|
}
|
|
}
|
|
}
|
|
|
|
$table = $next->getSubtable();
|
|
if ($table === false) {
|
|
// if the row has no table (and thus no child rows), and we're not adding
|
|
// missing rows, return false
|
|
if ($missingRowColumns === false) {
|
|
return array(false, $i);
|
|
} elseif ($i != $pathLength - 1) {
|
|
// create subtable if missing, but only if not on the last segment
|
|
|
|
$table = new DataTable();
|
|
$table->setMaximumAllowedRows($maxSubtableRows);
|
|
$table->metadata[self::COLUMN_AGGREGATION_OPS_METADATA_NAME]
|
|
= $this->getMetadata(self::COLUMN_AGGREGATION_OPS_METADATA_NAME);
|
|
$next->setSubtable($table);
|
|
// Summary row, has no metadata
|
|
$next->deleteMetadata();
|
|
}
|
|
}
|
|
}
|
|
|
|
return array($next, $i);
|
|
}
|
|
|
|
/**
|
|
* Returns a new DataTable in which the rows of this table are replaced with the aggregatated rows of all its subtables.
|
|
*
|
|
* @param string|bool $labelColumn If supplied the label of the parent row will be added to
|
|
* a new column in each subtable row.
|
|
*
|
|
* If set to, `'label'` each subtable row's label will be prepended
|
|
* w/ the parent row's label. So `'child_label'` becomes
|
|
* `'parent_label - child_label'`.
|
|
* @param bool $useMetadataColumn If true and if `$labelColumn` is supplied, the parent row's
|
|
* label will be added as metadata and not a new column.
|
|
* @return \Piwik\DataTable
|
|
*/
|
|
public function mergeSubtables($labelColumn = false, $useMetadataColumn = false)
|
|
{
|
|
$result = new DataTable();
|
|
$result->setAllTableMetadata($this->getAllTableMetadata());
|
|
foreach ($this->getRowsWithoutSummaryRow() as $row) {
|
|
$subtable = $row->getSubtable();
|
|
if ($subtable !== false) {
|
|
$parentLabel = $row->getColumn('label');
|
|
|
|
// add a copy of each subtable row to the new datatable
|
|
foreach ($subtable->getRows() as $id => $subRow) {
|
|
$copy = clone $subRow;
|
|
|
|
// if the summary row, add it to the existing summary row (or add a new one)
|
|
if ($id == self::ID_SUMMARY_ROW) {
|
|
$existing = $result->getRowFromId(self::ID_SUMMARY_ROW);
|
|
if ($existing === false) {
|
|
$result->addSummaryRow($copy);
|
|
} else {
|
|
$existing->sumRow($copy, $copyMeta = true, $this->getMetadata(self::COLUMN_AGGREGATION_OPS_METADATA_NAME));
|
|
}
|
|
} else {
|
|
if ($labelColumn !== false) {
|
|
// if we're modifying the subtable's rows' label column, then we make
|
|
// sure to prepend the existing label w/ the parent row's label. otherwise
|
|
// we're just adding the parent row's label as a new column/metadata.
|
|
$newLabel = $parentLabel;
|
|
if ($labelColumn == 'label') {
|
|
$newLabel .= ' - ' . $copy->getColumn('label');
|
|
}
|
|
|
|
// modify the child row's label or add new column/metadata
|
|
if ($useMetadataColumn) {
|
|
$copy->setMetadata($labelColumn, $newLabel);
|
|
} else {
|
|
$copy->setColumn($labelColumn, $newLabel);
|
|
}
|
|
}
|
|
|
|
$result->addRow($copy);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Returns a new DataTable created with data from a 'simple' array.
|
|
*
|
|
* See {@link addRowsFromSimpleArray()}.
|
|
*
|
|
* @param array $array
|
|
* @return \Piwik\DataTable
|
|
*/
|
|
public static function makeFromSimpleArray($array)
|
|
{
|
|
$dataTable = new DataTable();
|
|
$dataTable->addRowsFromSimpleArray($array);
|
|
return $dataTable;
|
|
}
|
|
|
|
/**
|
|
* Creates a new DataTable instance from a serialized DataTable string.
|
|
*
|
|
* See {@link getSerialized()} and {@link addRowsFromSerializedArray()}
|
|
* for more information on DataTable serialization.
|
|
*
|
|
* @param string $data
|
|
* @return \Piwik\DataTable
|
|
*/
|
|
public static function fromSerializedArray($data)
|
|
{
|
|
$result = new DataTable();
|
|
$result->addRowsFromSerializedArray($data);
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Aggregates the $row columns to this table.
|
|
*
|
|
* $row must have a column "label". The $row will be summed to this table's row with the same label.
|
|
*
|
|
* @param $row
|
|
* @params null|array $columnAggregationOps
|
|
* @throws \Exception
|
|
*/
|
|
protected function aggregateRowWithLabel(Row $row, $columnAggregationOps)
|
|
{
|
|
$labelToLookFor = $row->getColumn('label');
|
|
if ($labelToLookFor === false) {
|
|
$message = sprintf("Label column not found in the table to add in addDataTable(). Row: %s",
|
|
var_export($row->getColumns(), 1)
|
|
);
|
|
throw new Exception($message);
|
|
}
|
|
$rowFound = $this->getRowFromLabel($labelToLookFor);
|
|
// if we find the summary row in the other table, ignore it, since we're aggregating normal rows in this method.
|
|
// the summary row is aggregated explicitly after this method is called.
|
|
if (!empty($rowFound)
|
|
&& $rowFound->isSummaryRow()
|
|
) {
|
|
$rowFound = false;
|
|
}
|
|
$this->aggregateRow($rowFound, $row, $columnAggregationOps, $isSummaryRow = false);
|
|
}
|
|
|
|
private function aggregateRow($thisRow, Row $otherRow, $columnAggregationOps, $isSummaryRow)
|
|
{
|
|
if (empty($thisRow)) {
|
|
$thisRow = new Row();
|
|
$otherRowLabel = $otherRow->getColumn('label');
|
|
if ($otherRowLabel !== false) {
|
|
$thisRow->addColumn('label', $otherRowLabel);
|
|
}
|
|
$thisRow->setAllMetadata($otherRow->getMetadata());
|
|
|
|
if ($isSummaryRow) {
|
|
$this->addSummaryRow($thisRow);
|
|
} else {
|
|
$this->addRow($thisRow);
|
|
}
|
|
}
|
|
|
|
$thisRow->sumRow($otherRow, $copyMeta = true, $columnAggregationOps);
|
|
|
|
// if the row to add has a subtable whereas the current row doesn't
|
|
// we simply add it (cloning the subtable)
|
|
// if the row has the subtable already
|
|
// then we have to recursively sum the subtables
|
|
$subTable = $otherRow->getSubtable();
|
|
if ($subTable) {
|
|
$subTable->metadata[self::COLUMN_AGGREGATION_OPS_METADATA_NAME] = $columnAggregationOps;
|
|
$thisRow->sumSubtable($subTable);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param $row
|
|
*/
|
|
protected function aggregateRowFromSimpleTable($row)
|
|
{
|
|
if ($row === false) {
|
|
return;
|
|
}
|
|
$thisRow = $this->getFirstRow();
|
|
if ($thisRow === false) {
|
|
$thisRow = new Row;
|
|
$this->addRow($thisRow);
|
|
}
|
|
$thisRow->sumRow($row, $copyMeta = true, $this->getMetadata(self::COLUMN_AGGREGATION_OPS_METADATA_NAME));
|
|
}
|
|
|
|
/**
|
|
* Unsets all queued filters.
|
|
*/
|
|
public function clearQueuedFilters()
|
|
{
|
|
$this->queuedFilters = array();
|
|
}
|
|
|
|
public function getQueuedFilters()
|
|
{
|
|
return $this->queuedFilters;
|
|
}
|
|
|
|
/**
|
|
* @return \ArrayIterator|Row[]
|
|
*/
|
|
public function getIterator(): \ArrayIterator
|
|
{
|
|
return new \ArrayIterator($this->getRows());
|
|
}
|
|
|
|
public function offsetExists($offset): bool
|
|
{
|
|
$row = $this->getRowFromId($offset);
|
|
|
|
return false !== $row;
|
|
}
|
|
|
|
public function offsetGet($offset): Row
|
|
{
|
|
return $this->getRowFromId($offset);
|
|
}
|
|
|
|
public function offsetSet($offset, $value): void
|
|
{
|
|
$this->rows[$offset] = $value;
|
|
}
|
|
|
|
public function offsetUnset($offset): void
|
|
{
|
|
$this->deleteRow($offset);
|
|
}
|
|
}
|