1<?php 2/** 3 * Matomo - free/libre analytics platform 4 * 5 * @link https://matomo.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\Archive\DataTableFactory; 15use Piwik\DataTable\DataTableInterface; 16use Piwik\DataTable\Manager; 17use Piwik\DataTable\Renderer\Html; 18use Piwik\DataTable\Row; 19use Piwik\DataTable\Row\DataTableSummaryRow; 20use Piwik\DataTable\Simple; 21use ReflectionClass; 22 23/** 24 * @see Common::destroy() 25 */ 26require_once PIWIK_INCLUDE_PATH . '/core/Common.php'; 27require_once PIWIK_INCLUDE_PATH . "/core/DataTable/Bridges.php"; 28 29/** 30 * The primary data structure used to store analytics data in Piwik. 31 * 32 * <a name="class-desc-the-basics"></a> 33 * ### The Basics 34 * 35 * DataTables consist of rows and each row consists of columns. A column value can be 36 * a numeric, a string or an array. 37 * 38 * Every row has an ID. The ID is either the index of the row or {@link ID_SUMMARY_ROW}. 39 * 40 * DataTables are hierarchical data structures. Each row can also contain an additional 41 * nested sub-DataTable (commonly referred to as a 'subtable'). 42 * 43 * Both DataTables and DataTable rows can hold **metadata**. _DataTable metadata_ is information 44 * regarding all the data, such as the site or period that the data is for. _Row metadata_ 45 * is information regarding that row, such as a browser logo or website URL. 46 * 47 * Finally, all DataTables contain a special _summary_ row. This row, if it exists, is 48 * always at the end of the DataTable. 49 * 50 * ### Populating DataTables 51 * 52 * Data can be added to DataTables in three different ways. You can either: 53 * 54 * 1. create rows one by one and add them through {@link addRow()} then truncate if desired, 55 * 2. create an array of DataTable\Row instances or an array of arrays and add them using 56 * {@link addRowsFromArray()} or {@link addRowsFromSimpleArray()} 57 * then truncate if desired, 58 * 3. or set the maximum number of allowed rows (with {@link setMaximumAllowedRows()}) 59 * and add rows one by one. 60 * 61 * If you want to eventually truncate your data (standard practice for all Piwik plugins), 62 * the third method is the most memory efficient. It is, unfortunately, not always possible 63 * to use since it requires that the data be sorted before adding. 64 * 65 * ### Manipulating DataTables 66 * 67 * There are two ways to manipulate a DataTable. You can either: 68 * 69 * 1. manually iterate through each row and manipulate the data, 70 * 2. or you can use predefined filters. 71 * 72 * A filter is a class that has a 'filter' method which will manipulate a DataTable in 73 * some way. There are several predefined Filters that allow you to do common things, 74 * such as, 75 * 76 * - add a new column to each row, 77 * - add new metadata to each row, 78 * - modify an existing column value for each row, 79 * - sort an entire DataTable, 80 * - and more. 81 * 82 * Using these filters instead of writing your own code will increase code clarity and 83 * reduce code redundancy. Additionally, filters have the advantage that they can be 84 * applied to DataTable\Map instances. So you can visit every DataTable in a {@link DataTable\Map} 85 * without having to write a recursive visiting function. 86 * 87 * All predefined filters exist in the **Piwik\DataTable\BaseFilter** namespace. 88 * 89 * _Note: For convenience, [anonymous functions](http://www.php.net/manual/en/functions.anonymous.php) 90 * can be used as DataTable filters._ 91 * 92 * ### Applying Filters 93 * 94 * Filters can be applied now (via {@link filter()}), or they can be applied later (via 95 * {@link queueFilter()}). 96 * 97 * Filters that sort rows or manipulate the number of rows should be applied right away. 98 * Non-essential, presentation filters should be queued. 99 * 100 * ### Learn more 101 * 102 * - See **{@link ArchiveProcessor}** to learn how DataTables are persisted. 103 * 104 * ### Examples 105 * 106 * **Populating a DataTable** 107 * 108 * // adding one row at a time 109 * $dataTable = new DataTable(); 110 * $dataTable->addRow(new Row(array( 111 * Row::COLUMNS => array('label' => 'thing1', 'nb_visits' => 1, 'nb_actions' => 1), 112 * Row::METADATA => array('url' => 'http://thing1.com') 113 * ))); 114 * $dataTable->addRow(new Row(array( 115 * Row::COLUMNS => array('label' => 'thing2', 'nb_visits' => 2, 'nb_actions' => 2), 116 * Row::METADATA => array('url' => 'http://thing2.com') 117 * ))); 118 * 119 * // using an array of rows 120 * $dataTable = new DataTable(); 121 * $dataTable->addRowsFromArray(array( 122 * array( 123 * Row::COLUMNS => array('label' => 'thing1', 'nb_visits' => 1, 'nb_actions' => 1), 124 * Row::METADATA => array('url' => 'http://thing1.com') 125 * ), 126 * array( 127 * Row::COLUMNS => array('label' => 'thing2', 'nb_visits' => 2, 'nb_actions' => 2), 128 * Row::METADATA => array('url' => 'http://thing2.com') 129 * ) 130 * )); 131 * 132 * // using a "simple" array 133 * $dataTable->addRowsFromSimpleArray(array( 134 * array('label' => 'thing1', 'nb_visits' => 1, 'nb_actions' => 1), 135 * array('label' => 'thing2', 'nb_visits' => 2, 'nb_actions' => 2) 136 * )); 137 * 138 * **Getting & setting metadata** 139 * 140 * $dataTable = \Piwik\Plugins\Referrers\API::getInstance()->getSearchEngines($idSite = 1, $period = 'day', $date = '2007-07-24'); 141 * $oldPeriod = $dataTable->metadata['period']; 142 * $dataTable->metadata['period'] = Period\Factory::build('week', Date::factory('2013-10-18')); 143 * 144 * **Serializing & unserializing** 145 * 146 * $maxRowsInTable = Config::getInstance()->General['datatable_archiving_maximum_rows_standard'];j 147 * 148 * $dataTable = // ... build by aggregating visits ... 149 * $serializedData = $dataTable->getSerialized($maxRowsInTable, $maxRowsInSubtable = $maxRowsInTable, 150 * $columnToSortBy = Metrics::INDEX_NB_VISITS); 151 * 152 * $serializedDataTable = $serializedData[0]; 153 * $serailizedSubTable = $serializedData[$idSubtable]; 154 * 155 * **Filtering for an API method** 156 * 157 * public function getMyReport($idSite, $period, $date, $segment = false, $expanded = false) 158 * { 159 * $dataTable = Archive::createDataTableFromArchive('MyPlugin_MyReport', $idSite, $period, $date, $segment, $expanded); 160 * $dataTable->filter('Sort', array(Metrics::INDEX_NB_VISITS, 'desc', $naturalSort = false, $expanded)); 161 * $dataTable->queueFilter('ColumnCallbackAddMetadata', array('label', 'url', __NAMESPACE__ . '\getUrlFromLabelForMyReport')); 162 * return $dataTable; 163 * } 164 * 165 * 166 * @api 167 */ 168class DataTable implements DataTableInterface, \IteratorAggregate, \ArrayAccess 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 = 'ts_archived'; 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 /** 197 * Name for metadata that stores array of generic filters that should not be run on the table. 198 */ 199 const GENERIC_FILTERS_TO_DISABLE_METADATA_NAME = 'generic_filters_to_disable'; 200 201 /** The ID of the Summary Row. */ 202 const ID_SUMMARY_ROW = -1; 203 204 /** 205 * The ID of the special metadata row. This row only exists in the serialized row data and stores the datatable metadata. 206 * 207 * This allows us to save datatable metadata in archive data. 208 */ 209 const ID_ARCHIVED_METADATA_ROW = -3; 210 211 /** The original label of the Summary Row. */ 212 const LABEL_SUMMARY_ROW = -1; 213 const LABEL_TOTALS_ROW = -2; 214 const LABEL_ARCHIVED_METADATA_ROW = '__datatable_metadata__'; 215 216 /** 217 * Name for metadata that contains extra {@link Piwik\Plugin\ProcessedMetric}s for a DataTable. 218 * These metrics will be added in addition to the ones specified in the table's associated 219 * {@link Piwik\Plugin\Report} class. 220 */ 221 const EXTRA_PROCESSED_METRICS_METADATA_NAME = 'extra_processed_metrics'; 222 223 /** 224 * Maximum nesting level. 225 */ 226 private static $maximumDepthLevelAllowed = self::MAX_DEPTH_DEFAULT; 227 228 /** 229 * Array of Row 230 * 231 * @var Row[] 232 */ 233 protected $rows = array(); 234 235 /** 236 * Id assigned to the DataTable, used to lookup the table using the DataTable_Manager 237 * 238 * @var int 239 */ 240 protected $currentId; 241 242 /** 243 * Current depth level of this data table 244 * 0 is the parent data table 245 * 246 * @var int 247 */ 248 protected $depthLevel = 0; 249 250 /** 251 * This flag is set to false once we modify the table in a way that outdates the index 252 * 253 * @var bool 254 */ 255 protected $indexNotUpToDate = true; 256 257 /** 258 * This flag sets the index to be rebuild whenever a new row is added, 259 * as opposed to re-building the full index when getRowFromLabel is called. 260 * This is to optimize and not rebuild the full Index in the case where we 261 * add row, getRowFromLabel, addRow, getRowFromLabel thousands of times. 262 * 263 * @var bool 264 */ 265 protected $rebuildIndexContinuously = false; 266 267 /** 268 * Column name of last time the table was sorted 269 * 270 * @var string 271 */ 272 protected $tableSortedBy = false; 273 274 /** 275 * List of BaseFilter queued to this table 276 * 277 * @var array 278 */ 279 protected $queuedFilters = array(); 280 281 /** 282 * List of disabled filter names eg 'Limit' or 'Sort' 283 * 284 * @var array 285 */ 286 protected $disabledFilters = array(); 287 288 /** 289 * We keep track of the number of rows before applying the LIMIT filter that deletes some rows 290 * 291 * @var int 292 */ 293 protected $rowsCountBeforeLimitFilter = 0; 294 295 /** 296 * Defaults to false for performance reasons (most of the time we don't need recursive sorting so we save a looping over the dataTable) 297 * 298 * @var bool 299 */ 300 protected $enableRecursiveSort = false; 301 302 /** 303 * When the table and all subtables are loaded, this flag will be set to true to ensure filters are applied to all subtables 304 * 305 * @var bool 306 */ 307 protected $enableRecursiveFilters = false; 308 309 /** 310 * @var array 311 */ 312 protected $rowsIndexByLabel = array(); 313 314 /** 315 * @var \Piwik\DataTable\Row 316 */ 317 protected $summaryRow = null; 318 319 /** 320 * @var \Piwik\DataTable\Row 321 */ 322 protected $totalsRow = null; 323 324 /** 325 * Table metadata. Read [this](#class-desc-the-basics) to learn more. 326 * 327 * Any data that describes the data held in the table's rows should go here. 328 * 329 * Note: this field is protected so derived classes will serialize it. 330 * 331 * @var array 332 */ 333 protected $metadata = array(); 334 335 /** 336 * Maximum number of rows allowed in this datatable (including the summary row). 337 * If adding more rows is attempted, the extra rows get summed to the summary row. 338 * 339 * @var int 340 */ 341 protected $maximumAllowedRows = 0; 342 343 /** 344 * Constructor. Creates an empty DataTable. 345 */ 346 public function __construct() 347 { 348 // registers this instance to the manager 349 $this->currentId = Manager::getInstance()->addTable($this); 350 } 351 352 /** 353 * Destructor. Makes sure DataTable memory will be cleaned up. 354 */ 355 public function __destruct() 356 { 357 static $depth = 0; 358 // destruct can be called several times 359 if ($depth < self::$maximumDepthLevelAllowed 360 && isset($this->rows) 361 ) { 362 $depth++; 363 foreach ($this->rows as $row) { 364 Common::destroy($row); 365 } 366 if (isset($this->summaryRow)) { 367 Common::destroy($this->summaryRow); 368 } 369 unset($this->rows); 370 Manager::getInstance()->setTableDeleted($this->currentId); 371 $depth--; 372 } 373 } 374 375 /** 376 * Clone. Called when cloning the datatable. We need to make sure to create a new datatableId. 377 * If we do not increase tableId it can result in segmentation faults when destructing a datatable. 378 */ 379 public function __clone() 380 { 381 // registers this instance to the manager 382 $this->currentId = Manager::getInstance()->addTable($this); 383 } 384 385 public function setLabelsHaveChanged() 386 { 387 $this->indexNotUpToDate = true; 388 } 389 390 /** 391 * @ignore 392 * does not update the summary row! 393 */ 394 public function setRows($rows) 395 { 396 unset($this->rows); 397 $this->rows = $rows; 398 $this->indexNotUpToDate = true; 399 } 400 401 /** 402 * Sorts the DataTable rows using the supplied callback function. 403 * 404 * @param string $functionCallback A comparison callback compatible with {@link usort}. 405 * @param string $columnSortedBy The column name `$functionCallback` sorts by. This is stored 406 * so we can determine how the DataTable was sorted in the future. 407 */ 408 public function sort($functionCallback, $columnSortedBy) 409 { 410 $this->setTableSortedBy($columnSortedBy); 411 412 usort($this->rows, $functionCallback); 413 414 if ($this->isSortRecursiveEnabled()) { 415 foreach ($this->getRowsWithoutSummaryRow() as $row) { 416 $subTable = $row->getSubtable(); 417 if ($subTable) { 418 $subTable->enableRecursiveSort(); 419 $subTable->sort($functionCallback, $columnSortedBy); 420 } 421 } 422 } 423 } 424 425 public function setTotalsRow(Row $totalsRow) 426 { 427 $this->totalsRow = $totalsRow; 428 } 429 430 public function getTotalsRow() 431 { 432 return $this->totalsRow; 433 } 434 435 public function getSummaryRow() 436 { 437 return $this->summaryRow; 438 } 439 440 /** 441 * Returns the name of the column this table was sorted by (if any). 442 * 443 * See {@link sort()}. 444 * 445 * @return false|string The sorted column name or false if none. 446 */ 447 public function getSortedByColumnName() 448 { 449 return $this->tableSortedBy; 450 } 451 452 /** 453 * Enables recursive sorting. If this method is called {@link sort()} will also sort all 454 * subtables. 455 */ 456 public function enableRecursiveSort() 457 { 458 $this->enableRecursiveSort = true; 459 } 460 461 /** 462 * @ignore 463 */ 464 public function isSortRecursiveEnabled() 465 { 466 return $this->enableRecursiveSort === true; 467 } 468 469 /** 470 * @ignore 471 */ 472 public function setTableSortedBy($column) 473 { 474 $this->indexNotUpToDate = true; 475 $this->tableSortedBy = $column; 476 } 477 478 /** 479 * Enables recursive filtering. If this method is called then the {@link filter()} method 480 * will apply filters to every subtable in addition to this instance. 481 */ 482 public function enableRecursiveFilters() 483 { 484 $this->enableRecursiveFilters = true; 485 } 486 487 /** 488 * @ignore 489 */ 490 public function disableRecursiveFilters() 491 { 492 $this->enableRecursiveFilters = false; 493 } 494 495 /** 496 * Applies a filter to this datatable. 497 * 498 * If {@link enableRecursiveFilters()} was called, the filter will be applied 499 * to all subtables as well. 500 * 501 * @param string|Closure $className Class name, eg. `"Sort"` or "Piwik\DataTable\Filters\Sort"`. If no 502 * namespace is supplied, `Piwik\DataTable\BaseFilter` is assumed. This parameter 503 * can also be a closure that takes a DataTable as its first parameter. 504 * @param array $parameters Array of extra parameters to pass to the filter. 505 */ 506 public function filter($className, $parameters = array()) 507 { 508 if ($className instanceof \Closure 509 || is_array($className) 510 ) { 511 array_unshift($parameters, $this); 512 call_user_func_array($className, $parameters); 513 return; 514 } 515 516 if (in_array($className, $this->disabledFilters)) { 517 return; 518 } 519 520 if (!class_exists($className, true)) { 521 $className = 'Piwik\DataTable\Filter\\' . $className; 522 } 523 $reflectionObj = new ReflectionClass($className); 524 525 // the first parameter of a filter is the DataTable 526 // we add the current datatable as the parameter 527 $parameters = array_merge(array($this), $parameters); 528 529 $filter = $reflectionObj->newInstanceArgs($parameters); 530 531 $filter->enableRecursive($this->enableRecursiveFilters); 532 533 $filter->filter($this); 534 } 535 536 /** 537 * Applies a filter to all subtables but not to this datatable. 538 * 539 * @param string|Closure $className Class name, eg. `"Sort"` or "Piwik\DataTable\Filters\Sort"`. If no 540 * namespace is supplied, `Piwik\DataTable\BaseFilter` is assumed. This parameter 541 * can also be a closure that takes a DataTable as its first parameter. 542 * @param array $parameters Array of extra parameters to pass to the filter. 543 */ 544 public function filterSubtables($className, $parameters = array()) 545 { 546 foreach ($this->getRowsWithoutSummaryRow() as $row) { 547 $subtable = $row->getSubtable(); 548 if ($subtable) { 549 $subtable->filter($className, $parameters); 550 $subtable->filterSubtables($className, $parameters); 551 } 552 } 553 } 554 555 /** 556 * Adds a filter and a list of parameters to the list of queued filters of all subtables. These filters will be 557 * executed when {@link applyQueuedFilters()} is called. 558 * 559 * Filters that prettify the column values or don't need the full set of rows should be queued. This 560 * way they will be run after the table is truncated which will result in better performance. 561 * 562 * @param string|Closure $className The class name of the filter, eg. `'Limit'`. 563 * @param array $parameters The parameters to give to the filter, eg. `array($offset, $limit)` for the Limit filter. 564 */ 565 public function queueFilterSubtables($className, $parameters = array()) 566 { 567 foreach ($this->getRowsWithoutSummaryRow() as $row) { 568 $subtable = $row->getSubtable(); 569 if ($subtable) { 570 $subtable->queueFilter($className, $parameters); 571 $subtable->queueFilterSubtables($className, $parameters); 572 } 573 } 574 } 575 576 /** 577 * Adds a filter and a list of parameters to the list of queued filters. These filters will be 578 * executed when {@link applyQueuedFilters()} is called. 579 * 580 * Filters that prettify the column values or don't need the full set of rows should be queued. This 581 * way they will be run after the table is truncated which will result in better performance. 582 * 583 * @param string|Closure $className The class name of the filter, eg. `'Limit'`. 584 * @param array $parameters The parameters to give to the filter, eg. `array($offset, $limit)` for the Limit filter. 585 */ 586 public function queueFilter($className, $parameters = array()) 587 { 588 if (!is_array($parameters)) { 589 $parameters = array($parameters); 590 } 591 $this->queuedFilters[] = array('className' => $className, 'parameters' => $parameters); 592 } 593 594 /** 595 * Disable a specific filter to run on this DataTable in case you have already applied this filter or if you will 596 * handle this filter manually by using a custom filter. Be aware if you disable a given filter, that filter won't 597 * be ever executed. Even if another filter calls this filter on the DataTable. 598 * 599 * @param string $className eg 'Limit' or 'Sort'. Passing a `Closure` or an `array($class, $methodName)` is not 600 * supported yet. We check for exact match. So if you disable 'Limit' and 601 * call `->filter('Limit')` this filter won't be executed. If you call 602 * `->filter('Piwik\DataTable\Filter\Limit')` that filter will be executed. See it as a 603 * feature. 604 * @ignore 605 */ 606 public function disableFilter($className) 607 { 608 $this->disabledFilters[] = $className; 609 } 610 611 /** 612 * Applies all filters that were previously queued to the table. See {@link queueFilter()} 613 * for more information. 614 */ 615 public function applyQueuedFilters() 616 { 617 foreach ($this->queuedFilters as $filter) { 618 $this->filter($filter['className'], $filter['parameters']); 619 } 620 $this->clearQueuedFilters(); 621 } 622 623 /** 624 * Sums a DataTable to this one. 625 * 626 * This method will sum rows that have the same label. If a row is found in `$tableToSum` whose 627 * label is not found in `$this`, the row will be added to `$this`. 628 * 629 * If the subtables for this table are loaded, they will be summed as well. 630 * 631 * Rows are summed together by summing individual columns. By default columns are summed by 632 * adding one column value to another. Some columns cannot be aggregated this way. In these 633 * cases, the {@link COLUMN_AGGREGATION_OPS_METADATA_NAME} 634 * metadata can be used to specify a different type of operation. 635 * 636 * @param \Piwik\DataTable $tableToSum 637 * @throws Exception 638 */ 639 public function addDataTable(DataTable $tableToSum) 640 { 641 if ($tableToSum instanceof Simple) { 642 if ($tableToSum->getRowsCount() > 1) { 643 throw new Exception("Did not expect a Simple table with more than one row in addDataTable()"); 644 } 645 $row = $tableToSum->getFirstRow(); 646 $this->aggregateRowFromSimpleTable($row); 647 } else { 648 $columnAggregationOps = $this->getMetadata(self::COLUMN_AGGREGATION_OPS_METADATA_NAME); 649 foreach ($tableToSum->getRowsWithoutSummaryRow() as $row) { 650 $this->aggregateRowWithLabel($row, $columnAggregationOps); 651 } 652 // we do not use getRows() as this method might get called 100k times when aggregating many datatables and 653 // this takes a lot of time. 654 $row = $tableToSum->getRowFromId(DataTable::ID_SUMMARY_ROW); 655 if ($row) { 656 $this->aggregateRow($this->summaryRow, $row, $columnAggregationOps, true); 657 } 658 } 659 } 660 661 /** 662 * Returns the Row whose `'label'` column is equal to `$label`. 663 * 664 * This method executes in constant time except for the first call which caches row 665 * label => row ID mappings. 666 * 667 * @param string $label `'label'` column value to look for. 668 * @return Row|false The row if found, `false` if otherwise. 669 */ 670 public function getRowFromLabel($label) 671 { 672 $rowId = $this->getRowIdFromLabel($label); 673 if (is_int($rowId) && isset($this->rows[$rowId])) { 674 return $this->rows[$rowId]; 675 } 676 if ($rowId == self::ID_SUMMARY_ROW 677 && !empty($this->summaryRow) 678 ) { 679 return $this->summaryRow; 680 } 681 if (empty($rowId) 682 && !empty($this->totalsRow) 683 && $label == $this->totalsRow->getColumn('label') 684 ) { 685 return $this->totalsRow; 686 } 687 if ($rowId instanceof Row) { 688 return $rowId; 689 } 690 return false; 691 } 692 693 /** 694 * Returns the row id for the row whose `'label'` column is equal to `$label`. 695 * 696 * This method executes in constant time except for the first call which caches row 697 * label => row ID mappings. 698 * 699 * @param string $label `'label'` column value to look for. 700 * @return int The row ID. 701 */ 702 public function getRowIdFromLabel($label) 703 { 704 if ($this->indexNotUpToDate) { 705 $this->rebuildIndex(); 706 } 707 708 $label = (string) $label; 709 710 if (!isset($this->rowsIndexByLabel[$label])) { 711 // in case label is '-1' and there is no normal row w/ that label. Note: this is for BC since 712 // in the past, it was possible to get the summary row by searching for the label '-1' 713 if ($label == self::LABEL_SUMMARY_ROW 714 && !is_null($this->summaryRow) 715 ) { 716 return self::ID_SUMMARY_ROW; 717 } 718 719 return false; 720 } 721 722 return $this->rowsIndexByLabel[$label]; 723 } 724 725 /** 726 * Returns an empty DataTable with the same metadata and queued filters as `$this` one. 727 * 728 * @param bool $keepFilters Whether to pass the queued filter list to the new DataTable or not. 729 * @return DataTable 730 */ 731 public function getEmptyClone($keepFilters = true) 732 { 733 $clone = new DataTable; 734 if ($keepFilters) { 735 $clone->queuedFilters = $this->queuedFilters; 736 } 737 $clone->metadata = $this->metadata; 738 return $clone; 739 } 740 741 /** 742 * Rebuilds the index used to lookup a row by label 743 * @internal 744 */ 745 public function rebuildIndex() 746 { 747 $this->rowsIndexByLabel = array(); 748 $this->rebuildIndexContinuously = true; 749 750 foreach ($this->rows as $id => $row) { 751 $label = $row->getColumn('label'); 752 if ($label !== false) { 753 $this->rowsIndexByLabel[$label] = $id; 754 } 755 } 756 757 $this->indexNotUpToDate = false; 758 } 759 760 /** 761 * Returns a row by ID. The ID is either the index of the row or {@link ID_SUMMARY_ROW}. 762 * 763 * @param int $id The row ID. 764 * @return Row|false The Row or false if not found. 765 */ 766 public function getRowFromId($id) 767 { 768 if ($id == self::ID_SUMMARY_ROW 769 && !is_null($this->summaryRow) 770 ) { 771 return $this->summaryRow; 772 } 773 774 if (!isset($this->rows[$id])) { 775 return false; 776 } 777 return $this->rows[$id]; 778 } 779 780 /** 781 * Returns the row that has a subtable with ID matching `$idSubtable`. 782 * 783 * @param int $idSubTable The subtable ID. 784 * @return Row|false The row or false if not found 785 */ 786 public function getRowFromIdSubDataTable($idSubTable) 787 { 788 $idSubTable = (int)$idSubTable; 789 foreach ($this->rows as $row) { 790 if ($row->getIdSubDataTable() === $idSubTable) { 791 return $row; 792 } 793 } 794 return false; 795 } 796 797 /** 798 * Adds a row to this table. 799 * 800 * If {@link setMaximumAllowedRows()} was called and the current row count is 801 * at the maximum, the new row will be summed to the summary row. If there is no summary row, 802 * this row is set as the summary row. 803 * 804 * @param Row $row 805 * @return Row `$row` or the summary row if we're at the maximum number of rows. 806 */ 807 public function addRow(Row $row) 808 { 809 // if there is a upper limit on the number of allowed rows and the table is full, 810 // add the new row to the summary row 811 if ($this->maximumAllowedRows > 0 812 && $this->getRowsCount() >= $this->maximumAllowedRows - 1 813 ) { 814 if ($this->summaryRow === null) { 815 // create the summary row if necessary 816 817 $columns = array('label' => self::LABEL_SUMMARY_ROW) + $row->getColumns(); 818 $this->addSummaryRow(new Row(array(Row::COLUMNS => $columns))); 819 } else { 820 $this->summaryRow->sumRow( 821 $row, $enableCopyMetadata = false, $this->getMetadata(self::COLUMN_AGGREGATION_OPS_METADATA_NAME)); 822 } 823 return $this->summaryRow; 824 } 825 826 $this->rows[] = $row; 827 if (!$this->indexNotUpToDate 828 && $this->rebuildIndexContinuously 829 ) { 830 $label = $row->getColumn('label'); 831 if ($label !== false) { 832 $this->rowsIndexByLabel[$label] = count($this->rows) - 1; 833 } 834 } 835 return $row; 836 } 837 838 /** 839 * Sets the summary row. 840 * 841 * _Note: A DataTable can have only one summary row._ 842 * 843 * @param Row $row 844 * @return Row Returns `$row`. 845 */ 846 public function addSummaryRow(Row $row) 847 { 848 $this->summaryRow = $row; 849 $row->setIsSummaryRow(); 850 851 // NOTE: the summary row does not go in the index, since it will overwrite rows w/ label == -1 852 853 return $row; 854 } 855 856 /** 857 * Returns the DataTable ID. 858 * 859 * @return int 860 */ 861 public function getId() 862 { 863 return $this->currentId; 864 } 865 866 /** 867 * Adds a new row from an array. 868 * 869 * You can add row metadata with this method. 870 * 871 * @param array $row eg. `array(Row::COLUMNS => array('visits' => 13, 'test' => 'toto'), 872 * Row::METADATA => array('mymetadata' => 'myvalue'))` 873 */ 874 public function addRowFromArray($row) 875 { 876 $this->addRowsFromArray(array($row)); 877 } 878 879 /** 880 * Adds a new row a from an array of column values. 881 * 882 * Row metadata cannot be added with this method. 883 * 884 * @param array $row eg. `array('name' => 'google analytics', 'license' => 'commercial')` 885 */ 886 public function addRowFromSimpleArray($row) 887 { 888 $this->addRowsFromSimpleArray(array($row)); 889 } 890 891 /** 892 * Returns the array of Rows. 893 * Internal logic in Matomo core should avoid using this method as it is time and memory consuming when being 894 * executed thousands of times. The alternative is to use {@link getRowsWithoutSummaryRow()} + get the summary 895 * row manually. 896 * 897 * @return Row[] 898 */ 899 public function getRows() 900 { 901 if (is_null($this->summaryRow)) { 902 return $this->rows; 903 } else { 904 return $this->rows + array(self::ID_SUMMARY_ROW => $this->summaryRow); 905 } 906 } 907 908 /** 909 * @ignore 910 */ 911 public function getRowsWithoutSummaryRow() 912 { 913 return $this->rows; 914 } 915 916 /** 917 * @ignore 918 */ 919 public function getRowsCountWithoutSummaryRow() 920 { 921 return count($this->rows); 922 } 923 924 /** 925 * Returns an array containing all column values for the requested column. 926 * 927 * @param string $name The column name. 928 * @return array The array of column values. 929 */ 930 public function getColumn($name) 931 { 932 $columnValues = array(); 933 foreach ($this->getRows() as $row) { 934 $columnValues[] = $row->getColumn($name); 935 } 936 return $columnValues; 937 } 938 939 /** 940 * Returns an array containing all column values of columns whose name starts with `$name`. 941 * 942 * @param string $namePrefix The column name prefix. 943 * @return array The array of column values. 944 */ 945 public function getColumnsStartingWith($namePrefix) 946 { 947 $columnValues = array(); 948 foreach ($this->getRows() as $row) { 949 $columns = $row->getColumns(); 950 foreach ($columns as $column => $value) { 951 if (strpos($column, $namePrefix) === 0) { 952 $columnValues[] = $row->getColumn($column); 953 } 954 } 955 } 956 return $columnValues; 957 } 958 959 /** 960 * Returns the names of every column this DataTable contains. This method will return the 961 * columns of the first row with data and will assume they occur in every other row as well. 962 * 963 *_ Note: If column names still use their in-database INDEX values (@see Metrics), they 964 * will be converted to their string name in the array result._ 965 * 966 * @return array Array of string column names. 967 */ 968 public function getColumns() 969 { 970 $result = array(); 971 foreach ($this->getRows() as $row) { 972 $columns = $row->getColumns(); 973 if (!empty($columns)) { 974 $result = array_keys($columns); 975 break; 976 } 977 } 978 979 // make sure column names are not DB index values 980 foreach ($result as &$column) { 981 if (isset(Metrics::$mappingFromIdToName[$column])) { 982 $column = Metrics::$mappingFromIdToName[$column]; 983 } 984 } 985 986 return $result; 987 } 988 989 /** 990 * Returns an array containing the requested metadata value of each row. 991 * 992 * @param string $name The metadata column to return. 993 * @return array 994 */ 995 public function getRowsMetadata($name) 996 { 997 $metadataValues = array(); 998 foreach ($this->getRows() as $row) { 999 $metadataValues[] = $row->getMetadata($name); 1000 } 1001 return $metadataValues; 1002 } 1003 1004 /** 1005 * Delete row metadata by name in every row. 1006 * 1007 * @param $name 1008 * @param bool $deleteRecursiveInSubtables 1009 */ 1010 public function deleteRowsMetadata($name, $deleteRecursiveInSubtables = false) 1011 { 1012 foreach ($this->rows as $row) { 1013 $row->deleteMetadata($name); 1014 1015 $subTable = $row->getSubtable(); 1016 if ($subTable) { 1017 $subTable->deleteRowsMetadata($name, $deleteRecursiveInSubtables); 1018 } 1019 } 1020 if (!is_null($this->summaryRow)) { 1021 $this->summaryRow->deleteMetadata($name); 1022 } 1023 if (!is_null($this->totalsRow)) { 1024 $this->totalsRow->deleteMetadata($name); 1025 } 1026 1027 } 1028 1029 /** 1030 * Returns the number of rows in the table including the summary row. 1031 * 1032 * @return int 1033 */ 1034 public function getRowsCount() 1035 { 1036 if (is_null($this->summaryRow)) { 1037 return count($this->rows); 1038 } else { 1039 return count($this->rows) + 1; 1040 } 1041 } 1042 1043 /** 1044 * Returns the first row of the DataTable. 1045 * 1046 * @return Row|false The first row or `false` if it cannot be found. 1047 */ 1048 public function getFirstRow() 1049 { 1050 if (count($this->rows) == 0) { 1051 if (!is_null($this->summaryRow)) { 1052 return $this->summaryRow; 1053 } 1054 return false; 1055 } 1056 return reset($this->rows); 1057 } 1058 1059 /** 1060 * Returns the last row of the DataTable. If there is a summary row, it 1061 * will always be considered the last row. 1062 * 1063 * @return Row|false The last row or `false` if it cannot be found. 1064 */ 1065 public function getLastRow() 1066 { 1067 if (!is_null($this->summaryRow)) { 1068 return $this->summaryRow; 1069 } 1070 1071 if (count($this->rows) == 0) { 1072 return false; 1073 } 1074 1075 return end($this->rows); 1076 } 1077 1078 /** 1079 * Returns the number of rows in the entire DataTable hierarchy. This is the number of rows in this DataTable 1080 * summed with the row count of each descendant subtable. 1081 * 1082 * @return int 1083 */ 1084 public function getRowsCountRecursive() 1085 { 1086 $totalCount = 0; 1087 foreach ($this->rows as $row) { 1088 $subTable = $row->getSubtable(); 1089 if ($subTable) { 1090 $count = $subTable->getRowsCountRecursive(); 1091 $totalCount += $count; 1092 } 1093 } 1094 1095 $totalCount += $this->getRowsCount(); 1096 return $totalCount; 1097 } 1098 1099 /** 1100 * Delete a column by name in every row. This change is NOT applied recursively to all 1101 * subtables. 1102 * 1103 * @param string $name Column name to delete. 1104 */ 1105 public function deleteColumn($name) 1106 { 1107 $this->deleteColumns(array($name)); 1108 } 1109 1110 public function __sleep() 1111 { 1112 return array('rows', 'summaryRow', 'metadata', 'totalsRow'); 1113 } 1114 1115 /** 1116 * Rename a column in every row. This change is applied recursively to all subtables. 1117 * 1118 * @param string $oldName Old column name. 1119 * @param string $newName New column name. 1120 */ 1121 public function renameColumn($oldName, $newName) 1122 { 1123 foreach ($this->rows as $row) { 1124 $row->renameColumn($oldName, $newName); 1125 1126 $subTable = $row->getSubtable(); 1127 if ($subTable) { 1128 $subTable->renameColumn($oldName, $newName); 1129 } 1130 } 1131 if (!is_null($this->summaryRow)) { 1132 $this->summaryRow->renameColumn($oldName, $newName); 1133 } 1134 if (!is_null($this->totalsRow)) { 1135 $this->totalsRow->renameColumn($oldName, $newName); 1136 } 1137 } 1138 1139 /** 1140 * Deletes several columns by name in every row. 1141 * 1142 * @param array $names List of column names to delete. 1143 * @param bool $deleteRecursiveInSubtables Whether to apply this change to all subtables or not. 1144 */ 1145 public function deleteColumns($names, $deleteRecursiveInSubtables = false) 1146 { 1147 foreach ($this->rows as $row) { 1148 foreach ($names as $name) { 1149 $row->deleteColumn($name); 1150 } 1151 $subTable = $row->getSubtable(); 1152 if ($subTable) { 1153 $subTable->deleteColumns($names, $deleteRecursiveInSubtables); 1154 } 1155 } 1156 if (!is_null($this->summaryRow)) { 1157 foreach ($names as $name) { 1158 $this->summaryRow->deleteColumn($name); 1159 } 1160 } 1161 if (!is_null($this->totalsRow)) { 1162 foreach ($names as $name) { 1163 $this->totalsRow->deleteColumn($name); 1164 } 1165 } 1166 } 1167 1168 /** 1169 * Deletes a row by ID. 1170 * 1171 * @param int $id The row ID. 1172 * @throws Exception If the row `$id` cannot be found. 1173 */ 1174 public function deleteRow($id) 1175 { 1176 if ($id === self::ID_SUMMARY_ROW) { 1177 $this->summaryRow = null; 1178 return; 1179 } 1180 if (!isset($this->rows[$id])) { 1181 throw new Exception("Trying to delete unknown row with idkey = $id"); 1182 } 1183 unset($this->rows[$id]); 1184 } 1185 1186 /** 1187 * Deletes rows from `$offset` to `$offset + $limit`. 1188 * 1189 * @param int $offset The offset to start deleting rows from. 1190 * @param int|null $limit The number of rows to delete. If `null` all rows after the offset 1191 * will be removed. 1192 * @return int The number of rows deleted. 1193 */ 1194 public function deleteRowsOffset($offset, $limit = null) 1195 { 1196 if ($limit === 0) { 1197 return 0; 1198 } 1199 1200 $count = $this->getRowsCount(); 1201 if ($offset >= $count) { 1202 return 0; 1203 } 1204 1205 // if we delete until the end, we delete the summary row as well 1206 if (is_null($limit) 1207 || $limit >= $count 1208 ) { 1209 $this->summaryRow = null; 1210 } 1211 1212 if (is_null($limit)) { 1213 array_splice($this->rows, $offset); 1214 } else { 1215 array_splice($this->rows, $offset, $limit); 1216 } 1217 1218 return $count - $this->getRowsCount(); 1219 } 1220 1221 /** 1222 * Deletes a set of rows by ID. 1223 * 1224 * @param array $rowIds The list of row IDs to delete. 1225 * @throws Exception If a row ID cannot be found. 1226 */ 1227 public function deleteRows(array $rowIds) 1228 { 1229 foreach ($rowIds as $key) { 1230 $this->deleteRow($key); 1231 } 1232 } 1233 1234 /** 1235 * Returns a string representation of this DataTable for convenient viewing. 1236 * 1237 * _Note: This uses the **html** DataTable renderer._ 1238 * 1239 * @return string 1240 */ 1241 public function __toString() 1242 { 1243 $renderer = new Html(); 1244 $renderer->setTable($this); 1245 return (string)$renderer; 1246 } 1247 1248 /** 1249 * Returns true if both DataTable instances are exactly the same. 1250 * 1251 * DataTables are equal if they have the same number of rows, if 1252 * each row has a label that exists in the other table, and if each row 1253 * is equal to the row in the other table with the same label. The order 1254 * of rows is not important. 1255 * 1256 * @param \Piwik\DataTable $table1 1257 * @param \Piwik\DataTable $table2 1258 * @return bool 1259 */ 1260 public static function isEqual(DataTable $table1, DataTable $table2) 1261 { 1262 $table1->rebuildIndex(); 1263 $table2->rebuildIndex(); 1264 1265 if ($table1->getRowsCount() != $table2->getRowsCount()) { 1266 return false; 1267 } 1268 1269 $rows1 = $table1->getRows(); 1270 1271 foreach ($rows1 as $row1) { 1272 $row2 = $table2->getRowFromLabel($row1->getColumn('label')); 1273 if ($row2 === false 1274 || !Row::isEqual($row1, $row2) 1275 ) { 1276 return false; 1277 } 1278 } 1279 1280 return true; 1281 } 1282 1283 /** 1284 * Serializes an entire DataTable hierarchy and returns the array of serialized DataTables. 1285 * 1286 * The first element in the returned array will be the serialized representation of this DataTable. 1287 * Every subsequent element will be a serialized subtable. 1288 * 1289 * This DataTable and subtables can optionally be truncated before being serialized. In most 1290 * cases where DataTables can become quite large, they should be truncated before being persisted 1291 * in an archive. 1292 * 1293 * The result of this method is intended for use with the {@link ArchiveProcessor::insertBlobRecord()} method. 1294 * 1295 * @throws Exception If infinite recursion detected. This will occur if a table's subtable is one of its parent tables. 1296 * @param int $maximumRowsInDataTable If not null, defines the maximum number of rows allowed in the serialized DataTable. 1297 * @param int $maximumRowsInSubDataTable If not null, defines the maximum number of rows allowed in serialized subtables. 1298 * @param string $columnToSortByBeforeTruncation The column to sort by before truncating, eg, `Metrics::INDEX_NB_VISITS`. 1299 * @param array $aSerializedDataTable Will contain all the output arrays 1300 * @return array The array of serialized DataTables: 1301 * 1302 * array( 1303 * // this DataTable (the root) 1304 * 0 => 'eghuighahgaueytae78yaet7yaetae', 1305 * 1306 * // a subtable 1307 * 1 => 'gaegae gh gwrh guiwh uigwhuige', 1308 * 1309 * // another subtable 1310 * 2 => 'gqegJHUIGHEQjkgneqjgnqeugUGEQHGUHQE', 1311 * 1312 * // etc. 1313 * ); 1314 */ 1315 public function getSerialized($maximumRowsInDataTable = null, 1316 $maximumRowsInSubDataTable = null, 1317 $columnToSortByBeforeTruncation = null, 1318 &$aSerializedDataTable = array()) 1319 { 1320 static $depth = 0; 1321 // make sure subtableIds are consecutive from 1 to N 1322 static $subtableId = 0; 1323 1324 if ($depth > self::$maximumDepthLevelAllowed) { 1325 $depth = 0; 1326 $subtableId = 0; 1327 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?"); 1328 } 1329 1330 // gather metadata before filters are called, so their metadata is not stored in serialized form 1331 $metadata = $this->getAllTableMetadata(); 1332 foreach ($metadata as $key => $value) { 1333 if (!is_scalar($value) && !is_string($value)) { 1334 unset($metadata[$key]); 1335 } 1336 } 1337 1338 if (!is_null($maximumRowsInDataTable)) { 1339 $this->filter('Truncate', 1340 array($maximumRowsInDataTable - 1, 1341 DataTable::LABEL_SUMMARY_ROW, 1342 $columnToSortByBeforeTruncation, 1343 $filterRecursive = false) 1344 ); 1345 } 1346 1347 $consecutiveSubtableIds = array(); 1348 $forcedId = $subtableId; 1349 1350 // For each row (including the summary row), get the serialized row 1351 // If it is associated to a sub table, get the serialized table recursively ; 1352 // but returns all serialized tables and subtable in an array of 1 dimension 1353 foreach ($this->getRows() as $id => $row) { 1354 $subTable = $row->getSubtable(); 1355 if ($subTable) { 1356 $consecutiveSubtableIds[$id] = ++$subtableId; 1357 $depth++; 1358 $subTable->getSerialized($maximumRowsInSubDataTable, $maximumRowsInSubDataTable, $columnToSortByBeforeTruncation, $aSerializedDataTable); 1359 $depth--; 1360 } else { 1361 $row->removeSubtable(); 1362 } 1363 } 1364 1365 // if the datatable is the parent we force the Id at 0 (this is part of the specification) 1366 if ($depth == 0) { 1367 $forcedId = 0; 1368 $subtableId = 0; 1369 } 1370 1371 // we then serialize the rows and store them in the serialized dataTable 1372 $rows = array(); 1373 foreach ($this->rows as $id => $row) { 1374 if (isset($consecutiveSubtableIds[$id])) { 1375 $backup = $row->subtableId; 1376 $row->subtableId = $consecutiveSubtableIds[$id]; 1377 $rows[$id] = $row->export(); 1378 $row->subtableId = $backup; 1379 } else { 1380 $rows[$id] = $row->export(); 1381 } 1382 } 1383 1384 if (isset($this->summaryRow)) { 1385 $id = self::ID_SUMMARY_ROW; 1386 $row = $this->summaryRow; 1387 1388 // duplicating code above so we don't create a new array w/ getRows() above in this function which is 1389 // used heavily in matomo. 1390 if (isset($consecutiveSubtableIds[$id])) { 1391 $backup = $row->subtableId; 1392 $row->subtableId = $consecutiveSubtableIds[$id]; 1393 $rows[$id] = $row->export(); 1394 $row->subtableId = $backup; 1395 } else { 1396 $rows[$id] = $row->export(); 1397 } 1398 } 1399 1400 if (!empty($metadata)) { 1401 $metadataRow = new Row(); 1402 $metadataRow->setColumns($metadata); 1403 1404 // set the label so the row will be indexed correctly internally 1405 $metadataRow->setColumn('label', self::LABEL_ARCHIVED_METADATA_ROW); 1406 1407 $rows[self::ID_ARCHIVED_METADATA_ROW] = $metadataRow->export(); 1408 } 1409 1410 $aSerializedDataTable[$forcedId] = serialize($rows); 1411 unset($rows); 1412 1413 return $aSerializedDataTable; 1414 } 1415 1416 private static $previousRowClasses = array('O:39:"Piwik\DataTable\Row\DataTableSummaryRow"', 'O:19:"Piwik\DataTable\Row"', 'O:36:"Piwik_DataTable_Row_DataTableSummary"', 'O:19:"Piwik_DataTable_Row"'); 1417 private static $rowClassToUseForUnserialize = 'O:29:"Piwik_DataTable_SerializedRow"'; 1418 1419 /** 1420 * It is faster to unserialize existing serialized Row instances to "Piwik_DataTable_SerializedRow" and access the 1421 * `$row->c` property than implementing a "__wakeup" method in the Row instance to map the "$row->c" to $row->columns 1422 * etc. We're talking here about 15% faster reports aggregation in some cases. To be concrete: We have a test where 1423 * Archiving a year takes 1700 seconds with "__wakeup" and 1400 seconds with this method. Yes, it takes 300 seconds 1424 * to wake up millions of rows. We should be able to remove this code here end 2015 and use the "__wakeup" way by then. 1425 * Why? By then most new archives will have only arrays serialized anyway and therefore this mapping is rather an overhead. 1426 * 1427 * @param string $serialized 1428 * @return array 1429 * @throws Exception In case the unserialize fails 1430 */ 1431 private function unserializeRows($serialized) 1432 { 1433 $serialized = str_replace(self::$previousRowClasses, self::$rowClassToUseForUnserialize, $serialized); 1434 $rows = Common::safe_unserialize($serialized, [ 1435 Row::class, 1436 DataTableSummaryRow::class, 1437 \Piwik_DataTable_SerializedRow::class 1438 ]); 1439 1440 if ($rows === false) { 1441 throw new Exception("The unserialization has failed!"); 1442 } 1443 1444 return $rows; 1445 } 1446 1447 /** 1448 * Adds a set of rows from a serialized DataTable string. 1449 * 1450 * See {@link serialize()}. 1451 * 1452 * _Note: This function will successfully load DataTables serialized by Piwik 1.X._ 1453 * 1454 * @param string $serialized A string with the format of a string in the array returned by 1455 * {@link serialize()}. 1456 * @throws Exception if `$serialized` is invalid. 1457 */ 1458 public function addRowsFromSerializedArray($serialized) 1459 { 1460 $rows = $this->unserializeRows($serialized); 1461 1462 if (array_key_exists(self::ID_SUMMARY_ROW, $rows)) { 1463 if (is_array($rows[self::ID_SUMMARY_ROW])) { 1464 $this->summaryRow = new Row($rows[self::ID_SUMMARY_ROW]); 1465 $this->summaryRow->setIsSummaryRow(); 1466 } elseif (isset($rows[self::ID_SUMMARY_ROW]->c)) { 1467 $this->summaryRow = new Row($rows[self::ID_SUMMARY_ROW]->c); // Pre Piwik 2.13 1468 $this->summaryRow->setIsSummaryRow(); 1469 } 1470 unset($rows[self::ID_SUMMARY_ROW]); 1471 } 1472 1473 if (array_key_exists(self::ID_ARCHIVED_METADATA_ROW, $rows)) { 1474 $metadata = $rows[self::ID_ARCHIVED_METADATA_ROW][Row::COLUMNS]; 1475 unset($metadata['label']); 1476 $this->setAllTableMetadata($metadata); 1477 unset($rows[self::ID_ARCHIVED_METADATA_ROW]); 1478 } 1479 1480 foreach ($rows as $id => $row) { 1481 if (isset($row->c)) { 1482 $this->addRow(new Row($row->c)); // Pre Piwik 2.13 1483 } else { 1484 $this->addRow(new Row($row)); 1485 } 1486 } 1487 } 1488 1489 /** 1490 * Adds multiple rows from an array. 1491 * 1492 * You can add row metadata with this method. 1493 * 1494 * @param array $array Array with the following structure 1495 * 1496 * array( 1497 * // row1 1498 * array( 1499 * Row::COLUMNS => array( col1_name => value1, col2_name => value2, ...), 1500 * Row::METADATA => array( metadata1_name => value1, ...), // see Row 1501 * ), 1502 * // row2 1503 * array( ... ), 1504 * ) 1505 */ 1506 public function addRowsFromArray($array) 1507 { 1508 foreach ($array as $id => $row) { 1509 if (is_array($row)) { 1510 $row = new Row($row); 1511 } 1512 1513 if ($id == self::ID_SUMMARY_ROW) { 1514 $this->summaryRow = $row; 1515 $this->summaryRow->setIsSummaryRow(); 1516 } else { 1517 $this->addRow($row); 1518 } 1519 } 1520 } 1521 1522 /** 1523 * Adds multiple rows from an array containing arrays of column values. 1524 * 1525 * Row metadata cannot be added with this method. 1526 * 1527 * @param array $array Array with the following structure: 1528 * 1529 * array( 1530 * array( col1_name => valueA, col2_name => valueC, ...), 1531 * array( col1_name => valueB, col2_name => valueD, ...), 1532 * ) 1533 * @throws Exception if `$array` is in an incorrect format. 1534 */ 1535 public function addRowsFromSimpleArray($array) 1536 { 1537 if (count($array) === 0) { 1538 return; 1539 } 1540 1541 $exceptionText = " Data structure returned is not convertible in the requested format." . 1542 " Try to call this method with the parameters '&format=original&serialize=1'" . 1543 "; you will get the original php data structure serialized." . 1544 " The data structure looks like this: \n \$data = %s; "; 1545 1546 // first pass to see if the array has the structure 1547 // array(col1_name => val1, col2_name => val2, etc.) 1548 // with val* that are never arrays (only strings/numbers/bool/etc.) 1549 // if we detect such a "simple" data structure we convert it to a row with the correct columns' names 1550 $thisIsNotThatSimple = false; 1551 1552 foreach ($array as $columnValue) { 1553 if (is_array($columnValue) || is_object($columnValue)) { 1554 $thisIsNotThatSimple = true; 1555 break; 1556 } 1557 } 1558 if ($thisIsNotThatSimple === false) { 1559 // case when the array is indexed by the default numeric index 1560 if (array_keys($array) === array_keys(array_fill(0, count($array), true))) { 1561 foreach ($array as $row) { 1562 $this->addRow(new Row(array(Row::COLUMNS => array($row)))); 1563 } 1564 } else { 1565 $this->addRow(new Row(array(Row::COLUMNS => $array))); 1566 } 1567 // we have converted our simple array to one single row 1568 // => we exit the method as the job is now finished 1569 return; 1570 } 1571 1572 foreach ($array as $key => $row) { 1573 // stuff that looks like a line 1574 if (is_array($row)) { 1575 /** 1576 * We make sure we can convert this PHP array without losing information. 1577 * We are able to convert only simple php array (no strings keys, no sub arrays, etc.) 1578 * 1579 */ 1580 1581 // if the key is a string it means that some information was contained in this key. 1582 // it cannot be lost during the conversion. Because we are not able to handle properly 1583 // this key, we throw an explicit exception. 1584 if (is_string($key)) { 1585 // we define an exception we may throw if at one point we notice that we cannot handle the data structure 1586 throw new Exception(sprintf($exceptionText, var_export($array, true))); 1587 } 1588 // if any of the sub elements of row is an array we cannot handle this data structure... 1589 foreach ($row as $subRow) { 1590 if (is_array($subRow)) { 1591 throw new Exception(sprintf($exceptionText, var_export($array, true))); 1592 } 1593 } 1594 $row = new Row(array(Row::COLUMNS => $row)); 1595 } // other (string, numbers...) => we build a line from this value 1596 else { 1597 $row = new Row(array(Row::COLUMNS => array($key => $row))); 1598 } 1599 $this->addRow($row); 1600 } 1601 } 1602 1603 /** 1604 * Rewrites the input `$array` 1605 * 1606 * array ( 1607 * LABEL => array(col1 => X, col2 => Y), 1608 * LABEL2 => array(col1 => X, col2 => Y), 1609 * ) 1610 * 1611 * to a DataTable with rows that look like: 1612 * 1613 * array ( 1614 * array( Row::COLUMNS => array('label' => LABEL, col1 => X, col2 => Y)), 1615 * array( Row::COLUMNS => array('label' => LABEL2, col1 => X, col2 => Y)), 1616 * ) 1617 * 1618 * Will also convert arrays like: 1619 * 1620 * array ( 1621 * LABEL => X, 1622 * LABEL2 => Y, 1623 * ) 1624 * 1625 * to: 1626 * 1627 * array ( 1628 * array( Row::COLUMNS => array('label' => LABEL, 'value' => X)), 1629 * array( Row::COLUMNS => array('label' => LABEL2, 'value' => Y)), 1630 * ) 1631 * 1632 * @param array $array Indexed array, two formats supported, see above. 1633 * @param array|null $subtablePerLabel An array mapping label values with DataTable instances to associate as a subtable. 1634 * @return \Piwik\DataTable 1635 */ 1636 public static function makeFromIndexedArray($array, $subtablePerLabel = null) 1637 { 1638 $table = new DataTable(); 1639 foreach ($array as $label => $row) { 1640 $cleanRow = array(); 1641 1642 // Support the case of an $array of single values 1643 if (!is_array($row)) { 1644 $row = array('value' => $row); 1645 } 1646 // Put the 'label' column first 1647 $cleanRow[Row::COLUMNS] = array('label' => $label) + $row; 1648 // Assign subtable if specified 1649 if (isset($subtablePerLabel[$label])) { 1650 $cleanRow[Row::DATATABLE_ASSOCIATED] = $subtablePerLabel[$label]; 1651 } 1652 1653 if ($label === RankingQuery::LABEL_SUMMARY_ROW) { 1654 $table->addSummaryRow(new Row($cleanRow)); 1655 } else { 1656 $table->addRow(new Row($cleanRow)); 1657 } 1658 } 1659 return $table; 1660 } 1661 1662 /** 1663 * Sets the maximum depth level to at least a certain value. If the current value is 1664 * greater than `$atLeastLevel`, the maximum nesting level is not changed. 1665 * 1666 * The maximum depth level determines the maximum number of subtable levels in the 1667 * DataTable tree. For example, if it is set to `2`, this DataTable is allowed to 1668 * have subtables, but the subtables are not. 1669 * 1670 * @param int $atLeastLevel 1671 */ 1672 public static function setMaximumDepthLevelAllowedAtLeast($atLeastLevel) 1673 { 1674 self::$maximumDepthLevelAllowed = max($atLeastLevel, self::$maximumDepthLevelAllowed); 1675 if (self::$maximumDepthLevelAllowed < 1) { 1676 self::$maximumDepthLevelAllowed = 1; 1677 } 1678 } 1679 1680 /** 1681 * Returns metadata by name. 1682 * 1683 * @param string $name The metadata name. 1684 * @return mixed|false The metadata value or `false` if it cannot be found. 1685 */ 1686 public function getMetadata($name) 1687 { 1688 if (!isset($this->metadata[$name])) { 1689 return false; 1690 } 1691 return $this->metadata[$name]; 1692 } 1693 1694 /** 1695 * Sets a metadata value by name. 1696 * 1697 * @param string $name The metadata name. 1698 * @param mixed $value 1699 */ 1700 public function setMetadata($name, $value) 1701 { 1702 $this->metadata[$name] = $value; 1703 } 1704 1705 /** 1706 * Returns all table metadata. 1707 * 1708 * @return array 1709 */ 1710 public function getAllTableMetadata() 1711 { 1712 return $this->metadata; 1713 } 1714 1715 /** 1716 * Sets several metadata values by name. 1717 * 1718 * @param array $values Array mapping metadata names with metadata values. 1719 */ 1720 public function setMetadataValues($values) 1721 { 1722 foreach ($values as $name => $value) { 1723 $this->metadata[$name] = $value; 1724 } 1725 } 1726 1727 /** 1728 * Sets metadata, erasing existing values. 1729 * 1730 * @param array $values Array mapping metadata names with metadata values. 1731 */ 1732 public function setAllTableMetadata($metadata) 1733 { 1734 $this->metadata = $metadata; 1735 } 1736 1737 /** 1738 * Sets the maximum number of rows allowed in this datatable (including the summary 1739 * row). If adding more then the allowed number of rows is attempted, the extra 1740 * rows are summed to the summary row. 1741 * 1742 * @param int $maximumAllowedRows If `0`, the maximum number of rows is unset. 1743 */ 1744 public function setMaximumAllowedRows($maximumAllowedRows) 1745 { 1746 $this->maximumAllowedRows = $maximumAllowedRows; 1747 } 1748 1749 /** 1750 * Traverses a DataTable tree using an array of labels and returns the row 1751 * it finds or `false` if it cannot find one. The number of path segments that 1752 * were successfully walked is also returned. 1753 * 1754 * If `$missingRowColumns` is supplied, the specified path is created. When 1755 * a subtable is encountered w/o the required label, a new row is created 1756 * with the label, and a new subtable is added to the row. 1757 * 1758 * Read [http://en.wikipedia.org/wiki/Tree_(data_structure)#Traversal_methods](http://en.wikipedia.org/wiki/Tree_(data_structure)#Traversal_methods) 1759 * for more information about tree walking. 1760 * 1761 * @param array $path The path to walk. An array of label values. The first element 1762 * refers to a row in this DataTable, the second in a subtable of 1763 * the first row, the third a subtable of the second row, etc. 1764 * @param array|bool $missingRowColumns The default columns to use when creating new rows. 1765 * If this parameter is supplied, new rows will be 1766 * created for path labels that cannot be found. 1767 * @param int $maxSubtableRows The maximum number of allowed rows in new subtables. New 1768 * subtables are only created if `$missingRowColumns` is provided. 1769 * @return array First element is the found row or `false`. Second element is 1770 * the number of path segments walked. If a row is found, this 1771 * will be == to `count($path)`. Otherwise, it will be the index 1772 * of the path segment that we could not find. 1773 */ 1774 public function walkPath($path, $missingRowColumns = false, $maxSubtableRows = 0) 1775 { 1776 $pathLength = count($path); 1777 1778 $table = $this; 1779 $next = false; 1780 for ($i = 0; $i < $pathLength; ++$i) { 1781 $segment = $path[$i]; 1782 1783 $next = $table->getRowFromLabel($segment); 1784 if ($next === false) { 1785 // if there is no table to advance to, and we're not adding missing rows, return false 1786 if ($missingRowColumns === false) { 1787 return array(false, $i); 1788 } else { 1789 // if we're adding missing rows, add a new row 1790 1791 $row = new DataTableSummaryRow(); 1792 $row->setColumns(array('label' => $segment) + $missingRowColumns); 1793 1794 $next = $table->addRow($row); 1795 1796 if ($next !== $row) { 1797 // if the row wasn't added, the table is full 1798 1799 // Summary row, has no metadata 1800 $next->deleteMetadata(); 1801 return array($next, $i); 1802 } 1803 } 1804 } 1805 1806 $table = $next->getSubtable(); 1807 if ($table === false) { 1808 // if the row has no table (and thus no child rows), and we're not adding 1809 // missing rows, return false 1810 if ($missingRowColumns === false) { 1811 return array(false, $i); 1812 } elseif ($i != $pathLength - 1) { 1813 // create subtable if missing, but only if not on the last segment 1814 1815 $table = new DataTable(); 1816 $table->setMaximumAllowedRows($maxSubtableRows); 1817 $table->metadata[self::COLUMN_AGGREGATION_OPS_METADATA_NAME] 1818 = $this->getMetadata(self::COLUMN_AGGREGATION_OPS_METADATA_NAME); 1819 $next->setSubtable($table); 1820 // Summary row, has no metadata 1821 $next->deleteMetadata(); 1822 } 1823 } 1824 } 1825 1826 return array($next, $i); 1827 } 1828 1829 /** 1830 * Returns a new DataTable in which the rows of this table are replaced with the aggregatated rows of all its subtables. 1831 * 1832 * @param string|bool $labelColumn If supplied the label of the parent row will be added to 1833 * a new column in each subtable row. 1834 * 1835 * If set to, `'label'` each subtable row's label will be prepended 1836 * w/ the parent row's label. So `'child_label'` becomes 1837 * `'parent_label - child_label'`. 1838 * @param bool $useMetadataColumn If true and if `$labelColumn` is supplied, the parent row's 1839 * label will be added as metadata and not a new column. 1840 * @return \Piwik\DataTable 1841 */ 1842 public function mergeSubtables($labelColumn = false, $useMetadataColumn = false) 1843 { 1844 $result = new DataTable(); 1845 $result->setAllTableMetadata($this->getAllTableMetadata()); 1846 foreach ($this->getRowsWithoutSummaryRow() as $row) { 1847 $subtable = $row->getSubtable(); 1848 if ($subtable !== false) { 1849 $parentLabel = $row->getColumn('label'); 1850 1851 // add a copy of each subtable row to the new datatable 1852 foreach ($subtable->getRows() as $id => $subRow) { 1853 $copy = clone $subRow; 1854 1855 // if the summary row, add it to the existing summary row (or add a new one) 1856 if ($id == self::ID_SUMMARY_ROW) { 1857 $existing = $result->getRowFromId(self::ID_SUMMARY_ROW); 1858 if ($existing === false) { 1859 $result->addSummaryRow($copy); 1860 } else { 1861 $existing->sumRow($copy, $copyMeta = true, $this->getMetadata(self::COLUMN_AGGREGATION_OPS_METADATA_NAME)); 1862 } 1863 } else { 1864 if ($labelColumn !== false) { 1865 // if we're modifying the subtable's rows' label column, then we make 1866 // sure to prepend the existing label w/ the parent row's label. otherwise 1867 // we're just adding the parent row's label as a new column/metadata. 1868 $newLabel = $parentLabel; 1869 if ($labelColumn == 'label') { 1870 $newLabel .= ' - ' . $copy->getColumn('label'); 1871 } 1872 1873 // modify the child row's label or add new column/metadata 1874 if ($useMetadataColumn) { 1875 $copy->setMetadata($labelColumn, $newLabel); 1876 } else { 1877 $copy->setColumn($labelColumn, $newLabel); 1878 } 1879 } 1880 1881 $result->addRow($copy); 1882 } 1883 } 1884 } 1885 } 1886 return $result; 1887 } 1888 1889 /** 1890 * Returns a new DataTable created with data from a 'simple' array. 1891 * 1892 * See {@link addRowsFromSimpleArray()}. 1893 * 1894 * @param array $array 1895 * @return \Piwik\DataTable 1896 */ 1897 public static function makeFromSimpleArray($array) 1898 { 1899 $dataTable = new DataTable(); 1900 $dataTable->addRowsFromSimpleArray($array); 1901 return $dataTable; 1902 } 1903 1904 /** 1905 * Creates a new DataTable instance from a serialized DataTable string. 1906 * 1907 * See {@link getSerialized()} and {@link addRowsFromSerializedArray()} 1908 * for more information on DataTable serialization. 1909 * 1910 * @param string $data 1911 * @return \Piwik\DataTable 1912 */ 1913 public static function fromSerializedArray($data) 1914 { 1915 $result = new DataTable(); 1916 $result->addRowsFromSerializedArray($data); 1917 return $result; 1918 } 1919 1920 /** 1921 * Aggregates the $row columns to this table. 1922 * 1923 * $row must have a column "label". The $row will be summed to this table's row with the same label. 1924 * 1925 * @param $row 1926 * @params null|array $columnAggregationOps 1927 * @throws \Exception 1928 */ 1929 protected function aggregateRowWithLabel(Row $row, $columnAggregationOps) 1930 { 1931 $labelToLookFor = $row->getColumn('label'); 1932 if ($labelToLookFor === false) { 1933 $message = sprintf("Label column not found in the table to add in addDataTable(). Row: %s", 1934 var_export($row->getColumns(), 1) 1935 ); 1936 throw new Exception($message); 1937 } 1938 $rowFound = $this->getRowFromLabel($labelToLookFor); 1939 // if we find the summary row in the other table, ignore it, since we're aggregating normal rows in this method. 1940 // the summary row is aggregated explicitly after this method is called. 1941 if (!empty($rowFound) 1942 && $rowFound->isSummaryRow() 1943 ) { 1944 $rowFound = false; 1945 } 1946 $this->aggregateRow($rowFound, $row, $columnAggregationOps, $isSummaryRow = false); 1947 } 1948 1949 private function aggregateRow($thisRow, Row $otherRow, $columnAggregationOps, $isSummaryRow) 1950 { 1951 if (empty($thisRow)) { 1952 $thisRow = new Row(); 1953 $otherRowLabel = $otherRow->getColumn('label'); 1954 if ($otherRowLabel !== false) { 1955 $thisRow->addColumn('label', $otherRowLabel); 1956 } 1957 $thisRow->setAllMetadata($otherRow->getMetadata()); 1958 1959 if ($isSummaryRow) { 1960 $this->addSummaryRow($thisRow); 1961 } else { 1962 $this->addRow($thisRow); 1963 } 1964 } 1965 1966 $thisRow->sumRow($otherRow, $copyMeta = true, $columnAggregationOps); 1967 1968 // if the row to add has a subtable whereas the current row doesn't 1969 // we simply add it (cloning the subtable) 1970 // if the row has the subtable already 1971 // then we have to recursively sum the subtables 1972 $subTable = $otherRow->getSubtable(); 1973 if ($subTable) { 1974 $subTable->metadata[self::COLUMN_AGGREGATION_OPS_METADATA_NAME] = $columnAggregationOps; 1975 $thisRow->sumSubtable($subTable); 1976 } 1977 } 1978 1979 /** 1980 * @param $row 1981 */ 1982 protected function aggregateRowFromSimpleTable($row) 1983 { 1984 if ($row === false) { 1985 return; 1986 } 1987 $thisRow = $this->getFirstRow(); 1988 if ($thisRow === false) { 1989 $thisRow = new Row; 1990 $this->addRow($thisRow); 1991 } 1992 $thisRow->sumRow($row, $copyMeta = true, $this->getMetadata(self::COLUMN_AGGREGATION_OPS_METADATA_NAME)); 1993 } 1994 1995 /** 1996 * Unsets all queued filters. 1997 */ 1998 public function clearQueuedFilters() 1999 { 2000 $this->queuedFilters = array(); 2001 } 2002 2003 public function getQueuedFilters() 2004 { 2005 return $this->queuedFilters; 2006 } 2007 2008 /** 2009 * @return \ArrayIterator|Row[] 2010 */ 2011 public function getIterator(): \ArrayIterator 2012 { 2013 return new \ArrayIterator($this->getRows()); 2014 } 2015 2016 public function offsetExists($offset): bool 2017 { 2018 $row = $this->getRowFromId($offset); 2019 2020 return false !== $row; 2021 } 2022 2023 public function offsetGet($offset): Row 2024 { 2025 return $this->getRowFromId($offset); 2026 } 2027 2028 public function offsetSet($offset, $value): void 2029 { 2030 $this->rows[$offset] = $value; 2031 } 2032 2033 public function offsetUnset($offset): void 2034 { 2035 $this->deleteRow($offset); 2036 } 2037} 2038