PageRenderTime 7ms CodeModel.GetById 54ms app.highlight 24ms RepoModel.GetById 1ms app.codeStats 1ms

/core/DataTable.php

https://github.com/CodeYellowBV/piwik
PHP | 1637 lines | 754 code | 127 blank | 756 comment | 125 complexity | 3c7d86fe39dce9eef6ba72d1180d740c MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1<?php
   2/**
   3 * Piwik - free/libre analytics platform
   4 *
   5 * @link http://piwik.org
   6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
   7 *
   8 */
   9
  10namespace Piwik;
  11
  12use Closure;
  13use Exception;
  14use Piwik\DataTable\DataTableInterface;
  15use Piwik\DataTable\Manager;
  16use Piwik\DataTable\Renderer\Html;
  17use Piwik\DataTable\Row;
  18use Piwik\DataTable\Row\DataTableSummaryRow;
  19use Piwik\DataTable\Simple;
  20use Piwik\DataTable\TableNotFoundException;
  21use ReflectionClass;
  22
  23/**
  24 * @see Common::destroy()
  25 */
  26require_once PIWIK_INCLUDE_PATH . '/core/Common.php';
  27
  28/**
  29 * The primary data structure used to store analytics data in Piwik.
  30 * 
  31 * <a name="class-desc-the-basics"></a>
  32 * ### The Basics
  33 * 
  34 * DataTables consist of rows and each row consists of columns. A column value can be
  35 * a numeric, a string or an array.
  36 * 
  37 * Every row has an ID. The ID is either the index of the row or {@link ID_SUMMARY_ROW}.
  38 * 
  39 * DataTables are hierarchical data structures. Each row can also contain an additional
  40 * nested sub-DataTable (commonly referred to as a 'subtable').
  41 * 
  42 * Both DataTables and DataTable rows can hold **metadata**. _DataTable metadata_ is information
  43 * regarding all the data, such as the site or period that the data is for. _Row metadata_
  44 * is information regarding that row, such as a browser logo or website URL.
  45 * 
  46 * Finally, all DataTables contain a special _summary_ row. This row, if it exists, is
  47 * always at the end of the DataTable.
  48 * 
  49 * ### Populating DataTables
  50 * 
  51 * Data can be added to DataTables in three different ways. You can either:
  52 * 
  53 * 1. create rows one by one and add them through {@link addRow()} then truncate if desired,
  54 * 2. create an array of DataTable\Row instances or an array of arrays and add them using
  55 *    {@link addRowsFromArray()} or {@link addRowsFromSimpleArray()}
  56 *    then truncate if desired,
  57 * 3. or set the maximum number of allowed rows (with {@link setMaximumAllowedRows()})
  58 *    and add rows one by one.
  59 * 
  60 * If you want to eventually truncate your data (standard practice for all Piwik plugins),
  61 * the third method is the most memory efficient. It is, unfortunately, not always possible
  62 * to use since it requires that the data be sorted before adding.
  63 * 
  64 * ### Manipulating DataTables
  65 * 
  66 * There are two ways to manipulate a DataTable. You can either:
  67 * 
  68 * 1. manually iterate through each row and manipulate the data,
  69 * 2. or you can use predefined filters.
  70 * 
  71 * A filter is a class that has a 'filter' method which will manipulate a DataTable in
  72 * some way. There are several predefined Filters that allow you to do common things,
  73 * such as,
  74 * 
  75 * - add a new column to each row,
  76 * - add new metadata to each row,
  77 * - modify an existing column value for each row,
  78 * - sort an entire DataTable,
  79 * - and more.
  80 * 
  81 * Using these filters instead of writing your own code will increase code clarity and
  82 * reduce code redundancy. Additionally, filters have the advantage that they can be
  83 * applied to DataTable\Map instances. So you can visit every DataTable in a {@link DataTable\Map}
  84 * without having to write a recursive visiting function.
  85 * 
  86 * All predefined filters exist in the **Piwik\DataTable\BaseFilter** namespace.
  87 * 
  88 * _Note: For convenience, [anonymous functions](http://www.php.net/manual/en/functions.anonymous.php)
  89 * can be used as DataTable filters._
  90 * 
  91 * ### Applying Filters
  92 * 
  93 * Filters can be applied now (via {@link filter()}), or they can be applied later (via
  94 * {@link queueFilter()}).
  95 * 
  96 * Filters that sort rows or manipulate the number of rows should be applied right away.
  97 * Non-essential, presentation filters should be queued.
  98 * 
  99 * ### Learn more
 100 * 
 101 * - See **{@link ArchiveProcessor}** to learn how DataTables are persisted.
 102 * 
 103 * ### Examples
 104 * 
 105 * **Populating a DataTable**
 106 * 
 107 *     // adding one row at a time
 108 *     $dataTable = new DataTable();
 109 *     $dataTable->addRow(new Row(array(
 110 *         Row::COLUMNS => array('label' => 'thing1', 'nb_visits' => 1, 'nb_actions' => 1),
 111 *         Row::METADATA => array('url' => 'http://thing1.com')
 112 *     )));
 113 *     $dataTable->addRow(new Row(array(
 114 *         Row::COLUMNS => array('label' => 'thing2', 'nb_visits' => 2, 'nb_actions' => 2),
 115 *         Row::METADATA => array('url' => 'http://thing2.com')
 116 *     )));
 117 *     
 118 *     // using an array of rows
 119 *     $dataTable = new DataTable();
 120 *     $dataTable->addRowsFromArray(array(
 121 *         array(
 122 *             Row::COLUMNS => array('label' => 'thing1', 'nb_visits' => 1, 'nb_actions' => 1),
 123 *             Row::METADATA => array('url' => 'http://thing1.com')
 124 *         ),
 125 *         array(
 126 *             Row::COLUMNS => array('label' => 'thing2', 'nb_visits' => 2, 'nb_actions' => 2),
 127 *             Row::METADATA => array('url' => 'http://thing2.com')
 128 *         )
 129 *     ));
 130 * 
 131 *     // using a "simple" array
 132 *     $dataTable->addRowsFromSimpleArray(array(
 133 *         array('label' => 'thing1', 'nb_visits' => 1, 'nb_actions' => 1),
 134 *         array('label' => 'thing2', 'nb_visits' => 2, 'nb_actions' => 2)
 135 *     ));
 136 * 
 137 * **Getting & setting metadata**
 138 * 
 139 *     $dataTable = \Piwik\Plugins\Referrers\API::getInstance()->getSearchEngines($idSite = 1, $period = 'day', $date = '2007-07-24');
 140 *     $oldPeriod = $dataTable->metadata['period'];
 141 *     $dataTable->metadata['period'] = Period\Factory::build('week', Date::factory('2013-10-18'));
 142 * 
 143 * **Serializing & unserializing**
 144 * 
 145 *     $maxRowsInTable = Config::getInstance()->General['datatable_archiving_maximum_rows_standard'];j
 146 *     
 147 *     $dataTable = // ... build by aggregating visits ...
 148 *     $serializedData = $dataTable->getSerialized($maxRowsInTable, $maxRowsInSubtable = $maxRowsInTable,
 149 *                                                 $columnToSortBy = Metrics::INDEX_NB_VISITS);
 150 *     
 151 *     $serializedDataTable = $serializedData[0];
 152 *     $serailizedSubTable = $serializedData[$idSubtable];
 153 * 
 154 * **Filtering for an API method**
 155 * 
 156 *     public function getMyReport($idSite, $period, $date, $segment = false, $expanded = false)
 157 *     {
 158 *         $dataTable = Archive::getDataTableFromArchive('MyPlugin_MyReport', $idSite, $period, $date, $segment, $expanded);
 159 *         $dataTable->filter('Sort', array(Metrics::INDEX_NB_VISITS, 'desc', $naturalSort = false, $expanded));
 160 *         $dataTable->queueFilter('ReplaceColumnNames');
 161 *         $dataTable->queueFilter('ColumnCallbackAddMetadata', array('label', 'url', __NAMESPACE__ . '\getUrlFromLabelForMyReport'));
 162 *         return $dataTable;
 163 *     }
 164 * 
 165 *
 166 * @api
 167 */
 168class DataTable implements DataTableInterface
 169{
 170    const MAX_DEPTH_DEFAULT = 15;
 171
 172    /** Name for metadata that describes when a report was archived. */
 173    const ARCHIVED_DATE_METADATA_NAME = 'archived_date';
 174
 175    /** Name for metadata that describes which columns are empty and should not be shown. */
 176    const EMPTY_COLUMNS_METADATA_NAME = 'empty_column';
 177
 178    /** Name for metadata that describes the number of rows that existed before the Limit filter was applied. */
 179    const TOTAL_ROWS_BEFORE_LIMIT_METADATA_NAME = 'total_rows_before_limit';
 180
 181    /**
 182     * Name for metadata that describes how individual columns should be aggregated when {@link addDataTable()}
 183     * or {@link Piwik\DataTable\Row::sumRow()} is called.
 184     * 
 185     * This metadata value must be an array that maps column names with valid operations. Valid aggregation operations are:
 186     * 
 187     * - `'skip'`: do nothing
 188     * - `'max'`: does `max($column1, $column2)`
 189     * - `'min'`: does `min($column1, $column2)`
 190     * - `'sum'`: does `$column1 + $column2`
 191     * 
 192     * See {@link addDataTable()} and {@link DataTable\Row::sumRow()} for more information.
 193     */
 194    const COLUMN_AGGREGATION_OPS_METADATA_NAME = 'column_aggregation_ops';
 195
 196    /** The ID of the Summary Row. */
 197    const ID_SUMMARY_ROW = -1;
 198
 199    /** The original label of the Summary Row. */
 200    const LABEL_SUMMARY_ROW = -1;
 201
 202    /**
 203     * Maximum nesting level.
 204     */
 205    private static $maximumDepthLevelAllowed = self::MAX_DEPTH_DEFAULT;
 206
 207    /**
 208     * Array of Row
 209     *
 210     * @var Row[]
 211     */
 212    protected $rows = array();
 213
 214    /**
 215     * Id assigned to the DataTable, used to lookup the table using the DataTable_Manager
 216     *
 217     * @var int
 218     */
 219    protected $currentId;
 220
 221    /**
 222     * Current depth level of this data table
 223     * 0 is the parent data table
 224     *
 225     * @var int
 226     */
 227    protected $depthLevel = 0;
 228
 229    /**
 230     * This flag is set to false once we modify the table in a way that outdates the index
 231     *
 232     * @var bool
 233     */
 234    protected $indexNotUpToDate = true;
 235
 236    /**
 237     * This flag sets the index to be rebuild whenever a new row is added,
 238     * as opposed to re-building the full index when getRowFromLabel is called.
 239     * This is to optimize and not rebuild the full Index in the case where we
 240     * add row, getRowFromLabel, addRow, getRowFromLabel thousands of times.
 241     *
 242     * @var bool
 243     */
 244    protected $rebuildIndexContinuously = false;
 245
 246    /**
 247     * Column name of last time the table was sorted
 248     *
 249     * @var string
 250     */
 251    protected $tableSortedBy = false;
 252
 253    /**
 254     * List of BaseFilter queued to this table
 255     *
 256     * @var array
 257     */
 258    protected $queuedFilters = array();
 259
 260    /**
 261     * We keep track of the number of rows before applying the LIMIT filter that deletes some rows
 262     *
 263     * @var int
 264     */
 265    protected $rowsCountBeforeLimitFilter = 0;
 266
 267    /**
 268     * Defaults to false for performance reasons (most of the time we don't need recursive sorting so we save a looping over the dataTable)
 269     *
 270     * @var bool
 271     */
 272    protected $enableRecursiveSort = false;
 273
 274    /**
 275     * When the table and all subtables are loaded, this flag will be set to true to ensure filters are applied to all subtables
 276     *
 277     * @var bool
 278     */
 279    protected $enableRecursiveFilters = false;
 280
 281    /**
 282     * @var array
 283     */
 284    protected $rowsIndexByLabel = array();
 285
 286    /**
 287     * @var \Piwik\DataTable\Row
 288     */
 289    protected $summaryRow = null;
 290
 291    /**
 292     * Table metadata. Read [this](#class-desc-the-basics) to learn more.
 293     * 
 294     * Any data that describes the data held in the table's rows should go here.
 295     *
 296     * @var array
 297     */
 298    private $metadata = array();
 299
 300    /**
 301     * Maximum number of rows allowed in this datatable (including the summary row).
 302     * If adding more rows is attempted, the extra rows get summed to the summary row.
 303     *
 304     * @var int
 305     */
 306    protected $maximumAllowedRows = 0;
 307
 308    /**
 309     * Constructor. Creates an empty DataTable.
 310     */
 311    public function __construct()
 312    {
 313        // registers this instance to the manager
 314        $this->currentId = Manager::getInstance()->addTable($this);
 315    }
 316
 317    /**
 318     * Destructor. Makes sure DataTable memory will be cleaned up.
 319     */
 320    public function __destruct()
 321    {
 322        static $depth = 0;
 323        // destruct can be called several times
 324        if ($depth < self::$maximumDepthLevelAllowed
 325            && isset($this->rows)
 326        ) {
 327            $depth++;
 328            foreach ($this->getRows() as $row) {
 329                Common::destroy($row);
 330            }
 331            unset($this->rows);
 332            Manager::getInstance()->setTableDeleted($this->getId());
 333            $depth--;
 334        }
 335    }
 336
 337    /**
 338     * Sorts the DataTable rows using the supplied callback function.
 339     *
 340     * @param string $functionCallback A comparison callback compatible with {@link usort}.
 341     * @param string $columnSortedBy The column name `$functionCallback` sorts by. This is stored
 342     *                               so we can determine how the DataTable was sorted in the future.
 343     */
 344    public function sort($functionCallback, $columnSortedBy)
 345    {
 346        $this->indexNotUpToDate = true;
 347        $this->tableSortedBy = $columnSortedBy;
 348        usort($this->rows, $functionCallback);
 349
 350        if ($this->enableRecursiveSort === true) {
 351            foreach ($this->getRows() as $row) {
 352                if (($idSubtable = $row->getIdSubDataTable()) !== null) {
 353                    $table = Manager::getInstance()->getTable($idSubtable);
 354                    $table->enableRecursiveSort();
 355                    $table->sort($functionCallback, $columnSortedBy);
 356                }
 357            }
 358        }
 359    }
 360
 361    /**
 362     * Returns the name of the column this table was sorted by (if any).
 363     *
 364     * See {@link sort()}.
 365     *
 366     * @return false|string The sorted column name or false if none.
 367     */
 368    public function getSortedByColumnName()
 369    {
 370        return $this->tableSortedBy;
 371    }
 372
 373    /**
 374     * Enables recursive sorting. If this method is called {@link sort()} will also sort all
 375     * subtables.
 376     */
 377    public function enableRecursiveSort()
 378    {
 379        $this->enableRecursiveSort = true;
 380    }
 381
 382    /**
 383     * Enables recursive filtering. If this method is called then the {@link filter()} method
 384     * will apply filters to every subtable in addition to this instance.
 385     */
 386    public function enableRecursiveFilters()
 387    {
 388        $this->enableRecursiveFilters = true;
 389    }
 390
 391    /**
 392     * Applies a filter to this datatable.
 393     * 
 394     * If {@link enableRecursiveFilters()} was called, the filter will be applied
 395     * to all subtables as well.
 396     *
 397     * @param string|Closure $className Class name, eg. `"Sort"` or "Piwik\DataTable\Filters\Sort"`. If no
 398     *                                  namespace is supplied, `Piwik\DataTable\BaseFilter` is assumed. This parameter
 399     *                                  can also be a closure that takes a DataTable as its first parameter.
 400     * @param array $parameters Array of extra parameters to pass to the filter.
 401     */
 402    public function filter($className, $parameters = array())
 403    {
 404        if ($className instanceof \Closure
 405            || is_array($className)
 406        ) {
 407            array_unshift($parameters, $this);
 408            call_user_func_array($className, $parameters);
 409            return;
 410        }
 411
 412        if (!class_exists($className, true)) {
 413            $className = 'Piwik\DataTable\Filter\\' . $className;
 414        }
 415        $reflectionObj = new ReflectionClass($className);
 416
 417        // the first parameter of a filter is the DataTable
 418        // we add the current datatable as the parameter
 419        $parameters = array_merge(array($this), $parameters);
 420
 421        $filter = $reflectionObj->newInstanceArgs($parameters);
 422
 423        $filter->enableRecursive($this->enableRecursiveFilters);
 424
 425        $filter->filter($this);
 426    }
 427
 428    /**
 429     * Adds a filter and a list of parameters to the list of queued filters. These filters will be
 430     * executed when {@link applyQueuedFilters()} is called.
 431     * 
 432     * Filters that prettify the column values or don't need the full set of rows should be queued. This
 433     * way they will be run after the table is truncated which will result in better performance.
 434     *
 435     * @param string|Closure $className The class name of the filter, eg. `'Limit'`.
 436     * @param array $parameters The parameters to give to the filter, eg. `array($offset, $limit)` for the Limit filter.
 437     */
 438    public function queueFilter($className, $parameters = array())
 439    {
 440        if (!is_array($parameters)) {
 441            $parameters = array($parameters);
 442        }
 443        $this->queuedFilters[] = array('className' => $className, 'parameters' => $parameters);
 444    }
 445
 446    /**
 447     * Applies all filters that were previously queued to the table. See {@link queueFilter()}
 448     * for more information.
 449     */
 450    public function applyQueuedFilters()
 451    {
 452        foreach ($this->queuedFilters as $filter) {
 453            $this->filter($filter['className'], $filter['parameters']);
 454        }
 455        $this->queuedFilters = array();
 456    }
 457
 458    /**
 459     * Sums a DataTable to this one.
 460     * 
 461     * This method will sum rows that have the same label. If a row is found in `$tableToSum` whose
 462     * label is not found in `$this`, the row will be added to `$this`.
 463     * 
 464     * If the subtables for this table are loaded, they will be summed as well.
 465     * 
 466     * Rows are summed together by summing individual columns. By default columns are summed by
 467     * adding one column value to another. Some columns cannot be aggregated this way. In these
 468     * cases, the {@link COLUMN_AGGREGATION_OPS_METADATA_NAME}
 469     * metadata can be used to specify a different type of operation.
 470     * 
 471     * @param \Piwik\DataTable $tableToSum
 472     */
 473    public function addDataTable(DataTable $tableToSum, $doAggregateSubTables = true)
 474    {
 475        if($tableToSum instanceof Simple) {
 476            if($tableToSum->getRowsCount() > 1) {
 477                throw new Exception("Did not expect a Simple table with more than one row in addDataTable()");
 478            }
 479            $row = $tableToSum->getFirstRow();
 480            $this->aggregateRowFromSimpleTable($row);
 481        } else {
 482            foreach ($tableToSum->getRows() as $row) {
 483                $this->aggregateRowWithLabel($row, $doAggregateSubTables);
 484            }
 485        }
 486    }
 487
 488    /**
 489     * Returns the Row whose `'label'` column is equal to `$label`.
 490     * 
 491     * This method executes in constant time except for the first call which caches row
 492     * label => row ID mappings.
 493     * 
 494     * @param string $label `'label'` column value to look for.
 495     * @return Row|false The row if found, `false` if otherwise.
 496     */
 497    public function getRowFromLabel($label)
 498    {
 499        $rowId = $this->getRowIdFromLabel($label);
 500        if ($rowId instanceof Row) {
 501            return $rowId;
 502        }
 503        if (is_int($rowId) && isset($this->rows[$rowId])) {
 504            return $this->rows[$rowId];
 505        }
 506        if ($rowId == self::ID_SUMMARY_ROW
 507            && !empty($this->summaryRow)
 508        ) {
 509            return $this->summaryRow;
 510        }
 511        return false;
 512    }
 513
 514    /**
 515     * Returns the row id for the row whose `'label'` column is equal to `$label`.
 516     *
 517     * This method executes in constant time except for the first call which caches row
 518     * label => row ID mappings.
 519     * 
 520     * @param string $label `'label'` column value to look for.
 521     * @return int The row ID.
 522     */
 523    public function getRowIdFromLabel($label)
 524    {
 525        $this->rebuildIndexContinuously = true;
 526        if ($this->indexNotUpToDate) {
 527            $this->rebuildIndex();
 528        }
 529
 530        if ($label === self::LABEL_SUMMARY_ROW
 531            && !is_null($this->summaryRow)
 532        ) {
 533            return self::ID_SUMMARY_ROW;
 534        }
 535
 536        $label = (string)$label;
 537        if (!isset($this->rowsIndexByLabel[$label])) {
 538            return false;
 539        }
 540        return $this->rowsIndexByLabel[$label];
 541    }
 542
 543    /**
 544     * Returns an empty DataTable with the same metadata and queued filters as `$this` one.
 545     *
 546     * @param bool $keepFilters Whether to pass the queued filter list to the new DataTable or not.
 547     * @return DataTable
 548     */
 549    public function getEmptyClone($keepFilters = true)
 550    {
 551        $clone = new DataTable;
 552        if ($keepFilters) {
 553            $clone->queuedFilters = $this->queuedFilters;
 554        }
 555        $clone->metadata = $this->metadata;
 556        return $clone;
 557    }
 558
 559    /**
 560     * Rebuilds the index used to lookup a row by label
 561     */
 562    private function rebuildIndex()
 563    {
 564        foreach ($this->getRows() as $id => $row) {
 565            $label = $row->getColumn('label');
 566            if ($label !== false) {
 567                $this->rowsIndexByLabel[$label] = $id;
 568            }
 569        }
 570        $this->indexNotUpToDate = false;
 571    }
 572
 573    /**
 574     * Returns a row by ID. The ID is either the index of the row or {@link ID_SUMMARY_ROW}.
 575     *
 576     * @param int $id The row ID.
 577     * @return Row|false The Row or false if not found.
 578     */
 579    public function getRowFromId($id)
 580    {
 581        if (!isset($this->rows[$id])) {
 582            if ($id == self::ID_SUMMARY_ROW
 583                && !is_null($this->summaryRow)
 584            ) {
 585                return $this->summaryRow;
 586            }
 587            return false;
 588        }
 589        return $this->rows[$id];
 590    }
 591
 592    /**
 593     * Returns the row that has a subtable with ID matching `$idSubtable`.
 594     * 
 595     * @param int $idSubTable The subtable ID.
 596     * @return Row|false The row or false if not found
 597     */
 598    public function getRowFromIdSubDataTable($idSubTable)
 599    {
 600        $idSubTable = (int)$idSubTable;
 601        foreach ($this->rows as $row) {
 602            if ($row->getIdSubDataTable() === $idSubTable) {
 603                return $row;
 604            }
 605        }
 606        return false;
 607    }
 608
 609    /**
 610     * Adds a row to this table.
 611     * 
 612     * If {@link setMaximumAllowedRows()} was called and the current row count is
 613     * at the maximum, the new row will be summed to the summary row. If there is no summary row,
 614     * this row is set as the summary row.
 615     *
 616     * @param Row $row
 617     * @return Row `$row` or the summary row if we're at the maximum number of rows.
 618     */
 619    public function addRow(Row $row)
 620    {
 621        // if there is a upper limit on the number of allowed rows and the table is full,
 622        // add the new row to the summary row
 623        if ($this->maximumAllowedRows > 0
 624            && $this->getRowsCount() >= $this->maximumAllowedRows - 1
 625        ) {
 626            if ($this->summaryRow === null) // create the summary row if necessary
 627            {
 628                $columns = array('label' => self::LABEL_SUMMARY_ROW) + $row->getColumns();
 629                $this->addSummaryRow(new Row(array(Row::COLUMNS => $columns)));
 630            } else {
 631                $this->summaryRow->sumRow(
 632                    $row, $enableCopyMetadata = false, $this->getMetadata(self::COLUMN_AGGREGATION_OPS_METADATA_NAME));
 633            }
 634            return $this->summaryRow;
 635        }
 636
 637        $this->rows[] = $row;
 638        if (!$this->indexNotUpToDate
 639            && $this->rebuildIndexContinuously
 640        ) {
 641            $label = $row->getColumn('label');
 642            if ($label !== false) {
 643                $this->rowsIndexByLabel[$label] = count($this->rows) - 1;
 644            }
 645        }
 646        return $row;
 647    }
 648
 649    /**
 650     * Sets the summary row.
 651     * 
 652     * _Note: A DataTable can have only one summary row._
 653     *
 654     * @param Row $row
 655     * @return Row Returns `$row`.
 656     */
 657    public function addSummaryRow(Row $row)
 658    {
 659        $this->summaryRow = $row;
 660
 661        // add summary row to index
 662        if (!$this->indexNotUpToDate
 663            && $this->rebuildIndexContinuously
 664        ) {
 665            $label = $row->getColumn('label');
 666            if ($label !== false) {
 667                $this->rowsIndexByLabel[$label] = self::ID_SUMMARY_ROW;
 668            }
 669        }
 670
 671        return $row;
 672    }
 673
 674    /**
 675     * Returns the DataTable ID.
 676     *
 677     * @return int
 678     */
 679    public function getId()
 680    {
 681        return $this->currentId;
 682    }
 683
 684    /**
 685     * Adds a new row from an array.
 686     * 
 687     * You can add row metadata with this method.
 688     *
 689     * @param array $row eg. `array(Row::COLUMNS => array('visits' => 13, 'test' => 'toto'),
 690     *                              Row::METADATA => array('mymetadata' => 'myvalue'))`
 691     */
 692    public function addRowFromArray($row)
 693    {
 694        $this->addRowsFromArray(array($row));
 695    }
 696
 697    /**
 698     * Adds a new row a from an array of column values.
 699     * 
 700     * Row metadata cannot be added with this method.
 701     *
 702     * @param array $row eg. `array('name' => 'google analytics', 'license' => 'commercial')`
 703     */
 704    public function addRowFromSimpleArray($row)
 705    {
 706        $this->addRowsFromSimpleArray(array($row));
 707    }
 708
 709    /**
 710     * Returns the array of Rows.
 711     *
 712     * @return Row[]
 713     */
 714    public function getRows()
 715    {
 716        if (is_null($this->summaryRow)) {
 717            return $this->rows;
 718        } else {
 719            return $this->rows + array(self::ID_SUMMARY_ROW => $this->summaryRow);
 720        }
 721    }
 722
 723    /**
 724     * Returns an array containing all column values for the requested column.
 725     *
 726     * @param string $name The column name.
 727     * @return array The array of column values.
 728     */
 729    public function getColumn($name)
 730    {
 731        $columnValues = array();
 732        foreach ($this->getRows() as $row) {
 733            $columnValues[] = $row->getColumn($name);
 734        }
 735        return $columnValues;
 736    }
 737
 738    /**
 739     * Returns an array containing all column values of columns whose name starts with `$name`.
 740     *
 741     * @param $namePrefix The column name prefix.
 742     * @return array The array of column values.
 743     */
 744    public function getColumnsStartingWith($namePrefix)
 745    {
 746        $columnValues = array();
 747        foreach ($this->getRows() as $row) {
 748            $columns = $row->getColumns();
 749            foreach ($columns as $column => $value) {
 750                if (strpos($column, $namePrefix) === 0) {
 751                    $columnValues[] = $row->getColumn($column);
 752                }
 753            }
 754        }
 755        return $columnValues;
 756    }
 757
 758    /**
 759     * Returns the names of every column this DataTable contains. This method will return the
 760     * columns of the first row with data and will assume they occur in every other row as well.
 761     * 
 762     *_ Note: If column names still use their in-database INDEX values (@see Metrics), they
 763     *        will be converted to their string name in the array result._
 764     * 
 765     * @return array Array of string column names.
 766     */
 767    public function getColumns()
 768    {
 769        $result = array();
 770        foreach ($this->getRows() as $row) {
 771            $columns = $row->getColumns();
 772            if (!empty($columns)) {
 773                $result = array_keys($columns);
 774                break;
 775            }
 776        }
 777
 778        // make sure column names are not DB index values
 779        foreach ($result as &$column) {
 780            if (isset(Metrics::$mappingFromIdToName[$column])) {
 781                $column = Metrics::$mappingFromIdToName[$column];
 782            }
 783        }
 784
 785        return $result;
 786    }
 787
 788    /**
 789     * Returns an array containing the requested metadata value of each row.
 790     * 
 791     * @param string $name The metadata column to return.
 792     * @return array
 793     */
 794    public function getRowsMetadata($name)
 795    {
 796        $metadataValues = array();
 797        foreach ($this->getRows() as $row) {
 798            $metadataValues[] = $row->getMetadata($name);
 799        }
 800        return $metadataValues;
 801    }
 802
 803    /**
 804     * Returns the number of rows in the table including the summary row.
 805     * 
 806     * @return int
 807     */
 808    public function getRowsCount()
 809    {
 810        if (is_null($this->summaryRow)) {
 811            return count($this->rows);
 812        } else {
 813            return count($this->rows) + 1;
 814        }
 815    }
 816
 817    /**
 818     * Returns the first row of the DataTable.
 819     *
 820     * @return Row|false The first row or `false` if it cannot be found.
 821     */
 822    public function getFirstRow()
 823    {
 824        if (count($this->rows) == 0) {
 825            if (!is_null($this->summaryRow)) {
 826                return $this->summaryRow;
 827            }
 828            return false;
 829        }
 830        return reset($this->rows);
 831    }
 832
 833    /**
 834     * Returns the last row of the DataTable. If there is a summary row, it
 835     * will always be considered the last row.
 836     *
 837     * @return Row|false The last row or `false` if it cannot be found.
 838     */
 839    public function getLastRow()
 840    {
 841        if (!is_null($this->summaryRow)) {
 842            return $this->summaryRow;
 843        }
 844
 845        if (count($this->rows) == 0) {
 846            return false;
 847        }
 848
 849        return end($this->rows);
 850    }
 851
 852    /**
 853     * Returns the number of rows in the entire DataTable hierarchy. This is the number of rows in this DataTable
 854     * summed with the row count of each descendant subtable.
 855     *
 856     * @return int
 857     */
 858    public function getRowsCountRecursive()
 859    {
 860        $totalCount = 0;
 861        foreach ($this->rows as $row) {
 862            if (($idSubTable = $row->getIdSubDataTable()) !== null) {
 863                $subTable = Manager::getInstance()->getTable($idSubTable);
 864                $count = $subTable->getRowsCountRecursive();
 865                $totalCount += $count;
 866            }
 867        }
 868
 869        $totalCount += $this->getRowsCount();
 870        return $totalCount;
 871    }
 872
 873    /**
 874     * Delete a column by name in every row. This change is NOT applied recursively to all
 875     * subtables.
 876     *
 877     * @param string $name Column name to delete.
 878     */
 879    public function deleteColumn($name)
 880    {
 881        $this->deleteColumns(array($name));
 882    }
 883
 884    public function __sleep()
 885    {
 886        return array('rows', 'summaryRow');
 887    }
 888
 889    /**
 890     * Rename a column in every row. This change is applied recursively to all subtables.
 891     *
 892     * @param string $oldName Old column name.
 893     * @param string $newName New column name.
 894     */
 895    public function renameColumn($oldName, $newName, $doRenameColumnsOfSubTables = true)
 896    {
 897        foreach ($this->getRows() as $row) {
 898            $row->renameColumn($oldName, $newName);
 899
 900            if($doRenameColumnsOfSubTables) {
 901                if (($idSubDataTable = $row->getIdSubDataTable()) !== null) {
 902                    Manager::getInstance()->getTable($idSubDataTable)->renameColumn($oldName, $newName);
 903                }
 904            }
 905        }
 906        if (!is_null($this->summaryRow)) {
 907            $this->summaryRow->renameColumn($oldName, $newName);
 908        }
 909    }
 910
 911    /**
 912     * Deletes several columns by name in every row.
 913     *
 914     * @param array $names List of column names to delete.
 915     * @param bool $deleteRecursiveInSubtables Whether to apply this change to all subtables or not.
 916     */
 917    public function deleteColumns($names, $deleteRecursiveInSubtables = false)
 918    {
 919        foreach ($this->getRows() as $row) {
 920            foreach ($names as $name) {
 921                $row->deleteColumn($name);
 922            }
 923            if (($idSubDataTable = $row->getIdSubDataTable()) !== null) {
 924                Manager::getInstance()->getTable($idSubDataTable)->deleteColumns($names, $deleteRecursiveInSubtables);
 925            }
 926        }
 927        if (!is_null($this->summaryRow)) {
 928            foreach ($names as $name) {
 929                $this->summaryRow->deleteColumn($name);
 930            }
 931        }
 932    }
 933
 934    /**
 935     * Deletes a row by ID.
 936     *
 937     * @param int $id The row ID.
 938     * @throws Exception If the row `$id` cannot be found.
 939     */
 940    public function deleteRow($id)
 941    {
 942        if ($id === self::ID_SUMMARY_ROW) {
 943            $this->summaryRow = null;
 944            return;
 945        }
 946        if (!isset($this->rows[$id])) {
 947            throw new Exception("Trying to delete unknown row with idkey = $id");
 948        }
 949        unset($this->rows[$id]);
 950    }
 951
 952    /**
 953     * Deletes rows from `$offset` to `$offset + $limit`.
 954     *
 955     * @param int $offset The offset to start deleting rows from.
 956     * @param int|null $limit The number of rows to delete. If `null` all rows after the offset
 957     *                        will be removed.
 958     * @return int The number of rows deleted.
 959     */
 960    public function deleteRowsOffset($offset, $limit = null)
 961    {
 962        if ($limit === 0) {
 963            return 0;
 964        }
 965
 966        $count = $this->getRowsCount();
 967        if ($offset >= $count) {
 968            return 0;
 969        }
 970
 971        // if we delete until the end, we delete the summary row as well
 972        if (is_null($limit)
 973            || $limit >= $count
 974        ) {
 975            $this->summaryRow = null;
 976        }
 977
 978        if (is_null($limit)) {
 979            $spliced = array_splice($this->rows, $offset);
 980        } else {
 981            $spliced = array_splice($this->rows, $offset, $limit);
 982        }
 983        $countDeleted = count($spliced);
 984        return $countDeleted;
 985    }
 986
 987    /**
 988     * Deletes a set of rows by ID.
 989     *
 990     * @param array $rowIds The list of row IDs to delete.
 991     * @throws Exception If a row ID cannot be found.
 992     */
 993    public function deleteRows(array $rowIds)
 994    {
 995        foreach ($rowIds as $key) {
 996            $this->deleteRow($key);
 997        }
 998    }
 999
1000    /**
1001     * Returns a string representation of this DataTable for convenient viewing.
1002     * 
1003     * _Note: This uses the **html** DataTable renderer._
1004     *
1005     * @return string
1006     */
1007    public function __toString()
1008    {
1009        $renderer = new Html();
1010        $renderer->setTable($this);
1011        return (string)$renderer;
1012    }
1013
1014    /**
1015     * Returns true if both DataTable instances are exactly the same.
1016     *
1017     * DataTables are equal if they have the same number of rows, if
1018     * each row has a label that exists in the other table, and if each row
1019     * is equal to the row in the other table with the same label. The order
1020     * of rows is not important.
1021     * 
1022     * @param \Piwik\DataTable $table1
1023     * @param \Piwik\DataTable $table2
1024     * @return bool
1025     */
1026    public static function isEqual(DataTable $table1, DataTable $table2)
1027    {
1028        $rows1 = $table1->getRows();
1029        $rows2 = $table2->getRows();
1030
1031        $table1->rebuildIndex();
1032        $table2->rebuildIndex();
1033
1034        if ($table1->getRowsCount() != $table2->getRowsCount()) {
1035            return false;
1036        }
1037
1038        foreach ($rows1 as $row1) {
1039            $row2 = $table2->getRowFromLabel($row1->getColumn('label'));
1040            if ($row2 === false
1041                || !Row::isEqual($row1, $row2)
1042            ) {
1043                return false;
1044            }
1045        }
1046
1047        return true;
1048    }
1049
1050    /**
1051     * Serializes an entire DataTable hierarchy and returns the array of serialized DataTables.
1052     * 
1053     * The first element in the returned array will be the serialized representation of this DataTable.
1054     * Every subsequent element will be a serialized subtable.
1055     * 
1056     * This DataTable and subtables can optionally be truncated before being serialized. In most
1057     * cases where DataTables can become quite large, they should be truncated before being persisted
1058     * in an archive.
1059     *
1060     * The result of this method is intended for use with the {@link ArchiveProcessor::insertBlobRecord()} method.
1061     *
1062     * @throws Exception If infinite recursion detected. This will occur if a table's subtable is one of its parent tables.
1063     * @param int $maximumRowsInDataTable If not null, defines the maximum number of rows allowed in the serialized DataTable.
1064     * @param int $maximumRowsInSubDataTable If not null, defines the maximum number of rows allowed in serialized subtables.
1065     * @param string $columnToSortByBeforeTruncation The column to sort by before truncating, eg, `Metrics::INDEX_NB_VISITS`.
1066     * @return array The array of serialized DataTables:
1067     * 
1068     *                   array(
1069     *                       // this DataTable (the root)
1070     *                       0 => 'eghuighahgaueytae78yaet7yaetae',
1071     *
1072     *                       // a subtable
1073     *                       1 => 'gaegae gh gwrh guiwh uigwhuige',
1074     *
1075     *                       // another subtable
1076     *                       2 => 'gqegJHUIGHEQjkgneqjgnqeugUGEQHGUHQE',
1077     *                    
1078     *                       // etc.
1079     *                   );
1080     */
1081    public function getSerialized($maximumRowsInDataTable = null,
1082                                  $maximumRowsInSubDataTable = null,
1083                                  $columnToSortByBeforeTruncation = null)
1084    {
1085        static $depth = 0;
1086
1087        if ($depth > self::$maximumDepthLevelAllowed) {
1088            $depth = 0;
1089            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?");
1090        }
1091        if (!is_null($maximumRowsInDataTable)) {
1092            $this->filter('Truncate',
1093                array($maximumRowsInDataTable - 1,
1094                      DataTable::LABEL_SUMMARY_ROW,
1095                      $columnToSortByBeforeTruncation,
1096                      $filterRecursive = false)
1097            );
1098        }
1099
1100        // For each row, get the serialized row
1101        // If it is associated to a sub table, get the serialized table recursively ;
1102        // but returns all serialized tables and subtable in an array of 1 dimension
1103        $aSerializedDataTable = array();
1104        foreach ($this->rows as $row) {
1105            if (($idSubTable = $row->getIdSubDataTable()) !== null) {
1106                $subTable = null;
1107                try {
1108                    $subTable = Manager::getInstance()->getTable($idSubTable);
1109                } catch(TableNotFoundException $e) {
1110                    // This occurs is an unknown & random data issue. Catch Exception and remove subtable from the row.
1111                    $row->removeSubtable();
1112                    // Go to next row
1113                    continue;
1114                }
1115
1116                $depth++;
1117                $aSerializedDataTable = $aSerializedDataTable + $subTable->getSerialized($maximumRowsInSubDataTable, $maximumRowsInSubDataTable, $columnToSortByBeforeTruncation);
1118                $depth--;
1119            }
1120        }
1121        // we load the current Id of the DataTable
1122        $forcedId = $this->getId();
1123
1124        // if the datatable is the parent we force the Id at 0 (this is part of the specification)
1125        if ($depth == 0) {
1126            $forcedId = 0;
1127        }
1128
1129        // we then serialize the rows and store them in the serialized dataTable
1130        $addToRows = array(self::ID_SUMMARY_ROW => $this->summaryRow);
1131
1132        $aSerializedDataTable[$forcedId] = serialize($this->rows + $addToRows);
1133        foreach ($this->rows as &$row) {
1134            $row->cleanPostSerialize();
1135        }
1136
1137        return $aSerializedDataTable;
1138    }
1139
1140    /**
1141     * Adds a set of rows from a serialized DataTable string.
1142     *
1143     * See {@link serialize()}.
1144     * 
1145     * _Note: This function will successfully load DataTables serialized by Piwik 1.X._
1146     * 
1147     * @param string $stringSerialized A string with the format of a string in the array returned by
1148     *                                 {@link serialize()}.
1149     * @throws Exception if `$stringSerialized` is invalid.
1150     */
1151    public function addRowsFromSerializedArray($stringSerialized)
1152    {
1153        require_once PIWIK_INCLUDE_PATH . "/core/DataTable/Bridges.php";
1154        
1155        $serialized = unserialize($stringSerialized);
1156        if ($serialized === false) {
1157            throw new Exception("The unserialization has failed!");
1158        }
1159        $this->addRowsFromArray($serialized);
1160    }
1161
1162    /**
1163     * Adds multiple rows from an array.
1164     * 
1165     * You can add row metadata with this method.
1166     *
1167     * @param array $array Array with the following structure
1168     * 
1169     *                         array(
1170     *                             // row1
1171     *                             array(
1172     *                                 Row::COLUMNS => array( col1_name => value1, col2_name => value2, ...),
1173     *                                 Row::METADATA => array( metadata1_name => value1,  ...), // see Row
1174     *                             ),
1175     *                             // row2
1176     *                             array( ... ),
1177     *                         )
1178     */
1179    public function addRowsFromArray($array)
1180    {
1181        foreach ($array as $id => $row) {
1182            if (is_array($row)) {
1183                $row = new Row($row);
1184            }
1185            if ($id == self::ID_SUMMARY_ROW) {
1186                $this->summaryRow = $row;
1187            } else {
1188                $this->addRow($row);
1189            }
1190        }
1191    }
1192
1193    /**
1194     * Adds multiple rows from an array containing arrays of column values.
1195     * 
1196     * Row metadata cannot be added with this method.
1197     *
1198     * @param array $array Array with the following structure:
1199     * 
1200     *                       array(
1201     *                             array( col1_name => valueA, col2_name => valueC, ...),
1202     *                             array( col1_name => valueB, col2_name => valueD, ...),
1203     *                       )
1204     * @throws Exception if `$array` is in an incorrect format.
1205     */
1206    public function addRowsFromSimpleArray($array)
1207    {
1208        if (count($array) === 0) {
1209            return;
1210        }
1211
1212        // we define an exception we may throw if at one point we notice that we cannot handle the data structure
1213        $e = new Exception(" Data structure returned is not convertible in the requested format." .
1214            " Try to call this method with the parameters '&format=original&serialize=1'" .
1215            "; you will get the original php data structure serialized." .
1216            " The data structure looks like this: \n \$data = " . var_export($array, true) . "; ");
1217
1218        // first pass to see if the array has the structure
1219        // array(col1_name => val1, col2_name => val2, etc.)
1220        // with val* that are never arrays (only strings/numbers/bool/etc.)
1221        // if we detect such a "simple" data structure we convert it to a row with the correct columns' names
1222        $thisIsNotThatSimple = false;
1223
1224        foreach ($array as $columnValue) {
1225            if (is_array($columnValue) || is_object($columnValue)) {
1226                $thisIsNotThatSimple = true;
1227                break;
1228            }
1229        }
1230        if ($thisIsNotThatSimple === false) {
1231            // case when the array is indexed by the default numeric index
1232            if (array_keys($array) == array_keys(array_fill(0, count($array), true))) {
1233                foreach ($array as $row) {
1234                    $this->addRow(new Row(array(Row::COLUMNS => array($row))));
1235                }
1236            } else {
1237                $this->addRow(new Row(array(Row::COLUMNS => $array)));
1238            }
1239            // we have converted our simple array to one single row
1240            // => we exit the method as the job is now finished
1241            return;
1242        }
1243
1244        foreach ($array as $key => $row) {
1245            // stuff that looks like a line
1246            if (is_array($row)) {
1247                /**
1248                 * We make sure we can convert this PHP array without losing information.
1249                 * We are able to convert only simple php array (no strings keys, no sub arrays, etc.)
1250                 *
1251                 */
1252
1253                // if the key is a string it means that some information was contained in this key.
1254                // it cannot be lost during the conversion. Because we are not able to handle properly
1255                // this key, we throw an explicit exception.
1256                if (is_string($key)) {
1257                    throw $e;
1258                }
1259                // if any of the sub elements of row is an array we cannot handle this data structure...
1260                foreach ($row as $subRow) {
1261                    if (is_array($subRow)) {
1262                        throw $e;
1263                    }
1264                }
1265                $row = new Row(array(Row::COLUMNS => $row));
1266            } // other (string, numbers...) => we build a line from this value
1267            else {
1268                $row = new Row(array(Row::COLUMNS => array($key => $row)));
1269            }
1270            $this->addRow($row);
1271        }
1272    }
1273
1274    /**
1275     * Rewrites the input `$array`
1276     * 
1277     *     array (
1278     *         LABEL => array(col1 => X, col2 => Y),
1279     *         LABEL2 => array(col1 => X, col2 => Y),
1280     *     )
1281     * 
1282     * to a DataTable with rows that look like:
1283     * 
1284     *     array (
1285     *         array( Row::COLUMNS => array('label' => LABEL, col1 => X, col2 => Y)),
1286     *         array( Row::COLUMNS => array('label' => LABEL2, col1 => X, col2 => Y)),
1287     *     )
1288     *
1289     * Will also convert arrays like: 
1290     * 
1291     *     array (
1292     *         LABEL => X,
1293     *         LABEL2 => Y,
1294     *     )
1295     * 
1296     * to:
1297     * 
1298     *     array (
1299     *         array( Row::COLUMNS => array('label' => LABEL, 'value' => X)),
1300     *         array( Row::COLUMNS => array('label' => LABEL2, 'value' => Y)),
1301     *     )
1302     *
1303     * @param array $array Indexed array, two formats supported, see above.
1304     * @param array|null $subtablePerLabel An array mapping label values with DataTable instances to associate as a subtable.
1305     * @return \Piwik\DataTable
1306     */
1307    public static function makeFromIndexedArray($array, $subtablePerLabel = null)
1308    {
1309        $table = new DataTable();
1310        foreach ($array as $label => $row) {
1311            $cleanRow = array();
1312
1313            // Support the case of an $array of single values
1314            if (!is_array($row)) {
1315                $row = array('value' => $row);
1316            }
1317            // Put the 'label' column first
1318            $cleanRow[Row::COLUMNS] = array('label' => $label) + $row;
1319            // Assign subtable if specified
1320            if (isset($subtablePerLabel[$label])) {
1321                $cleanRow[Row::DATATABLE_ASSOCIATED] = $subtablePerLabel[$label];
1322            }
1323            $table->addRow(new Row($cleanRow));
1324        }
1325        return $table;
1326    }
1327
1328    /**
1329     * Sets the maximum depth level to at least a certain value. If the current value is
1330     * greater than `$atLeastLevel`, the maximum nesting level is not changed.
1331     * 
1332     * The maximum depth level determines the maximum number of subtable levels in the
1333     * DataTable tree. For example, if it is set to `2`, this DataTable is allowed to
1334     * have subtables, but the subtables are not.
1335     * 
1336     * @param int $atLeastLevel
1337     */
1338    public static function setMaximumDepthLevelAllowedAtLeast($atLeastLevel)
1339    {
1340        self::$maximumDepthLevelAllowed = max($atLeastLevel, self::$maximumDepthLevelAllowed);
1341        if (self::$maximumDepthLevelAllowed < 1) {
1342            self::$maximumDepthLevelAllowed = 1;
1343        }
1344    }
1345
1346    /**
1347     * Returns metadata by name.
1348     *
1349     * @param string $name The metadata name.
1350     * @return mixed|false The metadata value or `false` if it cannot be found.
1351     */
1352    public function getMetadata($name)
1353    {
1354        if (!isset($this->metadata[$name])) {
1355            return false;
1356        }
1357        return $this->metadata[$name];
1358    }
1359
1360    /**
1361     * Sets a metadata value by name.
1362     *
1363     * @param string $name The metadata name.
1364     * @param mixed $value
1365     */
1366    public function setMetadata($name, $value)
1367    {
1368        $this->metadata[$name] = $value;
1369    }
1370
1371    /**
1372     * Returns all table metadata.
1373     *
1374     * @return array
1375     */
1376    public function getAllTableMetadata()
1377    {
1378        return $this->metadata;
1379    }
1380
1381    /**
1382     * Sets several metadata values by name.
1383     * 
1384     * @param array $values Array mapping metadata names with metadata values.
1385     */
1386    public function setMetadataValues($values)
1387    {
1388        foreach ($values as $name => $value) {
1389            $this->metadata[$name] = $value;
1390        }
1391    }
1392
1393    /**
1394     * Sets metadata, erasing existing values.
1395     * 
1396     * @param array $values Array mapping metadata names with metadata values.
1397     */
1398    public function setAllTableMetadata($metadata)
1399    {
1400        $this->metadata = $metadata;
1401    }
1402
1403    /**
1404     * Sets the maximum number of rows allowed in this datatable (including the summary
1405     * row). If adding more then the allowed number of rows is attempted, the extra
1406     * rows are summed to the summary row.
1407     *
1408     * @param int $maximumAllowedRows If `0`, the maximum number of rows is unset.
1409     */
1410    public function setMaximumAllowedRows($maximumAllowedRows)
1411    {
1412        $this->maximumAllowedRows = $maximumAllowedRows;
1413    }
1414
1415    /**
1416     * Traverses a DataTable tree using an array of labels and returns the row
1417     * it finds or `false` if it cannot find one. The number of path segments that
1418     * were successfully walked is also returned.
1419     * 
1420     * If `$missingRowColumns` is supplied, the specified path is created. When
1421     * a subtable is encountered w/o the required label, a new row is created
1422     * with the label, and a new subtable is added to the row.
1423     *
1424     * Read [http://en.wikipedia.org/wiki/Tree_(data_structure)#Traversal_methods](http://en.wikipedia.org/wiki/Tree_(data_structure)#Traversal_methods)
1425     * for more information about tree walking.
1426     * 
1427     * @param array $path The path to walk. An array of label values. The first element
1428     *                    refers to a row in this DataTable, the second in a subtable of
1429     *                    the first row, the third a subtable of the second row, etc.
1430     * @param array|bool $missingRowColumns The default columns to use when creating new rows.
1431     *                                      If this parameter is supplied, new rows will be
1432     *                                      created for path labels that cannot be found.
1433     * @param int $maxSubtableRows The maximum number of allowed rows in new subtables. New
1434     *                             subtables are only created if `$missingRowColumns` is provided.
1435     * @return array First element is the found row or `false`. Second element is
1436     *               the number of path segments walked. If a row is found, this
1437     *               will be == to `count($path)`. Otherwise, it will be the index
1438     *               of the path segment that we could not find.
1439     */
1440    public function walkPath($path, $missingRowColumns = false, $maxSubtableRows = 0)
1441    {
1442        $pathLength = count($path);
1443
1444        $table = $this;
1445        $next = false;
1446        for ($i = 0; $i < $pathLength; ++$i) {
1447            $segment = $path[$i];
1448
1449            $next = $table->getRowFromLabel($segment);
1450            if ($next === false) {
1451                // if there is no table to advance to, and we're not adding missing rows, return false
1452                if ($missingRowColumns === false) {
1453          

Large files files are truncated, but you can click here to view the full file