PageRenderTime 228ms CodeModel.GetById 23ms RepoModel.GetById 2ms app.codeStats 0ms

/core/DataTable.php

https://github.com/quarkness/piwik
PHP | 1255 lines | 607 code | 91 blank | 557 comment | 78 complexity | aa752823430ea54df62103581d71a6e4 MD5 | raw file
  1. <?php
  2. /**
  3. * Piwik - Open source web analytics
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. * @version $Id$
  8. *
  9. * @category Piwik
  10. * @package Piwik
  11. */
  12. /**
  13. * @see destroy()
  14. */
  15. require_once PIWIK_INCLUDE_PATH . '/core/Common.php';
  16. /**
  17. *
  18. * ---- DataTable
  19. * A DataTable is a data structure used to store complex tables of data.
  20. *
  21. * A DataTable is composed of multiple DataTable_Row.
  22. * A DataTable can be applied one or several DataTable_Filter.
  23. * A DataTable can be given to a DataTable_Renderer that would export the data under a given format (XML, HTML, etc.).
  24. *
  25. * A DataTable has the following features:
  26. * - serializable to be stored in the DB
  27. * - loadable from the serialized version
  28. * - efficient way of loading data from an external source (from a PHP array structure)
  29. * - very simple interface to get data from the table
  30. *
  31. * ---- DataTable_Row
  32. * A DataTableRow in the table is defined by
  33. * - multiple columns (a label, multiple values, ...)
  34. * - optional metadata
  35. * - optional - a sub DataTable associated to this row
  36. *
  37. * Simple row example:
  38. * - columns = array( 'label' => 'Firefox',
  39. * 'visitors' => 155,
  40. * 'pages' => 214,
  41. * 'bounce_rate' => 67)
  42. * - metadata = array('logo' => '/img/browsers/FF.png')
  43. * - no sub DataTable
  44. *
  45. * A more complex example would be a DataTable_Row that is associated to a sub DataTable.
  46. * For example, for the row of the search engine Google,
  47. * we want to get the list of keywords associated, with their statistics.
  48. * - columns = array( 'label' => 'Google',
  49. * 'visits' => 1550,
  50. * 'visits_length' => 514214,
  51. * 'returning_visits' => 77)
  52. * - metadata = array( 'logo' => '/img/search/google.png',
  53. * 'url' => 'http://google.com')
  54. * - DataTable = DataTable containing several DataTable_Row containing the keywords information for this search engine
  55. * Example of one DataTable_Row
  56. * - the keyword columns specific to this search engine =
  57. * array( 'label' => 'Piwik', // the keyword
  58. * 'visitors' => 155, // Piwik has been searched on Google by 155 visitors
  59. * 'pages' => 214 // Visitors coming from Google with the kwd Piwik have seen 214 pages
  60. * )
  61. * - the keyword metadata = array() // nothing here, but we could imagining storing the URL of the search in Google for example
  62. * - no subTable
  63. *
  64. *
  65. * ---- DataTable_Filter
  66. * A DataTable_Filter is a applied to a DataTable and so
  67. * can filter information in the multiple DataTable_Row.
  68. *
  69. * For example a DataTable_Filter can:
  70. * - remove rows from the table,
  71. * for example the rows' labels that do not match a given searched pattern
  72. * for example the rows' values that are less than a given percentage (low population)
  73. * - return a subset of the DataTable
  74. * for example a function that apply a limit: $offset, $limit
  75. * - add / remove columns
  76. * for example adding a column that gives the percentage of a given value
  77. * - add some metadata
  78. * for example the 'logo' path if the filter detects the logo
  79. * - edit the value, the label
  80. * - change the rows order
  81. * for example if we want to sort by Label alphabetical order, or by any column value
  82. *
  83. * When several DataTable_Filter are to be applied to a DataTable they are applied sequentially.
  84. * A DataTable_Filter is assigned a priority.
  85. * For example, filters that
  86. * - sort rows should be applied with the highest priority
  87. * - remove rows should be applied with a high priority as they prune the data and improve performance.
  88. *
  89. * ---- Code example
  90. *
  91. * $table = new DataTable();
  92. * $table->addRowsFromArray( array(...) );
  93. *
  94. * # sort the table by visits asc
  95. * $filter = new DataTable_Filter_Sort( $table, 'visits', 'asc');
  96. * $tableFiltered = $filter->getTableFiltered();
  97. *
  98. * # add a filter to select only the website with a label matching '*.com' (regular expression)
  99. * $filter = new DataTable_Filter_Pattern( $table, 'label', '*(.com)');
  100. * $tableFiltered = $filter->getTableFiltered();
  101. *
  102. * # keep the 20 elements from offset 15
  103. * $filter = new DataTable_Filter_Limit( $tableFiltered, 15, 20);
  104. * $tableFiltered = $filter->getTableFiltered();
  105. *
  106. * # add a column computing the percentage of visits
  107. * # params = table, column containing the value, new column name to add, number of total visits to use to compute the %
  108. * $filter = new DataTable_Filter_AddColumnPercentage( $tableFiltered, 'visits', 'visits_percentage', 2042);
  109. * $tableFiltered = $filter->getTableFiltered();
  110. *
  111. * # we get the table as XML
  112. * $xmlOutput = new DataTable_Exporter_Xml( $table );
  113. * $xmlOutput->setHeader( ... );
  114. * $xmlOutput->setColumnsToExport( array('visits', 'visits_percent', 'label') );
  115. * $XMLstring = $xmlOutput->getOutput();
  116. *
  117. *
  118. * ---- Other (ideas)
  119. * We can also imagine building a DataTable_Compare which would take N DataTable that have the same
  120. * structure and would compare them, by computing the percentages of differences, etc.
  121. *
  122. * For example
  123. * DataTable1 = [ keyword1, 1550 visits]
  124. * [ keyword2, 154 visits ]
  125. * DataTable2 = [ keyword1, 1004 visits ]
  126. * [ keyword3, 659 visits ]
  127. * DataTable_Compare = result of comparison of table1 with table2
  128. * [ keyword1, +154% ]
  129. * [ keyword2, +1000% ]
  130. * [ keyword3, -430% ]
  131. *
  132. * @see Piwik_DataTable_Row A Piwik_DataTable is composed of Piwik_DataTable_Row
  133. *
  134. * @package Piwik
  135. * @subpackage Piwik_DataTable
  136. */
  137. class Piwik_DataTable
  138. {
  139. /**
  140. * Array of Piwik_DataTable_Row
  141. *
  142. * @var array
  143. */
  144. protected $rows = array();
  145. /**
  146. * Array of parent IDs
  147. *
  148. * @var array
  149. */
  150. protected $parents = null;
  151. /**
  152. * Id assigned to the DataTable, used to lookup the table using the DataTable_Manager
  153. *
  154. * @var int
  155. */
  156. protected $currentId;
  157. /**
  158. * Current depth level of this data table
  159. * 0 is the parent data table
  160. *
  161. * @var int
  162. */
  163. protected $depthLevel = 0;
  164. /**
  165. * This flag is set to false once we modify the table in a way that outdates the index
  166. *
  167. * @var bool
  168. */
  169. protected $indexNotUpToDate = true;
  170. /**
  171. * This flag sets the index to be rebuild whenever a new row is added,
  172. * as opposed to re-building the full index when getRowFromLabel is called.
  173. * This is to optimize and not rebuild the full Index in the case where we
  174. * add row, getRowFromLabel, addRow, getRowFromLabel thousands of times.
  175. *
  176. * @var bool
  177. */
  178. protected $rebuildIndexContinuously = false;
  179. /**
  180. * Column name of last time the table was sorted
  181. *
  182. * @var string
  183. */
  184. protected $tableSortedBy = false;
  185. /**
  186. * List of Piwik_DataTable_Filter queued to this table
  187. *
  188. * @var array
  189. */
  190. protected $queuedFilters = array();
  191. /**
  192. * We keep track of the number of rows before applying the LIMIT filter that deletes some rows
  193. *
  194. * @var int
  195. */
  196. protected $rowsCountBeforeLimitFilter = 0;
  197. /**
  198. * Defaults to false for performance reasons (most of the time we don't need recursive sorting so we save a looping over the dataTable)
  199. *
  200. * @var bool
  201. */
  202. protected $enableRecursiveSort = false;
  203. /**
  204. * When the table and all subtables are loaded, this flag will be set to true to ensure filters are applied to all subtables
  205. *
  206. * @var bool
  207. */
  208. protected $enableRecursiveFilters = false;
  209. /*
  210. * @var Piwik_DataTable_Row
  211. */
  212. protected $summaryRow = null;
  213. const ID_SUMMARY_ROW = -1;
  214. const LABEL_SUMMARY_ROW = -1;
  215. const ID_PARENTS = -2;
  216. /**
  217. * Maximum nesting level
  218. *
  219. * @var int
  220. */
  221. const MAXIMUM_DEPTH_LEVEL_ALLOWED = 15;
  222. /**
  223. * Builds the DataTable, registers itself to the manager
  224. *
  225. */
  226. public function __construct()
  227. {
  228. $this->currentId = Piwik_DataTable_Manager::getInstance()->addTable($this);
  229. }
  230. /**
  231. * At destruction we free all memory
  232. */
  233. public function __destruct()
  234. {
  235. static $depth = 0;
  236. // destruct can be called several times
  237. if($depth < self::MAXIMUM_DEPTH_LEVEL_ALLOWED
  238. && isset($this->rows))
  239. {
  240. $depth++;
  241. foreach($this->getRows() as $row) {
  242. destroy($row);
  243. }
  244. unset($this->rows);
  245. Piwik_DataTable_Manager::getInstance()->setTableDeleted($this->getId());
  246. $depth--;
  247. }
  248. }
  249. /**
  250. * Sort the dataTable rows using the php callback function
  251. *
  252. * @param string $functionCallback
  253. * @param string $columnSortedBy The column name. Used to then ask the datatable what column are you sorted by
  254. */
  255. public function sort( $functionCallback, $columnSortedBy )
  256. {
  257. $this->indexNotUpToDate = true;
  258. $this->tableSortedBy = $columnSortedBy;
  259. usort( $this->rows, $functionCallback );
  260. if($this->enableRecursiveSort === true)
  261. {
  262. foreach($this->getRows() as $row)
  263. {
  264. if(($idSubtable = $row->getIdSubDataTable()) !== null)
  265. {
  266. $table = Piwik_DataTable_Manager::getInstance()->getTable($idSubtable);
  267. $table->enableRecursiveSort();
  268. $table->sort($functionCallback, $columnSortedBy);
  269. }
  270. }
  271. }
  272. }
  273. public function getSortedByColumnName()
  274. {
  275. return $this->tableSortedBy;
  276. }
  277. /**
  278. * Enables the recursive sort. Means that when using $table->sort()
  279. * it will also sort all subtables using the same callback
  280. */
  281. public function enableRecursiveSort()
  282. {
  283. $this->enableRecursiveSort = true;
  284. }
  285. public function enableRecursiveFilters()
  286. {
  287. $this->enableRecursiveFilters = true;
  288. }
  289. /**
  290. * Returns the number of rows before we applied the limit filter
  291. *
  292. * @return int
  293. */
  294. public function getRowsCountBeforeLimitFilter()
  295. {
  296. $toReturn = $this->rowsCountBeforeLimitFilter;
  297. if($toReturn == 0)
  298. {
  299. return $this->getRowsCount();
  300. }
  301. return $toReturn;
  302. }
  303. /**
  304. * Saves the current number of rows
  305. */
  306. function setRowsCountBeforeLimitFilter()
  307. {
  308. $this->rowsCountBeforeLimitFilter = $this->getRowsCount();
  309. }
  310. /**
  311. * Apply a filter to this datatable
  312. *
  313. * @param string $className Class name, eg. "Sort" or "Piwik_DataTable_Filter_Sort"
  314. * @param array $parameters Array of parameters to the filter, eg. array('nb_visits', 'asc')
  315. */
  316. public function filter( $className, $parameters = array() )
  317. {
  318. if(!class_exists($className, false))
  319. {
  320. $className = "Piwik_DataTable_Filter_" . $className;
  321. }
  322. $reflectionObj = new ReflectionClass($className);
  323. // the first parameter of a filter is the DataTable
  324. // we add the current datatable as the parameter
  325. $parameters = array_merge(array($this), $parameters);
  326. $filter = $reflectionObj->newInstanceArgs($parameters);
  327. $filter->enableRecursive( $this->enableRecursiveFilters );
  328. $filter->filter($this);
  329. }
  330. /**
  331. * Queue a DataTable_Filter that will be applied when applyQueuedFilters() is called.
  332. * (just before sending the datatable back to the browser (or API, etc.)
  333. *
  334. * @param string $className The class name of the filter, eg. Piwik_DataTable_Filter_Limit
  335. * @param array $parameters The parameters to give to the filter, eg. array( $offset, $limit) for the filter Piwik_DataTable_Filter_Limit
  336. */
  337. public function queueFilter( $className, $parameters = array() )
  338. {
  339. if(!is_array($parameters))
  340. {
  341. $parameters = array($parameters);
  342. }
  343. $this->queuedFilters[] = array('className' => $className, 'parameters' => $parameters);
  344. }
  345. /**
  346. * Apply all filters that were previously queued to this table
  347. * @see queueFilter()
  348. */
  349. public function applyQueuedFilters()
  350. {
  351. foreach($this->queuedFilters as $filter)
  352. {
  353. $this->filter($filter['className'], $filter['parameters']);
  354. }
  355. $this->queuedFilters = array();
  356. }
  357. /**
  358. * Adds a new DataTable to this DataTable
  359. * Go through all the rows of the new DataTable and applies the algorithm:
  360. * - if a row in $table doesnt exist in $this we add the new row to $this
  361. * - if a row exists in both $table and $this we sum the columns values into $this
  362. * - if a row in $this doesnt exist in $table we add in $this the row of $table without modification
  363. *
  364. * A common row to 2 DataTable is defined by the same label
  365. *
  366. * @example tests/core/DataTable.test.php
  367. */
  368. public function addDataTable( Piwik_DataTable $tableToSum )
  369. {
  370. foreach($tableToSum->getRows() as $row)
  371. {
  372. $labelToLookFor = $row->getColumn('label');
  373. $rowFound = $this->getRowFromLabel( $labelToLookFor );
  374. if($rowFound === false)
  375. {
  376. if( $labelToLookFor === self::LABEL_SUMMARY_ROW )
  377. {
  378. $this->addSummaryRow( $row );
  379. }
  380. else
  381. {
  382. $this->addRow( $row );
  383. }
  384. }
  385. else
  386. {
  387. $rowFound->sumRow( $row );
  388. // if the row to add has a subtable whereas the current row doesn't
  389. // we simply add it (cloning the subtable)
  390. // if the row has the subtable already
  391. // then we have to recursively sum the subtables
  392. if(($idSubTable = $row->getIdSubDataTable()) !== null)
  393. {
  394. $rowFound->sumSubtable( Piwik_DataTable_Manager::getInstance()->getTable($idSubTable) );
  395. }
  396. }
  397. }
  398. }
  399. /**
  400. * Returns the Piwik_DataTable_Row that has a column 'label' with the value $label
  401. *
  402. * @param string $label Value of the column 'label' of the row to return
  403. * @return Piwik_DataTable_Row |false The row if found, false otherwise
  404. */
  405. public function getRowFromLabel( $label )
  406. {
  407. $rowId = $this->getRowIdFromLabel($label);
  408. if(is_int($rowId))
  409. {
  410. return $this->rows[$rowId];
  411. }
  412. return $rowId;
  413. }
  414. public function getRowIdFromLabel($label)
  415. {
  416. $this->rebuildIndexContinuously = true;
  417. if($this->indexNotUpToDate)
  418. {
  419. $this->rebuildIndex();
  420. }
  421. if($label === self::LABEL_SUMMARY_ROW
  422. && !is_null($this->summaryRow))
  423. {
  424. return $this->summaryRow;
  425. }
  426. $label = (string)$label;
  427. if(!isset($this->rowsIndexByLabel[$label]))
  428. {
  429. return false;
  430. }
  431. return $this->rowsIndexByLabel[$label];
  432. }
  433. /**
  434. * Returns a Piwik_DataTable that has only the one column that matches $label.
  435. * If no matches are found, an empty data table is returned.
  436. *
  437. * @param string $label Value of the column 'label' to search for
  438. * @return Piwik_DataTable
  439. */
  440. public function getFilteredTableFromLabel($label)
  441. {
  442. $newTable = $this->getEmptyClone();
  443. $row = $this->getRowFromLabel($label);
  444. if ($row !== false)
  445. {
  446. $newTable->addRow($row);
  447. }
  448. return $newTable;
  449. }
  450. /**
  451. * Get an empty table with the same properties as this one
  452. *
  453. * @return Piwik_DataTable
  454. */
  455. public function getEmptyClone()
  456. {
  457. $clone = new Piwik_DataTable;
  458. $clone->queuedFilters = $this->queuedFilters;
  459. return $clone;
  460. }
  461. /**
  462. * Rebuilds the index used to lookup a row by label
  463. */
  464. private function rebuildIndex()
  465. {
  466. foreach($this->rows as $id => $row)
  467. {
  468. $label = $row->getColumn('label');
  469. if($label !== false)
  470. {
  471. $this->rowsIndexByLabel[$label] = $id;
  472. }
  473. }
  474. $this->indexNotUpToDate = false;
  475. }
  476. /**
  477. * Returns the ith row in the array
  478. *
  479. * @param int $id
  480. * @return Piwik_DataTable_Row or false if not found
  481. */
  482. public function getRowFromId($id)
  483. {
  484. if(!isset($this->rows[$id]))
  485. {
  486. if($id == self::ID_SUMMARY_ROW
  487. && !is_null($this->summaryRow))
  488. {
  489. return $this->summaryRow;
  490. }
  491. return false;
  492. }
  493. return $this->rows[$id];
  494. }
  495. /**
  496. * Returns a row that has the subtable ID matching the parameter
  497. *
  498. * @param int $idSubTable
  499. * @return Piwik_DataTable_Row or false if not found
  500. */
  501. public function getRowFromIdSubDataTable($idSubTable)
  502. {
  503. $idSubTable = (int)$idSubTable;
  504. foreach($this->rows as $row)
  505. {
  506. if($row->getIdSubDataTable() === $idSubTable)
  507. {
  508. return $row;
  509. }
  510. }
  511. return false;
  512. }
  513. /**
  514. * Add a row to the table and rebuild the index if necessary
  515. *
  516. * @param Piwik_DataTable_Row $row to add at the end of the array
  517. */
  518. public function addRow( Piwik_DataTable_Row $row )
  519. {
  520. $this->rows[] = $row;
  521. if(!$this->indexNotUpToDate
  522. && $this->rebuildIndexContinuously)
  523. {
  524. $label = $row->getColumn('label');
  525. if($label !== false)
  526. {
  527. $this->rowsIndexByLabel[$label] = count($this->rows)-1;
  528. }
  529. $this->indexNotUpToDate = false;
  530. }
  531. }
  532. /**
  533. * Sets the summary row (a dataTable can have only one summary row)
  534. *
  535. * @param Piwik_DataTable_Row $row
  536. */
  537. public function addSummaryRow( Piwik_DataTable_Row $row )
  538. {
  539. $this->summaryRow = $row;
  540. }
  541. /**
  542. * Returns the dataTable ID
  543. *
  544. * @return int
  545. */
  546. public function getId()
  547. {
  548. return $this->currentId;
  549. }
  550. /**
  551. * Adds a new row from a PHP array data structure
  552. *
  553. * @param array $row, eg. array(Piwik_DataTable_Row::COLUMNS => array( 'visits' => 13, 'test' => 'toto'),)
  554. */
  555. public function addRowFromArray( $row )
  556. {
  557. $this->addRowsFromArray(array($row));
  558. }
  559. /**
  560. * Adds a new row a PHP array data structure
  561. *
  562. * @param array $row, eg. array('name' => 'google analytics', 'license' => 'commercial')
  563. */
  564. public function addRowFromSimpleArray( $row )
  565. {
  566. $this->addRowsFromSimpleArray(array($row));
  567. }
  568. /**
  569. * Returns the array of Piwik_DataTable_Row
  570. *
  571. * @return Piwik_DataTable_Row
  572. */
  573. public function getRows()
  574. {
  575. if(is_null($this->summaryRow))
  576. {
  577. return $this->rows;
  578. }
  579. else
  580. {
  581. return $this->rows + array(self::ID_SUMMARY_ROW => $this->summaryRow);
  582. }
  583. }
  584. /**
  585. * Returns the array containing all rows values for the requested column
  586. *
  587. * @return array
  588. */
  589. public function getColumn( $name )
  590. {
  591. $columnValues = array();
  592. foreach($this->getRows() as $row)
  593. {
  594. $columnValues[] = $row->getColumn($name);
  595. }
  596. return $columnValues;
  597. }
  598. /**
  599. * Returns an array containing the rows Metadata values
  600. *
  601. * @param string $name Metadata column to return
  602. * @return array
  603. */
  604. public function getRowsMetadata( $name )
  605. {
  606. $metadataValues = array();
  607. foreach($this->getRows() as $row)
  608. {
  609. $metadataValues[] = $row->getMetadata($name);
  610. }
  611. return $metadataValues;
  612. }
  613. /**
  614. * Returns the number of rows in the table
  615. *
  616. * @return int
  617. */
  618. public function getRowsCount()
  619. {
  620. if(is_null($this->summaryRow))
  621. {
  622. return count($this->rows);
  623. }
  624. else
  625. {
  626. return count($this->rows) + 1;
  627. }
  628. }
  629. /**
  630. * Returns the first row of the DataTable
  631. *
  632. * @return Piwik_DataTable_Row
  633. */
  634. public function getFirstRow()
  635. {
  636. if(count($this->rows) == 0)
  637. {
  638. if(!is_null($this->summaryRow))
  639. {
  640. return $this->summaryRow;
  641. }
  642. return false;
  643. }
  644. $row = array_slice($this->rows, 0, 1);
  645. return $row[0];
  646. }
  647. /**
  648. * Returns the last row of the DataTable
  649. *
  650. * @return Piwik_DataTable_Row
  651. */
  652. public function getLastRow()
  653. {
  654. if(!is_null($this->summaryRow))
  655. {
  656. return $this->summaryRow;
  657. }
  658. if(count($this->rows) == 0)
  659. {
  660. return false;
  661. }
  662. $row = array_slice($this->rows, -1);
  663. return $row[0];
  664. }
  665. /**
  666. * Returns the sum of the number of rows of all the subtables
  667. * + the number of rows in the parent table
  668. *
  669. * @return int
  670. */
  671. public function getRowsCountRecursive()
  672. {
  673. $totalCount = 0;
  674. foreach($this->rows as $row)
  675. {
  676. if(($idSubTable = $row->getIdSubDataTable()) !== null)
  677. {
  678. $subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable);
  679. $count = $subTable->getRowsCountRecursive();
  680. $totalCount += $count;
  681. }
  682. }
  683. $totalCount += $this->getRowsCount();
  684. return $totalCount;
  685. }
  686. /**
  687. * Delete a given column $name in all the rows
  688. *
  689. * @param string $name
  690. */
  691. public function deleteColumn( $name )
  692. {
  693. $this->deleteColumns(array($name));
  694. }
  695. /**
  696. * Rename a column in all rows
  697. *
  698. * @param string $oldName Old column name
  699. * @param string $newName New column name
  700. */
  701. public function renameColumn( $oldName, $newName )
  702. {
  703. foreach($this->getRows() as $row)
  704. {
  705. $row->renameColumn($oldName, $newName);
  706. if(($idSubDataTable = $row->getIdSubDataTable()) !== null)
  707. {
  708. Piwik_DataTable_Manager::getInstance()->getTable($idSubDataTable)->renameColumn($oldName, $newName);
  709. }
  710. }
  711. if(!is_null($this->summaryRow))
  712. {
  713. $this->summaryRow->renameColumn($oldName, $newName);
  714. }
  715. }
  716. /**
  717. * Delete columns by name in all rows
  718. *
  719. * @param string $name
  720. */
  721. public function deleteColumns($names, $deleteRecursiveInSubtables = false)
  722. {
  723. foreach($this->getRows() as $row)
  724. {
  725. foreach($names as $name)
  726. {
  727. $row->deleteColumn($name);
  728. }
  729. if(($idSubDataTable = $row->getIdSubDataTable()) !== null)
  730. {
  731. Piwik_DataTable_Manager::getInstance()->getTable($idSubDataTable)->deleteColumns($names, $deleteRecursiveInSubtables);
  732. }
  733. }
  734. if(!is_null($this->summaryRow))
  735. {
  736. foreach($names as $name)
  737. {
  738. $this->summaryRow->deleteColumn($name);
  739. }
  740. }
  741. }
  742. /**
  743. * Deletes the ith row
  744. *
  745. * @param int $key
  746. * @throws Exception if the row $id cannot be found
  747. */
  748. public function deleteRow( $id )
  749. {
  750. if($id === self::ID_SUMMARY_ROW)
  751. {
  752. $this->summaryRow = null;
  753. return;
  754. }
  755. if(!isset($this->rows[$id]))
  756. {
  757. throw new Exception("Trying to delete unknown row with idkey = $id");
  758. }
  759. unset($this->rows[$id]);
  760. }
  761. /**
  762. * Deletes all row from offset, offset + limit.
  763. * If limit is null then limit = $table->getRowsCount()
  764. *
  765. * @param int $offset
  766. * @param int $limit
  767. */
  768. public function deleteRowsOffset( $offset, $limit = null )
  769. {
  770. if($limit === 0)
  771. {
  772. return 0;
  773. }
  774. $count = $this->getRowsCount();
  775. if($offset >= $count)
  776. {
  777. return 0;
  778. }
  779. // if we delete until the end, we delete the summary row as well
  780. if( is_null($limit)
  781. || $limit >= $count )
  782. {
  783. $this->summaryRow = null;
  784. }
  785. if(is_null($limit))
  786. {
  787. $spliced = array_splice($this->rows, $offset);
  788. }
  789. else
  790. {
  791. $spliced = array_splice($this->rows, $offset, $limit);
  792. }
  793. $countDeleted = count($spliced);
  794. return $countDeleted;
  795. }
  796. /**
  797. * Deletes the rows from the list of rows ID
  798. *
  799. * @param array $aKeys ID of the rows to delete
  800. * @throws Exception if any of the row to delete couldn't be found
  801. */
  802. public function deleteRows( array $aKeys )
  803. {
  804. foreach($aKeys as $key)
  805. {
  806. $this->deleteRow($key);
  807. }
  808. }
  809. /**
  810. * Returns a simple output of the DataTable for easy visualization
  811. * Example: echo $datatable;
  812. *
  813. * @return string
  814. */
  815. public function __toString()
  816. {
  817. $renderer = new Piwik_DataTable_Renderer_Html();
  818. $renderer->setTable($this);
  819. return (string)$renderer;
  820. }
  821. /**
  822. * Returns true if both DataTable are exactly the same.
  823. * Used in unit tests.
  824. *
  825. * @param Piwik_DataTable $table1
  826. * @param Piwik_DataTable $table2
  827. * @return bool
  828. */
  829. static public function isEqual(Piwik_DataTable $table1, Piwik_DataTable $table2)
  830. {
  831. $rows1 = $table1->getRows();
  832. $rows2 = $table2->getRows();
  833. $table1->rebuildIndex();
  834. $table2->rebuildIndex();
  835. if($table1->getRowsCount() != $table2->getRowsCount())
  836. {
  837. return false;
  838. }
  839. foreach($rows1 as $row1)
  840. {
  841. $row2 = $table2->getRowFromLabel($row1->getColumn('label'));
  842. if($row2 === false
  843. || !Piwik_DataTable_Row::isEqual($row1,$row2))
  844. {
  845. return false;
  846. }
  847. }
  848. return true;
  849. }
  850. /**
  851. * The serialization returns a one dimension array containing all the
  852. * serialized DataTable contained in this DataTable.
  853. * We save DataTable in serialized format in the Database.
  854. * Each row of this returned PHP array will be a row in the DB table.
  855. * At the end of the method execution, the dataTable may be truncated (if $maximum* parameters are set).
  856. *
  857. * The keys of the array are very important as they are used to define the DataTable
  858. *
  859. * IMPORTANT: The main table (level 0, parent of all tables) will always be indexed by 0
  860. * even it was created after some other tables.
  861. * It also means that all the parent tables (level 0) will be indexed with 0 in their respective
  862. * serialized arrays. You should never lookup a parent table using the getTable( $id = 0) as it
  863. * won't work.
  864. *
  865. * @throws Exception if an infinite recursion is found (a table row's has a subtable that is one of its parent table)
  866. * @param int If not null, defines the number of rows maximum of the serialized dataTable
  867. * If $addSummaryRowAfterNRows is less than the size of the table, a SummaryRow will be added at the end of the table, that
  868. * is the sum of the values of all the rows after the Nth row. All the rows after the Nth row will be deleted.
  869. *
  870. * @return array Serialized arrays
  871. * array( // Datatable level0
  872. * 0 => 'eghuighahgaueytae78yaet7yaetae',
  873. *
  874. * // first Datatable level1
  875. * 1 => 'gaegae gh gwrh guiwh uigwhuige',
  876. *
  877. * //second Datatable level1
  878. * 2 => 'gqegJHUIGHEQjkgneqjgnqeugUGEQHGUHQE',
  879. *
  880. * //first Datatable level3 (child of second Datatable level1 for example)
  881. * 3 => 'eghuighahgaueytae78yaet7yaetaeGRQWUBGUIQGH&QE',
  882. * );
  883. */
  884. public function getSerialized( $maximumRowsInDataTable = null,
  885. $maximumRowsInSubDataTable = null,
  886. $columnToSortByBeforeTruncation = null )
  887. {
  888. static $depth = 0;
  889. if($depth > self::MAXIMUM_DEPTH_LEVEL_ALLOWED)
  890. {
  891. $depth = 0;
  892. throw new Exception("Maximum recursion level of ".self::MAXIMUM_DEPTH_LEVEL_ALLOWED. " reached. You have probably set a DataTable_Row with an associated DataTable which belongs already to its parent hierarchy.");
  893. }
  894. if( !is_null($maximumRowsInDataTable) )
  895. {
  896. $this->filter('AddSummaryRow',
  897. array( $maximumRowsInDataTable - 1,
  898. Piwik_DataTable::LABEL_SUMMARY_ROW,
  899. $columnToSortByBeforeTruncation)
  900. );
  901. }
  902. // For each row, get the serialized row
  903. // If it is associated to a sub table, get the serialized table recursively ;
  904. // but returns all serialized tables and subtable in an array of 1 dimension
  905. $aSerializedDataTable = array();
  906. foreach($this->rows as $row)
  907. {
  908. if(($idSubTable = $row->getIdSubDataTable()) !== null)
  909. {
  910. $subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable);
  911. $depth++;
  912. $aSerializedDataTable = $aSerializedDataTable + $subTable->getSerialized( $maximumRowsInSubDataTable, $maximumRowsInSubDataTable, $columnToSortByBeforeTruncation );
  913. $depth--;
  914. }
  915. }
  916. // we load the current Id of the DataTable
  917. $forcedId = $this->getId();
  918. // if the datatable is the parent we force the Id at 0 (this is part of the specification)
  919. if($depth == 0)
  920. {
  921. $forcedId = 0;
  922. }
  923. // we then serialize the rows and store them in the serialized dataTable
  924. $addToRows = array( self::ID_SUMMARY_ROW => $this->summaryRow );
  925. if ($this->parents && Zend_Registry::get('config')->General->enable_archive_parents_of_datatable)
  926. {
  927. $addToRows[self::ID_PARENTS] = $this->parents;
  928. }
  929. $aSerializedDataTable[$forcedId] = serialize($this->rows + $addToRows);
  930. return $aSerializedDataTable;
  931. }
  932. /**
  933. * Load a serialized string of a datatable.
  934. *
  935. * Does not load recursively all the sub DataTable.
  936. * They will be loaded only when requesting them specifically.
  937. *
  938. * The function creates all the necessary DataTable_Row
  939. *
  940. * @param string string of serialized datatable
  941. */
  942. public function addRowsFromSerializedArray( $stringSerialized )
  943. {
  944. $serialized = unserialize($stringSerialized);
  945. if($serialized === false)
  946. {
  947. throw new Exception("The unserialization has failed!");
  948. }
  949. $this->addRowsFromArray($serialized);
  950. }
  951. /**
  952. * Loads the DataTable from a PHP array data structure
  953. *
  954. * @param array Array with the following structure
  955. * array(
  956. * // row1
  957. * array(
  958. * Piwik_DataTable_Row::COLUMNS => array( col1_name => value1, col2_name => value2, ...),
  959. * Piwik_DataTable_Row::METADATA => array( metadata1_name => value1, ...), // see Piwik_DataTable_Row
  960. *
  961. * ),
  962. *
  963. * // row2
  964. * array( ... ),
  965. *
  966. * )
  967. */
  968. public function addRowsFromArray( $array )
  969. {
  970. foreach($array as $id => $row)
  971. {
  972. if($id == self::ID_PARENTS)
  973. {
  974. $this->parents = $row;
  975. continue;
  976. }
  977. if(is_array($row))
  978. {
  979. $row = new Piwik_DataTable_Row($row);
  980. }
  981. if($id == self::ID_SUMMARY_ROW)
  982. {
  983. $this->summaryRow = $row;
  984. }
  985. else
  986. {
  987. $this->addRow($row);
  988. }
  989. }
  990. }
  991. /**
  992. * Loads the data from a simple php array.
  993. * Basically maps a simple multidimensional php array to a DataTable.
  994. * Not recursive (if a row contains a php array itself, it won't be loaded)
  995. *
  996. * @param array Array with the simple structure:
  997. * array(
  998. * array( col1_name => valueA, col2_name => valueC, ...),
  999. * array( col1_name => valueB, col2_name => valueD, ...),
  1000. * )
  1001. */
  1002. public function addRowsFromSimpleArray( $array )
  1003. {
  1004. if(count($array) === 0)
  1005. {
  1006. return;
  1007. }
  1008. // we define an exception we may throw if at one point we notice that we cannot handle the data structure
  1009. $e = new Exception(" Data structure returned is not convertible in the requested format.".
  1010. " Try to call this method with the parameters '&format=original&serialize=1'".
  1011. "; you will get the original php data structure serialized.".
  1012. " The data structure looks like this: \n \$data = " . var_export($array, true) . "; ");
  1013. // first pass to see if the array has the structure
  1014. // array(col1_name => val1, col2_name => val2, etc.)
  1015. // with val* that are never arrays (only strings/numbers/bool/etc.)
  1016. // if we detect such a "simple" data structure we convert it to a row with the correct columns' names
  1017. $thisIsNotThatSimple = false;
  1018. foreach($array as $columnName => $columnValue )
  1019. {
  1020. if(is_array($columnValue) || is_object($columnValue))
  1021. {
  1022. $thisIsNotThatSimple = true;
  1023. break;
  1024. }
  1025. }
  1026. if($thisIsNotThatSimple === false)
  1027. {
  1028. // case when the array is indexed by the default numeric index
  1029. if( array_keys($array) == array_keys(array_fill(0, count($array), true)) )
  1030. {
  1031. foreach($array as $row)
  1032. {
  1033. $this->addRow( new Piwik_DataTable_Row( array( Piwik_DataTable_Row::COLUMNS => array($row) ) ) );
  1034. }
  1035. }
  1036. else
  1037. {
  1038. $this->addRow( new Piwik_DataTable_Row( array( Piwik_DataTable_Row::COLUMNS => $array ) ) );
  1039. }
  1040. // we have converted our simple array to one single row
  1041. // => we exit the method as the job is now finished
  1042. return;
  1043. }
  1044. foreach($array as $key => $row)
  1045. {
  1046. // stuff that looks like a line
  1047. if(is_array($row))
  1048. {
  1049. /**
  1050. * We make sure we can convert this PHP array without losing information.
  1051. * We are able to convert only simple php array (no strings keys, no sub arrays, etc.)
  1052. *
  1053. */
  1054. // if the key is a string it means that some information was contained in this key.
  1055. // it cannot be lost during the conversion. Because we are not able to handle properly
  1056. // this key, we throw an explicit exception.
  1057. if(is_string($key))
  1058. {
  1059. throw $e;
  1060. }
  1061. // if any of the sub elements of row is an array we cannot handle this data structure...
  1062. foreach($row as $subRow)
  1063. {
  1064. if(is_array($subRow))
  1065. {
  1066. throw $e;
  1067. }
  1068. }
  1069. $row = new Piwik_DataTable_Row( array( Piwik_DataTable_Row::COLUMNS => $row ) );
  1070. }
  1071. // other (string, numbers...) => we build a line from this value
  1072. else
  1073. {
  1074. $row = new Piwik_DataTable_Row( array( Piwik_DataTable_Row::COLUMNS => array($key => $row)) );
  1075. }
  1076. $this->addRow($row);
  1077. }
  1078. }
  1079. /**
  1080. * Rewrites the input $array
  1081. * array (
  1082. * LABEL => array(col1 => X, col2 => Y),
  1083. * LABEL2 => array(col1 => X, col2 => Y),
  1084. * )
  1085. * to the structure
  1086. * array (
  1087. * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL, col1 => X, col2 => Y)),
  1088. * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL2, col1 => X, col2 => Y)),
  1089. * )
  1090. *
  1091. * It also works with array having only one value per row, eg.
  1092. * array (
  1093. * LABEL => X,
  1094. * LABEL2 => Y,
  1095. * )
  1096. * would be converted to the structure
  1097. * array (
  1098. * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL, 'value' => X)),
  1099. * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL2, 'value' => Y)),
  1100. * )
  1101. *
  1102. * The optional parameter $subtablePerLabel is an array of subTable associated to the rows of the $array
  1103. * For example if $subtablePerLabel is given
  1104. * array(
  1105. * LABEL => #Piwik_DataTable_ForLABEL,
  1106. * LABEL2 => #Piwik_DataTable_ForLABEL2,
  1107. * )
  1108. *
  1109. * the $array would become
  1110. * array (
  1111. * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL, col1 => X, col2 => Y),
  1112. * Piwik_DataTable_Row::DATATABLE_ASSOCIATED => #ID DataTable For LABEL
  1113. * ),
  1114. * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL2, col1 => X, col2 => Y)
  1115. * Piwik_DataTable_Row::DATATABLE_ASSOCIATED => #ID2 DataTable For LABEL2
  1116. * ),
  1117. * )
  1118. *
  1119. * @param array $array See method description
  1120. * @param array|null $subtablePerLabel see method description
  1121. */
  1122. public function addRowsFromArrayWithIndexLabel( $array, $subtablePerLabel = null)
  1123. {
  1124. $cleanRow = array();
  1125. foreach($array as $label => $row)
  1126. {
  1127. if(!is_array($row))
  1128. {
  1129. $row = array('value' => $row);
  1130. }
  1131. $cleanRow[Piwik_DataTable_Row::DATATABLE_ASSOCIATED] = null;
  1132. // we put the 'label' column first as it looks prettier in API results
  1133. $cleanRow[Piwik_DataTable_Row::COLUMNS] = array('label' => $label) + $row;
  1134. if(!is_null($subtablePerLabel)
  1135. // some rows of this table don't have subtables
  1136. // (for example case of campaigns without keywords)
  1137. && isset($subtablePerLabel[$label])
  1138. )
  1139. {
  1140. $cleanRow[Piwik_DataTable_Row::DATATABLE_ASSOCIATED] = $subtablePerLabel[$label];
  1141. }
  1142. $this->addRow( new Piwik_DataTable_Row($cleanRow) );
  1143. }
  1144. }
  1145. /**
  1146. * Set the array of parent ids
  1147. * @param array $parents
  1148. */
  1149. public function setParents($parents)
  1150. {
  1151. $this->parents = $parents;
  1152. }
  1153. /**
  1154. * Get parents
  1155. * @return array of all parents, root level first
  1156. */
  1157. public function getParents() {
  1158. if ($this->parents == null)
  1159. {
  1160. return array();
  1161. }
  1162. return $this->parents;
  1163. }
  1164. }