Skip to content
Extraits de code Groupes Projets
DataTable.php 49 ko
Newer Older
<?php
/**
 * Piwik - Open source web analytics
robocoder's avatar
robocoder a validé
 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
robocoder's avatar
robocoder a validé
 * @category Piwik
 * @package Piwik
/**
 * @see destroy()
 */
require_once PIWIK_INCLUDE_PATH . '/core/Common.php';

 * ---- DataTable
 * A DataTable is a data structure used to store complex tables of data.
 * A DataTable is composed of multiple DataTable_Row.
 * A DataTable can be applied one or several DataTable_Filter.
 * A DataTable can be given to a DataTable_Renderer that would export the data under a given format (XML, HTML, etc.).
 * A DataTable has the following features:
 * - serializable to be stored in the DB
 * - loadable from the serialized version
 * - efficient way of loading data from an external source (from a PHP array structure)
 * - very simple interface to get data from the table
 * ---- DataTable_Row
 * A DataTableRow in the table is defined by
 * - multiple columns (a label, multiple values, ...)
 * - optional metadata
 * - optional - a sub DataTable associated to this row
 * - columns = array(   'label' => 'Firefox',
 *                        'visitors' => 155,
 *                        'pages' => 214,
 *                        'bounce_rate' => 67)
 * - metadata = array('logo' => '/plugins/UserSettings/images/browsers/FF.gif')
 * A more complex example would be a DataTable_Row that is associated to a sub DataTable.
 * For example, for the row of the search engine Google,
 * we want to get the list of keywords associated, with their statistics.
 * - columns = array(   'label' => 'Google',
 *                        'visits' => 1550,
 *                        'visits_length' => 514214,
 *                        'returning_visits' => 77)
 * - metadata = array(    'logo' => '/plugins/Referers/images/searchEngines/google.com.png',
 *                        'url' => 'http://google.com')
 * - DataTable = DataTable containing several DataTable_Row containing the keywords information for this search engine
 *            Example of one DataTable_Row
 *            - the keyword columns specific to this search engine =
 *                    array(  'label' => 'Piwik', // the keyword
 *                            'visitors' => 155,  // Piwik has been searched on Google by 155 visitors
 *                            'pages' => 214 // Visitors coming from Google with the kwd Piwik have seen 214 pages
 *                    )
 *            - the keyword metadata = array() // nothing here, but we could imagining storing the URL of the search in Google for example
 *            - no subTable
 *
 *
 * A DataTable_Filter is a applied to a DataTable and so
 * can filter information in the multiple DataTable_Row.
 * - remove rows from the table,
 *        for example the rows' labels that do not match a given searched pattern
 *        for example the rows' values that are less than a given percentage (low population)
 * - return a subset of the DataTable
 *        for example a function that apply a limit: $offset, $limit
 *        for example adding a column that gives the percentage of a given value
 *        for example the 'logo' path if the filter detects the logo
 * - edit the value, the label
 * - change the rows order
 *        for example if we want to sort by Label alphabetical order, or by any column value
 *
 * When several DataTable_Filter are to be applied to a DataTable they are applied sequentially.
 * A DataTable_Filter is assigned a priority.
 * For example, filters that
 *    - sort rows should be applied with the highest priority
 *    - remove rows should be applied with a high priority as they prune the data and improve performance.
 *
 * $table = new DataTable();
