PageRenderTime 75ms CodeModel.GetById 6ms RepoModel.GetById 0ms app.codeStats 0ms

/core/DataTable/Row.php

https://github.com/CodeYellowBV/piwik
PHP | 729 lines | 393 code | 70 blank | 266 comment | 74 complexity | 75481ac108cf0fd319ff36ff078e28fd MD5 | raw file
Possible License(s): LGPL-3.0, JSON, MIT, GPL-3.0, LGPL-2.1, GPL-2.0, AGPL-1.0, BSD-2-Clause, BSD-3-Clause
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. */
  9. namespace Piwik\DataTable;
  10. use Exception;
  11. use Piwik\DataTable;
  12. use Piwik\Metrics;
  13. /**
  14. * This is what a {@link Piwik\DataTable} is composed of.
  15. *
  16. * DataTable rows contain columns, metadata and a subtable ID. Columns and metadata
  17. * are stored as an array of name => value mappings.
  18. *
  19. *
  20. * @api
  21. */
  22. class Row
  23. {
  24. /**
  25. * List of columns that cannot be summed. An associative array for speed.
  26. *
  27. * @var array
  28. */
  29. private static $unsummableColumns = array(
  30. 'label' => true,
  31. 'full_url' => true // column used w/ old Piwik versions,
  32. );
  33. /**
  34. * This array contains the row information:
  35. * - array indexed by self::COLUMNS contains the columns, pairs of (column names, value)
  36. * - (optional) array indexed by self::METADATA contains the metadata, pairs of (metadata name, value)
  37. * - (optional) integer indexed by self::DATATABLE_ASSOCIATED contains the ID of the DataTable associated to this row.
  38. * This ID can be used to read the DataTable from the DataTable_Manager.
  39. *
  40. * @var array
  41. * @see constructor for more information
  42. * @ignore
  43. */
  44. public $c = array();
  45. private $subtableIdWasNegativeBeforeSerialize = false;
  46. // @see sumRow - implementation detail
  47. public $maxVisitsSummed = 0;
  48. const COLUMNS = 0;
  49. const METADATA = 1;
  50. const DATATABLE_ASSOCIATED = 3;
  51. /**
  52. * Constructor.
  53. *
  54. * @param array $row An array with the following structure:
  55. *
  56. * array(
  57. * Row::COLUMNS => array('label' => 'Piwik',
  58. * 'column1' => 42,
  59. * 'visits' => 657,
  60. * 'time_spent' => 155744),
  61. * Row::METADATA => array('logo' => 'test.png'),
  62. * Row::DATATABLE_ASSOCIATED => $subtable // DataTable object
  63. * // (but in the row only the ID will be stored)
  64. * )
  65. */
  66. public function __construct($row = array())
  67. {
  68. $this->c[self::COLUMNS] = array();
  69. $this->c[self::METADATA] = array();
  70. $this->c[self::DATATABLE_ASSOCIATED] = null;
  71. if (isset($row[self::COLUMNS])) {
  72. $this->c[self::COLUMNS] = $row[self::COLUMNS];
  73. }
  74. if (isset($row[self::METADATA])) {
  75. $this->c[self::METADATA] = $row[self::METADATA];
  76. }
  77. if (isset($row[self::DATATABLE_ASSOCIATED])
  78. && $row[self::DATATABLE_ASSOCIATED] instanceof DataTable
  79. ) {
  80. $this->setSubtable($row[self::DATATABLE_ASSOCIATED]);
  81. }
  82. }
  83. /**
  84. * Because $this->c[self::DATATABLE_ASSOCIATED] is negative when the table is in memory,
  85. * we must prior to serialize() call, make sure the ID is saved as positive integer
  86. *
  87. * Only serialize the "c" member
  88. * @ignore
  89. */
  90. public function __sleep()
  91. {
  92. if (!empty($this->c[self::DATATABLE_ASSOCIATED])
  93. && $this->c[self::DATATABLE_ASSOCIATED] < 0
  94. ) {
  95. $this->c[self::DATATABLE_ASSOCIATED] = -1 * $this->c[self::DATATABLE_ASSOCIATED];
  96. $this->subtableIdWasNegativeBeforeSerialize = true;
  97. }
  98. return array('c');
  99. }
  100. /**
  101. * Must be called after the row was serialized and __sleep was called.
  102. * @ignore
  103. */
  104. public function cleanPostSerialize()
  105. {
  106. if ($this->subtableIdWasNegativeBeforeSerialize) {
  107. $this->c[self::DATATABLE_ASSOCIATED] = -1 * $this->c[self::DATATABLE_ASSOCIATED];
  108. $this->subtableIdWasNegativeBeforeSerialize = false;
  109. }
  110. }
  111. /**
  112. * When destroyed, a row destroys its associated subtable if there is one.
  113. * @ignore
  114. */
  115. public function __destruct()
  116. {
  117. if ($this->isSubtableLoaded()) {
  118. Manager::getInstance()->deleteTable($this->getIdSubDataTable());
  119. $this->c[self::DATATABLE_ASSOCIATED] = null;
  120. }
  121. }
  122. /**
  123. * Applies a basic rendering to the Row and returns the output.
  124. *
  125. * @return string describing the row. Example:
  126. * "- 1 ['label' => 'piwik', 'nb_uniq_visitors' => 1685, 'nb_visits' => 1861] [] [idsubtable = 1375]"
  127. */
  128. public function __toString()
  129. {
  130. $columns = array();
  131. foreach ($this->getColumns() as $column => $value) {
  132. if (is_string($value)) $value = "'$value'";
  133. elseif (is_array($value)) $value = var_export($value, true);
  134. $columns[] = "'$column' => $value";
  135. }
  136. $columns = implode(", ", $columns);
  137. $metadata = array();
  138. foreach ($this->getMetadata() as $name => $value) {
  139. if (is_string($value)) $value = "'$value'";
  140. elseif (is_array($value)) $value = var_export($value, true);
  141. $metadata[] = "'$name' => $value";
  142. }
  143. $metadata = implode(", ", $metadata);
  144. $output = "# [" . $columns . "] [" . $metadata . "] [idsubtable = " . $this->getIdSubDataTable() . "]<br />\n";
  145. return $output;
  146. }
  147. /**
  148. * Deletes the given column.
  149. *
  150. * @param string $name The column name.
  151. * @return bool `true` on success, `false` if the column does not exist.
  152. */
  153. public function deleteColumn($name)
  154. {
  155. if (!$this->hasColumn($name)) {
  156. return false;
  157. }
  158. unset($this->c[self::COLUMNS][$name]);
  159. return true;
  160. }
  161. /**
  162. * Renames a column.
  163. *
  164. * @param string $oldName The current name of the column.
  165. * @param string $newName The new name of the column.
  166. */
  167. public function renameColumn($oldName, $newName)
  168. {
  169. if (isset($this->c[self::COLUMNS][$oldName])) {
  170. $this->c[self::COLUMNS][$newName] = $this->c[self::COLUMNS][$oldName];
  171. }
  172. // outside the if() since we want to delete nulled columns
  173. unset($this->c[self::COLUMNS][$oldName]);
  174. }
  175. /**
  176. * Returns a column by name.
  177. *
  178. * @param string $name The column name.
  179. * @return mixed|false The column value or false if it doesn't exist.
  180. */
  181. public function getColumn($name)
  182. {
  183. if (!isset($this->c[self::COLUMNS][$name])) {
  184. return false;
  185. }
  186. if ($this->isColumnValueCallable($this->c[self::COLUMNS][$name])) {
  187. $value = $this->resolveCallableColumn($name);
  188. if (!isset($value)) {
  189. return false;
  190. }
  191. return $value;
  192. }
  193. return $this->c[self::COLUMNS][$name];
  194. }
  195. private function isColumnValueCallable($name)
  196. {
  197. if (is_object($name) && ($name instanceof \Closure)) {
  198. return true;
  199. }
  200. return is_array($name) && array_key_exists(0, $name) && is_object($name[0]) && is_callable($name);
  201. }
  202. private function resolveCallableColumn($columnName)
  203. {
  204. return call_user_func($this->c[self::COLUMNS][$columnName], $this);
  205. }
  206. /**
  207. * Returns the array of all metadata, or one requested metadata value.
  208. *
  209. * @param string|null $name The name of the metadata to return or null to return all metadata.
  210. * @return mixed
  211. */
  212. public function getMetadata($name = null)
  213. {
  214. if (is_null($name)) {
  215. return $this->c[self::METADATA];
  216. }
  217. if (!isset($this->c[self::METADATA][$name])) {
  218. return false;
  219. }
  220. return $this->c[self::METADATA][$name];
  221. }
  222. private function getColumnsRaw()
  223. {
  224. return $this->c[self::COLUMNS];
  225. }
  226. /**
  227. * Returns true if a column having the given name is already registered. The value will not be evaluated, it will
  228. * just check whether a column exists independent of its value.
  229. *
  230. * @param string $name
  231. * @return bool
  232. */
  233. public function hasColumn($name)
  234. {
  235. return array_key_exists($name, $this->c[self::COLUMNS]);
  236. }
  237. /**
  238. * Returns the array containing all the columns.
  239. *
  240. * @return array Example:
  241. *
  242. * array(
  243. * 'column1' => VALUE,
  244. * 'label' => 'www.php.net'
  245. * 'nb_visits' => 15894,
  246. * )
  247. */
  248. public function getColumns()
  249. {
  250. $values = array();
  251. foreach ($this->c[self::COLUMNS] as $columnName => $val) {
  252. if ($this->isColumnValueCallable($val)) {
  253. $values[$columnName] = $this->resolveCallableColumn($columnName);
  254. } else {
  255. $values[$columnName] = $val;
  256. }
  257. }
  258. return $values;
  259. }
  260. /**
  261. * Returns the ID of the subDataTable.
  262. * If there is no such a table, returns null.
  263. *
  264. * @return int|null
  265. */
  266. public function getIdSubDataTable()
  267. {
  268. return !is_null($this->c[self::DATATABLE_ASSOCIATED])
  269. // abs() is to ensure we return a positive int, @see isSubtableLoaded()
  270. ? abs($this->c[self::DATATABLE_ASSOCIATED])
  271. : null;
  272. }
  273. /**
  274. * Returns the associated subtable, if one exists. Returns `false` if none exists.
  275. *
  276. * @return DataTable|bool
  277. */
  278. public function getSubtable()
  279. {
  280. if ($this->isSubtableLoaded()) {
  281. return Manager::getInstance()->getTable($this->getIdSubDataTable());
  282. }
  283. return false;
  284. }
  285. /**
  286. * Sums a DataTable to this row's subtable. If this row has no subtable a new
  287. * one is created.
  288. *
  289. * See {@link Piwik\DataTable::addDataTable()} to learn how DataTables are summed.
  290. *
  291. * @param DataTable $subTable Table to sum to this row's subtable.
  292. */
  293. public function sumSubtable(DataTable $subTable)
  294. {
  295. if ($this->isSubtableLoaded()) {
  296. $thisSubTable = $this->getSubtable();
  297. } else {
  298. $thisSubTable = new DataTable();
  299. $this->addSubtable($thisSubTable);
  300. }
  301. $columnOps = $subTable->getMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME);
  302. $thisSubTable->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, $columnOps);
  303. $thisSubTable->addDataTable($subTable);
  304. }
  305. /**
  306. * Attaches a subtable to this row.
  307. *
  308. * @param DataTable $subTable DataTable to associate to this row.
  309. * @return DataTable Returns `$subTable`.
  310. * @throws Exception if a subtable already exists for this row.
  311. */
  312. public function addSubtable(DataTable $subTable)
  313. {
  314. if (!is_null($this->c[self::DATATABLE_ASSOCIATED])) {
  315. throw new Exception("Adding a subtable to the row, but it already has a subtable associated.");
  316. }
  317. return $this->setSubtable($subTable);
  318. }
  319. /**
  320. * Attaches a subtable to this row, overwriting the existing subtable,
  321. * if any.
  322. *
  323. * @param DataTable $subTable DataTable to associate to this row.
  324. * @return DataTable Returns `$subTable`.
  325. */
  326. public function setSubtable(DataTable $subTable)
  327. {
  328. // Hacking -1 to ensure value is negative, so we know the table was loaded
  329. // @see isSubtableLoaded()
  330. $this->c[self::DATATABLE_ASSOCIATED] = -1 * $subTable->getId();
  331. return $subTable;
  332. }
  333. /**
  334. * Returns `true` if the subtable is currently loaded in memory via {@link Piwik\DataTable\Manager}.
  335. *
  336. * @return bool
  337. */
  338. public function isSubtableLoaded()
  339. {
  340. // self::DATATABLE_ASSOCIATED are set as negative values,
  341. // as a flag to signify that the subtable is loaded in memory
  342. return !is_null($this->c[self::DATATABLE_ASSOCIATED])
  343. && $this->c[self::DATATABLE_ASSOCIATED] < 0;
  344. }
  345. /**
  346. * Removes the subtable reference.
  347. */
  348. public function removeSubtable()
  349. {
  350. $this->c[self::DATATABLE_ASSOCIATED] = null;
  351. }
  352. /**
  353. * Set all the columns at once. Overwrites **all** previously set columns.
  354. *
  355. * @param array eg, `array('label' => 'www.php.net', 'nb_visits' => 15894)`
  356. */
  357. public function setColumns($columns)
  358. {
  359. $this->c[self::COLUMNS] = $columns;
  360. }
  361. /**
  362. * Set the value `$value` to the column called `$name`.
  363. *
  364. * @param string $name name of the column to set.
  365. * @param mixed $value value of the column to set.
  366. */
  367. public function setColumn($name, $value)
  368. {
  369. $this->c[self::COLUMNS][$name] = $value;
  370. }
  371. /**
  372. * Set the value `$value` to the metadata called `$name`.
  373. *
  374. * @param string $name name of the metadata to set.
  375. * @param mixed $value value of the metadata to set.
  376. */
  377. public function setMetadata($name, $value)
  378. {
  379. $this->c[self::METADATA][$name] = $value;
  380. }
  381. /**
  382. * Deletes one metadata value or all metadata values.
  383. *
  384. * @param bool|string $name Metadata name (omit to delete entire metadata).
  385. * @return bool `true` on success, `false` if the column didn't exist
  386. */
  387. public function deleteMetadata($name = false)
  388. {
  389. if ($name === false) {
  390. $this->c[self::METADATA] = array();
  391. return true;
  392. }
  393. if (!isset($this->c[self::METADATA][$name])) {
  394. return false;
  395. }
  396. unset($this->c[self::METADATA][$name]);
  397. return true;
  398. }
  399. /**
  400. * Add a new column to the row. If the column already exists, throws an exception.
  401. *
  402. * @param string $name name of the column to add.
  403. * @param mixed $value value of the column to set or a PHP callable.
  404. * @throws Exception if the column already exists.
  405. */
  406. public function addColumn($name, $value)
  407. {
  408. if (isset($this->c[self::COLUMNS][$name])) {
  409. throw new Exception("Column $name already in the array!");
  410. }
  411. $this->setColumn($name, $value);
  412. }
  413. /**
  414. * Add many columns to this row.
  415. *
  416. * @param array $columns Name/Value pairs, e.g., `array('name' => $value , ...)`
  417. * @throws Exception if any column name does not exist.
  418. * @return void
  419. */
  420. public function addColumns($columns)
  421. {
  422. foreach ($columns as $name => $value) {
  423. try {
  424. $this->addColumn($name, $value);
  425. } catch (Exception $e) {
  426. }
  427. }
  428. if (!empty($e)) {
  429. throw $e;
  430. }
  431. }
  432. /**
  433. * Add a new metadata to the row. If the metadata already exists, throws an exception.
  434. *
  435. * @param string $name name of the metadata to add.
  436. * @param mixed $value value of the metadata to set.
  437. * @throws Exception if the metadata already exists.
  438. */
  439. public function addMetadata($name, $value)
  440. {
  441. if (isset($this->c[self::METADATA][$name])) {
  442. throw new Exception("Metadata $name already in the array!");
  443. }
  444. $this->setMetadata($name, $value);
  445. }
  446. private function isSummableColumn($columnName)
  447. {
  448. return empty(self::$unsummableColumns[$columnName]);
  449. }
  450. /**
  451. * Sums the given `$rowToSum` columns values to the existing row column values.
  452. * Only the int or float values will be summed. Label columns will be ignored
  453. * even if they have a numeric value.
  454. *
  455. * Columns in `$rowToSum` that don't exist in `$this` are added to `$this`.
  456. *
  457. * @param \Piwik\DataTable\Row $rowToSum The row to sum to this row.
  458. * @param bool $enableCopyMetadata Whether metadata should be copied or not.
  459. * @param array $aggregationOperations for columns that should not be summed, determine which
  460. * aggregation should be used (min, max). format:
  461. * `array('column name' => 'function name')`
  462. */
  463. public function sumRow(Row $rowToSum, $enableCopyMetadata = true, $aggregationOperations = false)
  464. {
  465. foreach ($rowToSum->getColumnsRaw() as $columnToSumName => $columnToSumValue) {
  466. if (!$this->isSummableColumn($columnToSumName)) {
  467. continue;
  468. }
  469. if ($this->isColumnValueCallable($columnToSumValue)) {
  470. $this->setColumn($columnToSumName, $columnToSumValue);
  471. continue;
  472. }
  473. $thisColumnValue = $this->getColumn($columnToSumName);
  474. $operation = 'sum';
  475. if (is_array($aggregationOperations) && isset($aggregationOperations[$columnToSumName])) {
  476. $operation = strtolower($aggregationOperations[$columnToSumName]);
  477. }
  478. // max_actions is a core metric that is generated in ArchiveProcess_Day. Therefore, it can be
  479. // present in any data table and is not part of the $aggregationOperations mechanism.
  480. if ($columnToSumName == Metrics::INDEX_MAX_ACTIONS) {
  481. $operation = 'max';
  482. }
  483. if(empty($operation)) {
  484. throw new Exception("Unknown aggregation operation for column $columnToSumName.");
  485. }
  486. $newValue = $this->getColumnValuesMerged($operation, $thisColumnValue, $columnToSumValue);
  487. $this->setColumn($columnToSumName, $newValue);
  488. }
  489. if ($enableCopyMetadata) {
  490. $this->sumRowMetadata($rowToSum);
  491. }
  492. }
  493. /**
  494. */
  495. private function getColumnValuesMerged($operation, $thisColumnValue, $columnToSumValue)
  496. {
  497. switch ($operation) {
  498. case 'skip':
  499. $newValue = null;
  500. break;
  501. case 'max':
  502. $newValue = max($thisColumnValue, $columnToSumValue);
  503. break;
  504. case 'min':
  505. if (!$thisColumnValue) {
  506. $newValue = $columnToSumValue;
  507. } else if (!$columnToSumValue) {
  508. $newValue = $thisColumnValue;
  509. } else {
  510. $newValue = min($thisColumnValue, $columnToSumValue);
  511. }
  512. break;
  513. case 'sum':
  514. $newValue = $this->sumRowArray($thisColumnValue, $columnToSumValue);
  515. break;
  516. default:
  517. throw new Exception("Unknown operation '$operation'.");
  518. }
  519. return $newValue;
  520. }
  521. /**
  522. * Sums the metadata in `$rowToSum` with the metadata in `$this` row.
  523. *
  524. * @param Row $rowToSum
  525. */
  526. public function sumRowMetadata($rowToSum)
  527. {
  528. if (!empty($rowToSum->c[self::METADATA])
  529. && !$this->isSummaryRow()
  530. ) {
  531. // We shall update metadata, and keep the metadata with the _most visits or pageviews_, rather than first or last seen
  532. $visits = max($rowToSum->getColumn(Metrics::INDEX_PAGE_NB_HITS) || $rowToSum->getColumn(Metrics::INDEX_NB_VISITS),
  533. // Old format pre-1.2, @see also method doSumVisitsMetrics()
  534. $rowToSum->getColumn('nb_actions') || $rowToSum->getColumn('nb_visits'));
  535. if (($visits && $visits > $this->maxVisitsSummed)
  536. || empty($this->c[self::METADATA])
  537. ) {
  538. $this->maxVisitsSummed = $visits;
  539. $this->c[self::METADATA] = $rowToSum->c[self::METADATA];
  540. }
  541. }
  542. }
  543. /**
  544. * Returns `true` if this row is the summary row, `false` if otherwise. This function
  545. * depends on the label of the row, and so, is not 100% accurate.
  546. *
  547. * @return bool
  548. */
  549. public function isSummaryRow()
  550. {
  551. return $this->getColumn('label') === DataTable::LABEL_SUMMARY_ROW;
  552. }
  553. /**
  554. * Helper function: sums 2 values
  555. *
  556. * @param number|bool $thisColumnValue
  557. * @param number|array $columnToSumValue
  558. *
  559. * @throws Exception
  560. * @return array|int
  561. */
  562. protected function sumRowArray($thisColumnValue, $columnToSumValue)
  563. {
  564. if (is_numeric($columnToSumValue)) {
  565. if ($thisColumnValue === false) {
  566. $thisColumnValue = 0;
  567. }
  568. return $thisColumnValue + $columnToSumValue;
  569. }
  570. if ($columnToSumValue === false) {
  571. return $thisColumnValue;
  572. }
  573. if ($thisColumnValue === false) {
  574. return $columnToSumValue;
  575. }
  576. if (is_array($columnToSumValue)) {
  577. $newValue = $thisColumnValue;
  578. foreach ($columnToSumValue as $arrayIndex => $arrayValue) {
  579. if (!isset($newValue[$arrayIndex])) {
  580. $newValue[$arrayIndex] = false;
  581. }
  582. $newValue[$arrayIndex] = $this->sumRowArray($newValue[$arrayIndex], $arrayValue);
  583. }
  584. return $newValue;
  585. }
  586. if (is_string($columnToSumValue)) {
  587. throw new Exception("Trying to add two strings in DataTable\Row::sumRowArray: "
  588. . "'$thisColumnValue' + '$columnToSumValue'" . " for row " . $this->__toString());
  589. }
  590. return 0;
  591. }
  592. /**
  593. * Helper function to compare array elements
  594. *
  595. * @param mixed $elem1
  596. * @param mixed $elem2
  597. * @return bool
  598. * @ignore
  599. */
  600. static public function compareElements($elem1, $elem2)
  601. {
  602. if (is_array($elem1)) {
  603. if (is_array($elem2)) {
  604. return strcmp(serialize($elem1), serialize($elem2));
  605. }
  606. return 1;
  607. }
  608. if (is_array($elem2))
  609. return -1;
  610. if ((string)$elem1 === (string)$elem2)
  611. return 0;
  612. return ((string)$elem1 > (string)$elem2) ? 1 : -1;
  613. }
  614. /**
  615. * Helper function that tests if two rows are equal.
  616. *
  617. * Two rows are equal if:
  618. *
  619. * - they have exactly the same columns / metadata
  620. * - they have a subDataTable associated, then we check that both of them are the same.
  621. *
  622. * Column order is not important.
  623. *
  624. * @param \Piwik\DataTable\Row $row1 first to compare
  625. * @param \Piwik\DataTable\Row $row2 second to compare
  626. * @return bool
  627. */
  628. static public function isEqual(Row $row1, Row $row2)
  629. {
  630. //same columns
  631. $cols1 = $row1->getColumns();
  632. $cols2 = $row2->getColumns();
  633. $diff1 = array_udiff($cols1, $cols2, array(__CLASS__, 'compareElements'));
  634. $diff2 = array_udiff($cols2, $cols1, array(__CLASS__, 'compareElements'));
  635. if ($diff1 != $diff2) {
  636. return false;
  637. }
  638. $dets1 = $row1->getMetadata();
  639. $dets2 = $row2->getMetadata();
  640. ksort($dets1);
  641. ksort($dets2);
  642. if ($dets1 != $dets2) {
  643. return false;
  644. }
  645. // either both are null
  646. // or both have a value
  647. if (!(is_null($row1->getIdSubDataTable())
  648. && is_null($row2->getIdSubDataTable())
  649. )
  650. ) {
  651. $subtable1 = $row1->getSubtable();
  652. $subtable2 = $row2->getSubtable();
  653. if (!DataTable::isEqual($subtable1, $subtable2)) {
  654. return false;
  655. }
  656. }
  657. return true;
  658. }
  659. }