| <?php |
|
|
| |
| |
| |
| |
| |
| |
|
|
| namespace Piwik\DataTable; |
|
|
| use Exception; |
| use Piwik\Container\StaticContainer; |
| use Piwik\DataTable; |
| use Piwik\Date; |
| use Piwik\Metrics; |
| use Piwik\Period; |
| use Piwik\Log\LoggerInterface; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| class Row extends \ArrayObject |
| { |
| public const COMPARISONS_METADATA_NAME = 'comparisons'; |
|
|
| |
| |
| |
| |
| |
| private static $unsummableColumns = array( |
| 'label' => true, |
| 'full_url' => true, |
| DataTable::ARCHIVED_DATE_METADATA_NAME => true, |
| DataTable::ARCHIVE_STATE_METADATA_NAME => true, |
| ); |
|
|
| |
| public $maxVisitsSummed = 0; |
|
|
| private $metadata = array(); |
| private $isSubtableLoaded = false; |
|
|
| |
| |
| |
| public $subtableId = null; |
|
|
| private $isSummaryRow = false; |
|
|
| public const COLUMNS = 0; |
| public const METADATA = 1; |
| public const DATATABLE_ASSOCIATED = 3; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public function __construct($row = array()) |
| { |
| if (isset($row[self::COLUMNS])) { |
| $this->exchangeArray($row[self::COLUMNS]); |
| } |
| if (isset($row[self::METADATA])) { |
| $this->metadata = $row[self::METADATA]; |
| } |
| if (isset($row[self::DATATABLE_ASSOCIATED])) { |
| if ($row[self::DATATABLE_ASSOCIATED] instanceof DataTable) { |
| $this->setSubtable($row[self::DATATABLE_ASSOCIATED]); |
| } else { |
| $this->subtableId = $row[self::DATATABLE_ASSOCIATED]; |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| public function export() |
| { |
| $metadataToPersist = $this->metadata; |
| unset($metadataToPersist[self::COMPARISONS_METADATA_NAME]); |
| return array( |
| self::COLUMNS => $this->getArrayCopy(), |
| self::METADATA => $metadataToPersist, |
| self::DATATABLE_ASSOCIATED => $this->subtableId, |
| ); |
| } |
|
|
| |
| |
| |
| |
| public function __destruct() |
| { |
| if ($this->isSubtableLoaded) { |
| Manager::getInstance()->deleteTable($this->subtableId); |
| $this->subtableId = null; |
| $this->isSubtableLoaded = false; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function __toString() |
| { |
| $columns = array(); |
| foreach ($this->getColumns() as $column => $value) { |
| if (is_string($value)) { |
| $value = "'$value'"; |
| } elseif (is_array($value)) { |
| $value = var_export($value, true); |
| } |
| $columns[] = "'$column' => $value"; |
| } |
| $columns = implode(", ", $columns); |
| $metadata = array(); |
| foreach ($this->getMetadata() as $name => $value) { |
| if (is_string($value)) { |
| $value = "'$value'"; |
| } elseif (is_array($value)) { |
| $value = var_export($value, true); |
| } |
| $metadata[] = "'$name' => $value"; |
| } |
| $metadata = implode(", ", $metadata); |
| $output = "# [" . $columns . "] [" . $metadata . "] [idsubtable = " . $this->getIdSubDataTable() . "]<br />\n"; |
| return $output; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function deleteColumn($name) |
| { |
| if (!$this->offsetExists($name)) { |
| return false; |
| } |
|
|
| unset($this[$name]); |
| return true; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function renameColumn($oldName, $newName) |
| { |
| if (isset($this[$oldName])) { |
| $this[$newName] = $this[$oldName]; |
| } |
|
|
| |
| if ($this->offsetExists($oldName)) { |
| unset($this[$oldName]); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function getColumn($name) |
| { |
| if (!isset($this[$name])) { |
| return false; |
| } |
|
|
| return $this[$name]; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function getMetadata($name = null) |
| { |
| if (is_null($name)) { |
| return $this->metadata; |
| } |
| if (!isset($this->metadata[$name])) { |
| return false; |
| } |
| return $this->metadata[$name]; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| public function hasColumn($name) |
| { |
| return $this->offsetExists($name); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public function getColumns() |
| { |
| return $this->getArrayCopy(); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function getIdSubDataTable() |
| { |
| return $this->subtableId; |
| } |
|
|
| |
| |
| |
| |
| |
| public function getSubtable() |
| { |
| if ($this->isSubtableLoaded) { |
| try { |
| return Manager::getInstance()->getTable($this->subtableId); |
| } catch (TableNotFoundException $e) { |
| |
| } |
| } |
| return false; |
| } |
|
|
| |
| |
| |
| |
| public function setNonLoadedSubtableId($subtableId) |
| { |
| $this->subtableId = $subtableId; |
| $this->isSubtableLoaded = false; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| public function sumSubtable(DataTable $subTable) |
| { |
| if ($this->isSubtableLoaded) { |
| $thisSubTable = $this->getSubtable(); |
| } else { |
| $this->warnIfSubtableAlreadyExists($subTable); |
|
|
| $thisSubTable = new DataTable(); |
| $this->setSubtable($thisSubTable); |
| } |
| $columnOps = $subTable->getMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME); |
| $thisSubTable->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, $columnOps); |
| $thisSubTable->addDataTable($subTable); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| public function setSubtable(DataTable $subTable) |
| { |
| $this->subtableId = $subTable->getId(); |
| $this->isSubtableLoaded = true; |
|
|
| return $subTable; |
| } |
|
|
| |
| |
| |
| |
| |
| public function isSubtableLoaded() |
| { |
| |
| |
| return $this->isSubtableLoaded; |
| } |
|
|
| |
| |
| |
| public function removeSubtable() |
| { |
| $this->subtableId = null; |
| $this->isSubtableLoaded = false; |
| } |
|
|
| |
| |
| |
| |
| |
| public function setColumns($columns) |
| { |
| $this->exchangeArray($columns); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function setColumn($name, $value) |
| { |
| $this[$name] = $value; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function setMetadata($name, $value) |
| { |
| $this->metadata[$name] = $value; |
| } |
|
|
| |
| |
| |
| |
| |
| public function setAllMetadata($metadata) |
| { |
| $this->metadata = $metadata; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function deleteMetadata($name = false) |
| { |
| if ($name === false) { |
| $this->metadata = array(); |
| return true; |
| } |
| if (!isset($this->metadata[$name])) { |
| return false; |
| } |
| unset($this->metadata[$name]); |
| return true; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| public function addColumn($name, $value) |
| { |
| if (isset($this[$name])) { |
| throw new Exception("Column $name already in the array!"); |
| } |
| $this->setColumn($name, $value); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| public function addColumns($columns) |
| { |
| foreach ($columns as $name => $value) { |
| try { |
| $this->addColumn($name, $value); |
| } catch (Exception $e) { |
| } |
| } |
|
|
| if (!empty($e)) { |
| throw $e; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| public function addMetadata($name, $value) |
| { |
| if (isset($this->metadata[$name])) { |
| throw new Exception("Metadata $name already in the array!"); |
| } |
| $this->setMetadata($name, $value); |
| } |
|
|
| private function isSummableColumn($columnName) |
| { |
| return empty(self::$unsummableColumns[$columnName]); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public function sumRow(Row $rowToSum, $enableCopyMetadata = true, $aggregationOperations = false) |
| { |
| $operationsIsArray = is_array($aggregationOperations); |
| foreach ($rowToSum as $columnToSumName => $columnToSumValue) { |
| if (!$this->isSummableColumn($columnToSumName)) { |
| continue; |
| } |
|
|
| $thisColumnValue = $this->getColumn($columnToSumName); |
|
|
| $operation = 'sum'; |
| if ($operationsIsArray && isset($aggregationOperations[$columnToSumName])) { |
| $operationName = $aggregationOperations[$columnToSumName]; |
| if (is_string($operationName)) { |
| $operation = strtolower($operationName); |
| } elseif (is_callable($operationName)) { |
| $operation = $operationName; |
| } |
| } |
|
|
| |
| |
| if ($columnToSumName == Metrics::INDEX_MAX_ACTIONS) { |
| $operation = 'max'; |
| } |
| if (empty($operation)) { |
| throw new Exception("Unknown aggregation operation for column $columnToSumName."); |
| } |
|
|
| $newValue = $this->getColumnValuesMerged($operation, $thisColumnValue, $columnToSumValue, $this, $rowToSum, $columnToSumName); |
|
|
| $this->setColumn($columnToSumName, $newValue); |
| } |
|
|
| if ($enableCopyMetadata) { |
| $this->sumRowMetadata($rowToSum, $aggregationOperations); |
| } |
| } |
|
|
| private function getColumnValuesMerged($operation, $thisColumnValue, $columnToSumValue, $thisRow, $rowToSum, $columnName = null) |
| { |
| switch ($operation) { |
| case 'skip': |
| $newValue = null; |
| break; |
| case 'max': |
| if ($this->isValueConsideredEmpty($thisColumnValue)) { |
| $newValue = $columnToSumValue; |
| } elseif ($this->isValueConsideredEmpty($columnToSumValue)) { |
| $newValue = $thisColumnValue; |
| } else { |
| $newValue = max($thisColumnValue, $columnToSumValue); |
| } |
| break; |
| case 'min': |
| if ($this->isValueConsideredEmpty($thisColumnValue)) { |
| $newValue = $columnToSumValue; |
| } elseif ($this->isValueConsideredEmpty($columnToSumValue)) { |
| $newValue = $thisColumnValue; |
| } else { |
| $newValue = min($thisColumnValue, $columnToSumValue); |
| } |
| break; |
| case 'sum': |
| $newValue = $this->sumRowArray($thisColumnValue, $columnToSumValue, $columnName); |
| break; |
| case 'uniquearraymerge': |
| if (is_array($thisColumnValue) && is_array($columnToSumValue)) { |
| foreach ($columnToSumValue as $columnSum) { |
| if (!in_array($columnSum, $thisColumnValue)) { |
| $thisColumnValue[] = $columnSum; |
| } |
| } |
| } elseif (!is_array($thisColumnValue) && is_array($columnToSumValue)) { |
| $thisColumnValue = $columnToSumValue; |
| } |
|
|
| $newValue = $thisColumnValue; |
| break; |
| default: |
| if (is_callable($operation)) { |
| return call_user_func($operation, $thisColumnValue, $columnToSumValue, $thisRow, $rowToSum); |
| } |
|
|
| throw new Exception("Unknown operation '$operation'."); |
| } |
| return $newValue; |
| } |
|
|
| private function isValueConsideredEmpty($value): bool |
| { |
| return in_array($value, [null, false, ''], true); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function sumRowMetadata($rowToSum, $aggregationOperations = array()) |
| { |
| if ( |
| !empty($rowToSum->metadata) |
| && !$this->isSummaryRow() |
| ) { |
| $aggregatedMetadata = array(); |
|
|
| if (is_array($aggregationOperations)) { |
| |
| foreach ($aggregationOperations as $column => $operation) { |
| $thisMetadata = $this->getMetadata($column); |
| $sumMetadata = $rowToSum->getMetadata($column); |
|
|
| if ($thisMetadata === false && $sumMetadata === false) { |
| continue; |
| } |
|
|
| $aggregatedMetadata[$column] = $this->getColumnValuesMerged($operation, $thisMetadata, $sumMetadata, $this, $rowToSum, $column); |
| } |
| } |
|
|
| |
| $visits = max( |
| $rowToSum->getColumn(Metrics::INDEX_PAGE_NB_HITS) || $rowToSum->getColumn(Metrics::INDEX_NB_VISITS), |
| // Old format pre-1.2, @see also method doSumVisitsMetrics() |
| $rowToSum->getColumn('nb_actions') || $rowToSum->getColumn('nb_visits') |
| ); |
| if ( |
| ($visits && $visits > $this->maxVisitsSummed) |
| || empty($this->metadata) |
| ) { |
| $this->maxVisitsSummed = $visits; |
| $this->metadata = $rowToSum->metadata; |
| } |
|
|
| foreach ($aggregatedMetadata as $column => $value) { |
| |
| $this->setMetadata($column, $value); |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| public function isSummaryRow() |
| { |
| return $this->isSummaryRow; |
| } |
|
|
| public function setIsSummaryRow() |
| { |
| $this->isSummaryRow = true; |
| } |
|
|
| |
| |
| |
| |
| |
| public function getComparisons() |
| { |
| $dataTableId = $this->getMetadata(self::COMPARISONS_METADATA_NAME); |
| if (empty($dataTableId)) { |
| return null; |
| } |
| return Manager::getInstance()->getTable($dataTableId); |
| } |
|
|
| |
| |
| |
| public function setComparisons(DataTable $table) |
| { |
| $this->setMetadata(self::COMPARISONS_METADATA_NAME, $table->getId()); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| protected function sumRowArray($thisColumnValue, $columnToSumValue, $columnName = null) |
| { |
| if ($columnToSumValue === false) { |
| return $thisColumnValue; |
| } |
|
|
| if (is_numeric($columnToSumValue)) { |
| if ($thisColumnValue === false) { |
| $thisColumnValue = 0; |
| } elseif (!is_numeric($thisColumnValue)) { |
| $label = $this->getColumn('label'); |
| $thisColumnDescription = $this->getColumnValueDescriptionForError($thisColumnValue); |
| $columnToSumValueDescription = $this->getColumnValueDescriptionForError($columnToSumValue); |
| throw new \Exception(sprintf( |
| 'Trying to sum unsupported operands for column %s in row with label = %s: %s + %s', |
| $columnName, |
| $label, |
| $thisColumnDescription, |
| $columnToSumValueDescription |
| )); |
| } |
|
|
| return $thisColumnValue + $columnToSumValue; |
| } |
|
|
| if ($thisColumnValue === false) { |
| return $columnToSumValue; |
| } |
|
|
| if (is_array($columnToSumValue)) { |
| $newValue = $thisColumnValue; |
| foreach ($columnToSumValue as $arrayIndex => $arrayValue) { |
| if (!is_numeric($arrayIndex) && !$this->isSummableColumn($arrayIndex)) { |
| continue; |
| } |
| if (!isset($newValue[$arrayIndex])) { |
| $newValue[$arrayIndex] = false; |
| } |
| $newValue[$arrayIndex] = $this->sumRowArray($newValue[$arrayIndex], $arrayValue, $columnName); |
| } |
| return $newValue; |
| } |
|
|
| $this->warnWhenSummingTwoStrings($thisColumnValue, $columnToSumValue, $columnName); |
|
|
| return 0; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| public static function compareElements($elem1, $elem2) |
| { |
| if (is_array($elem1)) { |
| if (is_array($elem2)) { |
| return strcmp(serialize($elem1), serialize($elem2)); |
| } |
| return 1; |
| } |
| if (is_array($elem2)) { |
| return -1; |
| } |
|
|
| if ((string)$elem1 === (string)$elem2) { |
| return 0; |
| } |
|
|
| return ((string)$elem1 > (string)$elem2) ? 1 : -1; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public static function isEqual(Row $row1, Row $row2) |
| { |
| |
| $cols1 = $row1->getColumns(); |
| $cols2 = $row2->getColumns(); |
|
|
| $diff1 = array_udiff($cols1, $cols2, array(__CLASS__, 'compareElements')); |
| $diff2 = array_udiff($cols2, $cols1, array(__CLASS__, 'compareElements')); |
|
|
| if ($diff1 != $diff2) { |
| return false; |
| } |
|
|
| $dets1 = $row1->getMetadata(); |
| $dets2 = $row2->getMetadata(); |
|
|
| ksort($dets1); |
| ksort($dets2); |
|
|
| if ($dets1 != $dets2) { |
| return false; |
| } |
|
|
| |
| |
| if ( |
| !(is_null($row1->getIdSubDataTable()) |
| && is_null($row2->getIdSubDataTable()) |
| ) |
| ) { |
| $subtable1 = $row1->getSubtable(); |
| $subtable2 = $row2->getSubtable(); |
| if (!DataTable::isEqual($subtable1, $subtable2)) { |
| return false; |
| } |
| } |
| return true; |
| } |
|
|
| private function warnIfSubtableAlreadyExists(DataTable $subTable) |
| { |
| if (!is_null($this->subtableId)) { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| $period = $subTable->getMetadata('period'); |
| if ( |
| !$this->isSummaryRow() |
| || $this->isStartDateLaterThanCloud441DeployDate($period) |
| ) { |
| $ex = new \Exception(sprintf( |
| "Row with label '%s' (columns = %s) has already a subtable id=%s but it was not loaded - overwriting the existing sub-table.", |
| $this->getColumn('label'), |
| implode(", ", $this->getColumns()), |
| $this->getIdSubDataTable() |
| )); |
| StaticContainer::get(LoggerInterface::class)->warning("{exception}", ['exception' => $ex]); |
| } |
| } |
| } |
|
|
| protected function warnWhenSummingTwoStrings($thisColumnValue, $columnToSumValue, $columnName = null) |
| { |
| if (is_string($columnToSumValue)) { |
| $ex = new \Exception(sprintf( |
| "Trying to add two strings in DataTable\Row::sumRowArray: %s + %s for column %s in row %s", |
| $thisColumnValue, |
| $columnToSumValue, |
| $columnName, |
| $this->__toString() |
| )); |
| StaticContainer::get(LoggerInterface::class)->warning("{exception}", ['exception' => $ex]); |
| } |
| } |
|
|
| private function getColumnValueDescriptionForError($value) |
| { |
| $result = gettype($value); |
| if (is_array($result)) { |
| $result .= ' ' . json_encode($value); |
| } |
| return $result; |
| } |
|
|
| private function isStartDateLaterThanCloud441DeployDate($period) |
| { |
| if ( |
| empty($period) |
| || !($period instanceof Period) |
| ) { |
| return true; |
| } |
|
|
| $periodStartDate = $period->getDateStart(); |
|
|
| $cloudDeployDate = Date::factory('2021-08-11 12:00:00'); |
| return $periodStartDate->isLater($cloudDeployDate); |
| } |
|
|
| public function sumRowWithLabelToSubtable(string $label, array $columns, ?array $aggregationOps = null): Row |
| { |
| $subtable = $this->getSubtable(); |
| if (empty($subtable)) { |
| $subtable = new DataTable(); |
| $subtable->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, $aggregationOps); |
| $this->setSubtable($subtable); |
| } |
|
|
| return $subtable->sumRowWithLabel($label, $columns, $aggregationOps); |
| } |
| } |
|
|