mattpiwik's avatar
mattpiwik a validé
 * $table->addRowsFromArray( array(...) );
 * # sort the table by visits asc
 * $filter = new DataTable_Filter_Sort( $table, 'visits', 'asc');
 * $tableFiltered = $filter->getTableFiltered();
 * # add a filter to select only the website with a label matching '*.com' (regular expression)
 * $filter = new DataTable_Filter_Pattern( $table, 'label', '*(.com)');
 * $tableFiltered = $filter->getTableFiltered();
 * # keep the 20 elements from offset 15
 * $filter = new DataTable_Filter_Limit( $tableFiltered, 15, 20);
 * $tableFiltered = $filter->getTableFiltered();
 * # add a column computing the percentage of visits
 * # params = table, column containing the value, new column name to add, number of total visits to use to compute the %
 * $filter = new DataTable_Filter_AddColumnPercentage( $tableFiltered, 'visits', 'visits_percentage', 2042);
 * $tableFiltered = $filter->getTableFiltered();
 * # we get the table as XML
 * $xmlOutput = new DataTable_Exporter_Xml( $table );
 * $xmlOutput->setHeader( ... );
 * $xmlOutput->setColumnsToExport( array('visits', 'visits_percent', 'label') );
 * $XMLstring = $xmlOutput->getOutput();
 * ---- Other (ideas)
 * We can also imagine building a DataTable_Compare which would take N DataTable that have the same
 * structure and would compare them, by computing the percentages of differences, etc.
 * DataTable1 = [ keyword1, 1550 visits]
 * DataTable2 = [ keyword1, 1004 visits ]
 * DataTable_Compare = result of comparison of table1 with table2
 *                        [ keyword1, +154% ]
 *                        [ keyword2, +1000% ]
 *                        [ keyword3, -430% ]
 *
 * @see Piwik_DataTable_Row A Piwik_DataTable is composed of Piwik_DataTable_Row
 * @package Piwik
 * @subpackage Piwik_DataTable
 */
