-
mattpiwik a rédigé
git-svn-id: http://dev.piwik.org/svn/trunk@6307 59fd770c-687e-43c8-a1e3-f5a4ff64c105
mattpiwik a rédigégit-svn-id: http://dev.piwik.org/svn/trunk@6307 59fd770c-687e-43c8-a1e3-f5a4ff64c105
DataTable.php 33,50 Kio
<?php
/**
* Piwik - Open source web analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
* @version $Id$
*
* @category Piwik
* @package Piwik
*/
/**
* @see destroy()
*/
require_once PIWIK_INCLUDE_PATH . '/core/Common.php';
/**
*
* ---- DataTable
* A DataTable is a data structure used to store complex tables of data.
*
* A DataTable is composed of multiple DataTable_Row.
* A DataTable can be applied one or several DataTable_Filter.
* A DataTable can be given to a DataTable_Renderer that would export the data under a given format (XML, HTML, etc.).
*
* A DataTable has the following features:
* - serializable to be stored in the DB
* - loadable from the serialized version
* - efficient way of loading data from an external source (from a PHP array structure)
* - very simple interface to get data from the table
*
* ---- DataTable_Row
* A DataTableRow in the table is defined by
* - multiple columns (a label, multiple values, ...)
* - optional metadata
* - optional - a sub DataTable associated to this row
*
* Simple row example:
* - columns = array( 'label' => 'Firefox',
* 'visitors' => 155,
* 'pages' => 214,
* 'bounce_rate' => 67)
* - metadata = array('logo' => '/img/browsers/FF.png')
* - no sub DataTable
*
* A more complex example would be a DataTable_Row that is associated to a sub DataTable.
* For example, for the row of the search engine Google,
* we want to get the list of keywords associated, with their statistics.
* - columns = array( 'label' => 'Google',
* 'visits' => 1550,
* 'visits_length' => 514214,
* 'returning_visits' => 77)
* - metadata = array( 'logo' => '/img/search/google.png',
* 'url' => 'http://google.com')
* - DataTable = DataTable containing several DataTable_Row containing the keywords information for this search engine
* Example of one DataTable_Row
* - the keyword columns specific to this search engine =
* array( 'label' => 'Piwik', // the keyword
* 'visitors' => 155, // Piwik has been searched on Google by 155 visitors
* 'pages' => 214 // Visitors coming from Google with the kwd Piwik have seen 214 pages
* )
* - the keyword metadata = array() // nothing here, but we could imagining storing the URL of the search in Google for example
* - no subTable
*
*
* ---- DataTable_Filter
* A DataTable_Filter is a applied to a DataTable and so
* can filter information in the multiple DataTable_Row.
*
* For example a DataTable_Filter can:
* - remove rows from the table,
* for example the rows' labels that do not match a given searched pattern
* for example the rows' values that are less than a given percentage (low population)
* - return a subset of the DataTable
* for example a function that apply a limit: $offset, $limit
* - add / remove columns
* for example adding a column that gives the percentage of a given value
* - add some metadata
* for example the 'logo' path if the filter detects the logo
* - edit the value, the label
* - change the rows order
* for example if we want to sort by Label alphabetical order, or by any column value
*
* When several DataTable_Filter are to be applied to a DataTable they are applied sequentially.
* A DataTable_Filter is assigned a priority.
* For example, filters that
* - sort rows should be applied with the highest priority
* - remove rows should be applied with a high priority as they prune the data and improve performance.
*
* ---- Code example
*
* $table = new DataTable();
* $table->addRowsFromArray( array(...) );
*
* # sort the table by visits asc
* $filter = new DataTable_Filter_Sort( $table, 'visits', 'asc');
* $tableFiltered = $filter->getTableFiltered();
*
* # add a filter to select only the website with a label matching '*.com' (regular expression)
* $filter = new DataTable_Filter_Pattern( $table, 'label', '*(.com)');
* $tableFiltered = $filter->getTableFiltered();
*
* # keep the 20 elements from offset 15
* $filter = new DataTable_Filter_Limit( $tableFiltered, 15, 20);
* $tableFiltered = $filter->getTableFiltered();
*
* # add a column computing the percentage of visits
* # params = table, column containing the value, new column name to add, number of total visits to use to compute the %
* $filter = new DataTable_Filter_AddColumnPercentage( $tableFiltered, 'visits', 'visits_percentage', 2042);
* $tableFiltered = $filter->getTableFiltered();
*
* # we get the table as XML
* $xmlOutput = new DataTable_Exporter_Xml( $table );
* $xmlOutput->setHeader( ... );
* $xmlOutput->setColumnsToExport( array('visits', 'visits_percent', 'label') );
* $XMLstring = $xmlOutput->getOutput();
*
*
* ---- Other (ideas)
* We can also imagine building a DataTable_Compare which would take N DataTable that have the same
* structure and would compare them, by computing the percentages of differences, etc.
*
* For example
* DataTable1 = [ keyword1, 1550 visits]
* [ keyword2, 154 visits ]
* DataTable2 = [ keyword1, 1004 visits ]
* [ keyword3, 659 visits ]
* DataTable_Compare = result of comparison of table1 with table2
* [ keyword1, +154% ]
* [ keyword2, +1000% ]
* [ keyword3, -430% ]
*
* @see Piwik_DataTable_Row A Piwik_DataTable is composed of Piwik_DataTable_Row
*
* @package Piwik
* @subpackage Piwik_DataTable
*/
class Piwik_DataTable
{
/**
* Maximum nesting level
*
* @var int
*/
static private $maximumDepthLevelAllowed = 15;
/**
* Array of Piwik_DataTable_Row
*
* @var array
*/
protected $rows = array();
/**
* Array of parent IDs
*
* @var array
*/
protected $parents = null;
/**
* Id assigned to the DataTable, used to lookup the table using the DataTable_Manager
*
* @var int
*/
protected $currentId;
/**
* Current depth level of this data table
* 0 is the parent data table
*
* @var int
*/
protected $depthLevel = 0;
/**
* This flag is set to false once we modify the table in a way that outdates the index
*
* @var bool
*/
protected $indexNotUpToDate = true;
/**
* This flag sets the index to be rebuild whenever a new row is added,
* as opposed to re-building the full index when getRowFromLabel is called.
* This is to optimize and not rebuild the full Index in the case where we
* add row, getRowFromLabel, addRow, getRowFromLabel thousands of times.
*
* @var bool
*/
protected $rebuildIndexContinuously = false;
/**
* Column name of last time the table was sorted
*
* @var string
*/
protected $tableSortedBy = false;
/**
* List of Piwik_DataTable_Filter queued to this table
*
* @var array
*/
protected $queuedFilters = array();
/**
* We keep track of the number of rows before applying the LIMIT filter that deletes some rows
*
* @var int
*/
protected $rowsCountBeforeLimitFilter = 0;
/**
* Defaults to false for performance reasons (most of the time we don't need recursive sorting so we save a looping over the dataTable)
*
* @var bool
*/
protected $enableRecursiveSort = false;
/**
* When the table and all subtables are loaded, this flag will be set to true to ensure filters are applied to all subtables
*
* @var bool
*/
protected $enableRecursiveFilters = false;
/*
* @var Piwik_DataTable_Row
*/
protected $summaryRow = null;
const ID_SUMMARY_ROW = -1;
const LABEL_SUMMARY_ROW = -1;
const ID_PARENTS = -2;
/**
* Builds the DataTable, registers itself to the manager
*
*/
public function __construct()
{
$this->currentId = Piwik_DataTable_Manager::getInstance()->addTable($this);
}
/**
* At destruction we free all memory
*/
public function __destruct()
{
static $depth = 0;
// destruct can be called several times
if($depth < self::$maximumDepthLevelAllowed
&& isset($this->rows))
{
$depth++;
foreach($this->getRows() as $row) {
destroy($row);
}
unset($this->rows);
Piwik_DataTable_Manager::getInstance()->setTableDeleted($this->getId());
$depth--;
}
}
/**
* Sort the dataTable rows using the php callback function
*
* @param string $functionCallback
* @param string $columnSortedBy The column name. Used to then ask the datatable what column are you sorted by
*/
public function sort( $functionCallback, $columnSortedBy )
{
$this->indexNotUpToDate = true;
$this->tableSortedBy = $columnSortedBy;
usort( $this->rows, $functionCallback );
if($this->enableRecursiveSort === true)
{
foreach($this->getRows() as $row)
{
if(($idSubtable = $row->getIdSubDataTable()) !== null)
{
$table = Piwik_DataTable_Manager::getInstance()->getTable($idSubtable);
$table->enableRecursiveSort();
$table->sort($functionCallback, $columnSortedBy);
}
}
}
}
public function getSortedByColumnName()
{
return $this->tableSortedBy;
}
/**
* Enables the recursive sort. Means that when using $table->sort()
* it will also sort all subtables using the same callback
*/
public function enableRecursiveSort()
{
$this->enableRecursiveSort = true;
}
public function enableRecursiveFilters()
{
$this->enableRecursiveFilters = true;
}
/**
* Returns the number of rows before we applied the limit filter
*
* @return int
*/
public function getRowsCountBeforeLimitFilter()
{
$toReturn = $this->rowsCountBeforeLimitFilter;
if($toReturn == 0)
{
return $this->getRowsCount();
}
return $toReturn;
}
/**
* Saves the current number of rows
*/
function setRowsCountBeforeLimitFilter()
{
$this->rowsCountBeforeLimitFilter = $this->getRowsCount();
}
/**
* Apply a filter to this datatable
*
* @param string $className Class name, eg. "Sort" or "Piwik_DataTable_Filter_Sort"
* @param array $parameters Array of parameters to the filter, eg. array('nb_visits', 'asc')
*/
public function filter( $className, $parameters = array() )
{
if(!class_exists($className, false))
{
$className = "Piwik_DataTable_Filter_" . $className;
}
$reflectionObj = new ReflectionClass($className);
// the first parameter of a filter is the DataTable
// we add the current datatable as the parameter
$parameters = array_merge(array($this), $parameters);
$filter = $reflectionObj->newInstanceArgs($parameters);
$filter->enableRecursive( $this->enableRecursiveFilters );
$filter->filter($this);
}
/**
* Queue a DataTable_Filter that will be applied when applyQueuedFilters() is called.
* (just before sending the datatable back to the browser (or API, etc.)
*
* @param string $className The class name of the filter, eg. Piwik_DataTable_Filter_Limit
* @param array $parameters The parameters to give to the filter, eg. array( $offset, $limit) for the filter Piwik_DataTable_Filter_Limit
*/
public function queueFilter( $className, $parameters = array() )
{
if(!is_array($parameters))
{
$parameters = array($parameters);
}
$this->queuedFilters[] = array('className' => $className, 'parameters' => $parameters);
}
/**
* Apply all filters that were previously queued to this table
* @see queueFilter()
*/
public function applyQueuedFilters()
{
foreach($this->queuedFilters as $filter)
{
$this->filter($filter['className'], $filter['parameters']);
}
$this->queuedFilters = array();
}
/**
* Adds a new DataTable to this DataTable
* Go through all the rows of the new DataTable and applies the algorithm:
* - if a row in $table doesnt exist in $this we add the new row to $this
* - if a row exists in both $table and $this we sum the columns values into $this
* - if a row in $this doesnt exist in $table we add in $this the row of $table without modification
*
* A common row to 2 DataTable is defined by the same label
*
* @example tests/core/DataTable.test.php
* @param Piwik_DataTable $tableToSum
*/
public function addDataTable( Piwik_DataTable $tableToSum )
{
foreach($tableToSum->getRows() as $row)
{
$labelToLookFor = $row->getColumn('label');
$rowFound = $this->getRowFromLabel( $labelToLookFor );
if($rowFound === false)
{
if( $labelToLookFor === self::LABEL_SUMMARY_ROW )
{
$this->addSummaryRow( $row );
}
else
{
$this->addRow( $row );
}
}
else
{
$rowFound->sumRow( $row );
// 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
if(($idSubTable = $row->getIdSubDataTable()) !== null)
{
$rowFound->sumSubtable( Piwik_DataTable_Manager::getInstance()->getTable($idSubTable) );
}
}
}
}
/**
* Returns the Piwik_DataTable_Row that has a column 'label' with the value $label
*
* @param string $label Value of the column 'label' of the row to return
* @return Piwik_DataTable_Row |false The row if found, false otherwise
*/
public function getRowFromLabel( $label )
{
$rowId = $this->getRowIdFromLabel($label);
if(is_int($rowId))
{
return $this->rows[$rowId];
}
return $rowId;
}
public function getRowIdFromLabel($label)
{
$this->rebuildIndexContinuously = true;
if($this->indexNotUpToDate)
{
$this->rebuildIndex();
}
if($label === self::LABEL_SUMMARY_ROW
&& !is_null($this->summaryRow))
{
return $this->summaryRow;
}
$label = (string)$label;
if(!isset($this->rowsIndexByLabel[$label]))
{
return false;
}
return $this->rowsIndexByLabel[$label];
}
/**
* Returns a Piwik_DataTable that has only the one column that matches $label.
* If no matches are found, an empty data table is returned.
*
* @param string $label Value of the column 'label' to search for
* @return Piwik_DataTable
*/
public function getFilteredTableFromLabel($label)
{
$newTable = $this->getEmptyClone();
$row = $this->getRowFromLabel($label);
if ($row !== false)
{
$newTable->addRow($row);
}
return $newTable;
}
/**
* Get an empty table with the same properties as this one
*
* @return Piwik_DataTable
*/
public function getEmptyClone()
{
$clone = new Piwik_DataTable;
$clone->queuedFilters = $this->queuedFilters;
return $clone;
}
/**
* Rebuilds the index used to lookup a row by label
*/
private function rebuildIndex()
{
foreach($this->rows as $id => $row)
{
$label = $row->getColumn('label');
if($label !== false)
{
$this->rowsIndexByLabel[$label] = $id;
}
}
$this->indexNotUpToDate = false;
}
/**
* Returns the ith row in the array
*
* @param int $id
* @return Piwik_DataTable_Row or false if not found
*/
public function getRowFromId($id)
{
if(!isset($this->rows[$id]))
{
if($id == self::ID_SUMMARY_ROW
&& !is_null($this->summaryRow))
{
return $this->summaryRow;
}
return false;
}
return $this->rows[$id];
}
/**
* Returns a row that has the subtable ID matching the parameter
*
* @param int $idSubTable
* @return Piwik_DataTable_Row or false if not found
*/
public function getRowFromIdSubDataTable($idSubTable)
{
$idSubTable = (int)$idSubTable;
foreach($this->rows as $row)
{
if($row->getIdSubDataTable() === $idSubTable)
{
return $row;
}
}
return false;
}
/**
* Add a row to the table and rebuild the index if necessary
*
* @param Piwik_DataTable_Row $row to add at the end of the array
*/
public function addRow( Piwik_DataTable_Row $row )
{
$this->rows[] = $row;
if(!$this->indexNotUpToDate
&& $this->rebuildIndexContinuously)
{
$label = $row->getColumn('label');
if($label !== false)
{
$this->rowsIndexByLabel[$label] = count($this->rows)-1;
}
$this->indexNotUpToDate = false;
}
}
/**
* Sets the summary row (a dataTable can have only one summary row)
*
* @param Piwik_DataTable_Row $row
*/
public function addSummaryRow( Piwik_DataTable_Row $row )
{
$this->summaryRow = $row;
}
/**
* Returns the dataTable ID
*
* @return int
*/
public function getId()
{
return $this->currentId;
}
/**
* Adds a new row from a PHP array data structure
*
* @param array $row, eg. array(Piwik_DataTable_Row::COLUMNS => array( 'visits' => 13, 'test' => 'toto'),)
*/
public function addRowFromArray( $row )
{
$this->addRowsFromArray(array($row));
}
/**
* Adds a new row a PHP array data structure
*
* @param array $row, eg. array('name' => 'google analytics', 'license' => 'commercial')
*/
public function addRowFromSimpleArray( $row )
{
$this->addRowsFromSimpleArray(array($row));
}
/**
* Returns the array of Piwik_DataTable_Row
*
* @return Piwik_DataTable_Row[]
*/
public function getRows()
{
if(is_null($this->summaryRow))
{
return $this->rows;
}
else
{
return $this->rows + array(self::ID_SUMMARY_ROW => $this->summaryRow);
}
}
/**
* Returns the array containing all rows values for the requested column
*
* @param $name
* @return array
*/
public function getColumn( $name )
{
$columnValues = array();
foreach($this->getRows() as $row)
{
$columnValues[] = $row->getColumn($name);
}
return $columnValues;
}
/**
* Returns an array containing the rows Metadata values
*
* @param string $name Metadata column to return
* @return array
*/
public function getRowsMetadata( $name )
{
$metadataValues = array();
foreach($this->getRows() as $row)
{
$metadataValues[] = $row->getMetadata($name);
}
return $metadataValues;
}
/**
* Returns the number of rows in the table
*
* @return int
*/
public function getRowsCount()
{
if(is_null($this->summaryRow))
{
return count($this->rows);
}
else
{
return count($this->rows) + 1;
}
}
/**
* Returns the first row of the DataTable
*
* @return Piwik_DataTable_Row
*/
public function getFirstRow()
{
if(count($this->rows) == 0)
{
if(!is_null($this->summaryRow))
{
return $this->summaryRow;
}
return false;
}
$row = array_slice($this->rows, 0, 1);
return $row[0];
}
/**
* Returns the last row of the DataTable
*
* @return Piwik_DataTable_Row
*/
public function getLastRow()
{
if(!is_null($this->summaryRow))
{
return $this->summaryRow;
}
if(count($this->rows) == 0)
{
return false;
}
$row = array_slice($this->rows, -1);
return $row[0];
}
/**
* Returns the sum of the number of rows of all the subtables
* + the number of rows in the parent table
*
* @return int
*/
public function getRowsCountRecursive()
{
$totalCount = 0;
foreach($this->rows as $row)
{
if(($idSubTable = $row->getIdSubDataTable()) !== null)
{
$subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable);
$count = $subTable->getRowsCountRecursive();
$totalCount += $count;
}
}
$totalCount += $this->getRowsCount();
return $totalCount;
}
/**
* Delete a given column $name in all the rows
*
* @param string $name
*/
public function deleteColumn( $name )
{
$this->deleteColumns(array($name));
}
/**
* Rename a column in all rows
*
* @param string $oldName Old column name
* @param string $newName New column name
*/
public function renameColumn( $oldName, $newName )
{
foreach($this->getRows() as $row)
{
$row->renameColumn($oldName, $newName);
if(($idSubDataTable = $row->getIdSubDataTable()) !== null)
{
Piwik_DataTable_Manager::getInstance()->getTable($idSubDataTable)->renameColumn($oldName, $newName);
}
}
if(!is_null($this->summaryRow))
{
$this->summaryRow->renameColumn($oldName, $newName);
}
}
/**
* Delete columns by name in all rows
*
* @param array $names
* @param bool $deleteRecursiveInSubtables
*/
public function deleteColumns($names, $deleteRecursiveInSubtables = false)
{
foreach($this->getRows() as $row)
{
foreach($names as $name)
{
$row->deleteColumn($name);
}
if(($idSubDataTable = $row->getIdSubDataTable()) !== null)
{
Piwik_DataTable_Manager::getInstance()->getTable($idSubDataTable)->deleteColumns($names, $deleteRecursiveInSubtables);
}
}
if(!is_null($this->summaryRow))
{
foreach($names as $name)
{
$this->summaryRow->deleteColumn($name);
}
}
}
/**
* Deletes the ith row
*
* @param int $id
* @throws Exception if the row $id cannot be found
* @return
*/
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]);
}
/**
* Deletes all row from offset, offset + limit.
* If limit is null then limit = $table->getRowsCount()
*
* @param int $offset
* @param int $limit
* @return int
*/
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))
{
$spliced = array_splice($this->rows, $offset);
}
else
{
$spliced = array_splice($this->rows, $offset, $limit);
}
$countDeleted = count($spliced);
return $countDeleted;
}
/**
* Deletes the rows from the list of rows ID
*
* @param array $aKeys ID of the rows to delete
* @throws Exception if any of the row to delete couldn't be found
*/
public function deleteRows( array $aKeys )
{
foreach($aKeys as $key)
{
$this->deleteRow($key);
}
}
/**
* Returns a simple output of the DataTable for easy visualization
* Example: echo $datatable;
*
* @return string
*/
public function __toString()
{
$renderer = new Piwik_DataTable_Renderer_Html();
$renderer->setTable($this);
return (string)$renderer;
}
/**
* Returns true if both DataTable are exactly the same.
* Used in unit tests.
*
* @param Piwik_DataTable $table1
* @param Piwik_DataTable $table2
* @return bool
*/
static public function isEqual(Piwik_DataTable $table1, Piwik_DataTable $table2)
{
$rows1 = $table1->getRows();
$rows2 = $table2->getRows();
$table1->rebuildIndex();
$table2->rebuildIndex();
if($table1->getRowsCount() != $table2->getRowsCount())
{
return false;
}
foreach($rows1 as $row1)
{
$row2 = $table2->getRowFromLabel($row1->getColumn('label'));
if($row2 === false
|| !Piwik_DataTable_Row::isEqual($row1,$row2))
{
return false;
}
}
return true;
}
/**
* The serialization returns a one dimension array containing all the
* serialized DataTable contained in this DataTable.
* We save DataTable in serialized format in the Database.
* Each row of this returned PHP array will be a row in the DB table.
* At the end of the method execution, the dataTable may be truncated (if $maximum* parameters are set).
*
* The keys of the array are very important as they are used to define the DataTable
*
* IMPORTANT: The main table (level 0, parent of all tables) will always be indexed by 0
* even it was created after some other tables.
* It also means that all the parent tables (level 0) will be indexed with 0 in their respective
* serialized arrays. You should never lookup a parent table using the getTable( $id = 0) as it
* won't work.
*
* @throws Exception if an infinite recursion is found (a table row's has a subtable that is one of its parent table)
* @param int If not null, defines the number of rows maximum of the serialized dataTable
* If $addSummaryRowAfterNRows is less than the size of the table, a SummaryRow will be added at the end of the table, that
* is the sum of the values of all the rows after the Nth row. All the rows after the Nth row will be deleted.
*
* @return array Serialized arrays
* array( // Datatable level0
* 0 => 'eghuighahgaueytae78yaet7yaetae',
*
* // first Datatable level1
* 1 => 'gaegae gh gwrh guiwh uigwhuige',
*
* //second Datatable level1
* 2 => 'gqegJHUIGHEQjkgneqjgnqeugUGEQHGUHQE',
*
* //first Datatable level3 (child of second Datatable level1 for example)
* 3 => 'eghuighahgaueytae78yaet7yaetaeGRQWUBGUIQGH&QE',
* );
*/
public function getSerialized( $maximumRowsInDataTable = null,
$maximumRowsInSubDataTable = null,
$columnToSortByBeforeTruncation = null )
{
static $depth = 0;
if($depth > self::$maximumDepthLevelAllowed)
{
$depth = 0;
throw new Exception("Maximum recursion level of ".self::$maximumDepthLevelAllowed. " reached. You have probably set a DataTable_Row with an associated DataTable which belongs already to its parent hierarchy.");
}
if( !is_null($maximumRowsInDataTable) )
{
$this->filter('AddSummaryRow',
array( $maximumRowsInDataTable - 1,
Piwik_DataTable::LABEL_SUMMARY_ROW,
$columnToSortByBeforeTruncation)
);
}
// 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();
foreach($this->rows as $row)
{
if(($idSubTable = $row->getIdSubDataTable()) !== null)
{
$subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable);
$depth++;
$aSerializedDataTable = $aSerializedDataTable + $subTable->getSerialized( $maximumRowsInSubDataTable, $maximumRowsInSubDataTable, $columnToSortByBeforeTruncation );
$depth--;
}
}
// we load the current Id of the DataTable
$forcedId = $this->getId();
// if the datatable is the parent we force the Id at 0 (this is part of the specification)
if($depth == 0)
{
$forcedId = 0;
}
// we then serialize the rows and store them in the serialized dataTable
$addToRows = array( self::ID_SUMMARY_ROW => $this->summaryRow );
if ($this->parents && Piwik_Config::getInstance()->General['enable_archive_parents_of_datatable'])
{
$addToRows[self::ID_PARENTS] = $this->parents;
}
$aSerializedDataTable[$forcedId] = serialize($this->rows + $addToRows);
return $aSerializedDataTable;
}
/**
* Load a serialized string of a datatable.
*
* Does not load recursively all the sub DataTable.
* They will be loaded only when requesting them specifically.
*
* The function creates all the necessary DataTable_Row
*
* @param string string of serialized datatable
* @throws Exception
*/
public function addRowsFromSerializedArray( $stringSerialized )
{
$serialized = unserialize($stringSerialized);
if($serialized === false)
{
throw new Exception("The unserialization has failed!");
}
$this->addRowsFromArray($serialized);
}
/**
* Loads the DataTable from a PHP array data structure
*
* @param array Array with the following structure
* array(
* // row1
* array(
* Piwik_DataTable_Row::COLUMNS => array( col1_name => value1, col2_name => value2, ...),
* Piwik_DataTable_Row::METADATA => array( metadata1_name => value1, ...), // see Piwik_DataTable_Row
*
* ),
*
* // row2
* array( ... ),
*
* )
*/
public function addRowsFromArray( $array )
{
foreach($array as $id => $row)
{
if($id == self::ID_PARENTS)
{
$this->parents = $row;
continue;
}
if(is_array($row))
{
$row = new Piwik_DataTable_Row($row);
}
if($id == self::ID_SUMMARY_ROW)
{
$this->summaryRow = $row;
}
else
{
$this->addRow($row);
}
}
}
/**
* Loads the data from a simple php array.
* Basically maps a simple multidimensional php array to a DataTable.
* Not recursive (if a row contains a php array itself, it won't be loaded)
*
* @param array Array with the simple structure:
* array(
* array( col1_name => valueA, col2_name => valueC, ...),
* array( col1_name => valueB, col2_name => valueD, ...),
* )
* @throws Exception
* @return
*/
public function addRowsFromSimpleArray( $array )
{
if(count($array) === 0)
{
return;
}
// we define an exception we may throw if at one point we notice that we cannot handle the data structure
$e = new Exception(" 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.".
" The data structure looks like this: \n \$data = " . var_export($array, true) . "; ");
// 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 $columnName => $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 Piwik_DataTable_Row( array( Piwik_DataTable_Row::COLUMNS => array($row) ) ) );
}
}
else
{
$this->addRow( new Piwik_DataTable_Row( array( Piwik_DataTable_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))
{
throw $e;
}
// 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))
{
throw $e;
}
}
$row = new Piwik_DataTable_Row( array( Piwik_DataTable_Row::COLUMNS => $row ) );
}
// other (string, numbers...) => we build a line from this value
else
{
$row = new Piwik_DataTable_Row( array( Piwik_DataTable_Row::COLUMNS => array($key => $row)) );
}
$this->addRow($row);
}
}
/**
* Rewrites the input $array
* array (
* LABEL => array(col1 => X, col2 => Y),
* LABEL2 => array(col1 => X, col2 => Y),
* )
* to the structure
* array (
* array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL, col1 => X, col2 => Y)),
* array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL2, col1 => X, col2 => Y)),
* )
*
* It also works with array having only one value per row, eg.
* array (
* LABEL => X,
* LABEL2 => Y,
* )
* would be converted to the structure
* array (
* array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL, 'value' => X)),
* array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL2, 'value' => Y)),
* )
*
* The optional parameter $subtablePerLabel is an array of subTable associated to the rows of the $array
* For example if $subtablePerLabel is given
* array(
* LABEL => #Piwik_DataTable_ForLABEL,
* LABEL2 => #Piwik_DataTable_ForLABEL2,
* )
*
* the $array would become
* array (
* array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL, col1 => X, col2 => Y),
* Piwik_DataTable_Row::DATATABLE_ASSOCIATED => #ID DataTable For LABEL
* ),
* array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL2, col1 => X, col2 => Y)
* Piwik_DataTable_Row::DATATABLE_ASSOCIATED => #ID2 DataTable For LABEL2
* ),
* )
*
* @param array $array See method description
* @param array|null $subtablePerLabel see method description
*/
public function addRowsFromArrayWithIndexLabel( $array, $subtablePerLabel = null)
{
$cleanRow = array();
foreach($array as $label => $row)
{
if(!is_array($row))
{
$row = array('value' => $row);
}
$cleanRow[Piwik_DataTable_Row::DATATABLE_ASSOCIATED] = null;
// we put the 'label' column first as it looks prettier in API results
$cleanRow[Piwik_DataTable_Row::COLUMNS] = array('label' => $label) + $row;
if(!is_null($subtablePerLabel)
// some rows of this table don't have subtables
// (for example case of campaigns without keywords)
&& isset($subtablePerLabel[$label])
)
{
$cleanRow[Piwik_DataTable_Row::DATATABLE_ASSOCIATED] = $subtablePerLabel[$label];
}
$this->addRow( new Piwik_DataTable_Row($cleanRow) );
}
}
/**
* Set the array of parent ids
* @param array $parents
*/
public function setParents($parents)
{
$this->parents = $parents;
}
/**
* Get parents
* @return array of all parents, root level first
*/
public function getParents() {
if ($this->parents == null)
{
return array();
}
return $this->parents;
}
/**
* Gets the maximum nesting level for datatables.
*
* @return int
*/
static public function getMaximumDepthLevelAllowed()
{
return self::$maximumDepthLevelAllowed;
}
/**
* Sets the maximum nesting level to at least a certain value. If the current value is
* greater than the supplied level, the maximum nesting level is not changed.
*
* @param int $atLeastLevel
*/
static public function setMaximumDepthLevelAllowedAtLeast( $atLeastLevel )
{
self::$maximumDepthLevelAllowed = max($atLeastLevel, self::$maximumDepthLevelAllowed);
}
}