Ticket #1128: DataTable.php

File DataTable.php, 31.7 KB (added by jetuelle, 2 years ago)

The file with change by jetuelle

Line 
1<?php
2/**
3 * Piwik - Open source web analytics
4 *
5 * @link http://piwik.org
6 * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later
7 * @version $Id: DataTable.php 1613 2009-11-27 20:57:13Z vipsoft $
8 *
9 * @category Piwik
10 * @package Piwik
11 */
12
13/**
14 *
15 * ---- DataTable
16 * A DataTable is a data structure used to store complex tables of data.
17 *
18 * A DataTable is composed of multiple DataTable_Row.
19 * A DataTable can be applied one or several DataTable_Filter.
20 * A DataTable can be given to a DataTable_Renderer that would export the data under a given format (XML, HTML, etc.).
21 *
22 * A DataTable has the following features:
23 * - serializable to be stored in the DB
24 * - loadable from the serialized version
25 * - efficient way of loading data from an external source (from a PHP array structure)
26 * - very simple interface to get data from the table
27 *
28 * ---- DataTable_Row
29 * A DataTableRow in the table is defined by
30 * - multiple columns (a label, multiple values, ...)
31 * - optional metadata
32 * - optional - a sub DataTable associated to this row
33 *
34 * Simple row example:
35 * - columns = array(   'label' => 'Firefox',
36 *                         'visitors' => 155,
37 *                         'pages' => 214,
38 *                         'bounce_rate' => 67)
39 * - metadata = array('logo' => '/img/browsers/FF.png')
40 * - no sub DataTable
41 *
42 * A more complex example would be a DataTable_Row that is associated to a sub DataTable.
43 * For example, for the row of the search engine Google,
44 * we want to get the list of keywords associated, with their statistics.
45 * - columns = array(   'label' => 'Google',
46 *                         'visits' => 1550,
47 *                         'visits_length' => 514214,
48 *                         'returning_visits' => 77)
49 * - metadata = array(    'logo' => '/img/search/google.png',
50 *                         'url' => 'http://google.com')
51 * - DataTable = DataTable containing several DataTable_Row containing the keywords information for this search engine
52 *             Example of one DataTable_Row
53 *             - the keyword columns specific to this search engine =
54 *                     array(  'label' => 'Piwik', // the keyword
55 *                             'visitors' => 155,  // Piwik has been searched on Google by 155 visitors
56 *                             'pages' => 214 // Visitors coming from Google with the kwd Piwik have seen 214 pages
57 *                     )
58 *             - the keyword metadata = array() // nothing here, but we could imagining storing the URL of the search in Google for example
59 *             - no subTable
60 * 
61 *
62 * ---- DataTable_Filter
63 * A DataTable_Filter is a applied to a DataTable and so
64 * can filter information in the multiple DataTable_Row.
65 *
66 * For example a DataTable_Filter can:
67 * - remove rows from the table,
68 *         for example the rows' labels that do not match a given searched pattern
69 *         for example the rows' values that are less than a given percentage (low population)
70 * - return a subset of the DataTable
71 *         for example a function that apply a limit: $offset, $limit
72 * - add / remove columns
73 *         for example adding a column that gives the percentage of a given value
74 * - add some metadata
75 *         for example the 'logo' path if the filter detects the logo
76 * - edit the value, the label
77 * - change the rows order
78 *         for example if we want to sort by Label alphabetical order, or by any column value
79 *
80 * When several DataTable_Filter are to be applied to a DataTable they are applied sequentially.
81 * A DataTable_Filter is assigned a priority.
82 * For example, filters that
83 *     - sort rows should be applied with the highest priority
84 *     - remove rows should be applied with a high priority as they prune the data and improve performance.
85 *     
86 * ---- Code example
87 *
88 * $table = new DataTable();
89 * $table->addRowsFromArray( array(...) );
90 *
91 * # sort the table by visits asc
92 * $filter = new DataTable_Filter_Sort( $table, 'visits', 'asc');
93 * $tableFiltered = $filter->getTableFiltered();
94 *
95 * # add a filter to select only the website with a label matching '*.com' (regular expression)
96 * $filter = new DataTable_Filter_Pattern( $table, 'label', '*(.com)');
97 * $tableFiltered = $filter->getTableFiltered();
98 *
99 * # keep the 20 elements from offset 15
100 * $filter = new DataTable_Filter_Limit( $tableFiltered, 15, 20);
101 * $tableFiltered = $filter->getTableFiltered();
102 *
103 * # add a column computing the percentage of visits
104 * # params = table, column containing the value, new column name to add, number of total visits to use to compute the %
105 * $filter = new DataTable_Filter_AddColumnPercentage( $tableFiltered, 'visits', 'visits_percentage', 2042);
106 * $tableFiltered = $filter->getTableFiltered();
107 *
108 * # we get the table as XML
109 * $xmlOutput = new DataTable_Exporter_Xml( $table );
110 * $xmlOutput->setHeader( ... );
111 * $xmlOutput->setColumnsToExport( array('visits', 'visits_percent', 'label') );
112 * $XMLstring = $xmlOutput->getOutput();
113 *
114 *
115 * ---- Other (ideas)
116 * We can also imagine building a DataTable_Compare which would take N DataTable that have the same
117 * structure and would compare them, by computing the percentages of differences, etc.
118 *
119 * For example
120 * DataTable1 = [ keyword1, 1550 visits]
121 *                 [ keyword2, 154 visits ]
122 * DataTable2 = [ keyword1, 1004 visits ]
123 *                 [ keyword3, 659 visits ]
124 * DataTable_Compare = result of comparison of table1 with table2
125 *                         [ keyword1, +154% ]
126 *                         [ keyword2, +1000% ]
127 *                         [ keyword3, -430% ]
128 *
129 * @see Piwik_DataTable_Row A Piwik_DataTable is composed of Piwik_DataTable_Row
130 *
131 * @package Piwik
132 * @subpackage Piwik_DataTable
133 */
134class Piwik_DataTable
135{   
136    /**
137     * Array of Piwik_DataTable_Row
138     *
139     * @var array
140     */
141    protected $rows = array();
142   
143    /**
144     * Id assigned to the DataTable, used to lookup the table using the DataTable_Manager
145     *
146     * @var int
147     */
148    protected $currentId;
149   
150    protected $nextRowId = 0;//new added
151   
152   
153    /**
154     * Current depth level of this data table
155     * 0 is the parent data table
156     *
157     * @var int
158     */
159    protected $depthLevel = 0;
160   
161    /**
162     * This flag is set to false once we modify the table in a way that outdates the index
163     *
164     * @var bool
165     */
166    //protected $indexNotUpToDate = true;
167   
168    /**
169     * This flag sets the index to be rebuild whenever a new row is added,
170     * as opposed to re-building the full index when getRowFromLabel is called.
171     * This is to optimize and not rebuild the full Index in the case where we
172     * add row, getRowFromLabel, addRow, getRowFromLabel thousands of times.
173     *
174     * @var bool
175     */
176    //protected $rebuildIndexContinuously = false;
177   
178    /**
179     * Column name of last time the table was sorted
180     *
181     * @var string
182     */
183    protected $tableSortedBy = false;
184   
185    /**
186     * List of Piwik_DataTable_Filter queued to this table
187     *
188     * @var array
189     */
190    protected $queuedFilters = array();
191   
192    /**
193     * We keep track of the number of rows before applying the LIMIT filter that deletes some rows
194     *
195     * @var int
196     */
197    protected $rowsCountBeforeLimitFilter = 0;
198   
199    /**
200     * Defaults to false for performance reasons (most of the time we don't need recursive sorting so we save a looping over the dataTable)
201     *
202     * @var bool
203     */
204    protected $enableRecursiveSort = false;
205
206    /*
207     * @var Piwik_DataTable_Row
208     */
209    protected $summaryRow = null;
210
211    const ID_SUMMARY_ROW = -1;
212    const LABEL_SUMMARY_ROW = -1;
213   
214    /**
215     * Maximum nesting level
216     *
217     * @var int
218     */
219    const MAXIMUM_DEPTH_LEVEL_ALLOWED = 15;
220
221    /**
222     * Builds the DataTable, registers itself to the manager
223     *
224     */
225    public function __construct()
226    {
227        $this->currentId = Piwik_DataTable_Manager::getInstance()->addTable($this);
228    }
229
230    /**
231     * At destruction we free all memory
232     */
233    public function __destruct()
234    {
235        static $depth = 0;
236        // destruct can be called several times
237        if($depth < self::MAXIMUM_DEPTH_LEVEL_ALLOWED
238            && isset($this->rows))
239        {
240            $depth++;
241            foreach($this->getRows() as $row) {
242                destroy($row);
243            }
244            unset($this->rows);
245            Piwik_DataTable_Manager::getInstance()->setTableDeleted($this->getId());   
246            $depth--;
247        }
248    }
249   
250    /**
251     * Sort the dataTable rows using the php callback function
252     *
253     * @param string $functionCallback
254     * @param string $columnSortedBy The column name. Used to then ask the datatable what column are you sorted by
255     */
256    public function sort( $functionCallback, $columnSortedBy )
257    {
258        //$this->indexNotUpToDate = true;
259        $this->tableSortedBy = $columnSortedBy;
260        usort( $this->rows, $functionCallback );
261       
262        if($this->enableRecursiveSort === true)
263        {
264            foreach($this->getRows() as $row)
265            {
266                if(($idSubtable = $row->getIdSubDataTable()) !== null)
267                {
268                    $table = Piwik_DataTable_Manager::getInstance()->getTable($idSubtable);
269                    $table->enableRecursiveSort();
270                    $table->sort($functionCallback, $columnSortedBy);
271                }
272            }
273        }
274    }
275
276    public function getSortedByColumnName()
277    {
278        return $this->tableSortedBy;
279    }
280   
281    /**
282     * Enables the recursive sort. Means that when using $table->sort()
283     * it will also sort all subtables using the same callback
284     */
285    public function enableRecursiveSort()
286    {
287        $this->enableRecursiveSort = true;
288    }
289
290    /**
291     * Returns the number of rows before we applied the limit filter
292     *
293     * @return int
294     */
295    public function getRowsCountBeforeLimitFilter()
296    {
297        $toReturn = $this->rowsCountBeforeLimitFilter;
298        if($toReturn == 0)
299        {
300            return $this->getRowsCount();
301        }
302        return $toReturn;
303    }
304
305    /**
306     * Saves the current number of rows
307     */
308    function setRowsCountBeforeLimitFilter()
309    {
310        $this->rowsCountBeforeLimitFilter = $this->getRowsCount();
311    }
312
313    /**
314     * Apply a filter to this datatable
315     *
316     * @param $className eg. "Sort" or "Piwik_DataTable_Filter_Sort"
317     * @param $parameters eg. array('nb_visits', 'asc')
318     */
319    public function filter( $className, $parameters = array() )
320    {
321        if(!class_exists($className, false))
322        {
323            $className = "Piwik_DataTable_Filter_" . $className;
324        }
325        $reflectionObj = new ReflectionClass($className);
326       
327        // the first parameter of a filter is the DataTable
328        // we add the current datatable as the parameter
329        $parameters = array_merge(array($this), $parameters);
330       
331        $filter = $reflectionObj->newInstanceArgs($parameters);
332    }
333   
334    /**
335     * Queue a DataTable_Filter that will be applied when applyQueuedFilters() is called.
336     * (just before sending the datatable back to the browser (or API, etc.)
337     *
338     * @param string $className The class name of the filter, eg. Piwik_DataTable_Filter_Limit
339     * @param array $parameters The parameters to give to the filter, eg. array( $offset, $limit) for the filter Piwik_DataTable_Filter_Limit
340     */
341    public function queueFilter( $className, $parameters = array() )
342    {
343        if(!is_array($parameters))
344        {
345            $parameters = array($parameters);
346        }
347        $this->queuedFilters[] = array('className' => $className, 'parameters' => $parameters);
348    }
349
350    /**
351     * Apply all filters that were previously queued to this table
352     * @see queueFilter()
353     */
354    public function applyQueuedFilters()
355    {
356        foreach($this->queuedFilters as $filter)
357        {
358            if($filter['className'] == 'Piwik_DataTable_Filter_Limit')
359            {
360                $this->setRowsCountBeforeLimitFilter();
361            }
362           
363            $this->filter($filter['className'], $filter['parameters']);
364        }
365        $this->queuedFilters = array();
366    }
367
368    /**
369     * Adds a new DataTable to this DataTable
370     * Go through all the rows of the new DataTable and applies the algorithm:
371     * - if a row in $table doesnt exist in $this we add the new row to $this
372     * - if a row exists in both $table and $this we sum the columns values into $this
373     * - if a row in $this doesnt exist in $table we keep the row of $this without modification
374     *
375     * A common row to 2 DataTable is defined by the same label
376     *     
377     * @example tests/core/DataTable.test.php
378     */
379    public function addDataTable( Piwik_DataTable $tableToSum )
380    {
381        foreach($tableToSum->getRows() as $row)
382        {
383            $labelToLookFor = $row->getColumn('label');
384            $rowFound = $this->getRowFromLabel( $labelToLookFor );
385            if($rowFound === false)
386            {
387                if( $labelToLookFor === self::LABEL_SUMMARY_ROW )
388                {
389                    $this->addSummaryRow( $row );
390                }
391                else
392                {
393                    $this->addRow( $row );
394                }
395            }
396            else
397            {
398                $rowFound->sumRow( $row );
399
400                // if the row to add has a subtable whereas the current row doesn't
401                // we simply add it (cloning the subtable)
402                // if the row has the subtable already
403                // then we have to recursively sum the subtables
404                if(($idSubTable = $row->getIdSubDataTable()) !== null)
405                {
406                    $rowFound->sumSubtable( Piwik_DataTable_Manager::getInstance()->getTable($idSubTable) );
407                }
408            }
409        }
410    }
411
412    /**
413     * Returns the Piwik_DataTable_Row that has a column 'label' with the value $label
414     *
415     * @param string $label Value of the column 'label' of the row to return
416     * @return Piwik_DataTable_Row|false The row if found, false otherwise
417     */
418    public function getRowFromLabel( $label )
419    {
420    /*    $this->rebuildIndexContinuously = true;
421        if($this->indexNotUpToDate)
422        {
423            $this->rebuildIndex();
424        }
425    */   
426        if($label === self::LABEL_SUMMARY_ROW
427            && !is_null($this->summaryRow))
428        {
429            return $this->summaryRow;
430        }
431       
432        $label = (string)$label;
433        if(!isset($this->rowsIndexByLabel[$label]))
434        {
435            return false;
436        }
437        return $this->rows[$this->rowsIndexByLabel[$label]];
438    }
439
440    /**
441     * Rebuilds the index used to lookup a row by label
442     */
443     
444    /*
445    private function rebuildIndex()
446    {
447        foreach($this->rows as $id => $row)
448        {
449            $label = $row->getColumn('label');
450            if($label !== false)
451            {
452                $this->rowsIndexByLabel[$label] = $id;
453            }
454        }
455        $this->indexNotUpToDate = false;
456    }
457         */
458    /**
459     * Returns the ith row in the array
460     *
461     * @param int $id
462     * @return Piwik_DataTable_Row or false if not found
463     */
464    public function getRowFromId($id)
465    {
466        if(!isset($this->rows[$id]))
467        {
468            if($id == self::ID_SUMMARY_ROW
469                && !is_null($this->summaryRow))
470            {
471                return $this->summaryRow;
472            }
473            return false;
474        }
475        return $this->rows[$id];
476    }
477
478    /**
479     * Returns a row that has the subtable ID matching the parameter
480     *
481     * @param int $idSubTable
482     * @return Piwik_DataTable_Row or false if not found
483     */
484    public function getRowFromIdSubDataTable($idSubTable)
485    {
486        $idSubTable = (int)$idSubTable;
487        foreach($this->rows as $row)
488        {
489            if($row->getIdSubDataTable() === $idSubTable)
490            {
491                return $row;
492            }
493        }
494        return false;
495    }
496   
497    /**
498     * Add a row to the table and rebuild the index if necessary
499     *
500     * @param Piwik_DataTable_Row $row to add at the end of the array
501     */
502    public function addRow( Piwik_DataTable_Row $row )
503    {
504        $this->rows[] = $row;       
505        //if(!$this->indexNotUpToDate
506        //    && $this->rebuildIndexContinuously)
507        //{
508            $label = $row->getColumn('label');
509            if($label !== false)
510            {
511                //$this->rowsIndexByLabel[$label] = count($this->rows)-1;
512                $this->rowsIndexByLabel[$label] = $this->nextRowId;
513                $this->nextRowId++;
514            }
515            $this->indexNotUpToDate = false;
516        //}
517    }
518
519    /**
520     * Sets the summary row (a dataTable can have only one summary row)
521     *
522     * @param Piwik_DataTable_Row $row
523     */
524    public function addSummaryRow( Piwik_DataTable_Row $row )
525    {
526        $this->summaryRow = $row;
527    }
528
529    /**
530     * Returns the dataTable ID
531     *
532     * @return int
533     */
534    public function getId()
535    {
536        return $this->currentId;
537    }
538
539    /**
540     * Adds a new row from a PHP array data structure
541     *
542     * @param array $row, eg. array(Piwik_DataTable_Row::COLUMNS => array( 'visits' => 13, 'test' => 'toto'),)
543     */
544    public function addRowFromArray( $row )
545    {
546        $this->addRowsFromArray(array($row));
547    }
548
549    /**
550     * Adds a new row a PHP array data structure
551     *
552     * @param array $row, eg.  array('name' => 'google analytics', 'license' => 'commercial')
553     */
554    public function addRowFromSimpleArray( $row )
555    {
556        $this->addRowsFromSimpleArray(array($row));
557    }
558
559    /**
560     * Returns the array of Piwik_DataTable_Row
561     *
562     * @return array of Piwik_DataTable_Row
563     */
564    public function getRows()
565    {
566        if(is_null($this->summaryRow))
567        {
568            return $this->rows;
569        }
570        else
571        {
572            return $this->rows + array(self::ID_SUMMARY_ROW => $this->summaryRow);
573        }
574    }
575
576    /**
577     * Returns the array containing all rows values for the requested column
578     *
579     * @return array
580     */
581    public function getColumn( $name )
582    {
583        $columnValues = array();
584        foreach($this->getRows() as $row)
585        {
586            $columnValues[] = $row->getColumn($name);
587        }
588        return $columnValues;
589    }
590   
591    /**
592     * Returns the number of rows in the table
593     *
594     * @return int
595     */
596    public function getRowsCount()
597    {
598        if(is_null($this->summaryRow))
599        {
600            return count($this->rows);
601        }
602        else
603        {
604            return count($this->rows) + 1;
605        }
606    }
607
608    /**
609     * Returns the first row of the DataTable
610     *
611     * @return Piwik_DataTable_Row
612     */
613    public function getFirstRow()
614    {
615        if(count($this->rows) == 0)
616        {
617            if(!is_null($this->summaryRow))
618            {
619                return $this->summaryRow;
620            }
621            return false;
622        }
623        $row = array_slice($this->rows, 0, 1);
624        return $row[0];
625    }
626
627    /**
628     * Returns the last row of the DataTable
629     *
630     * @return Piwik_DataTable_Row
631     */
632    public function getLastRow()
633    {
634        if(!is_null($this->summaryRow))
635        {
636            return $this->summaryRow;
637        }
638       
639        if(count($this->rows) == 0)
640        {
641            return false;
642        }
643        $row = array_slice($this->rows, -1);
644        return $row[0];
645    }
646
647    /**
648     * Returns the sum of the number of rows of all the subtables
649     *         + the number of rows in the parent table
650     *
651     * @return int
652     */
653    public function getRowsCountRecursive()
654    {
655        $totalCount = 0;
656        foreach($this->rows as $row)
657        {
658            if(($idSubTable = $row->getIdSubDataTable()) !== null)
659            {
660                $subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable);
661                $count = $subTable->getRowsCountRecursive();
662                $totalCount += $count;
663            }
664        }
665       
666        $totalCount += $this->getRowsCount();
667        return $totalCount;
668    }
669
670    /**
671     * Delete a given column $name in all the rows
672     *
673     * @param string $name
674     */
675    public function deleteColumn( $name )
676    {
677        $this->deleteColumns(array($name));
678    }
679
680    /**
681     * Rename a column in all rows
682     *
683     * @param $oldName
684     * @param $newName
685     */
686    public function renameColumn( $oldName, $newName )
687    {
688        foreach($this->getRows() as $row)
689        {
690            $row->renameColumn($oldName, $newName);
691            if(($idSubDataTable = $row->getIdSubDataTable()) !== null)
692            {
693                Piwik_DataTable_Manager::getInstance()->getTable($idSubDataTable)->renameColumn($oldName, $newName);
694            }
695        }
696        if(!is_null($this->summaryRow))
697        {           
698            $this->summaryRow->renameColumn($oldName, $newName);
699        }
700    }
701   
702    /**
703     * Delete columns by name in all rows
704     *
705     * @param string $name
706     */
707    public function deleteColumns($names, $deleteRecursiveInSubtables = false)
708    {
709        foreach($this->getRows() as $row)
710        {
711            foreach($names as $name)
712            {
713                $row->deleteColumn($name);
714            }
715            if(($idSubDataTable = $row->getIdSubDataTable()) !== null)
716            {
717                Piwik_DataTable_Manager::getInstance()->getTable($idSubDataTable)->deleteColumns($names, $deleteRecursiveInSubtables);
718            }
719        }
720        if(!is_null($this->summaryRow))
721        {           
722            foreach($names as $name)
723            {
724                $this->summaryRow->deleteColumn($name);
725            }
726        }
727    }
728   
729    /**
730     * Deletes the ith row
731     *
732     * @param int $key
733     * @throws Exception if the row $id cannot be found
734     */
735    public function deleteRow( $id )
736    {
737        if($id === self::ID_SUMMARY_ROW)
738        {
739            $this->summaryRow = null;
740            return;
741        }
742        if(!isset($this->rows[$id]))
743        {
744            throw new Exception("Trying to delete unknown row with idkey = $id");
745        }
746        unset($this->rows[$id]);
747    }
748
749    /**
750     * Deletes all row from offset, offset + limit.
751     * If limit is null then limit = $table->getRowsCount()
752     *
753     * @param int $offset
754     * @param int $limit
755     */
756    public function deleteRowsOffset( $offset, $limit = null )
757    {
758        if($limit === 0)
759        {
760            return;
761        }
762
763        $count = $this->getRowsCount();
764        if($offset >= $count)
765        {
766            return;
767        }
768
769        // if we delete until the end, we delete the summary row as well
770        if( is_null($limit)
771            || $limit >= $count )
772        {
773            $this->summaryRow = null;
774        }
775
776        if(is_null($limit))
777        {
778            array_splice($this->rows, $offset);
779        }
780        else
781        {
782            array_splice($this->rows, $offset, $limit);
783        }
784    }
785
786    /**
787     * Deletes the rows from the list of rows ID
788     *
789     * @param array $aKeys ID of the rows to delete
790     * @throws Exception if any of the row to delete couldn't be found
791     */
792    public function deleteRows( array $aKeys )
793    {
794        foreach($aKeys as $key)
795        {
796            $this->deleteRow($key);
797        }
798    }
799
800    /**
801     * Returns a simple output of the DataTable for easy visualization
802     * Example: echo $datatable;
803     *
804     * @return string
805     */
806    public function __toString()
807    {
808        $renderer = new Piwik_DataTable_Renderer_Console();
809        $renderer->setTable($this);
810        return (string)$renderer;
811    }
812
813    /**
814     * Returns true if both DataTable are exactly the same.
815     * Used in unit tests.
816     *
817     * @param Piwik_DataTable $table1
818     * @param Piwik_DataTable $table2
819     * @return bool
820     */
821    static public function isEqual(Piwik_DataTable $table1, Piwik_DataTable $table2)
822    {
823        $rows1 = $table1->getRows();
824        $rows2 = $table2->getRows();
825       
826        //$table1->rebuildIndex();
827        //$table2->rebuildIndex();
828       
829        if($table1->getRowsCount() != $table2->getRowsCount())
830        {
831            return false;
832        }
833       
834        foreach($rows1 as $row1)
835        {
836            $row2 = $table2->getRowFromLabel($row1->getColumn('label'));
837            if($row2 === false
838                || !Piwik_DataTable_Row::isEqual($row1,$row2))
839            {
840                return false;
841            }
842        }
843       
844        return true;
845    }
846
847    /**
848     * The serialization returns a one dimension array containing all the
849     * serialized DataTable contained in this DataTable.
850     * We save DataTable in serialized format in the Database.
851     * Each row of this returned PHP array will be a row in the DB table.
852     * At the end of the method execution, the dataTable may be truncated (if $maximum* parameters are set).
853     *
854     * The keys of the array are very important as they are used to define the DataTable
855     *
856     * IMPORTANT: The main table (level 0, parent of all tables) will always be indexed by 0
857     *     even it was created after some other tables.
858     *     It also means that all the parent tables (level 0) will be indexed with 0 in their respective
859     *  serialized arrays. You should never lookup a parent table using the getTable( $id = 0) as it
860     *  won't work.
861     *
862     * @throws Exception if an infinite recursion is found (a table row's has a subtable that is one of its parent table)
863     * @param int If not null, defines the number of rows maximum of the serialized dataTable
864     *               If $addSummaryRowAfterNRows is less than the size of the table, a SummaryRow will be added at the end of the table, that
865     *            is the sum of the values of all the rows after the Nth row. All the rows after the Nth row will be deleted.
866     *
867     * @return array Serialized arrays   
868     *             array(     // Datatable level0
869     *                     0 => 'eghuighahgaueytae78yaet7yaetae',
870     *
871     *                     // first Datatable level1
872     *                     1 => 'gaegae gh gwrh guiwh uigwhuige',
873     *                     
874     *                     //second Datatable level1
875     *                     2 => 'gqegJHUIGHEQjkgneqjgnqeugUGEQHGUHQE', 
876     *                     
877     *                     //first Datatable level3 (child of second Datatable level1 for example)
878      *                    3 => 'eghuighahgaueytae78yaet7yaetaeGRQWUBGUIQGH&QE',
879     *                     );
880     */
881    public function getSerialized(    $maximumRowsInDataTable = null,
882                                    $maximumRowsInSubDataTable = null,
883                                    $columnToSortByBeforeTruncation = null )
884    {
885        static $depth = 0;
886       
887        if($depth > self::MAXIMUM_DEPTH_LEVEL_ALLOWED)
888        {
889            $depth = 0;
890            throw new Exception("Maximum recursion level of ".self::MAXIMUM_DEPTH_LEVEL_ALLOWED. " reached. You have probably set a DataTable_Row with an associated DataTable which belongs already to its parent hierarchy.");
891        }
892        if( !is_null($maximumRowsInDataTable) )
893        {
894            $this->filter('AddSummaryRow',
895                            array(    $maximumRowsInDataTable - 1,
896                                    Piwik_DataTable::LABEL_SUMMARY_ROW,
897                                    $columnToSortByBeforeTruncation)
898                    );
899        }
900       
901        // For each row, get the serialized row
902        // If it is associated to a sub table, get the serialized table recursively ;
903        // but returns all serialized tables and subtable in an array of 1 dimension
904        $aSerializedDataTable = array();
905        foreach($this->rows as $row)
906        {
907            if(($idSubTable = $row->getIdSubDataTable()) !== null)
908            {
909                $subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable);
910                $depth++;
911                $aSerializedDataTable = $aSerializedDataTable + $subTable->getSerialized( $maximumRowsInSubDataTable, $maximumRowsInSubDataTable, $columnToSortByBeforeTruncation );
912                $depth--;
913            }
914        }
915        // we load the current Id of the DataTable
916        $forcedId = $this->getId();
917       
918        // if the datatable is the parent we force the Id at 0 (this is part of the specification)
919        if($depth == 0)
920        {
921            $forcedId = 0;
922        }
923       
924        // we then serialize the rows and store them in the serialized dataTable
925        $aSerializedDataTable[$forcedId] = serialize($this->rows + array( self::ID_SUMMARY_ROW => $this->summaryRow));
926       
927        return $aSerializedDataTable;
928    }
929
930     /**
931      * Load a serialized string of a datatable.
932      *
933      * Does not load recursively all the sub DataTable.
934      * They will be loaded only when requesting them specifically.
935      *
936      * The function creates all the necessary DataTable_Row
937      *
938      * @param string string of serialized datatable
939      */
940    public function addRowsFromSerializedArray( $stringSerialized )
941    {
942        $serialized = unserialize($stringSerialized);
943        if($serialized === false)
944        {
945            throw new Exception("The unserialization has failed!");
946        }
947        $this->addRowsFromArray($serialized);
948    }
949
950    /**
951     * Loads the DataTable from a PHP array data structure
952     *
953     * @param array Array with the following structure
954     *             array(
955      *                 // row1
956     *                 array(
957     *                 Piwik_DataTable_Row::COLUMNS => array( col1_name => value1, col2_name => value2, ...),
958     *                 Piwik_DataTable_Row::METADATA => array( metadata1_name => value1,  ...), // see Piwik_DataTable_Row
959     *
960     *                 ),
961     *                     
962     *                 // row2
963     *                 array( ... ),
964     *                 
965     *             )
966     */
967    public function addRowsFromArray( $array )
968    {
969        foreach($array as $id => $row)
970        {
971            if(is_array($row))
972            {
973                $row = new Piwik_DataTable_Row($row);
974            }
975            if($id == self::ID_SUMMARY_ROW)
976            {
977                $this->summaryRow = $row;
978            }
979            else
980            {
981                $this->addRow($row);
982            }
983        }
984    }
985
986    /**
987     * Loads the data from a simple php array.
988     * Basically maps a simple multidimensional php array to a DataTable.
989     * Not recursive (if a row contains a php array itself, it won't be loaded)
990     *
991     * @param array Array with the simple structure:
992     *         array(
993     *             array( col1_name => valueA, col2_name => valueC, ...),
994     *             array( col1_name => valueB, col2_name => valueD, ...),
995     *        )
996     */
997    public function addRowsFromSimpleArray( $array )
998    {
999        if(count($array) === 0)
1000        {
1001            return;
1002        }
1003       
1004        // we define an exception we may throw if at one point we notice that we cannot handle the data structure
1005        $e = new Exception(" Data structure returned is not convertible in the requested format.".
1006                        " Try to call this method with the parameters '&format=original&serialize=1'".
1007                        "; you will get the original php data structure serialized.".
1008                        " The data structure looks like this: \n \$data = " . var_export($array, true) . "; ");
1009               
1010       
1011        // first pass to see if the array has the structure
1012        // array(col1_name => val1, col2_name => val2, etc.)
1013        // with val* that are never arrays (only strings/numbers/bool/etc.)
1014        // if we detect such a "simple" data structure we convert it to a row with the correct columns' names
1015        $thisIsNotThatSimple = false;
1016       
1017        foreach($array as $columnName => $columnValue )
1018        {
1019            if(is_array($columnValue) || is_object($columnValue))
1020            {
1021                $thisIsNotThatSimple = true;
1022                break;
1023            }
1024        }
1025        if($thisIsNotThatSimple === false)
1026        {
1027            // case when the array is indexed by the default numeric index
1028            if( array_keys($array) == array_keys(array_fill(0, count($array), true)) )
1029            {
1030                foreach($array as $row)
1031                {
1032                    $this->addRow( new Piwik_DataTable_Row( array( Piwik_DataTable_Row::COLUMNS => array($row) ) ) );                   
1033                }
1034            }
1035            else
1036            {
1037                $this->addRow( new Piwik_DataTable_Row( array( Piwik_DataTable_Row::COLUMNS => $array ) ) );
1038            }
1039            // we have converted our simple array to one single row
1040            // => we exit the method as the job is now finished
1041            return;
1042        }
1043       
1044        foreach($array as $key => $row)
1045        {
1046            // stuff that looks like a line
1047            if(is_array($row))
1048            {
1049                /**
1050                 * We make sure we can convert this PHP array without losing information.
1051                 * We are able to convert only simple php array (no strings keys, no sub arrays, etc.)
1052                 *
1053                 */
1054               
1055                // if the key is a string it means that some information was contained in this key.
1056                // it cannot be lost during the conversion. Because we are not able to handle properly
1057                // this key, we throw an explicit exception.
1058                if(is_string($key))
1059                {
1060                    throw $e;
1061                }
1062                // if any of the sub elements of row is an array we cannot handle this data structure...
1063                foreach($row as $subRow)
1064                {
1065                    if(is_array($subRow))
1066                    {
1067                        throw $e;
1068                    }
1069                }
1070                $row = new Piwik_DataTable_Row( array( Piwik_DataTable_Row::COLUMNS => $row ) );       
1071            }
1072            // other (string, numbers...) => we build a line from this value
1073            else
1074            {
1075                $row = new Piwik_DataTable_Row( array( Piwik_DataTable_Row::COLUMNS => array($key => $row)) );
1076            }               
1077            $this->addRow($row);
1078        }
1079    }
1080
1081    /**
1082     * Rewrites the input $array
1083     * array (
1084     *      LABEL => array(col1 => X, col2 => Y),
1085     *      LABEL2 => array(col1 => X, col2 => Y),
1086     * )
1087     * to the structure
1088     * array (
1089     *      array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL, col1 => X, col2 => Y)),
1090     *      array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL2, col1 => X, col2 => Y)),
1091     * )
1092     *
1093     * It also works with array having only one value per row, eg.
1094     * array (
1095     *      LABEL => X,
1096     *      LABEL2 => Y,
1097     * )
1098     * would be converted to the structure
1099     * array (
1100     *      array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL, 'value' => X)),
1101     *      array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL2, 'value' => Y)),
1102     * )
1103     *
1104     * The optional parameter $subtablePerLabel is an array of subTable associated to the rows of the $array
1105     * For example if $subtablePerLabel is given
1106     * array(
1107     *         LABEL => #Piwik_DataTable_ForLABEL,
1108     *         LABEL2 => #Piwik_DataTable_ForLABEL2,
1109     * )
1110     *
1111     * the $array would become
1112     * array (
1113     *      array(     Piwik_DataTable_Row::COLUMNS => array('label' => LABEL, col1 => X, col2 => Y),
1114     *                 Piwik_DataTable_Row::DATATABLE_ASSOCIATED => #ID DataTable For LABEL
1115     *         ),
1116     *      array(     Piwik_DataTable_Row::COLUMNS => array('label' => LABEL2, col1 => X, col2 => Y)
1117     *                 Piwik_DataTable_Row::DATATABLE_ASSOCIATED => #ID2 DataTable For LABEL2
1118     *         ),
1119     * )
1120     *
1121     * @param array $array See method description
1122     * @param array|null $subtablePerLabel see method description
1123     */
1124    public function addRowsFromArrayWithIndexLabel( $array, $subtablePerLabel = null)
1125    {
1126        $cleanRow = array();
1127        foreach($array as $label => $row)
1128        {
1129            if(!is_array($row))
1130            {
1131                $row = array('value' => $row);
1132            }
1133            $cleanRow[Piwik_DataTable_Row::DATATABLE_ASSOCIATED] = null;
1134            // we put the 'label' column first as it looks prettier in API results
1135            $cleanRow[Piwik_DataTable_Row::COLUMNS] = array('label' => $label) + $row;
1136            if(!is_null($subtablePerLabel)
1137                // some rows of this table don't have subtables
1138                // (for example case of campaigns without keywords)
1139                && isset($subtablePerLabel[$label])
1140            )
1141            {
1142                $cleanRow[Piwik_DataTable_Row::DATATABLE_ASSOCIATED] = $subtablePerLabel[$label];
1143            }
1144            $this->addRow( new Piwik_DataTable_Row($cleanRow) );
1145        }
1146    }
1147}