class Piwik_DataTable
    /** Name for metadata that describes when a report was archived. */
    const ARCHIVED_DATE_METADATA_NAME = 'archived_date';
    const MAX_DEPTH_DEFAULT = 15;
    /** Name for metadata that describes which columns are empty and should not be shown. */
    const EMPTY_COLUMNS_METADATA_NAME = 'empty_column';

    /**
     * Maximum nesting level.
     */
    private static $maximumDepthLevelAllowed = self::MAX_DEPTH_DEFAULT;

    /**
     * Array of Piwik_DataTable_Row
     *
     * @var Piwik_DataTable_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 Piwik_DataTable_Filter queued to this table
     *
     * @var array
     */
    protected $queuedFilters = 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;

    /**
     * Table metadata.
     *
     * @var array
     */
    public $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;

    /**
     * The operations that should be used when aggregating columns from multiple rows.
     * @see self::addDataTable() and Piwik_DataTable_Row::sumRow() 
     */
    protected $columnAggregationOperations = array();

    const ID_SUMMARY_ROW = -1;
    const LABEL_SUMMARY_ROW = -1;

    /**
     * Builds the DataTable, registers itself to the manager
     *
     */
    public function __construct()
    {
        $this->currentId = Piwik_DataTable_Manager::getInstance()->addTable($this);
    }

    /**
     * At destruction we free all memory
     */
    public function __destruct()
    {
        static $depth = 0;
        // destruct can be called several times
        if ($depth < self::$maximumDepthLevelAllowed
            && isset($this->rows)
        ) {
            $depth++;
            foreach ($this->getRows() as $row) {
                destroy($row);
            }
            unset($this->rows);
            Piwik_DataTable_Manager::getInstance()->setTableDeleted($this->getId());
            $depth--;
        }
    }

    /**
     * Sort the dataTable rows using the php callback function
     *
     * @param string $functionCallback
     * @param string $columnSortedBy    The column name. Used to then ask the datatable what column are you sorted by
     */
    public function sort($functionCallback, $columnSortedBy)
    {
        $this->indexNotUpToDate = true;
        $this->tableSortedBy = $columnSortedBy;
        usort($this->rows, $functionCallback);

        if ($this->enableRecursiveSort === true) {
            foreach ($this->getRows() as $row) {
                if (($idSubtable = $row->getIdSubDataTable()) !== null) {
                    $table = Piwik_DataTable_Manager::getInstance()->getTable($idSubtable);
                    $table->enableRecursiveSort();
                    $table->sort($functionCallback, $columnSortedBy);
                }
            }
        }
    }

    /**
     * Returns the name of the column the tables is sorted by
     *
     * @return bool|string
     */
    public function getSortedByColumnName()
    {
        return $this->tableSortedBy;
    }

    /**
     * Enables the recursive sort. Means that when using $table->sort()
     * it will also sort all subtables using the same callback
     */
    public function enableRecursiveSort()
    {
        $this->enableRecursiveSort = true;
    }

    /**
     * Enables the recursive filter. Means that when using $table->filter()
     * it will also filter all subtables using the same callback
     */
    public function enableRecursiveFilters()
    {
        $this->enableRecursiveFilters = true;
    }

    /**
     * Returns the number of rows before we applied the limit filter
     *
     * @return int
     */
    public function getRowsCountBeforeLimitFilter()
    {
        $toReturn = $this->rowsCountBeforeLimitFilter;
        if ($toReturn == 0) {
            return $this->getRowsCount();
        }
        return $toReturn;
    }

    /**
     * Saves the current number of rows
     */
    public function setRowsCountBeforeLimitFilter()
    {
        $this->rowsCountBeforeLimitFilter = $this->getRowsCount();
    }

    /**
     * Apply a filter to this datatable
     *
     * @param string|Closure $className   Class name, eg. "Sort" or "Piwik_DataTable_Filter_Sort".
     *                                    If this variable is a closure, it will get executed immediately.
     * @param array $parameters  Array of parameters to the filter, eg. array('nb_visits', 'asc')
     */
    public function filter($className, $parameters = array())
    {
        if ($className instanceof Closure) {
            $className($this);
            return;
        }
        
        if (!class_exists($className, false)) {
            $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);
    }

    /**
     * Queue a DataTable_Filter that will be applied when applyQueuedFilters() is called.
     * (just before sending the datatable back to the browser (or API, etc.)
     *
     * @param string $className   The class name of the filter, eg. Piwik_DataTable_Filter_Limit
     * @param array $parameters  The parameters to give to the filter, eg. array( $offset, $limit) for the filter Piwik_DataTable_Filter_Limit
     */
    public function queueFilter($className, $parameters = array())
    {
        if (!is_array($parameters)) {
            $parameters = array($parameters);
        }
        $this->queuedFilters[] = array('className' => $className, 'parameters' => $parameters);
    }

    /**
     * Apply all filters that were previously queued to this table
     * @see queueFilter()
     */
    public function applyQueuedFilters()
    {
        foreach ($this->queuedFilters as $filter) {
            $this->filter($filter['className'], $filter['parameters']);
        }
        $this->queuedFilters = array();
    }

    /**
     * Adds a new DataTable to this DataTable
     * Go through all the rows of the new DataTable and applies the algorithm:
     * - if a row in $table doesnt exist in $this we add the new row to $this
     * - if a row exists in both $table and $this we sum the columns values into $this
     * - if a row in $this doesnt exist in $table we add in $this the row of $table without modification
     *
     * A common row to 2 DataTable is defined by the same label
     *
     * @example  tests/core/DataTable.test.php
     *
     * @param Piwik_DataTable $tableToSum
     */
    public function addDataTable(Piwik_DataTable $tableToSum)
    {
        foreach ($tableToSum->getRows() as $row) {
            $labelToLookFor = $row->getColumn('label');
            $rowFound = $this->getRowFromLabel($labelToLookFor);
            if ($rowFound === false) {
                if ($labelToLookFor === self::LABEL_SUMMARY_ROW) {
                    $this->addSummaryRow($row);
                } else {
                    $this->addRow($row);
                }
            } else {
                $rowFound->sumRow($row, $copyMeta = true, $this->columnAggregationOperations);

                // 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
                if (($idSubTable = $row->getIdSubDataTable()) !== null) {
Timo Besenreuther's avatar
Timo Besenreuther a validé
                    $subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable);
                    $subTable->setColumnAggregationOperations($this->columnAggregationOperations);
                    $rowFound->sumSubtable($subTable);
                }
            }
        }
    }

    /**
     * Returns the Piwik_DataTable_Row that has a column 'label' with the value $label
     *
     * @param string $label  Value of the column 'label' of the row to return
     * @return Piwik_DataTable_Row|false  The row if found, false otherwise
     */
    public function getRowFromLabel($label)
    {
        $rowId = $this->getRowIdFromLabel($label);
        if ($rowId instanceof Piwik_DataTable_Row) {
            return $rowId;
        }
        if (is_int($rowId) && isset($this->rows[$rowId])) {
            return $this->rows[$rowId];
        }
        return false;
    }

    /**
     * Returns the row id for the givel label
     *
     * @param string $label  Value of the column 'label' of the row to return
     * @return int|Row
     */
    public function getRowIdFromLabel($label)
    {
        $this->rebuildIndexContinuously = true;
        if ($this->indexNotUpToDate) {
            $this->rebuildIndex();
        }

        if ($label === self::LABEL_SUMMARY_ROW
            && !is_null($this->summaryRow)
        ) {
            return $this->summaryRow;
        }

        $label = (string)$label;
        if (!isset($this->rowsIndexByLabel[$label])) {
            return false;
        }
        return $this->rowsIndexByLabel[$label];
    }

    /**
     * Get an empty table with the same properties as this one
     *
     * @return Piwik_DataTable
     */
    public function getEmptyClone($keepFilters = true)
        if ($keepFilters) {
            $clone->queuedFilters = $this->queuedFilters;
        }
        $clone->metadata = $this->metadata;
        return $clone;
    }

    /**
     * Rebuilds the index used to lookup a row by label
     */
    private function rebuildIndex()
    {
        foreach ($this->rows as $id => $row) {
            $label = $row->getColumn('label');
            if ($label !== false) {
                $this->rowsIndexByLabel[$label] = $id;
            }
        }
        $this->indexNotUpToDate = false;
    }

    /**
     * Returns the ith row in the array
     *
     * @param int $id
     * @return Piwik_DataTable_Row or false if not found
     */
    public function getRowFromId($id)
    {
        if (!isset($this->rows[$id])) {
            if ($id == self::ID_SUMMARY_ROW
                && !is_null($this->summaryRow)
            ) {
                return $this->summaryRow;
            }
            return false;
        }
        return $this->rows[$id];
    }

    /**
     * Returns a row that has the subtable ID matching the parameter
     *
     * @param int $idSubTable
     * @return Piwik_DataTable_Row|false if not found
     */
    public function getRowFromIdSubDataTable($idSubTable)
    {
        $idSubTable = (int)$idSubTable;
        foreach ($this->rows as $row) {
            if ($row->getIdSubDataTable() === $idSubTable) {
                return $row;
            }
        }
        return false;
    }

    /**
     * Add a row to the table and rebuild the index if necessary
     *
     * @param Piwik_DataTable_Row $row  to add at the end of the array
sgiehl's avatar
sgiehl a validé
     * @return Piwik_DataTable_Row
     */
    public function addRow(Piwik_DataTable_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
            {
                $this->addSummaryRow(new Piwik_DataTable_Row(array(
                                                                  Piwik_DataTable_Row::COLUMNS => $row->getColumns()
                                                             )));
                $this->summaryRow->setColumn('label', self::LABEL_SUMMARY_ROW);
            } else {
mattab's avatar
mattab a validé
                $this->summaryRow->sumRow($row, $enableCopyMetadata = false, $this->columnAggregationOperations);
            }
            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;
            }
            $this->indexNotUpToDate = false;
        }
        return $row;
    }

    /**
     * Sets the summary row (a dataTable can have only one summary row)
     *
     * @param Piwik_DataTable_Row $row
     * @return Piwik_DataTable_Row Returns $row.
     */
    public function addSummaryRow(Piwik_DataTable_Row $row)
    {
        $this->summaryRow = $row;
        return $row;
    }

    /**
     * Returns the dataTable ID
     *
     * @return int
     */
    public function getId()
    {
        return $this->currentId;
    }

    /**
     * Adds a new row from a PHP array data structure
     *
     * @param array $row  eg. array(Piwik_DataTable_Row::COLUMNS => array( 'visits' => 13, 'test' => 'toto'),)
     */
    public function addRowFromArray($row)
    {
        $this->addRowsFromArray(array($row));
    }

    /**
     * Adds a new row a PHP array data structure
     *
     * @param array $row  eg. array('name' => 'google analytics', 'license' => 'commercial')
     */
    public function addRowFromSimpleArray($row)
    {
        $this->addRowsFromSimpleArray(array($row));
    }

    /**
     * Returns the array of Piwik_DataTable_Row
     *
     * @return Piwik_DataTable_Row[]
     */
    public function getRows()
    {
        if (is_null($this->summaryRow)) {
            return $this->rows;
        } else {
            return $this->rows + array(self::ID_SUMMARY_ROW => $this->summaryRow);
        }
    }

    /**
     * Returns the array containing all rows values for the requested column
     *
     * @param string $name
     * @return array
     */
    public function getColumn($name)
    {
        $columnValues = array();
        foreach ($this->getRows() as $row) {
            $columnValues[] = $row->getColumn($name);
        }
        return $columnValues;
    }

