Newer
Older
if (count($this->rows) == 0) {
return false;
}
}
/**
* Returns the number of rows in the entire DataTable hierarchy. This is the number of rows in this DataTable
* summed with the row count of each descendant subtable.
*
* @return int
*/
public function getRowsCountRecursive()
{
$totalCount = 0;
foreach ($this->rows as $row) {
$subTable = $row->getSubtable();
if ($subTable) {
$count = $subTable->getRowsCountRecursive();
$totalCount += $count;
}
}
$totalCount += $this->getRowsCount();
return $totalCount;
}
/**
* Delete a column by name in every row. This change is NOT applied recursively to all
* subtables.
*/
public function deleteColumn($name)
{
$this->deleteColumns(array($name));
}
public function __sleep()
{
return array('rows', 'summaryRow');
}
/**
* Rename a column in every row. This change is applied recursively to all subtables.
* @param string $oldName Old column name.
* @param string $newName New column name.
public function renameColumn($oldName, $newName)
foreach ($this->rows as $row) {
$row->renameColumn($oldName, $newName);
$subTable = $row->getSubtable();
if ($subTable) {
$subTable->renameColumn($oldName, $newName);
}
}
if (!is_null($this->summaryRow)) {
$this->summaryRow->renameColumn($oldName, $newName);
}
}
/**
* @param array $names List of column names to delete.
* @param bool $deleteRecursiveInSubtables Whether to apply this change to all subtables or not.
*/
public function deleteColumns($names, $deleteRecursiveInSubtables = false)
{
foreach ($this->rows as $row) {
foreach ($names as $name) {
$row->deleteColumn($name);
}
$subTable = $row->getSubtable();
if ($subTable) {
$subTable->deleteColumns($names, $deleteRecursiveInSubtables);
}
}
if (!is_null($this->summaryRow)) {
foreach ($names as $name) {
$this->summaryRow->deleteColumn($name);
}
}
}
/**
* @param int $id The row ID.
* @throws Exception If the row `$id` cannot be found.
*/
public function deleteRow($id)
{
if ($id === self::ID_SUMMARY_ROW) {
$this->summaryRow = null;
return;
}
if (!isset($this->rows[$id])) {
throw new Exception("Trying to delete unknown row with idkey = $id");
}
unset($this->rows[$id]);
}
/**
* @param int $offset The offset to start deleting rows from.
* @param int|null $limit The number of rows to delete. If `null` all rows after the offset
* will be removed.
* @return int The number of rows deleted.
*/
public function deleteRowsOffset($offset, $limit = null)
{
if ($limit === 0) {
return 0;
}
$count = $this->getRowsCount();
if ($offset >= $count) {
return 0;
}
// if we delete until the end, we delete the summary row as well
if (is_null($limit)
|| $limit >= $count
) {
$this->summaryRow = null;
}
if (is_null($limit)) {
Thomas Steur
a validé
array_splice($this->rows, $offset);
} else {
Thomas Steur
a validé
array_splice($this->rows, $offset, $limit);
Thomas Steur
a validé
return $count - $this->getRowsCount();
}
/**
* @param array $rowIds The list of row IDs to delete.
* @throws Exception If a row ID cannot be found.
$this->deleteRow($key);
}
}
/**
* Returns a string representation of this DataTable for convenient viewing.
* _Note: This uses the **html** DataTable renderer._
*
* @return string
*/
public function __toString()
{
$renderer = new Html();
$renderer->setTable($this);
return (string)$renderer;
}
/**
* DataTables are equal if they have the same number of rows, if
* each row has a label that exists in the other table, and if each row
* is equal to the row in the other table with the same label. The order
* of rows is not important.
* @param \Piwik\DataTable $table1
* @param \Piwik\DataTable $table2
* @return bool
*/
public static function isEqual(DataTable $table1, DataTable $table2)
{
$table1->rebuildIndex();
$table2->rebuildIndex();
if ($table1->getRowsCount() != $table2->getRowsCount()) {
return false;
}
$rows1 = $table1->getRows();
foreach ($rows1 as $row1) {
$row2 = $table2->getRowFromLabel($row1->getColumn('label'));
if ($row2 === false
|| !Row::isEqual($row1, $row2)
) {
return false;
}
}
return true;
}
/**
* Serializes an entire DataTable hierarchy and returns the array of serialized DataTables.
* The first element in the returned array will be the serialized representation of this DataTable.
* Every subsequent element will be a serialized subtable.
* This DataTable and subtables can optionally be truncated before being serialized. In most
* cases where DataTables can become quite large, they should be truncated before being persisted
* in an archive.
* The result of this method is intended for use with the {@link ArchiveProcessor::insertBlobRecord()} method.
* @throws Exception If infinite recursion detected. This will occur if a table's subtable is one of its parent tables.
* @param int $maximumRowsInDataTable If not null, defines the maximum number of rows allowed in the serialized DataTable.
* @param int $maximumRowsInSubDataTable If not null, defines the maximum number of rows allowed in serialized subtables.
* @param string $columnToSortByBeforeTruncation The column to sort by before truncating, eg, `Metrics::INDEX_NB_VISITS`.
* @return array The array of serialized DataTables:
* array(
* // this DataTable (the root)
* 0 => 'eghuighahgaueytae78yaet7yaetae',
* // another subtable
* 2 => 'gqegJHUIGHEQjkgneqjgnqeugUGEQHGUHQE',
*/
public function getSerialized($maximumRowsInDataTable = null,
$maximumRowsInSubDataTable = null,
$columnToSortByBeforeTruncation = null)
{
static $depth = 0;
Thomas Steur
a validé
// make sure subtableIds are consecutive from 1 to N
static $subtableId = 0;
if ($depth > self::$maximumDepthLevelAllowed) {
$depth = 0;
Thomas Steur
a validé
$subtableId = 0;
throw new Exception("Maximum recursion level of " . self::$maximumDepthLevelAllowed . " reached. Maybe you have set a DataTable\Row with an associated DataTable belonging already to one of its parent tables?");
}
if (!is_null($maximumRowsInDataTable)) {
$this->filter('Truncate',
array($maximumRowsInDataTable - 1,
DataTable::LABEL_SUMMARY_ROW,
$columnToSortByBeforeTruncation,
$filterRecursive = false)
);
}
Thomas Steur
a validé
$consecutiveSubtableIds = array();
$forcedId = $subtableId;
// For each row, get the serialized row
// If it is associated to a sub table, get the serialized table recursively ;
// but returns all serialized tables and subtable in an array of 1 dimension
$aSerializedDataTable = array();
Thomas Steur
a validé
foreach ($this->rows as $id => $row) {
$subTable = $row->getSubtable();
Thomas Steur
a validé
$consecutiveSubtableIds[$id] = ++$subtableId;
$depth++;
$aSerializedDataTable = $aSerializedDataTable + $subTable->getSerialized($maximumRowsInSubDataTable, $maximumRowsInSubDataTable, $columnToSortByBeforeTruncation);
$depth--;
mattab
a validé
} else {
$row->removeSubtable();
}
}
// if the datatable is the parent we force the Id at 0 (this is part of the specification)
if ($depth == 0) {
$forcedId = 0;
Thomas Steur
a validé
$subtableId = 0;
}
// we then serialize the rows and store them in the serialized dataTable
Thomas Steur
a validé
$rows = array();
foreach ($this->rows as $id => $row) {
if (array_key_exists($id, $consecutiveSubtableIds)) {
$backup = $row->subtableId;
$row->subtableId = $consecutiveSubtableIds[$id];
$rows[$id] = $row->export();
$row->subtableId = $backup;
} else {
$rows[$id] = $row->export();
}
}
Thomas Steur
a validé
if (isset($this->summaryRow)) {
$rows[self::ID_SUMMARY_ROW] = $this->summaryRow->export();
}
Thomas Steur
a validé
$aSerializedDataTable[$forcedId] = serialize($rows);
unset($rows);
return $aSerializedDataTable;
}
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
private static $previousRowClasses = array('O:39:"Piwik\DataTable\Row\DataTableSummaryRow"', 'O:19:"Piwik\DataTable\Row"', 'O:36:"Piwik_DataTable_Row_DataTableSummary"', 'O:19:"Piwik_DataTable_Row"');
private static $rowClassToUseForUnserialize = 'O:29:"Piwik_DataTable_SerializedRow"';
/**
* It is faster to unserialize existing serialized Row instances to "Piwik_DataTable_SerializedRow" and access the
* `$row->c` property than implementing a "__wakeup" method in the Row instance to map the "$row->c" to $row->columns
* etc. We're talking here about 15% faster reports aggregation in some cases. To be concrete: We have a test where
* Archiving a year takes 1700 seconds with "__wakeup" and 1400 seconds with this method. Yes, it takes 300 seconds
* to wake up millions of rows. We should be able to remove this code here end 2015 and use the "__wakeup" way by then.
* Why? By then most new archives will have only arrays serialized anyway and therefore this mapping is rather an overhead.
*
* @param string $serialized
* @return array
* @throws Exception In case the unserialize fails
*/
private function unserializeRows($serialized)
{
$serialized = str_replace(self::$previousRowClasses, self::$rowClassToUseForUnserialize, $serialized);
$rows = unserialize($serialized);
if ($rows === false) {
throw new Exception("The unserialization has failed!");
}
return $rows;
}
* _Note: This function will successfully load DataTables serialized by Piwik 1.X._
Thomas Steur
a validé
* @param string $serialized A string with the format of a string in the array returned by
Thomas Steur
a validé
* @throws Exception if `$serialized` is invalid.
Thomas Steur
a validé
public function addRowsFromSerializedArray($serialized)
$rows = $this->unserializeRows($serialized);
if (array_key_exists(self::ID_SUMMARY_ROW, $rows)) {
if (is_array($rows[self::ID_SUMMARY_ROW])) {
$this->summaryRow = new Row($rows[self::ID_SUMMARY_ROW]);
} elseif (isset($rows[self::ID_SUMMARY_ROW]->c)) {
$this->summaryRow = new Row($rows[self::ID_SUMMARY_ROW]->c); // Pre Piwik 2.13
}
unset($rows[self::ID_SUMMARY_ROW]);
Thomas Steur
a validé
foreach ($rows as $id => $row) {
if (isset($row->c)) {
} else {
$this->addRow(new Row($row));
}
}
}
/**
* Adds multiple rows from an array.
* You can add row metadata with this method.
* @param array $array Array with the following structure
* // row1
* array(
* Row::COLUMNS => array( col1_name => value1, col2_name => value2, ...),
* Row::METADATA => array( metadata1_name => value1, ...), // see Row
* ),
* // row2
* array( ... ),
*/
public function addRowsFromArray($array)
{
foreach ($array as $id => $row) {
if (is_array($row)) {
$row = new Row($row);
Thomas Steur
a validé
if ($id == self::ID_SUMMARY_ROW) {
$this->summaryRow = $row;
} else {
$this->addRow($row);
}
}
}
/**
* Adds multiple rows from an array containing arrays of column values.
* @param array $array Array with the following structure:
* array(
* array( col1_name => valueA, col2_name => valueC, ...),
* array( col1_name => valueB, col2_name => valueD, ...),
* )
* @throws Exception if `$array` is in an incorrect format.
*/
public function addRowsFromSimpleArray($array)
{
if (count($array) === 0) {
return;
}
Thomas Steur
a validé
$exceptionText = " Data structure returned is not convertible in the requested format." .
" Try to call this method with the parameters '&format=original&serialize=1'" .
"; you will get the original php data structure serialized." .
Thomas Steur
a validé
" The data structure looks like this: \n \$data = %s; ";
// first pass to see if the array has the structure
// array(col1_name => val1, col2_name => val2, etc.)
// with val* that are never arrays (only strings/numbers/bool/etc.)
// if we detect such a "simple" data structure we convert it to a row with the correct columns' names
$thisIsNotThatSimple = false;
foreach ($array as $columnValue) {
if (is_array($columnValue) || is_object($columnValue)) {
$thisIsNotThatSimple = true;
break;
}
}
if ($thisIsNotThatSimple === false) {
// case when the array is indexed by the default numeric index
if (array_keys($array) == array_keys(array_fill(0, count($array), true))) {
foreach ($array as $row) {
$this->addRow(new Row(array(Row::COLUMNS => array($row))));
}
} else {
$this->addRow(new Row(array(Row::COLUMNS => $array)));
}
// we have converted our simple array to one single row
// => we exit the method as the job is now finished
return;
}
foreach ($array as $key => $row) {
// stuff that looks like a line
if (is_array($row)) {
/**
* We make sure we can convert this PHP array without losing information.
* We are able to convert only simple php array (no strings keys, no sub arrays, etc.)
*
*/
// if the key is a string it means that some information was contained in this key.
// it cannot be lost during the conversion. Because we are not able to handle properly
// this key, we throw an explicit exception.
if (is_string($key)) {
Thomas Steur
a validé
// we define an exception we may throw if at one point we notice that we cannot handle the data structure
throw new Exception(sprintf($exceptionText, var_export($array, true)));
}
// if any of the sub elements of row is an array we cannot handle this data structure...
foreach ($row as $subRow) {
if (is_array($subRow)) {
Thomas Steur
a validé
throw new Exception(sprintf($exceptionText, var_export($array, true)));
}
}
$row = new Row(array(Row::COLUMNS => $row));
} // other (string, numbers...) => we build a line from this value
else {
$row = new Row(array(Row::COLUMNS => array($key => $row)));
}
$this->addRow($row);
}
}
/**
* array (
* LABEL => array(col1 => X, col2 => Y),
* LABEL2 => array(col1 => X, col2 => Y),
* )
* to a DataTable with rows that look like:
* array (
* array( Row::COLUMNS => array('label' => LABEL, col1 => X, col2 => Y)),
* array( Row::COLUMNS => array('label' => LABEL2, col1 => X, col2 => Y)),
* )
* Will also convert arrays like:
*
* array (
* LABEL => X,
* LABEL2 => Y,
* )
* array (
* array( Row::COLUMNS => array('label' => LABEL, 'value' => X)),
* array( Row::COLUMNS => array('label' => LABEL2, 'value' => Y)),
* )
* @param array $array Indexed array, two formats supported, see above.
* @param array|null $subtablePerLabel An array mapping label values with DataTable instances to associate as a subtable.
* @return \Piwik\DataTable
public static function makeFromIndexedArray($array, $subtablePerLabel = null)
$table = new DataTable();
foreach ($array as $label => $row) {
$cleanRow = array();
// Support the case of an $array of single values
if (!is_array($row)) {
$row = array('value' => $row);
}
// Put the 'label' column first
$cleanRow[Row::COLUMNS] = array('label' => $label) + $row;
// Assign subtable if specified
if (isset($subtablePerLabel[$label])) {
$cleanRow[Row::DATATABLE_ASSOCIATED] = $subtablePerLabel[$label];
$table->addRow(new Row($cleanRow));
}
/**
* Sets the maximum depth level to at least a certain value. If the current value is
* greater than `$atLeastLevel`, the maximum nesting level is not changed.
* The maximum depth level determines the maximum number of subtable levels in the
* DataTable tree. For example, if it is set to `2`, this DataTable is allowed to
* have subtables, but the subtables are not.
* @param int $atLeastLevel
*/
public static function setMaximumDepthLevelAllowedAtLeast($atLeastLevel)
{
self::$maximumDepthLevelAllowed = max($atLeastLevel, self::$maximumDepthLevelAllowed);
if (self::$maximumDepthLevelAllowed < 1) {
self::$maximumDepthLevelAllowed = 1;
}
}
/**
* Returns metadata by name.
*
* @param string $name The metadata name.
* @return mixed|false The metadata value or `false` if it cannot be found.
*/
public function getMetadata($name)
{
if (!isset($this->metadata[$name])) {
return false;
}
return $this->metadata[$name];
}
/**
* Sets a metadata value by name.
*
* @param string $name The metadata name.
* @param mixed $value
*/
public function setMetadata($name, $value)
{
$this->metadata[$name] = $value;
}
diosmosis
a validé
/**
* Returns all table metadata.
*
* @return array
*/
public function getAllTableMetadata()
{
return $this->metadata;
}
/**
* Sets several metadata values by name.
* @param array $values Array mapping metadata names with metadata values.
*/
public function setMetadataValues($values)
{
foreach ($values as $name => $value) {
$this->metadata[$name] = $value;
}
}
/**
* Sets metadata, erasing existing values.
* @param array $values Array mapping metadata names with metadata values.
*/
public function setAllTableMetadata($metadata)
{
$this->metadata = $metadata;
}
/**
* 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
* @param int $maximumAllowedRows If `0`, the maximum number of rows is unset.
*/
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. The number of path segments that
* were successfully walked is also returned.
* If `$missingRowColumns` is supplied, the specified path is created. When
* a subtable is encountered w/o the required label, a new row is created
* with the label, and a new subtable is added to the row.
* Read [http://en.wikipedia.org/wiki/Tree_(data_structure)#Traversal_methods](http://en.wikipedia.org/wiki/Tree_(data_structure)#Traversal_methods)
* for more information about tree walking.
* @param array $path The path to walk. An array of label values. The first element
* refers to a row in this DataTable, the second in a subtable of
* the first row, the third a subtable of the second row, etc.
* @param array|bool $missingRowColumns The default columns to use when creating new rows.
* created for path labels that cannot be found.
* @param int $maxSubtableRows The maximum number of allowed rows in new subtables. New
* subtables are only created if `$missingRowColumns` is provided.
* @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 DataTableSummaryRow();
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
$row->setColumns(array('label' => $segment) + $missingRowColumns);
$next = $table->addRow($row);
if ($next !== $row) // if the row wasn't added, the table is full
{
// Summary row, has no metadata
$next->deleteMetadata();
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 DataTable();
$table->setMaximumAllowedRows($maxSubtableRows);
= $this->getMetadata(self::COLUMN_AGGREGATION_OPS_METADATA_NAME);
$next->setSubtable($table);
// Summary row, has no metadata
$next->deleteMetadata();
}
}
}
return array($next, $i);
}
/**
* Returns a new DataTable in which the rows of this table are replaced with the aggregatated rows of all its subtables.
* @param string|bool $labelColumn If supplied the label of the parent row will be added to
* a new column in each subtable row.
* If set to, `'label'` each subtable row's label will be prepended
* w/ the parent row's label. So `'child_label'` becomes
* `'parent_label - child_label'`.
* @param bool $useMetadataColumn If true and if `$labelColumn` is supplied, the parent row's
* @return \Piwik\DataTable
*/
public function mergeSubtables($labelColumn = false, $useMetadataColumn = false)
{
$result = new DataTable();
Thomas Steur
a validé
foreach ($this->getRowsWithoutSummaryRow() as $row) {
$subtable = $row->getSubtable();
if ($subtable !== false) {
$parentLabel = $row->getColumn('label');
// add a copy of each subtable row to the new datatable
foreach ($subtable->getRows() as $id => $subRow) {
$copy = clone $subRow;
// if the summary row, add it to the existing summary row (or add a new one)
if ($id == self::ID_SUMMARY_ROW) {
$existing = $result->getRowFromId(self::ID_SUMMARY_ROW);
if ($existing === false) {
$result->addSummaryRow($copy);
} else {
$existing->sumRow($copy, $copyMeta = true, $this->getMetadata(self::COLUMN_AGGREGATION_OPS_METADATA_NAME));
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
}
} else {
if ($labelColumn !== false) {
// if we're modifying the subtable's rows' label column, then we make
// sure to prepend the existing label w/ the parent row's label. otherwise
// we're just adding the parent row's label as a new column/metadata.
$newLabel = $parentLabel;
if ($labelColumn == 'label') {
$newLabel .= ' - ' . $copy->getColumn('label');
}
// modify the child row's label or add new column/metadata
if ($useMetadataColumn) {
$copy->setMetadata($labelColumn, $newLabel);
} else {
$copy->setColumn($labelColumn, $newLabel);
}
}
$result->addRow($copy);
}
}
}
}
return $result;
}
/**
* Returns a new DataTable created with data from a 'simple' array.
* @param array $array
* @return \Piwik\DataTable
*/
public static function makeFromSimpleArray($array)
{
$dataTable = new DataTable();
$dataTable->addRowsFromSimpleArray($array);
return $dataTable;
}
/**
* Creates a new DataTable instance from a serialized DataTable string.
* See {@link getSerialized()} and {@link addRowsFromSerializedArray()}
Benaka Moorthi
a validé
* @param string $data
* @return \Piwik\DataTable
Benaka Moorthi
a validé
*/
public static function fromSerializedArray($data)
{
$result = new DataTable();
Benaka Moorthi
a validé
$result->addRowsFromSerializedArray($data);
return $result;
}
mattab
a validé
/**
* Aggregates the $row columns to this table.
*
* $row must have a column "label". The $row will be summed to this table's row with the same label.
*
* @param $row
* @params null|array $columnAggregationOps
mattab
a validé
* @throws \Exception
*/
protected function aggregateRowWithLabel(Row $row, $columnAggregationOps)
mattab
a validé
{
$labelToLookFor = $row->getColumn('label');
if ($labelToLookFor === false) {
throw new Exception("Label column not found in the table to add in addDataTable()");
}
$rowFound = $this->getRowFromLabel($labelToLookFor);
if ($rowFound === false) {
if ($labelToLookFor === self::LABEL_SUMMARY_ROW) {
$this->addSummaryRow($row);
} else {
$this->addRow($row);
}
} else {
$rowFound->sumRow($row, $copyMeta = true, $columnAggregationOps);
mattab
a validé
// if the row to add has a subtable whereas the current row doesn't
// we simply add it (cloning the subtable)
// if the row has the subtable already
// then we have to recursively sum the subtables
$subTable = $row->getSubtable();
if ($subTable) {
$subTable->metadata[self::COLUMN_AGGREGATION_OPS_METADATA_NAME] = $columnAggregationOps;
mattab
a validé
}
}
}
/**
* @param $row
*/
protected function aggregateRowFromSimpleTable($row)
{
if ($row === false) {
return;
}
$thisRow = $this->getFirstRow();
if ($thisRow === false) {
$thisRow = new Row;
$this->addRow($thisRow);
}
$thisRow->sumRow($row, $copyMeta = true, $this->getMetadata(self::COLUMN_AGGREGATION_OPS_METADATA_NAME));
}
Thomas Steur
a validé
/**
* Unsets all queued filters.
*/
public function clearQueuedFilters()
{
$this->queuedFilters = array();
}
/**
* @return \ArrayIterator|Row[]
*/
Thomas Steur
a validé
public function getIterator() {
return new \ArrayIterator($this->getRows());
Thomas Steur
a validé
}
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
public function offsetExists($offset)
{
$row = $this->getRowFromId($offset);
return false !== $row;
}
public function offsetGet($offset)
{
return $this->getRowFromId($offset);
}
public function offsetSet($offset, $value)
{
$this->rows[$offset] = $value;
}
public function offsetUnset($offset)
{
$this->deleteRow($offset);
}