diff --git a/core/DataTable.php b/core/DataTable.php index b697fad2c97f20c5d2d8d0b4dbb0a354426a217a..322300af63a623d2429f6d3a0a6de13956de3cb4 100644 --- a/core/DataTable.php +++ b/core/DataTable.php @@ -244,11 +244,19 @@ class Piwik_DataTable * @var array */ protected $metadata = array(); - + + /** + * Maximum number of rows allowed in this datatable (including the summary row). + * If adding more rows is attempted, the extra rows get summed to the summary row. + * + * @var int + */ + protected $maximumAllowedRows = 0; + const ID_SUMMARY_ROW = -1; const LABEL_SUMMARY_ROW = -1; const ID_PARENTS = -2; - + /** * Builds the DataTable, registers itself to the manager * @@ -595,6 +603,25 @@ class Piwik_DataTable */ public function addRow( Piwik_DataTable_Row $row ) { + // if there is a upper limit on the number of allowed rows and the table is full, + // add the new row to the summary row + if ($this->maximumAllowedRows > 0 + && $this->getRowsCount() >= $this->maximumAllowedRows - 1) + { + if ($this->summaryRow === null) // create the summary row if necessary + { + $this->addSummaryRow(new Piwik_DataTable_Row(array( + Piwik_DataTable_Row::COLUMNS => $row->getColumns() + ))); + $this->summaryRow->setColumn('label', self::LABEL_SUMMARY_ROW); + } + else + { + $this->summaryRow->sumRow($row); + } + return $this->summaryRow; + } + $this->rows[] = $row; if(!$this->indexNotUpToDate && $this->rebuildIndexContinuously) @@ -606,16 +633,19 @@ class Piwik_DataTable } $this->indexNotUpToDate = false; } + return $row; } /** * Sets the summary row (a dataTable can have only one summary row) * * @param Piwik_DataTable_Row $row + * @return Piwik_DataTable_Row Returns $row. */ public function addSummaryRow( Piwik_DataTable_Row $row ) { $this->summaryRow = $row; + return $row; } /** @@ -1350,4 +1380,89 @@ class Piwik_DataTable { $this->metadata[$name] = $value; } + + /** + * Sets the maximum number of rows allowed in this datatable (including the summary + * row). If adding more then the allowed number of rows is attempted, the extra + * rows are added to the summary row. + * + * @param int|null $maximumAllowedRows + */ + public function setMaximumAllowedRows( $maximumAllowedRows ) + { + $this->maximumAllowedRows = $maximumAllowedRows; + } + + /** + * Traverses a DataTable tree using an array of labels and returns the row + * it finds or false if it cannot find one, and the number of segments of + * the path successfully walked. + * + * If $missingRowColumns is supplied, the specified path is created. When + * a subtable is encountered w/o the queried label, a new row is created + * with the label, and a subtable is added to the row. + * + * @param array $path The path to walk. An array of label values. + * @param array|false $missingRowColumns + * The default columns to use when creating new arrays. + * If this parameter is supplied, new rows will be + * created if labels cannot be found. + * @param int $maxSubtableRows The maximum number of allowed rows in new + * subtables. + * @return array First element is the found row or false. Second element is + * the number of path segments walked. If a row is found, this + * will be == to count($path). Otherwise, it will be the index + * of the path segment that we could not find. + */ + public function walkPath( $path, $missingRowColumns = false, $maxSubtableRows = 0 ) + { + $pathLength = count($path); + + $table = $this; + $next = false; + for ($i = 0; $i < $pathLength; ++$i) + { + $segment = $path[$i]; + + $next = $table->getRowFromLabel($segment); + if ($next === false) + { + // if there is no table to advance to, and we're not adding missing rows, return false + if ($missingRowColumns === false) + { + return array(false, $i); + } + else // if we're adding missing rows, add a new row + { + $row = new Piwik_DataTable_Row_DataTableSummary(); + $row->setColumns(array('label' => $segment) + $missingRowColumns); + + $next = $table->addRow($row); + if ($next !== $row) // if the row wasn't added, the table is full + { + return array($next, $i); + } + } + } + + $table = $next->getSubtable(); + if ($table === false) + { + // if the row has no table (and thus no child rows), and we're not adding + // missing rows, return false + if ($missingRowColumns === false) + { + return array(false, $i); + } + else if ($i != $pathLength - 1) // create subtable if missing, but only if not on the last segment + { + $table = new Piwik_DataTable(); + $table->setMaximumAllowedRows($maxSubtableRows); + $next->setSubtable($table); + } + } + } + + return array($next, $i); + } } diff --git a/core/DataTable/Row.php b/core/DataTable/Row.php index caa8135b33ed448c9a362d98c519fb48d02eca05..c03dbdf595f53bf8ae17481493dd0c04df7f1c81 100644 --- a/core/DataTable/Row.php +++ b/core/DataTable/Row.php @@ -250,6 +250,20 @@ class Piwik_DataTable_Row : null; } + /** + * Returns the associated subtable, if one exists. + * + * @return Piwik_DataTable|false + */ + public function getSubtable() + { + if ($this->isSubtableLoaded()) + { + return Piwik_DataTable_Manager::getInstance()->getTable($this->getIdSubDataTable()); + } + return false; + } + /** * Sums a DataTable to this row subDataTable. * If this row doesn't have a SubDataTable yet, we create a new one. @@ -262,7 +276,7 @@ class Piwik_DataTable_Row { if($this->isSubtableLoaded()) { - $thisSubTable = Piwik_DataTable_Manager::getInstance()->getTable( $this->getIdSubDataTable() ); + $thisSubTable = $this->getSubtable(); } else { @@ -278,8 +292,8 @@ class Piwik_DataTable_Row * If the row already has a DataTable associated to it, throws an Exception. * * @param Piwik_DataTable $subTable DataTable to associate to this row + * @return Piwik_DataTable Returns $subTable. * @throws Exception - * */ public function addSubtable(Piwik_DataTable $subTable) { @@ -287,7 +301,7 @@ class Piwik_DataTable_Row { throw new Exception("Adding a subtable to the row, but it already has a subtable associated."); } - $this->setSubtable($subTable); + return $this->setSubtable($subTable); } /** @@ -295,12 +309,14 @@ class Piwik_DataTable_Row * a DataTable associated, it is simply overwritten. * * @param Piwik_DataTable $subTable DataTable to associate to this row + * @return Piwik_DataTable Returns $subTable. */ public function setSubtable(Piwik_DataTable $subTable) { // Hacking -1 to ensure value is negative, so we know the table was loaded // @see isSubtableLoaded() $this->c[self::DATATABLE_ASSOCIATED] = -1 * $subTable->getId(); + return $subTable; } /** @@ -592,8 +608,8 @@ class Piwik_DataTable_Row ) ) { - $subtable1 = Piwik_DataTable_Manager::getInstance()->getTable($row1->getIdSubDataTable()); - $subtable2 = Piwik_DataTable_Manager::getInstance()->getTable($row2->getIdSubDataTable()); + $subtable1 = $row1->getSubtable(); + $subtable2 = $row2->getSubtable(); if(!Piwik_DataTable::isEqual($subtable1, $subtable2)) { return false; diff --git a/core/DataTable/Row/DataTableSummary.php b/core/DataTable/Row/DataTableSummary.php index 17af9ecfd2b7adfb461e28c96f353f21a3769971..bf648a04849be25048a3a8ca2d412ab97001db84 100644 --- a/core/DataTable/Row/DataTableSummary.php +++ b/core/DataTable/Row/DataTableSummary.php @@ -26,10 +26,37 @@ class Piwik_DataTable_Row_DataTableSummary extends Piwik_DataTable_Row /** * @param Piwik_DataTable $subTable */ - function __construct($subTable) + function __construct($subTable = null) { parent::__construct(); - foreach($subTable->getRows() as $row) + + if ($subTable !== null) + { + $this->sumTable($subTable); + } + } + + /** + * Reset this row to an empty one and sum the associated subtable again. + */ + public function recalculate() + { + $id = $this->getIdSubDataTable(); + if ($id !== null) + { + $subtable = Piwik_DataTable_Manager::getInstance()->getTable($id); + $this->sumTable($subtable); + } + } + + /** + * Sums a tables row with this one. + * + * @param Piwik_DataTable $table + */ + private function sumTable( $table ) + { + foreach($table->getRows() as $row) { $this->sumRow($row); } diff --git a/plugins/Actions/Actions.php b/plugins/Actions/Actions.php index cae4d15401892868b286b880f5f4f83e4df0d31b..8fe996d92784939b4d51cefa147783eef893e66b 100644 --- a/plugins/Actions/Actions.php +++ b/plugins/Actions/Actions.php @@ -430,7 +430,7 @@ class Piwik_Actions extends Piwik_Plugin $actionsArchiving = new Piwik_Actions_Archiving; return $actionsArchiving->archivePeriod($archiveProcessing); } - + /** * Compute all the actions along with their hierarchies. * diff --git a/plugins/Actions/Archiving.php b/plugins/Actions/Archiving.php index 728685b15fe83587040740124fe759d9fd12def5..e98c61657b2b14a753acebedc55488f067b80bc2 100644 --- a/plugins/Actions/Archiving.php +++ b/plugins/Actions/Archiving.php @@ -17,13 +17,14 @@ */ class Piwik_Actions_Archiving { - protected $actionsTablesByType = array( - Piwik_Tracker_Action::TYPE_ACTION_URL => array(), - Piwik_Tracker_Action::TYPE_DOWNLOAD => array(), - Piwik_Tracker_Action::TYPE_OUTLINK => array(), - Piwik_Tracker_Action::TYPE_ACTION_NAME => array(), - ); + protected $actionsTablesByType = null; + public static $actionTypes = array( + Piwik_Tracker_Action::TYPE_ACTION_URL, + Piwik_Tracker_Action::TYPE_OUTLINK, + Piwik_Tracker_Action::TYPE_DOWNLOAD, + Piwik_Tracker_Action::TYPE_ACTION_NAME, + ); static protected $invalidSummedColumnNameToRenamedNameFromPeriodArchive = array( Piwik_Archive::INDEX_NB_UNIQ_VISITORS => Piwik_Archive::INDEX_SUM_DAILY_NB_UNIQ_VISITORS, @@ -80,6 +81,7 @@ class Piwik_Actions_Archiving $rankingQueryLimit = self::getRankingQueryLimit(); Piwik_Actions_ArchivingHelper::reloadConfig(); + $this->initActionsTables(); $this->archiveDayActions($archiveProcessing, $rankingQueryLimit); $this->archiveDayEntryActions($archiveProcessing, $rankingQueryLimit); $this->archiveDayExitActions($archiveProcessing, $rankingQueryLimit); @@ -297,7 +299,7 @@ class Piwik_Actions_Archiving { Piwik_Actions_ArchivingHelper::clearActionsCache(); - $dataTable = Piwik_ArchiveProcessing_Day::generateDataTable($this->actionsTablesByType[Piwik_Tracker_Action::TYPE_ACTION_URL]); + $dataTable = $this->actionsTablesByType[Piwik_Tracker_Action::TYPE_ACTION_URL]; self::deleteInvalidSummedColumnsFromDataTable($dataTable); $s = $dataTable->getSerialized( Piwik_Actions_ArchivingHelper::$maximumRowsInDataTableLevelZero, Piwik_Actions_ArchivingHelper::$maximumRowsInSubDataTable, Piwik_Actions_ArchivingHelper::$columnToSortByBeforeTruncation ); $archiveProcessing->insertBlobRecord('Actions_actions_url', $s); @@ -305,7 +307,7 @@ class Piwik_Actions_Archiving $archiveProcessing->insertNumericRecord('Actions_nb_uniq_pageviews', array_sum($dataTable->getColumn(Piwik_Archive::INDEX_NB_VISITS))); destroy($dataTable); - $dataTable = Piwik_ArchiveProcessing_Day::generateDataTable($this->actionsTablesByType[Piwik_Tracker_Action::TYPE_DOWNLOAD]); + $dataTable = $this->actionsTablesByType[Piwik_Tracker_Action::TYPE_DOWNLOAD]; self::deleteInvalidSummedColumnsFromDataTable($dataTable); $s = $dataTable->getSerialized(Piwik_Actions_ArchivingHelper::$maximumRowsInDataTableLevelZero, Piwik_Actions_ArchivingHelper::$maximumRowsInSubDataTable, Piwik_Actions_ArchivingHelper::$columnToSortByBeforeTruncation ); $archiveProcessing->insertBlobRecord('Actions_downloads', $s); @@ -313,7 +315,7 @@ class Piwik_Actions_Archiving $archiveProcessing->insertNumericRecord('Actions_nb_uniq_downloads', array_sum($dataTable->getColumn(Piwik_Archive::INDEX_NB_VISITS))); destroy($dataTable); - $dataTable = Piwik_ArchiveProcessing_Day::generateDataTable($this->actionsTablesByType[Piwik_Tracker_Action::TYPE_OUTLINK]); + $dataTable = $this->actionsTablesByType[Piwik_Tracker_Action::TYPE_OUTLINK]; self::deleteInvalidSummedColumnsFromDataTable($dataTable); $s = $dataTable->getSerialized( Piwik_Actions_ArchivingHelper::$maximumRowsInDataTableLevelZero, Piwik_Actions_ArchivingHelper::$maximumRowsInSubDataTable, Piwik_Actions_ArchivingHelper::$columnToSortByBeforeTruncation ); $archiveProcessing->insertBlobRecord('Actions_outlink', $s); @@ -321,7 +323,7 @@ class Piwik_Actions_Archiving $archiveProcessing->insertNumericRecord('Actions_nb_uniq_outlinks', array_sum($dataTable->getColumn(Piwik_Archive::INDEX_NB_VISITS))); destroy($dataTable); - $dataTable = Piwik_ArchiveProcessing_Day::generateDataTable($this->actionsTablesByType[Piwik_Tracker_Action::TYPE_ACTION_NAME]); + $dataTable = $this->actionsTablesByType[Piwik_Tracker_Action::TYPE_ACTION_NAME]; self::deleteInvalidSummedColumnsFromDataTable($dataTable); $s = $dataTable->getSerialized( Piwik_Actions_ArchivingHelper::$maximumRowsInDataTableLevelZero, Piwik_Actions_ArchivingHelper::$maximumRowsInSubDataTable, Piwik_Actions_ArchivingHelper::$columnToSortByBeforeTruncation ); $archiveProcessing->insertBlobRecord('Actions_actions', $s); @@ -408,18 +410,37 @@ class Piwik_Actions_Archiving if(($idSubtable = $row->getIdSubDataTable()) !== null || $id === Piwik_DataTable::ID_SUMMARY_ROW) { - foreach(self::$invalidSummedColumnNameToDeleteFromDayArchive as $name) - { - $row->deleteColumn($name); - } - if ($idSubtable !== null) { $subtable = Piwik_DataTable_Manager::getInstance()->getTable($idSubtable); self::deleteInvalidSummedColumnsFromDataTable($subtable); } + + if ($row instanceof Piwik_DataTable_Row_DataTableSummary) + { + $row->recalculate(); + } + + foreach(self::$invalidSummedColumnNameToDeleteFromDayArchive as $name) + { + $row->deleteColumn($name); + } } } } + /** + * Initializes the DataTables created by the archiveDay function. + */ + private function initActionsTables() + { + $this->actionsTablesByType = array(); + foreach (self::$actionTypes as $type) + { + $dataTable = new Piwik_DataTable(); + $dataTable->setMaximumAllowedRows(Piwik_Actions_ArchivingHelper::$maximumRowsInDataTableLevelZero); + + $this->actionsTablesByType[$type] = $dataTable; + } + } } diff --git a/plugins/Actions/ArchivingHelper.php b/plugins/Actions/ArchivingHelper.php index 8aa2baeb587f6a761a48e1691a3dd5b3ad64a56c..a7eaeba9bfdbb98af4e3aa1f1f9233539c44db10 100644 --- a/plugins/Actions/ArchivingHelper.php +++ b/plugins/Actions/ArchivingHelper.php @@ -21,7 +21,7 @@ class Piwik_Actions_ArchivingHelper { const OTHERS_ROW_KEY = ''; - + /** * @param Zend_Db_Statement|PDOStatement $query * @param string|bool $fieldQueried @@ -189,7 +189,8 @@ class Piwik_Actions_ArchivingHelper * @param array $actionsTablesByType * @return Piwik_DataTable */ - protected function parseActionNameCategoriesInDataTable($actionName, $actionType, $urlPrefix=null, &$actionsTablesByType) + protected static function parseActionNameCategoriesInDataTable( + $actionName, $actionType, $urlPrefix=null, &$actionsTablesByType ) { // we work on the root table of the given TYPE (either ACTION_URL or DOWNLOAD or OUTLINK etc.) $currentTable =& $actionsTablesByType[$actionType]; @@ -197,45 +198,33 @@ class Piwik_Actions_ArchivingHelper // check for ranking query cut-off if ($actionName == Piwik_DataTable::LABEL_SUMMARY_ROW) { - return self::createOthersRow($currentTable, $actionType); + $summaryRow = $currentTable->getRowFromId(Piwik_DataTable::ID_SUMMARY_ROW); + if ($summaryRow === false) + { + $summaryRow = $currentTable->addSummaryRow(self::createSummaryRow()); + } + return $summaryRow; } // go to the level of the subcategory $actionExplodedNames = self::getActionExplodedNames($actionName, $actionType, $urlPrefix); - $end = count($actionExplodedNames)-1; - $maxRows = self::$maximumRowsInDataTableLevelZero; - for($level = 0 ; $level < $end; $level++) + list($row, $level) = $currentTable->walkPath( + $actionExplodedNames, self::getDefaultRowColumns(), self::$maximumRowsInSubDataTable); + + // if we didn't traverse the entire path, the table the action belongs to is full, so we + // found a summary row. we don't set metadata on that row. + if ($level != count($actionExplodedNames)) { - $actionCategory = $actionExplodedNames[$level]; - - $othersRow = self::getOthersRowIfTableFull($currentTable, $actionCategory, $actionType, $maxRows); - if ($othersRow) - { - return $othersRow; - } - - $currentTable =& $currentTable[$actionCategory]; - $maxRows = self::$maximumRowsInSubDataTable; + return $row; } - $actionShortName = $actionExplodedNames[$end]; - - $othersRow = self::getOthersRowIfTableFull($currentTable, $actionShortName, $actionType, $maxRows); - if ($othersRow) + + // if the action type is a URL type, set the url as metadata + if ($actionType != Piwik_Tracker_Action::TYPE_ACTION_NAME) { - return $othersRow; + $row->setMetadata('url', Piwik_Tracker_Action::reconstructNormalizedUrl((string)$actionName, $urlPrefix)); } - - // currentTable is now the array element corresponding the the action - // at this point we may be for example at the 4th level of depth in the hierarchy - $currentTable =& $currentTable[$actionShortName]; - - // add the row to the matching sub category subtable - if(!($currentTable instanceof Piwik_DataTable_Row)) - { - $currentTable = self::createActionsTableRow( - (string)$actionShortName, $actionType, $actionName, $urlPrefix); - } - return $currentTable; + + return $row; } /** @@ -530,4 +519,29 @@ class Piwik_Actions_ArchivingHelper self::$cacheParsedAction[$cacheLabel] = $actionRow; } + /** + * Returns the default columns for a row in an Actions DataTable. + * + * @return array + */ + private static function getDefaultRowColumns() + { + return array(Piwik_Archive::INDEX_NB_VISITS => 0, + Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 0, + Piwik_Archive::INDEX_PAGE_NB_HITS => 0, + Piwik_Archive::INDEX_PAGE_SUM_TIME_SPENT => 0); + } + + /** + * Creates a summary row for an Actions DataTable. + * + * @return array + */ + private static function createSummaryRow() + { + return new Piwik_DataTable_Row(array( + Piwik_DataTable_Row::COLUMNS => + array('label' => Piwik_DataTable::LABEL_SUMMARY_ROW) + self::getDefaultRowColumns() + )); + } }