mattab's avatar
mattab a validé
    /**
     * Returns  the array containing all rows values of all columns which name starts with $name
     *
     * @param $name
     * @return array
     */
    public function getColumnsStartingWith($name)
    {
        $columnValues = array();
        foreach ($this->getRows() as $row) {
            $columns = $row->getColumns();
            foreach($columns as $column => $value) {
                if(strpos($column, $name) === 0) {
                    $columnValues[] = $row->getColumn($column);
                }
            }
        }
        return $columnValues;
    }

    /**
     * Returns an array containing the rows Metadata values
     *
     * @param string $name  Metadata column to return
     * @return array
     */
    public function getRowsMetadata($name)
    {
        $metadataValues = array();
        foreach ($this->getRows() as $row) {
            $metadataValues[] = $row->getMetadata($name);
        }
        return $metadataValues;
    }

    /**
     * Returns the number of rows in the table
     *
     * @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 Piwik_DataTable_Row
     */
    public function getFirstRow()
    {
        if (count($this->rows) == 0) {
            if (!is_null($this->summaryRow)) {
                return $this->summaryRow;
            }
            return false;
        }
        $row = array_slice($this->rows, 0, 1);
        return $row[0];
    }

    /**
     * Returns the last row of the DataTable
     *
     * @return Piwik_DataTable_Row
     */
    public function getLastRow()
    {
        if (!is_null($this->summaryRow)) {
            return $this->summaryRow;
        }

        if (count($this->rows) == 0) {
            return false;
        }
        $row = array_slice($this->rows, -1);
        return $row[0];
    }

    /**
     * Returns the sum of the number of rows of all the subtables
     *        + the number of rows in the parent table
     *
     * @return int
     */
    public function getRowsCountRecursive()
    {
        $totalCount = 0;
        foreach ($this->rows as $row) {
            if (($idSubTable = $row->getIdSubDataTable()) !== null) {
                $subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable);
                $count = $subTable->getRowsCountRecursive();
                $totalCount += $count;
            }
        }

        $totalCount += $this->getRowsCount();
        return $totalCount;
    }

    /**
     * Delete a given column $name in all the rows
     *
     * @param string $name
     */
    public function deleteColumn($name)
    {
        $this->deleteColumns(array($name));
    }

    public function __sleep()
    {
        return array('rows', 'summaryRow');
    }

    /**
     * Rename a column in all rows
     *
     * @param string $oldName  Old column name
     * @param string $newName  New column name
     */
    public function renameColumn($oldName, $newName)
    {
        foreach ($this->getRows() as $row) {
            $row->renameColumn($oldName, $newName);
            if (($idSubDataTable = $row->getIdSubDataTable()) !== null) {
                Piwik_DataTable_Manager::getInstance()->getTable($idSubDataTable)->renameColumn($oldName, $newName);
            }
        }
        if (!is_null($this->summaryRow)) {
            $this->summaryRow->renameColumn($oldName, $newName);
        }
    }

    /**
     * Delete columns by name in all rows
     *
     * @param array $names
     * @param bool $deleteRecursiveInSubtables
     */
    public function deleteColumns($names, $deleteRecursiveInSubtables = false)
    {
        foreach ($this->getRows() as $row) {
            foreach ($names as $name) {
                $row->deleteColumn($name);
            }
            if (($idSubDataTable = $row->getIdSubDataTable()) !== null) {
                Piwik_DataTable_Manager::getInstance()->getTable($idSubDataTable)->deleteColumns($names, $deleteRecursiveInSubtables);
            }
        }
        if (!is_null($this->summaryRow)) {
            foreach ($names as $name) {
                $this->summaryRow->deleteColumn($name);
            }
        }
    }

    /**
     * Deletes the ith row
     *
sgiehl's avatar
sgiehl a validé
     * @param int  $id
     *
     * @throws Exception if the row $id cannot be found
sgiehl's avatar
sgiehl a validé
     * @return void
     */
    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 all row from offset, offset + limit.
     * If limit is null then limit = $table->getRowsCount()
     *
     * @param int $offset
     * @param int $limit
     * @return int
     */
    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)) {
            $spliced = array_splice($this->rows, $offset);
        } else {
            $spliced = array_splice($this->rows, $offset, $limit);
        }
        $countDeleted = count($spliced);
        return $countDeleted;
    }

    /**
     * Deletes the rows from the list of rows ID
     *
     * @param array $aKeys  ID of the rows to delete
     * @throws Exception if any of the row to delete couldn't be found
     */
    public function deleteRows(array $aKeys)
    {
        foreach ($aKeys as $key) {
            $this->deleteRow($key);
        }
    }

    /**
     * Returns a simple output of the DataTable for easy visualization
     * Example: echo $datatable;
     *
     * @return string
     */
    public function __toString()
    {
        $renderer = new Piwik_DataTable_Renderer_Html();
        $renderer->setTable($this);
        return (string)$renderer;
    }

    /**
     * Returns true if both DataTable are exactly the same.
     * Used in unit tests.
     *
     * @param Piwik_DataTable $table1
     * @param Piwik_DataTable $table2
     * @return bool
     */
    public static function isEqual(Piwik_DataTable $table1, Piwik_DataTable $table2)
    {
        $rows1 = $table1->getRows();
        $rows2 = $table2->getRows();

        $table1->rebuildIndex();
        $table2->rebuildIndex();

        if ($table1->getRowsCount() != $table2->getRowsCount()) {
            return false;
        }

        foreach ($rows1 as $row1) {
            $row2 = $table2->getRowFromLabel($row1->getColumn('label'));
            if ($row2 === false
                || !Piwik_DataTable_Row::isEqual($row1, $row2)
            ) {
                return false;
            }
        }

        return true;
    }

    /**
     * The serialization returns a one dimension array containing all the
     * serialized DataTable contained in this DataTable.
     * We save DataTable in serialized format in the Database.
     * Each row of this returned PHP array will be a row in the DB table.
     * At the end of the method execution, the dataTable may be truncated (if $maximum* parameters are set).
     *
     * The keys of the array are very important as they are used to define the DataTable
     *
     * IMPORTANT: The main table (level 0, parent of all tables) will always be indexed by 0
     *    even it was created after some other tables.
     *    It also means that all the parent tables (level 0) will be indexed with 0 in their respective
     *  serialized arrays. You should never lookup a parent table using the getTable( $id = 0) as it
     *  won't work.
     *
     * @throws Exception if an infinite recursion is found (a table row's has a subtable that is one of its parent table)
     * @param int $maximumRowsInDataTable          If not null, defines the number of rows maximum of the serialized dataTable
     * @param int $maximumRowsInSubDataTable       If not null, defines the number of rows maximum of the serialized subDataTable
     * @param string $columnToSortByBeforeTruncation  Column to sort by before truncation
     * @return array  Serialized arrays
     *            array(    // Datatable level0
     *                    0 => 'eghuighahgaueytae78yaet7yaetae',
     *
     *                    // first Datatable level1
     *                    1 => 'gaegae gh gwrh guiwh uigwhuige',
     *
     *                    //second Datatable level1
     *                    2 => 'gqegJHUIGHEQjkgneqjgnqeugUGEQHGUHQE',
     *
     *                    //first Datatable level3 (child of second Datatable level1 for example)
     *                    3 => 'eghuighahgaueytae78yaet7yaetaeGRQWUBGUIQGH&QE',
     *                    );
     */
    public function getSerialized($maximumRowsInDataTable = null,
                                  $maximumRowsInSubDataTable = null,
                                  $columnToSortByBeforeTruncation = null)
    {
        static $depth = 0;

        if ($depth > self::$maximumDepthLevelAllowed) {
            $depth = 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?");
        }
        if (!is_null($maximumRowsInDataTable)) {
            $this->filter('AddSummaryRow',
                array($maximumRowsInDataTable - 1,
                      Piwik_DataTable::LABEL_SUMMARY_ROW,
                      $columnToSortByBeforeTruncation)
            );
        }