Skip to content
Extraits de code Groupes Projets
DataTable.php 47,4 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
use Piwik\DataTable\DataTableInterface;
use Piwik\DataTable\Manager;
use Piwik\DataTable\Renderer\Html;
use Piwik\DataTable\Row;
use Piwik\DataTable\Row\DataTableSummaryRow;
use ReflectionClass;
mattab's avatar
mattab a validé

mattab's avatar
mattab a validé
 * @see Common::destroy()
 */
require_once PIWIK_INCLUDE_PATH . '/core/Common.php';

 * The primary data structure used to store analytics data in Piwik.
 * 
 * ### The Basics
 * 
 * DataTables consist of rows and each row consists of columns. A column value can be
 * be a numeric, string or array.
 * 
 * DataTables are hierarchical data structures. Each row can also contain an additional
 * nested sub-DataTable.
 * 
 * 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, DataTables all contain a special _summary_ row.
 * 
 * ### Populating DataTables
 * 
 * Data can be added to DataTables in a couple different ways. You can either:
 * 
 * 1. create rows one by one and add them through [addRow](#addRow) then truncate if desired,
 * 2. create an array of DataTable\Row instances or an array of arrays and add them using
 *    [addRowsFromArray](#addRowsFromArray) or [addRowsFromSimpleArray](#addRowsFromSimpleArray)
 *    then truncate if desired,
 * 3. or set the maximum number of allowed rows (with [setMaximumAllowedRows](#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 main 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 DataTable\Map
 * without having to write a recursive visiting function.
 * 
 * Note: Anonymous functions can be used as DataTable Filters.
 * 
 * ### Applying Filters
 * 
 * Filters can be applied now (via [filter](#filter)), or they can be applied later (via
 * [queueFilter](#queueFilter)).
 * 
 * Filters that sort rows or manipulate the number of rows should be applied right away.
 * Non-essential, presentation filters should be queued.
 * 
 * See also:
 * 
 * - ArchiveProcessor &mdash; to learn how DataTables are persisted.
 * - DataTable\Renderer &mdash; to learn how DataTable data is exported to XML, JSON, etc.
 * - DataTable\Filter &mdash; to see all core Filters.
 * - DataTable\Manager &mdash; to learn how DataTables are loaded.
 * 
 * ### Examples
 * 
 * **Populating a DataTable**
 * **Serializing & unserializing**
 * **Filtering for an API method**
 * ??? TODO
 * 
class DataTable implements DataTableInterface
    /** 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;
     */
    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 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();

    /**
     */
    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 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 = 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) {
mattab's avatar
mattab a validé
                Common::destroy($row);
            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 = 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 "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) {
            array_unshift($parameters, $this);
            call_user_func_array($className, $parameters);
        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);
    }

    /**
     * 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. Limit
     * @param array $parameters The parameters to give to the filter, eg. array( $offset, $limit) for the 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(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) {
                    $subTable = Manager::getInstance()->getTable($idSubTable);
Timo Besenreuther's avatar
Timo Besenreuther a validé
                    $subTable->setColumnAggregationOperations($this->columnAggregationOperations);
                    $rowFound->sumSubtable($subTable);
     * Returns the 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|bool  The row if found, false otherwise
     */
    public function getRowFromLabel($label)
    {
        $rowId = $this->getRowIdFromLabel($label);
            return $rowId;
        }
        if (is_int($rowId) && isset($this->rows[$rowId])) {
            return $this->rows[$rowId];
        }
        if ($rowId == self::ID_SUMMARY_ROW
            && !empty($this->summaryRow)
        ) {
            return $this->summaryRow;
        }
        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
     *
sgiehl's avatar
sgiehl a validé
     * @param bool $keepFilters
     *
    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->getRows() 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|bool    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
    {
        // 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
            {
Benaka Moorthi's avatar
Benaka Moorthi a validé
                $columns = array('label' => self::LABEL_SUMMARY_ROW) + $row->getColumns();
                $this->addSummaryRow(new Row(array(Row::COLUMNS => $columns)));
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;
            }
        }
        return $row;
    }

    /**
     * Sets the summary row (a dataTable can have only one summary row)
     *
     * @param Row $row
     * @return Row Returns $row.
    public function addSummaryRow(Row $row)

        // add summary row to index
        if (!$this->indexNotUpToDate
            && $this->rebuildIndexContinuously
        ) {
            $label = $row->getColumn('label');
            if ($label !== false) {
                $this->rowsIndexByLabel[$label] = self::ID_SUMMARY_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(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));
    }

    /**
     */
    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();
mattab's avatar
mattab a validé
            foreach ($columns as $column => $value) {
                if (strpos($column, $name) === 0) {
mattab's avatar
mattab a validé
                    $columnValues[] = $row->getColumn($column);
                }
            }
        }
        return $columnValues;
    }
    /**
     * Returns the list of columns the rows in this datatable contain. This will return the
     * columns of the first row with data and assume they occur in every other row as well.
mattab's avatar
mattab a validé
     *
     * @return array
     */
    public function getColumns()
    {
        foreach ($this->getRows() as $row) {
            $columns = $row->getColumns();
            if (!empty($columns)) {

        // make sure column names are not DB index values
        foreach ($result as &$column) {
            if (isset(Metrics::$mappingFromIdToName[$column])) {
                $column = Metrics::$mappingFromIdToName[$column];
            }
        }

        return $result;
mattab's avatar
mattab a validé

    /**
     * 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
     *
     */
    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
     *
     */
    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 = 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');
     * @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) {
                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) {
                Manager::getInstance()->getTable($idSubDataTable)->deleteColumns($names, $deleteRecursiveInSubtables);
            }
        }
        if (!is_null($this->summaryRow)) {
            foreach ($names as $name) {
                $this->summaryRow->deleteColumn($name);
            }
        }
    }

    /**
     * Deletes the ith row
     *
mattab's avatar
mattab a validé
     * @param int $id
sgiehl's avatar
sgiehl a validé
     *
     * @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->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
    public static function isEqual(DataTable $table1, 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
            ) {
                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,