PageRenderTime 48ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/core/Archive/DataTableFactory.php

https://github.com/CodeYellowBV/piwik
PHP | 426 lines | 200 code | 58 blank | 168 comment | 34 complexity | 3b4fea1a1c834cad0acb0444c1a8f2a2 MD5 | raw file
Possible License(s): LGPL-3.0, JSON, MIT, GPL-3.0, LGPL-2.1, GPL-2.0, AGPL-1.0, BSD-2-Clause, BSD-3-Clause
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. */
  9. namespace Piwik\Archive;
  10. use Piwik\DataTable;
  11. use Piwik\DataTable\Row;
  12. use Piwik\Site;
  13. /**
  14. * Creates a DataTable or Set instance based on an array
  15. * index created by DataCollection.
  16. *
  17. * This class is only used by DataCollection.
  18. */
  19. class DataTableFactory
  20. {
  21. /**
  22. * @see DataCollection::$dataNames.
  23. */
  24. private $dataNames;
  25. /**
  26. * @see DataCollection::$dataType.
  27. */
  28. private $dataType;
  29. /**
  30. * Whether to expand the DataTables that're created or not. Expanding a DataTable
  31. * means creating DataTables using subtable blobs and correctly setting the subtable
  32. * IDs of all DataTables.
  33. *
  34. * @var bool
  35. */
  36. private $expandDataTable = false;
  37. /**
  38. * Whether to add the subtable ID used in the database to the in-memory DataTables
  39. * as metadata or not.
  40. *
  41. * @var bool
  42. */
  43. private $addMetadataSubtableId = false;
  44. /**
  45. * The maximum number of subtable levels to create when creating an expanded
  46. * DataTable.
  47. *
  48. * @var int
  49. */
  50. private $maxSubtableDepth = null;
  51. /**
  52. * @see DataCollection::$sitesId.
  53. */
  54. private $sitesId;
  55. /**
  56. * @see DataCollection::$periods.
  57. */
  58. private $periods;
  59. /**
  60. * The ID of the subtable to create a DataTable for. Only relevant for blob data.
  61. *
  62. * @var int|null
  63. */
  64. private $idSubtable = null;
  65. /**
  66. * @see DataCollection::$defaultRow.
  67. */
  68. private $defaultRow;
  69. const TABLE_METADATA_SITE_INDEX = 'site';
  70. const TABLE_METADATA_PERIOD_INDEX = 'period';
  71. /**
  72. * Constructor.
  73. */
  74. public function __construct($dataNames, $dataType, $sitesId, $periods, $defaultRow)
  75. {
  76. $this->dataNames = $dataNames;
  77. $this->dataType = $dataType;
  78. $this->sitesId = $sitesId;
  79. //here index period by string only
  80. $this->periods = $periods;
  81. $this->defaultRow = $defaultRow;
  82. }
  83. /**
  84. * Tells the factory instance to expand the DataTables that are created by
  85. * creating subtables and setting the subtable IDs of rows w/ subtables correctly.
  86. *
  87. * @param null|int $maxSubtableDepth max depth for subtables.
  88. * @param bool $addMetadataSubtableId Whether to add the subtable ID used in the
  89. * database to the in-memory DataTables as
  90. * metadata or not.
  91. */
  92. public function expandDataTable($maxSubtableDepth = null, $addMetadataSubtableId = false)
  93. {
  94. $this->expandDataTable = true;
  95. $this->maxSubtableDepth = $maxSubtableDepth;
  96. $this->addMetadataSubtableId = $addMetadataSubtableId;
  97. }
  98. /**
  99. * Tells the factory instance to create a DataTable using a blob with the
  100. * supplied subtable ID.
  101. *
  102. * @param int $idSubtable An in-database subtable ID.
  103. * @throws \Exception
  104. */
  105. public function useSubtable($idSubtable)
  106. {
  107. if (count($this->dataNames) !== 1) {
  108. throw new \Exception("DataTableFactory: Getting subtables for multiple records in one"
  109. . " archive query is not currently supported.");
  110. }
  111. $this->idSubtable = $idSubtable;
  112. }
  113. /**
  114. * Creates a DataTable|Set instance using an index of
  115. * archive data.
  116. *
  117. * @param array $index @see DataCollection
  118. * @param array $resultIndices an array mapping metadata names with pretty metadata
  119. * labels.
  120. * @return DataTable|DataTable\Map
  121. */
  122. public function make($index, $resultIndices)
  123. {
  124. if (empty($resultIndices)) {
  125. // for numeric data, if there's no index (and thus only 1 site & period in the query),
  126. // we want to display every queried metric name
  127. if (empty($index)
  128. && $this->dataType == 'numeric'
  129. ) {
  130. $index = $this->defaultRow;
  131. }
  132. $dataTable = $this->createDataTable($index, $keyMetadata = array());
  133. } else {
  134. $dataTable = $this->createDataTableMapFromIndex($index, $resultIndices, $keyMetadata = array());
  135. }
  136. $this->transformMetadata($dataTable);
  137. return $dataTable;
  138. }
  139. /**
  140. * Creates a DataTable|Set instance using an array
  141. * of blobs.
  142. *
  143. * If only one record is being queried, a single DataTable will
  144. * be returned. Otherwise, a DataTable\Map is returned that indexes
  145. * DataTables by record name.
  146. *
  147. * If expandDataTable was called, and only one record is being queried,
  148. * the created DataTable's subtables will be expanded.
  149. *
  150. * @param array $blobRow
  151. * @return DataTable|DataTable\Map
  152. */
  153. private function makeFromBlobRow($blobRow)
  154. {
  155. if ($blobRow === false) {
  156. return new DataTable();
  157. }
  158. if (count($this->dataNames) === 1) {
  159. return $this->makeDataTableFromSingleBlob($blobRow);
  160. } else {
  161. return $this->makeIndexedByRecordNameDataTable($blobRow);
  162. }
  163. }
  164. /**
  165. * Creates a DataTable for one record from an archive data row.
  166. *
  167. * @see makeFromBlobRow
  168. *
  169. * @param array $blobRow
  170. * @return DataTable
  171. */
  172. private function makeDataTableFromSingleBlob($blobRow)
  173. {
  174. $recordName = reset($this->dataNames);
  175. if ($this->idSubtable !== null) {
  176. $recordName .= '_' . $this->idSubtable;
  177. }
  178. if (!empty($blobRow[$recordName])) {
  179. $table = DataTable::fromSerializedArray($blobRow[$recordName]);
  180. } else {
  181. $table = new DataTable();
  182. }
  183. // set table metadata
  184. $table->setMetadataValues(DataCollection::getDataRowMetadata($blobRow));
  185. if ($this->expandDataTable) {
  186. $table->enableRecursiveFilters();
  187. $this->setSubtables($table, $blobRow);
  188. }
  189. return $table;
  190. }
  191. /**
  192. * Creates a DataTable for every record in an archive data row and puts them
  193. * in a DataTable\Map instance.
  194. *
  195. * @param array $blobRow
  196. * @return DataTable\Map
  197. */
  198. private function makeIndexedByRecordNameDataTable($blobRow)
  199. {
  200. $table = new DataTable\Map();
  201. $table->setKeyName('recordName');
  202. $tableMetadata = DataCollection::getDataRowMetadata($blobRow);
  203. foreach ($blobRow as $name => $blob) {
  204. $newTable = DataTable::fromSerializedArray($blob);
  205. $newTable->setAllTableMetadata($tableMetadata);
  206. $table->addTable($newTable, $name);
  207. }
  208. return $table;
  209. }
  210. /**
  211. * Creates a Set from an array index.
  212. *
  213. * @param array $index @see DataCollection
  214. * @param array $resultIndices @see make
  215. * @param array $keyMetadata The metadata to add to the table when it's created.
  216. * @return DataTable\Map
  217. */
  218. private function createDataTableMapFromIndex($index, $resultIndices, $keyMetadata = array())
  219. {
  220. $resultIndexLabel = reset($resultIndices);
  221. $resultIndex = key($resultIndices);
  222. array_shift($resultIndices);
  223. $result = new DataTable\Map();
  224. $result->setKeyName($resultIndexLabel);
  225. foreach ($index as $label => $value) {
  226. $keyMetadata[$resultIndex] = $label;
  227. if (empty($resultIndices)) {
  228. $newTable = $this->createDataTable($value, $keyMetadata);
  229. } else {
  230. $newTable = $this->createDataTableMapFromIndex($value, $resultIndices, $keyMetadata);
  231. }
  232. $result->addTable($newTable, $this->prettifyIndexLabel($resultIndex, $label));
  233. }
  234. return $result;
  235. }
  236. /**
  237. * Creates a DataTable instance from an index row.
  238. *
  239. * @param array $data An archive data row.
  240. * @param array $keyMetadata The metadata to add to the table(s) when created.
  241. * @return DataTable|DataTable\Map
  242. */
  243. private function createDataTable($data, $keyMetadata)
  244. {
  245. if ($this->dataType == 'blob') {
  246. $result = $this->makeFromBlobRow($data);
  247. } else {
  248. $result = $this->makeFromMetricsArray($data);
  249. }
  250. $this->setTableMetadata($keyMetadata, $result);
  251. return $result;
  252. }
  253. /**
  254. * Creates DataTables from $dataTable's subtable blobs (stored in $blobRow) and sets
  255. * the subtable IDs of each DataTable row.
  256. *
  257. * @param DataTable $dataTable
  258. * @param array $blobRow An array associating record names (w/ subtable if applicable)
  259. * with blob values. This should hold every subtable blob for
  260. * the loaded DataTable.
  261. * @param int $treeLevel
  262. */
  263. private function setSubtables($dataTable, $blobRow, $treeLevel = 0)
  264. {
  265. if ($this->maxSubtableDepth
  266. && $treeLevel >= $this->maxSubtableDepth
  267. ) {
  268. // unset the subtables so DataTableManager doesn't throw
  269. foreach ($dataTable->getRows() as $row) {
  270. $row->removeSubtable();
  271. }
  272. return;
  273. }
  274. $dataName = reset($this->dataNames);
  275. foreach ($dataTable->getRows() as $row) {
  276. $sid = $row->getIdSubDataTable();
  277. if ($sid === null) {
  278. continue;
  279. }
  280. $blobName = $dataName . "_" . $sid;
  281. if (isset($blobRow[$blobName])) {
  282. $subtable = DataTable::fromSerializedArray($blobRow[$blobName]);
  283. $this->setSubtables($subtable, $blobRow, $treeLevel + 1);
  284. // we edit the subtable ID so that it matches the newly table created in memory
  285. // NB: we dont overwrite the datatableid in the case we are displaying the table expanded.
  286. if ($this->addMetadataSubtableId) {
  287. // this will be written back to the column 'idsubdatatable' just before rendering,
  288. // see Renderer/Php.php
  289. $row->addMetadata('idsubdatatable_in_db', $row->getIdSubDataTable());
  290. }
  291. $row->setSubtable($subtable);
  292. }
  293. }
  294. }
  295. /**
  296. * Converts site IDs and period string ranges into Site instances and
  297. * Period instances in DataTable metadata.
  298. */
  299. private function transformMetadata($table)
  300. {
  301. $periods = $this->periods;
  302. $table->filter(function ($table) use ($periods) {
  303. $table->setMetadata(DataTableFactory::TABLE_METADATA_SITE_INDEX, new Site($table->getMetadata(DataTableFactory::TABLE_METADATA_SITE_INDEX)));
  304. $table->setMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX, $periods[$table->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX)]);
  305. });
  306. }
  307. /**
  308. * Returns the pretty version of an index label.
  309. *
  310. * @param string $labelType eg, 'site', 'period', etc.
  311. * @param string $label eg, '0', '1', '2012-01-01,2012-01-31', etc.
  312. * @return string
  313. */
  314. private function prettifyIndexLabel($labelType, $label)
  315. {
  316. if ($labelType == self::TABLE_METADATA_PERIOD_INDEX) { // prettify period labels
  317. return $this->periods[$label]->getPrettyString();
  318. }
  319. return $label;
  320. }
  321. /**
  322. * @param $keyMetadata
  323. * @param $result
  324. */
  325. private function setTableMetadata($keyMetadata, $result)
  326. {
  327. if (!isset($keyMetadata[DataTableFactory::TABLE_METADATA_SITE_INDEX])) {
  328. $keyMetadata[DataTableFactory::TABLE_METADATA_SITE_INDEX] = reset($this->sitesId);
  329. }
  330. if (!isset($keyMetadata[DataTableFactory::TABLE_METADATA_PERIOD_INDEX])) {
  331. reset($this->periods);
  332. $keyMetadata[DataTableFactory::TABLE_METADATA_PERIOD_INDEX] = key($this->periods);
  333. }
  334. // Note: $result can be a DataTable\Map
  335. $result->filter(function ($table) use ($keyMetadata) {
  336. foreach ($keyMetadata as $name => $value) {
  337. $table->setMetadata($name, $value);
  338. }
  339. });
  340. }
  341. /**
  342. * @param $data
  343. * @return DataTable\Simple
  344. */
  345. private function makeFromMetricsArray($data)
  346. {
  347. $table = new DataTable\Simple();
  348. if (!empty($data)) {
  349. $table->setAllTableMetadata(DataCollection::getDataRowMetadata($data));
  350. DataCollection::removeMetadataFromDataRow($data);
  351. $table->addRow(new Row(array(Row::COLUMNS => $data)));
  352. } else {
  353. // if we're querying numeric data, we couldn't find any, and we're only
  354. // looking for one metric, add a row w/ one column w/ value 0. this is to
  355. // ensure that the PHP renderer outputs 0 when only one column is queried.
  356. // w/o this code, an empty array would be created, and other parts of Piwik
  357. // would break.
  358. if (count($this->dataNames) == 1
  359. && $this->dataType == 'numeric'
  360. ) {
  361. $name = reset($this->dataNames);
  362. $table->addRow(new Row(array(Row::COLUMNS => array($name => 0))));
  363. }
  364. }
  365. $result = $table;
  366. return $result;
  367. }
  368. }