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 */ 9namespace Piwik\DataTable; 10 11use Closure; 12use Piwik\Common; 13use Piwik\DataTable; 14use Piwik\DataTable\Renderer\Console; 15use Piwik\DataTable\Renderer\Html; 16 17/** 18 * Stores an array of {@link DataTable}s indexed by one type of {@link DataTable} metadata (such as site ID 19 * or period). 20 * 21 * DataTable Maps are returned on all queries that involve multiple sites and/or multiple 22 * periods. The Maps will contain a {@link DataTable} for each site and period combination. 23 * 24 * The Map implements some {@link DataTable} such as {@link queueFilter()} and {@link getRowsCount}. 25 * 26 * 27 * @api 28 */ 29class Map implements DataTableInterface 30{ 31 /** 32 * Array containing the DataTable within this Set 33 * 34 * @var DataTable[] 35 */ 36 protected $array = array(); 37 38 /** 39 * @see self::getKeyName() 40 * @var string 41 */ 42 protected $keyName = 'defaultKeyName'; 43 44 /** 45 * Returns a string description of the data used to index the DataTables. 46 * 47 * This label is used by DataTable Renderers (it becomes a column name or the XML description tag). 48 * 49 * @return string eg, `'idSite'`, `'period'` 50 */ 51 public function getKeyName() 52 { 53 return $this->keyName; 54 } 55 56 /** 57 * Set the name of they metadata used to index {@link DataTable}s. See {@link getKeyName()}. 58 * 59 * @param string $name 60 */ 61 public function setKeyName($name) 62 { 63 $this->keyName = $name; 64 } 65 66 /** 67 * Returns the number of {@link DataTable}s in this DataTable\Map. 68 * 69 * @return int 70 */ 71 public function getRowsCount() 72 { 73 return count($this->getDataTables()); 74 } 75 76 /** 77 * Queue a filter to {@link DataTable} child of contained by this instance. 78 * 79 * See {@link Piwik\DataTable::queueFilter()} for more information.. 80 * 81 * @param string|Closure $className Filter name, eg. `'Limit'` or a Closure. 82 * @param array $parameters Filter parameters, eg. `array(50, 10)`. 83 */ 84 public function queueFilter($className, $parameters = array()) 85 { 86 foreach ($this->getDataTables() as $table) { 87 $table->queueFilter($className, $parameters); 88 } 89 } 90 91 /** 92 * Apply the filters previously queued to each DataTable contained by this DataTable\Map. 93 */ 94 public function applyQueuedFilters() 95 { 96 foreach ($this->getDataTables() as $table) { 97 $table->applyQueuedFilters(); 98 } 99 } 100 101 /** 102 * Apply a filter to all tables contained by this instance. 103 * 104 * @param string|Closure $className Name of filter class or a Closure. 105 * @param array $parameters Parameters to pass to the filter. 106 */ 107 public function filter($className, $parameters = array()) 108 { 109 foreach ($this->getDataTables() as $table) { 110 $table->filter($className, $parameters); 111 } 112 } 113 114 /** 115 * Apply a filter to all subtables contained by this instance. 116 * 117 * @param string|Closure $className Name of filter class or a Closure. 118 * @param array $parameters Parameters to pass to the filter. 119 */ 120 public function filterSubtables($className, $parameters = array()) 121 { 122 foreach ($this->getDataTables() as $table) { 123 $table->filterSubtables($className, $parameters); 124 } 125 } 126 127 /** 128 * Apply a queued filter to all subtables contained by this instance. 129 * 130 * @param string|Closure $className Name of filter class or a Closure. 131 * @param array $parameters Parameters to pass to the filter. 132 */ 133 public function queueFilterSubtables($className, $parameters = array()) 134 { 135 foreach ($this->getDataTables() as $table) { 136 $table->queueFilterSubtables($className, $parameters); 137 } 138 } 139 140 /** 141 * Returns the array of DataTables contained by this class. 142 * 143 * @return DataTable[]|Map[] 144 */ 145 public function getDataTables() 146 { 147 return $this->array; 148 } 149 150 /** 151 * Returns the table with the specific label. 152 * 153 * @param string $label 154 * @return DataTable|Map 155 */ 156 public function getTable($label) 157 { 158 return $this->array[$label]; 159 } 160 161 /** 162 * @param string $label 163 * @return bool 164 */ 165 public function hasTable($label) 166 { 167 return isset($this->array[$label]); 168 } 169 170 /** 171 * Returns the first element in the Map's array. 172 * 173 * @return DataTable|Map|false 174 */ 175 public function getFirstRow() 176 { 177 return reset($this->array); 178 } 179 180 /** 181 * Returns the last element in the Map's array. 182 * 183 * @return DataTable|Map|false 184 */ 185 public function getLastRow() 186 { 187 return end($this->array); 188 } 189 190 /** 191 * Adds a new {@link DataTable} or Map instance to this DataTable\Map. 192 * 193 * @param DataTable|Map $table 194 * @param string $label Label used to index this table in the array. 195 */ 196 public function addTable($table, $label) 197 { 198 $this->array[$label] = $table; 199 } 200 201 public function getRowFromIdSubDataTable($idSubtable) 202 { 203 $dataTables = $this->getDataTables(); 204 205 // find first datatable containing data 206 foreach ($dataTables as $subTable) { 207 $subTableRow = $subTable->getRowFromIdSubDataTable($idSubtable); 208 209 if (!empty($subTableRow)) { 210 return $subTableRow; 211 } 212 } 213 214 return null; 215 } 216 217 /** 218 * Returns a string output of this DataTable\Map (applying the default renderer to every {@link DataTable} 219 * of this DataTable\Map). 220 * 221 * @return string 222 */ 223 public function __toString() 224 { 225 $renderer = new Html(); 226 $renderer->setTable($this); 227 return (string)$renderer; 228 } 229 230 /** 231 * See {@link DataTable::enableRecursiveSort()}. 232 */ 233 public function enableRecursiveSort() 234 { 235 foreach ($this->getDataTables() as $table) { 236 $table->enableRecursiveSort(); 237 } 238 } 239 240 /** 241 * See {@link DataTable::disableFilter()}. 242 */ 243 public function disableFilter($className) 244 { 245 foreach ($this->getDataTables() as $table) { 246 $table->disableFilter($className); 247 } 248 } 249 250 /** 251 * @ignore 252 */ 253 public function disableRecursiveFilters() 254 { 255 foreach ($this->getDataTables() as $table) { 256 $table->disableRecursiveFilters(); 257 } 258 } 259 260 /** 261 * @ignore 262 */ 263 public function enableRecursiveFilters() 264 { 265 foreach ($this->getDataTables() as $table) { 266 $table->enableRecursiveFilters(); 267 } 268 } 269 270 /** 271 * Renames the given column in each contained {@link DataTable}. 272 * 273 * See {@link DataTable::renameColumn()}. 274 * 275 * @param string $oldName 276 * @param string $newName 277 */ 278 public function renameColumn($oldName, $newName) 279 { 280 foreach ($this->getDataTables() as $table) { 281 $table->renameColumn($oldName, $newName); 282 } 283 } 284 285 /** 286 * Deletes the specified columns in each contained {@link DataTable}. 287 * 288 * See {@link DataTable::deleteColumns()}. 289 * 290 * @param array $columns The columns to delete. 291 * @param bool $deleteRecursiveInSubtables This param is currently not used. 292 */ 293 public function deleteColumns($columns, $deleteRecursiveInSubtables = false) 294 { 295 foreach ($this->getDataTables() as $table) { 296 $table->deleteColumns($columns); 297 } 298 } 299 300 /** 301 * Deletes a table from the array of DataTables. 302 * 303 * @param string $id The label associated with {@link DataTable}. 304 */ 305 public function deleteRow($id) 306 { 307 unset($this->array[$id]); 308 } 309 310 /** 311 * Deletes the given column in every contained {@link DataTable}. 312 * 313 * @see DataTable::deleteColumn 314 * @param string $name 315 */ 316 public function deleteColumn($name) 317 { 318 foreach ($this->getDataTables() as $table) { 319 $table->deleteColumn($name); 320 } 321 } 322 323 /** 324 * Returns the array containing all column values in all contained {@link DataTable}s for the requested column. 325 * 326 * @param string $name The column name. 327 * @return array 328 */ 329 public function getColumn($name) 330 { 331 $values = array(); 332 333 foreach ($this->getDataTables() as $table) { 334 $moreValues = $table->getColumn($name); 335 foreach ($moreValues as &$value) { 336 $values[] = $value; 337 } 338 } 339 340 return $values; 341 } 342 343 /** 344 * Merges the rows of every child {@link DataTable} into a new one and 345 * returns it. This function will also set the label of the merged rows 346 * to the label of the {@link DataTable} they were originally from. 347 * 348 * The result of this function is determined by the type of DataTable 349 * this instance holds. If this DataTable\Map instance holds an array 350 * of DataTables, this function will transform it from: 351 * 352 * Label 0: 353 * DataTable(row1) 354 * Label 1: 355 * DataTable(row2) 356 * 357 * to: 358 * 359 * DataTable(row1[label = 'Label 0'], row2[label = 'Label 1']) 360 * 361 * If this instance holds an array of DataTable\Maps, this function will 362 * transform it from: 363 * 364 * Outer Label 0: // the outer DataTable\Map 365 * Inner Label 0: // one of the inner DataTable\Maps 366 * DataTable(row1) 367 * Inner Label 1: 368 * DataTable(row2) 369 * Outer Label 1: 370 * Inner Label 0: 371 * DataTable(row3) 372 * Inner Label 1: 373 * DataTable(row4) 374 * 375 * to: 376 * 377 * Inner Label 0: 378 * DataTable(row1[label = 'Outer Label 0'], row3[label = 'Outer Label 1']) 379 * Inner Label 1: 380 * DataTable(row2[label = 'Outer Label 0'], row4[label = 'Outer Label 1']) 381 * 382 * If this instance holds an array of DataTable\Maps, the 383 * metadata of the first child is used as the metadata of the result. 384 * 385 * This function can be used, for example, to smoosh IndexedBySite archive 386 * query results into one DataTable w/ different rows differentiated by site ID. 387 * 388 * Note: This DataTable/Map will be destroyed and will be no longer usable after the tables have been merged into 389 * the new dataTable to reduce memory usage. Destroying all DataTables witihn the Map also seems to fix a 390 * Segmentation Fault that occurred in the AllWebsitesDashboard when having > 16k sites. 391 * 392 * @return DataTable|Map 393 */ 394 public function mergeChildren() 395 { 396 $firstChild = reset($this->array); 397 398 if ($firstChild instanceof Map) { 399 $result = $firstChild->getEmptyClone(); 400 401 /** @var $subDataTableMap Map */ 402 foreach ($this->getDataTables() as $label => $subDataTableMap) { 403 foreach ($subDataTableMap->getDataTables() as $innerLabel => $subTable) { 404 if (!isset($result->array[$innerLabel])) { 405 $dataTable = new DataTable(); 406 $dataTable->setMetadataValues($subTable->getAllTableMetadata()); 407 408 $result->addTable($dataTable, $innerLabel); 409 } 410 411 $this->copyRowsAndSetLabel($result->array[$innerLabel], $subTable, $label); 412 } 413 } 414 } else { 415 $result = new DataTable(); 416 417 foreach ($this->getDataTables() as $label => $subTable) { 418 $this->copyRowsAndSetLabel($result, $subTable, $label); 419 Common::destroy($subTable); 420 } 421 422 $this->array = array(); 423 } 424 425 return $result; 426 } 427 428 /** 429 * Utility function used by mergeChildren. Copies the rows from one table, 430 * sets their 'label' columns to a value and adds them to another table. 431 * 432 * @param DataTable $toTable The table to copy rows to. 433 * @param DataTable $fromTable The table to copy rows from. 434 * @param string $label The value to set the 'label' column of every copied row. 435 */ 436 private function copyRowsAndSetLabel($toTable, $fromTable, $label) 437 { 438 foreach ($fromTable->getRows() as $fromRow) { 439 $oldColumns = $fromRow->getColumns(); 440 unset($oldColumns['label']); 441 442 $columns = array_merge(array('label' => $label), $oldColumns); 443 $row = new Row(array( 444 Row::COLUMNS => $columns, 445 Row::METADATA => $fromRow->getMetadata(), 446 Row::DATATABLE_ASSOCIATED => $fromRow->getIdSubDataTable() 447 )); 448 $toTable->addRow($row); 449 } 450 } 451 452 /** 453 * Sums a DataTable to all the tables in this array. 454 * 455 * _Note: Will only add `$tableToSum` if the childTable has some rows._ 456 * 457 * See {@link Piwik\DataTable::addDataTable()}. 458 * 459 * @param DataTable $tableToSum 460 */ 461 public function addDataTable(DataTable $tableToSum) 462 { 463 foreach ($this->getDataTables() as $childTable) { 464 $childTable->addDataTable($tableToSum); 465 } 466 } 467 468 /** 469 * Returns a new DataTable\Map w/ child tables that have had their 470 * subtables merged. 471 * 472 * See {@link DataTable::mergeSubtables()}. 473 * 474 * @return Map 475 */ 476 public function mergeSubtables() 477 { 478 $result = $this->getEmptyClone(); 479 foreach ($this->getDataTables() as $label => $childTable) { 480 $result->addTable($childTable->mergeSubtables(), $label); 481 } 482 return $result; 483 } 484 485 /** 486 * Returns a new DataTable\Map w/o any child DataTables, but with 487 * the same key name as this instance. 488 * 489 * @return Map 490 */ 491 public function getEmptyClone() 492 { 493 $dataTableMap = new Map; 494 $dataTableMap->setKeyName($this->getKeyName()); 495 return $dataTableMap; 496 } 497 498 /** 499 * Returns the intersection of children's metadata arrays (what they all have in common). 500 * 501 * @param string $name The metadata name. 502 * @return mixed 503 */ 504 public function getMetadataIntersectArray($name) 505 { 506 $data = array(); 507 foreach ($this->getDataTables() as $childTable) { 508 $childData = $childTable->getMetadata($name); 509 if (is_array($childData)) { 510 $data = array_intersect($data, $childData); 511 } 512 } 513 return array_values($data); 514 } 515 516 /** 517 * Delete row metadata by name in every row. 518 * 519 * @param $name 520 * @param bool $deleteRecursiveInSubtables 521 */ 522 public function deleteRowsMetadata($name, $deleteRecursiveInSubtables = false) 523 { 524 foreach ($this->getDataTables() as $table) { 525 $table->deleteRowsMetadata($name, $deleteRecursiveInSubtables); 526 } 527 } 528 529 /** 530 * See {@link DataTable::getColumns()}. 531 * 532 * @return array 533 */ 534 public function getColumns() 535 { 536 foreach ($this->getDataTables() as $childTable) { 537 if ($childTable->getRowsCount() > 0) { 538 return $childTable->getColumns(); 539 } 540 } 541 return array(); 542 } 543} 544