PageRenderTime 23ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 0ms

/core/Archive/Single.php

https://github.com/quarkness/piwik
PHP | 583 lines | 315 code | 67 blank | 201 comment | 50 complexity | 0ab6769a01d2ecb5e250ad7770657a0b MD5 | raw file
  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$
  8. *
  9. *
  10. * @category Piwik
  11. * @package Piwik
  12. */
  13. /**
  14. * Piwik_Archive_Single is used to store the data of a single archive,
  15. * for example the statistics for the 'day' '2008-02-21' for the website idSite '2'
  16. *
  17. * @package Piwik
  18. * @subpackage Piwik_Archive
  19. */
  20. class Piwik_Archive_Single extends Piwik_Archive
  21. {
  22. /**
  23. * The Piwik_ArchiveProcessing object used to check that the archive is available
  24. * and launch the processing if the archive was not yet processed
  25. *
  26. * @var Piwik_ArchiveProcessing
  27. */
  28. public $archiveProcessing = null;
  29. /**
  30. * @var bool Set to true if the archive has at least 1 visit
  31. */
  32. public $isThereSomeVisits = null;
  33. /**
  34. * Period of this Archive
  35. *
  36. * @var Piwik_Period
  37. */
  38. protected $period = null;
  39. /**
  40. * Set to true will activate numeric value caching for this archive.
  41. *
  42. * @var bool
  43. */
  44. protected $cacheEnabledForNumeric = true;
  45. /**
  46. * Array of cached numeric values, used to make requests faster
  47. * when requesting the same value again and again
  48. *
  49. * @var array of numeric
  50. */
  51. protected $numericCached = array();
  52. /**
  53. * Array of cached blob, used to make requests faster when requesting the same blob again and again
  54. *
  55. * @var array of mixed
  56. */
  57. protected $blobCached = array();
  58. /**
  59. * idarchive of this Archive in the database
  60. *
  61. * @var int
  62. */
  63. protected $idArchive = null;
  64. /**
  65. * name of requested report
  66. *
  67. * @var string
  68. */
  69. protected $requestedReport = null;
  70. /**
  71. * Flag set to true once the archive has been checked (when we make sure it is archived)
  72. *
  73. * @var bool
  74. */
  75. protected $alreadyChecked = array();
  76. protected function clearCache()
  77. {
  78. foreach($this->blobCached as $name => $blob)
  79. {
  80. $this->freeBlob($name);
  81. }
  82. $this->blobCached = array();
  83. }
  84. public function __destruct()
  85. {
  86. $this->clearCache();
  87. }
  88. /**
  89. * Returns the pretty date of this Archive, eg. 'Thursday 20th March 2008'
  90. *
  91. * @return string
  92. */
  93. public function getPrettyDate()
  94. {
  95. return $this->period->getPrettyString();
  96. }
  97. /**
  98. * Returns the idarchive of this Archive used to index this archive in the DB
  99. *
  100. * @return int
  101. */
  102. public function getIdArchive()
  103. {
  104. if(is_null($this->idArchive))
  105. {
  106. throw new Exception("idArchive is null");
  107. }
  108. return $this->idArchive;
  109. }
  110. /**
  111. * Set the period
  112. *
  113. * @param Piwik_Period $period
  114. */
  115. public function setPeriod( Piwik_Period $period )
  116. {
  117. $this->period = $period;
  118. }
  119. public function getPeriod()
  120. {
  121. return $this->period;
  122. }
  123. /**
  124. * Returns the timestamp of the first date in the period for this Archive.
  125. * This is used to sort archives by date when working on a Archive_Array
  126. *
  127. * @return int Unix timestamp
  128. */
  129. public function getTimestampStartDate()
  130. {
  131. if(!is_null($this->archiveProcessing))
  132. {
  133. $timestamp = $this->archiveProcessing->getTimestampStartDate();
  134. if(!empty($timestamp))
  135. {
  136. return $timestamp;
  137. }
  138. }
  139. return $this->period->getDateStart()->getTimestamp();
  140. }
  141. /**
  142. * Prepares the archive. Gets the idarchive from the ArchiveProcessing.
  143. *
  144. * This will possibly launch the archiving process if the archive was not available.
  145. */
  146. public function prepareArchive()
  147. {
  148. $archiveJustProcessed = false;
  149. $periodString = $this->period->getLabel();
  150. $plugin = Piwik_ArchiveProcessing::getPluginBeingProcessed($this->getRequestedReport());
  151. $cacheKey = 'all';
  152. if($periodString == 'range')
  153. {
  154. $cacheKey = $plugin;
  155. }
  156. if(!isset($this->alreadyChecked[$cacheKey]))
  157. {
  158. $this->isThereSomeVisits = false;
  159. $this->alreadyChecked[$cacheKey] = true;
  160. $dayString = $this->period->getPrettyString();
  161. $logMessage = "Preparing archive: " . $periodString . "(" . $dayString . "), plugin $plugin ";
  162. // if the END of the period is BEFORE the website creation date
  163. // we already know there are no stats for this period
  164. // we add one day to make sure we don't miss the day of the website creation
  165. if( $this->period->getDateEnd()->addDay(2)->isEarlier( $this->site->getCreationDate() ) )
  166. {
  167. Piwik::log("$logMessage skipped, archive is before the website was created.");
  168. return;
  169. }
  170. // if the starting date is in the future we know there is no visit
  171. if( $this->period->getDateStart()->subDay(2)->isLater( Piwik_Date::today() ) )
  172. {
  173. Piwik::log("$logMessage skipped, archive is after today.");
  174. return;
  175. }
  176. // we make sure the archive is available for the given date
  177. $periodLabel = $this->period->getLabel();
  178. $this->archiveProcessing = Piwik_ArchiveProcessing::factory($periodLabel);
  179. $this->archiveProcessing->setSite($this->site);
  180. $this->archiveProcessing->setPeriod($this->period);
  181. $this->archiveProcessing->setSegment($this->segment);
  182. $this->archiveProcessing->init();
  183. $this->archiveProcessing->setRequestedReport( $this->getRequestedReport() );
  184. $archivingDisabledArchiveNotProcessed = false;
  185. $idArchive = $this->archiveProcessing->loadArchive();
  186. if(empty($idArchive))
  187. {
  188. if($this->archiveProcessing->isArchivingDisabled())
  189. {
  190. $archivingDisabledArchiveNotProcessed = true;
  191. $logMessage = "* ARCHIVING DISABLED, for $logMessage";
  192. }
  193. else
  194. {
  195. Piwik::log("* PROCESSING $logMessage, not archived yet...");
  196. $archiveJustProcessed = true;
  197. // Process the reports
  198. $this->archiveProcessing->launchArchiving();
  199. $idArchive = $this->archiveProcessing->getIdArchive();
  200. $logMessage = "PROCESSED: idArchive = ".$idArchive.", for $logMessage";
  201. }
  202. }
  203. else
  204. {
  205. $logMessage = "* ALREADY PROCESSED, Fetching idArchive = $idArchive (idSite=".$this->site->getId()."), for $logMessage";
  206. }
  207. Piwik::log("$logMessage, Visits = ". $this->archiveProcessing->getNumberOfVisits());
  208. $this->isThereSomeVisits = !$archivingDisabledArchiveNotProcessed
  209. && $this->archiveProcessing->isThereSomeVisits();
  210. $this->idArchive = $idArchive;
  211. }
  212. return $archiveJustProcessed;
  213. }
  214. /**
  215. * Returns a value from the current archive with the name = $name
  216. * Method used by getNumeric or getBlob
  217. *
  218. * @param string $name
  219. * @param string $typeValue numeric|blob
  220. * @return mixed|false if no result
  221. */
  222. protected function get( $name, $typeValue = 'numeric' )
  223. {
  224. $this->setRequestedReport($name);
  225. $this->prepareArchive();
  226. // values previously "get" and now cached
  227. if($typeValue == 'numeric'
  228. && $this->cacheEnabledForNumeric
  229. && isset($this->numericCached[$name])
  230. )
  231. {
  232. return $this->numericCached[$name];
  233. }
  234. // During archiving we prefetch the blobs recursively
  235. // and we get them faster from memory after
  236. if($typeValue == 'blob'
  237. && isset($this->blobCached[$name]))
  238. {
  239. return $this->blobCached[$name];
  240. }
  241. if($name == 'idarchive')
  242. {
  243. return $this->idArchive;
  244. }
  245. if(!$this->isThereSomeVisits)
  246. {
  247. return false;
  248. }
  249. // select the table to use depending on the type of the data requested
  250. switch($typeValue)
  251. {
  252. case 'blob':
  253. $table = $this->archiveProcessing->getTableArchiveBlobName();
  254. break;
  255. case 'numeric':
  256. default:
  257. $table = $this->archiveProcessing->getTableArchiveNumericName();
  258. break;
  259. }
  260. $db = Zend_Registry::get('db');
  261. $value = $db->fetchOne("SELECT value
  262. FROM $table
  263. WHERE idarchive = ?
  264. AND name = ?",
  265. array( $this->idArchive , $name)
  266. );
  267. if($value === false)
  268. {
  269. if($typeValue == 'numeric'
  270. && $this->cacheEnabledForNumeric)
  271. {
  272. $this->numericCached[$name] = false;
  273. }
  274. return $value;
  275. }
  276. // uncompress when selecting from the BLOB table
  277. if($typeValue == 'blob' && $db->hasBlobDataType())
  278. {
  279. $value = $this->uncompress($value);
  280. }
  281. if($typeValue == 'numeric'
  282. && $this->cacheEnabledForNumeric)
  283. {
  284. $this->numericCached[$name] = $value;
  285. }
  286. return $value;
  287. }
  288. /**
  289. * This method loads in memory all the subtables for the main table called $name.
  290. * You have to give it the parent table $dataTableToLoad so we can lookup the sub tables ids to load.
  291. *
  292. * If $addMetadataSubtableId set to true, it will add for each row a 'metadata' called 'databaseSubtableId'
  293. * containing the child ID of the subtable associated to this row.
  294. *
  295. * @param string $name
  296. * @param Piwik_DataTable $dataTableToLoad
  297. * @param bool $addMetadataSubtableId
  298. */
  299. public function loadSubDataTables($name, Piwik_DataTable $dataTableToLoad, $addMetadataSubtableId = false)
  300. {
  301. // we have to recursively load all the subtables associated to this table's rows
  302. // and update the subtableID so that it matches the newly instanciated table
  303. foreach($dataTableToLoad->getRows() as $row)
  304. {
  305. $subTableID = $row->getIdSubDataTable();
  306. if($subTableID !== null)
  307. {
  308. $subDataTableLoaded = $this->getDataTable($name, $subTableID);
  309. $this->loadSubDataTables($name, $subDataTableLoaded, $addMetadataSubtableId);
  310. // we edit the subtable ID so that it matches the newly table created in memory
  311. // NB: we dont overwrite the datatableid in the case we are displaying the table expanded.
  312. if($addMetadataSubtableId)
  313. {
  314. // this will be written back to the column 'idsubdatatable' just before rendering, see Renderer/Php.php
  315. $row->addMetadata('idsubdatatable_in_db', $row->getIdSubDataTable());
  316. }
  317. $row->setSubtable( $subDataTableLoaded );
  318. }
  319. }
  320. }
  321. /**
  322. * Free the blob cache memory array
  323. */
  324. public function freeBlob( $name )
  325. {
  326. $this->blobCached[$name] = null;
  327. unset($this->blobCached[$name]);
  328. }
  329. protected function uncompress($data)
  330. {
  331. return @gzuncompress($data);
  332. }
  333. /**
  334. * Fetches all blob fields name_* at once for the current archive for performance reasons.
  335. *
  336. * @return false if no visits
  337. */
  338. public function preFetchBlob( $name )
  339. {
  340. $this->setRequestedReport($name);
  341. $this->prepareArchive();
  342. if(!$this->isThereSomeVisits) { return; }
  343. $tableBlob = $this->archiveProcessing->getTableArchiveBlobName();
  344. $db = Zend_Registry::get('db');
  345. $hasBlobs = $db->hasBlobDataType();
  346. $query = $db->query("SELECT value, name
  347. FROM $tableBlob
  348. WHERE idarchive = ?
  349. AND name LIKE '$name%'",
  350. array( $this->idArchive )
  351. );
  352. while($row = $query->fetch())
  353. {
  354. $value = $row['value'];
  355. $name = $row['name'];
  356. if($hasBlobs)
  357. {
  358. $this->blobCached[$name] = $this->uncompress($value);
  359. if($this->blobCached[$name] === false)
  360. {
  361. //throw new Exception("Error gzuncompress $name ");
  362. }
  363. }
  364. else
  365. {
  366. $this->blobCached[$name] = $value;
  367. }
  368. }
  369. }
  370. /**
  371. * Returns a numeric value from this Archive, with the name '$name'
  372. *
  373. * @param string $name
  374. * @return int|float
  375. */
  376. public function getNumeric( $name )
  377. {
  378. // we cast the result as float because returns false when no visitors
  379. return round((float)$this->get($name, 'numeric'), 2);
  380. }
  381. /**
  382. * Returns a blob value from this Archive, with the name '$name'
  383. * Blob values are all values except int and float.
  384. *
  385. * @param string $name
  386. * @return mixed
  387. */
  388. public function getBlob( $name )
  389. {
  390. return $this->get($name, 'blob');
  391. }
  392. /**
  393. * Given a list of fields defining numeric values, it will return a Piwik_DataTable_Simple
  394. * containing one row per field name.
  395. *
  396. * For example $fields = array( 'max_actions',
  397. * 'nb_uniq_visitors',
  398. * 'nb_visits',
  399. * 'nb_actions',
  400. * 'sum_visit_length',
  401. * 'bounce_count',
  402. * 'nb_visits_converted'
  403. * );
  404. *
  405. * @param string|array $fields Name or array of names of Archive fields
  406. *
  407. * @return Piwik_DataTable_Simple
  408. */
  409. public function getDataTableFromNumeric( $fields )
  410. {
  411. if(!is_array($fields))
  412. {
  413. $fields = array($fields);
  414. }
  415. $values = array();
  416. foreach($fields as $field)
  417. {
  418. $values[$field] = $this->getNumeric($field);
  419. }
  420. $table = new Piwik_DataTable_Simple();
  421. $table->addRowsFromArray($values);
  422. return $table;
  423. }
  424. /**
  425. * Returns a DataTable that has the name '$name' from the current Archive.
  426. * If $idSubTable is specified, returns the subDataTable called '$name_$idSubTable'
  427. *
  428. * @param string $name
  429. * @param int $idSubTable optional id SubDataTable
  430. * @return Piwik_DataTable
  431. */
  432. public function getDataTable( $name, $idSubTable = null )
  433. {
  434. if(!is_null($idSubTable))
  435. {
  436. $name .= "_$idSubTable";
  437. }
  438. $this->setRequestedReport($name);
  439. $data = $this->get($name, 'blob');
  440. $table = new Piwik_DataTable();
  441. if($data !== false)
  442. {
  443. $table->addRowsFromSerializedArray($data);
  444. }
  445. if($data === false
  446. && $idSubTable !== null)
  447. {
  448. // This is not expected, but somehow happens in some unknown cases and very rarely.
  449. // Do not throw error in this case
  450. // throw new Exception("not expected");
  451. return new Piwik_DataTable();
  452. }
  453. return $table;
  454. }
  455. public function setRequestedReport($requestedReport )
  456. {
  457. $this->requestedReport = $requestedReport;
  458. }
  459. protected function getRequestedReport()
  460. {
  461. // Core metrics are always processed in Core, for the requested date/period/segment
  462. if(in_array($this->requestedReport, Piwik_ArchiveProcessing::getCoreMetrics())
  463. || $this->requestedReport == 'max_actions')
  464. {
  465. return 'VisitsSummary_CoreMetrics';
  466. }
  467. // VisitFrequency metrics don't follow the same naming convention (HACK)
  468. if(strpos($this->requestedReport, '_returning') > 0
  469. // ignore Goal_visitor_returning_1_1_nb_conversions
  470. && strpos($this->requestedReport, 'Goal_') === false)
  471. {
  472. return 'VisitFrequency_Metrics';
  473. }
  474. // Goal_* metrics are processed by the Goals plugin (HACK)
  475. if(strpos($this->requestedReport, 'Goal_') === 0)
  476. {
  477. return 'Goals_Metrics';
  478. }
  479. return $this->requestedReport;
  480. }
  481. /**
  482. * Returns a DataTable that has the name '$name' from the current Archive.
  483. * Also loads in memory all subDataTable for this DataTable.
  484. *
  485. * For example, if $name = 'Referers_keywordBySearchEngine' it will load all DataTable
  486. * named 'Referers_keywordBySearchEngine_*' and they will be set as subDataTable to the
  487. * rows. You can then go through the rows
  488. * $rows = DataTable->getRows();
  489. * and for each row request the subDataTable (in this case the DataTable of the keywords for each search engines)
  490. * $idSubTable = $row->getIdSubDataTable();
  491. * $subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable);
  492. *
  493. * @param string $name
  494. * @param int $idSubTable Optional subDataTable to load instead of loading the parent DataTable
  495. * @return Piwik_DataTable
  496. */
  497. public function getDataTableExpanded($name, $idSubTable = null)
  498. {
  499. $this->preFetchBlob($name);
  500. $dataTableToLoad = $this->getDataTable($name, $idSubTable);
  501. $this->loadSubDataTables($name, $dataTableToLoad, $addMetadataSubtableId = true);
  502. $dataTableToLoad->enableRecursiveFilters();
  503. $this->freeBlob($name);
  504. return $dataTableToLoad;
  505. }
  506. /**
  507. * Returns true if Piwik can launch the archiving process for this archive,
  508. * false if otherwise.
  509. *
  510. * @return bool
  511. */
  512. public function isArchivingDisabled()
  513. {
  514. return Piwik_ArchiveProcessing::isArchivingDisabledFor($this->segment, $this->period);
  515. }
  516. }