PageRenderTime 53ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/plugins/Goals/Controller.php

https://github.com/CodeYellowBV/piwik
PHP | 503 lines | 402 code | 62 blank | 39 comment | 47 complexity | d0f49dd3e1df7418c7da62af9447690f 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\Plugins\Goals;
  10. use Exception;
  11. use Piwik\API\Request;
  12. use Piwik\Common;
  13. use Piwik\DataTable;
  14. use Piwik\DataTable\Filter\AddColumnsProcessedMetricsGoal;
  15. use Piwik\FrontController;
  16. use Piwik\Piwik;
  17. use Piwik\Plugins\Referrers\API as APIReferrers;
  18. use Piwik\View;
  19. use Piwik\View\ReportsByDimension;
  20. /**
  21. *
  22. */
  23. class Controller extends \Piwik\Plugin\Controller
  24. {
  25. const CONVERSION_RATE_PRECISION = 1;
  26. /**
  27. * Number of "Your top converting keywords/etc are" to display in the per Goal overview page
  28. * @var int
  29. */
  30. const COUNT_TOP_ROWS_TO_DISPLAY = 3;
  31. const ECOMMERCE_LOG_SHOW_ORDERS = 1;
  32. const ECOMMERCE_LOG_SHOW_ABANDONED_CARTS = 2;
  33. protected $goalColumnNameToLabel = array(
  34. 'avg_order_revenue' => 'General_AverageOrderValue',
  35. 'nb_conversions' => 'Goals_ColumnConversions',
  36. 'conversion_rate' => 'General_ColumnConversionRate',
  37. 'revenue' => 'General_TotalRevenue',
  38. 'items' => 'General_PurchasedProducts',
  39. );
  40. private function formatConversionRate($conversionRate)
  41. {
  42. if ($conversionRate instanceof DataTable) {
  43. if ($conversionRate->getRowsCount() == 0) {
  44. $conversionRate = 0;
  45. } else {
  46. $columns = $conversionRate->getFirstRow()->getColumns();
  47. $conversionRate = (float)reset($columns);
  48. }
  49. }
  50. return sprintf('%.' . self::CONVERSION_RATE_PRECISION . 'f%%', $conversionRate);
  51. }
  52. public function __construct()
  53. {
  54. parent::__construct();
  55. $this->idSite = Common::getRequestVar('idSite', null, 'int');
  56. $this->goals = API::getInstance()->getGoals($this->idSite);
  57. foreach ($this->goals as &$goal) {
  58. $goal['name'] = Common::sanitizeInputValue($goal['name']);
  59. if (isset($goal['pattern'])) {
  60. $goal['pattern'] = Common::sanitizeInputValue($goal['pattern']);
  61. }
  62. }
  63. }
  64. public function widgetGoalReport()
  65. {
  66. $view = $this->getGoalReportView($idGoal = Common::getRequestVar('idGoal', null, 'string'));
  67. $view->displayFullReport = false;
  68. return $view->render();
  69. }
  70. public function goalReport()
  71. {
  72. $view = $this->getGoalReportView($idGoal = Common::getRequestVar('idGoal', null, 'string'));
  73. $view->displayFullReport = true;
  74. return $view->render();
  75. }
  76. public function ecommerceReport()
  77. {
  78. if (!\Piwik\Plugin\Manager::getInstance()->isPluginActivated('CustomVariables')) {
  79. throw new Exception("Ecommerce Tracking requires that the plugin Custom Variables is enabled. Please enable the plugin CustomVariables (or ask your admin).");
  80. }
  81. $view = $this->getGoalReportView($idGoal = Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER);
  82. $view->displayFullReport = true;
  83. return $view->render();
  84. }
  85. public function getEcommerceLog($fetch = false)
  86. {
  87. $saveGET = $_GET;
  88. $filterEcommerce = Common::getRequestVar('filterEcommerce', self::ECOMMERCE_LOG_SHOW_ORDERS, 'int');
  89. if($filterEcommerce == self::ECOMMERCE_LOG_SHOW_ORDERS) {
  90. $segment = urlencode('visitEcommerceStatus==ordered,visitEcommerceStatus==orderedThenAbandonedCart');
  91. } else {
  92. $segment = urlencode('visitEcommerceStatus==abandonedCart,visitEcommerceStatus==orderedThenAbandonedCart');
  93. }
  94. $_GET['segment'] = $segment;
  95. $_GET['filterEcommerce'] = $filterEcommerce;
  96. $_GET['widget'] = 1;
  97. $output = FrontController::getInstance()->dispatch('Live', 'getVisitorLog', array($fetch));
  98. $_GET = $saveGET;
  99. return $output;
  100. }
  101. protected function getGoalReportView($idGoal = false)
  102. {
  103. $view = new View('@Goals/getGoalReportView');
  104. if ($idGoal == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER) {
  105. $goalDefinition['name'] = Piwik::translate('Goals_Ecommerce');
  106. $goalDefinition['allow_multiple'] = true;
  107. $ecommerce = $view->ecommerce = true;
  108. } else {
  109. if (!isset($this->goals[$idGoal])) {
  110. Piwik::redirectToModule('Goals', 'index', array('idGoal' => null));
  111. }
  112. $goalDefinition = $this->goals[$idGoal];
  113. }
  114. $this->setGeneralVariablesView($view);
  115. $goal = $this->getMetricsForGoal($idGoal);
  116. foreach ($goal as $name => $value) {
  117. $view->$name = $value;
  118. }
  119. if ($idGoal == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER) {
  120. $goal = $this->getMetricsForGoal(Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART);
  121. foreach ($goal as $name => $value) {
  122. $name = 'cart_' . $name;
  123. $view->$name = $value;
  124. }
  125. }
  126. $view->idGoal = $idGoal;
  127. $view->goalName = $goalDefinition['name'];
  128. $view->goalAllowMultipleConversionsPerVisit = $goalDefinition['allow_multiple'];
  129. $view->graphEvolution = $this->getEvolutionGraph(array(), $idGoal, array('nb_conversions'));
  130. $view->nameGraphEvolution = 'Goals.getEvolutionGraph' . $idGoal;
  131. $view->topDimensions = $this->getTopDimensions($idGoal);
  132. // conversion rate for new and returning visitors
  133. $segment = urldecode(\Piwik\Plugins\VisitFrequency\API::RETURNING_VISITOR_SEGMENT);
  134. $conversionRateReturning = API::getInstance()->getConversionRate($this->idSite, Common::getRequestVar('period'), Common::getRequestVar('date'), $segment, $idGoal);
  135. $view->conversion_rate_returning = $this->formatConversionRate($conversionRateReturning);
  136. $segment = 'visitorType==new';
  137. $conversionRateNew = API::getInstance()->getConversionRate($this->idSite, Common::getRequestVar('period'), Common::getRequestVar('date'), $segment, $idGoal);
  138. $view->conversion_rate_new = $this->formatConversionRate($conversionRateNew);
  139. $view->goalReportsByDimension = $this->getGoalReportsByDimensionTable(
  140. $view->nb_conversions, isset($ecommerce), !empty($view->cart_nb_conversions));
  141. return $view;
  142. }
  143. public function index()
  144. {
  145. $view = $this->getOverviewView();
  146. // unsanitize goal names and other text data (not done in API so as not to break
  147. // any other code/cause security issues)
  148. $goals = $this->goals;
  149. foreach ($goals as &$goal) {
  150. $goal['name'] = Common::unsanitizeInputValue($goal['name']);
  151. if (isset($goal['pattern'])) {
  152. $goal['pattern'] = Common::unsanitizeInputValue($goal['pattern']);
  153. }
  154. }
  155. $view->goalsJSON = Common::json_encode($goals);
  156. $view->userCanEditGoals = Piwik::isUserHasAdminAccess($this->idSite);
  157. $view->ecommerceEnabled = $this->site->isEcommerceEnabled();
  158. $view->displayFullReport = true;
  159. return $view->render();
  160. }
  161. public function widgetGoalsOverview()
  162. {
  163. $view = $this->getOverviewView();
  164. $view->displayFullReport = false;
  165. return $view->render();
  166. }
  167. protected function getOverviewView()
  168. {
  169. $view = new View('@Goals/getOverviewView');
  170. $this->setGeneralVariablesView($view);
  171. $view->graphEvolution = $this->getEvolutionGraph(array(), false, array('nb_conversions'));
  172. $view->nameGraphEvolution = 'GoalsgetEvolutionGraph';
  173. // sparkline for the historical data of the above values
  174. $view->urlSparklineConversions = $this->getUrlSparkline('getEvolutionGraph', array('columns' => array('nb_conversions'), 'idGoal' => ''));
  175. $view->urlSparklineConversionRate = $this->getUrlSparkline('getEvolutionGraph', array('columns' => array('conversion_rate'), 'idGoal' => ''));
  176. $view->urlSparklineRevenue = $this->getUrlSparkline('getEvolutionGraph', array('columns' => array('revenue'), 'idGoal' => ''));
  177. // Pass empty idGoal will return Goal overview
  178. $request = new Request("method=Goals.get&format=original&idGoal=");
  179. $datatable = $request->process();
  180. $dataRow = $datatable->getFirstRow();
  181. $view->nb_conversions = $dataRow->getColumn('nb_conversions');
  182. $view->nb_visits_converted = $dataRow->getColumn('nb_visits_converted');
  183. $view->conversion_rate = $this->formatConversionRate($dataRow->getColumn('conversion_rate'));
  184. $view->revenue = $dataRow->getColumn('revenue');
  185. $goalMetrics = array();
  186. foreach ($this->goals as $idGoal => $goal) {
  187. $goalMetrics[$idGoal] = $this->getMetricsForGoal($idGoal);
  188. $goalMetrics[$idGoal]['name'] = $goal['name'];
  189. $goalMetrics[$idGoal]['goalAllowMultipleConversionsPerVisit'] = $goal['allow_multiple'];
  190. }
  191. $view->goalMetrics = $goalMetrics;
  192. $view->goals = $this->goals;
  193. $view->goalReportsByDimension = $this->getGoalReportsByDimensionTable(
  194. $view->nb_conversions, $ecommerce = false, !empty($view->cart_nb_conversions));
  195. return $view;
  196. }
  197. public function getLastNbConversionsGraph()
  198. {
  199. $view = $this->getLastUnitGraph($this->pluginName, __FUNCTION__, 'Goals.getConversions');
  200. return $this->renderView($view);
  201. }
  202. public function getLastConversionRateGraph()
  203. {
  204. $view = $this->getLastUnitGraph($this->pluginName, __FUNCTION__, 'Goals.getConversionRate');
  205. return $this->renderView($view);
  206. }
  207. public function getLastRevenueGraph()
  208. {
  209. $view = $this->getLastUnitGraph($this->pluginName, __FUNCTION__, 'Goals.getRevenue');
  210. return $this->renderView($view);
  211. }
  212. public function addNewGoal()
  213. {
  214. $view = new View('@Goals/addNewGoal');
  215. $this->setGeneralVariablesView($view);
  216. $view->userCanEditGoals = Piwik::isUserHasAdminAccess($this->idSite);
  217. $view->onlyShowAddNewGoal = true;
  218. return $view->render();
  219. }
  220. public function getEvolutionGraph(array $columns = array(), $idGoal = false, array $defaultColumns = array())
  221. {
  222. if (empty($columns)) {
  223. $columns = Common::getRequestVar('columns', false);
  224. if (false !== $columns) {
  225. $columns = Piwik::getArrayFromApiParameter($columns);
  226. }
  227. }
  228. if (false !== $columns) {
  229. $columns = !is_array($columns) ? array($columns) : $columns;
  230. }
  231. if (empty($idGoal)) {
  232. $idGoal = Common::getRequestVar('idGoal', false, 'string');
  233. }
  234. $view = $this->getLastUnitGraph($this->pluginName, __FUNCTION__, 'Goals.get');
  235. $view->requestConfig->request_parameters_to_modify['idGoal'] = $idGoal;
  236. $nameToLabel = $this->goalColumnNameToLabel;
  237. if ($idGoal == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER) {
  238. $nameToLabel['nb_conversions'] = 'General_EcommerceOrders';
  239. } elseif ($idGoal == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART) {
  240. $nameToLabel['nb_conversions'] = Piwik::translate('General_VisitsWith', Piwik::translate('Goals_AbandonedCart'));
  241. $nameToLabel['conversion_rate'] = $nameToLabel['nb_conversions'];
  242. $nameToLabel['revenue'] = Piwik::translate('Goals_LeftInCart', Piwik::translate('General_ColumnRevenue'));
  243. $nameToLabel['items'] = Piwik::translate('Goals_LeftInCart', Piwik::translate('Goals_Products'));
  244. }
  245. $selectableColumns = array('nb_conversions', 'conversion_rate', 'revenue');
  246. if ($this->site->isEcommerceEnabled()) {
  247. $selectableColumns[] = 'items';
  248. $selectableColumns[] = 'avg_order_revenue';
  249. }
  250. foreach (array_merge($columns ? $columns : array(), $selectableColumns) as $columnName) {
  251. $columnTranslation = '';
  252. // find the right translation for this column, eg. find 'revenue' if column is Goal_1_revenue
  253. foreach ($nameToLabel as $metric => $metricTranslation) {
  254. if (strpos($columnName, $metric) !== false) {
  255. $columnTranslation = Piwik::translate($metricTranslation);
  256. break;
  257. }
  258. }
  259. if (!empty($idGoal) && isset($this->goals[$idGoal])) {
  260. $goalName = $this->goals[$idGoal]['name'];
  261. $columnTranslation = "$columnTranslation (" . Piwik::translate('Goals_GoalX', "$goalName") . ")";
  262. }
  263. $view->config->translations[$columnName] = $columnTranslation;
  264. }
  265. if (!empty($columns)) {
  266. $view->config->columns_to_display = $columns;
  267. } elseif (empty($view->config->columns_to_display) && !empty($defaultColumns)) {
  268. $view->config->columns_to_display = $defaultColumns;
  269. }
  270. $view->config->selectable_columns = $selectableColumns;
  271. $langString = $idGoal ? 'Goals_SingleGoalOverviewDocumentation' : 'Goals_GoalsOverviewDocumentation';
  272. $view->config->documentation = Piwik::translate($langString, '<br />');
  273. return $this->renderView($view);
  274. }
  275. protected function getTopDimensions($idGoal)
  276. {
  277. $columnNbConversions = 'goal_' . $idGoal . '_nb_conversions';
  278. $columnConversionRate = 'goal_' . $idGoal . '_conversion_rate';
  279. $topDimensionsToLoad = array();
  280. if (\Piwik\Plugin\Manager::getInstance()->isPluginActivated('UserCountry')) {
  281. $topDimensionsToLoad += array(
  282. 'country' => 'UserCountry.getCountry',
  283. );
  284. }
  285. $keywordNotDefinedString = '';
  286. if (\Piwik\Plugin\Manager::getInstance()->isPluginActivated('Referrers')) {
  287. $keywordNotDefinedString = APIReferrers::getKeywordNotDefinedString();
  288. $topDimensionsToLoad += array(
  289. 'keyword' => 'Referrers.getKeywords',
  290. 'website' => 'Referrers.getWebsites',
  291. );
  292. }
  293. $topDimensions = array();
  294. foreach ($topDimensionsToLoad as $dimensionName => $apiMethod) {
  295. $request = new Request("method=$apiMethod
  296. &format=original
  297. &filter_update_columns_when_show_all_goals=1
  298. &idGoal=" . AddColumnsProcessedMetricsGoal::GOALS_FULL_TABLE . "
  299. &filter_sort_order=desc
  300. &filter_sort_column=$columnNbConversions" .
  301. // select a couple more in case some are not valid (ie. conversions==0 or they are "Keyword not defined")
  302. "&filter_limit=" . (self::COUNT_TOP_ROWS_TO_DISPLAY + 2));
  303. $datatable = $request->process();
  304. $topDimension = array();
  305. $count = 0;
  306. foreach ($datatable->getRows() as $row) {
  307. $conversions = $row->getColumn($columnNbConversions);
  308. if ($conversions > 0
  309. && $count < self::COUNT_TOP_ROWS_TO_DISPLAY
  310. // Don't put the "Keyword not defined" in the best segment since it's irritating
  311. && !($dimensionName == 'keyword'
  312. && $row->getColumn('label') == $keywordNotDefinedString)
  313. ) {
  314. $topDimension[] = array(
  315. 'name' => $row->getColumn('label'),
  316. 'nb_conversions' => $conversions,
  317. 'conversion_rate' => $this->formatConversionRate($row->getColumn($columnConversionRate)),
  318. 'metadata' => $row->getMetadata(),
  319. );
  320. $count++;
  321. }
  322. }
  323. $topDimensions[$dimensionName] = $topDimension;
  324. }
  325. return $topDimensions;
  326. }
  327. protected function getMetricsForGoal($idGoal)
  328. {
  329. $request = new Request("method=Goals.get&format=original&idGoal=$idGoal");
  330. $datatable = $request->process();
  331. $dataRow = $datatable->getFirstRow();
  332. $nbConversions = $dataRow->getColumn('nb_conversions');
  333. $nbVisitsConverted = $dataRow->getColumn('nb_visits_converted');
  334. // Backward compatibilty before 1.3, this value was not processed
  335. if (empty($nbVisitsConverted)) {
  336. $nbVisitsConverted = $nbConversions;
  337. }
  338. $revenue = $dataRow->getColumn('revenue');
  339. $return = array(
  340. 'id' => $idGoal,
  341. 'nb_conversions' => (int)$nbConversions,
  342. 'nb_visits_converted' => (int)$nbVisitsConverted,
  343. 'conversion_rate' => $this->formatConversionRate($dataRow->getColumn('conversion_rate')),
  344. 'revenue' => $revenue ? $revenue : 0,
  345. 'urlSparklineConversions' => $this->getUrlSparkline('getEvolutionGraph', array('columns' => array('nb_conversions'), 'idGoal' => $idGoal)),
  346. 'urlSparklineConversionRate' => $this->getUrlSparkline('getEvolutionGraph', array('columns' => array('conversion_rate'), 'idGoal' => $idGoal)),
  347. 'urlSparklineRevenue' => $this->getUrlSparkline('getEvolutionGraph', array('columns' => array('revenue'), 'idGoal' => $idGoal)),
  348. );
  349. if ($idGoal == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER) {
  350. $items = $dataRow->getColumn('items');
  351. $aov = $dataRow->getColumn('avg_order_revenue');
  352. $return = array_merge($return, array(
  353. 'revenue_subtotal' => $dataRow->getColumn('revenue_subtotal'),
  354. 'revenue_tax' => $dataRow->getColumn('revenue_tax'),
  355. 'revenue_shipping' => $dataRow->getColumn('revenue_shipping'),
  356. 'revenue_discount' => $dataRow->getColumn('revenue_discount'),
  357. 'items' => $items ? $items : 0,
  358. 'avg_order_revenue' => $aov ? $aov : 0,
  359. 'urlSparklinePurchasedProducts' => $this->getUrlSparkline('getEvolutionGraph', array('columns' => array('items'), 'idGoal' => $idGoal)),
  360. 'urlSparklineAverageOrderValue' => $this->getUrlSparkline('getEvolutionGraph', array('columns' => array('avg_order_revenue'), 'idGoal' => $idGoal)),
  361. ));
  362. }
  363. return $return;
  364. }
  365. /**
  366. * Utility function that returns HTML that displays Goal information for reports. This
  367. * is the HTML that is at the bottom of every goals page.
  368. *
  369. * @param int $conversions The number of conversions for this goal (or all goals
  370. * in case of the overview).
  371. * @param bool $ecommerce Whether to show ecommerce reports or not.
  372. * @param bool $cartNbConversions Whether there are cart conversions or not for this
  373. * goal.
  374. * @return string
  375. */
  376. private function getGoalReportsByDimensionTable($conversions, $ecommerce = false, $cartNbConversions = false)
  377. {
  378. $preloadAbandonedCart = $cartNbConversions !== false && $conversions == 0;
  379. $goalReportsByDimension = new ReportsByDimension('Goals');
  380. // add ecommerce reports
  381. $ecommerceCustomParams = array();
  382. if ($ecommerce) {
  383. if ($preloadAbandonedCart) {
  384. $ecommerceCustomParams['viewDataTable'] = 'ecommerceAbandonedCart';
  385. $ecommerceCustomParams['filterEcommerce'] = self::ECOMMERCE_LOG_SHOW_ABANDONED_CARTS;
  386. }
  387. $goalReportsByDimension->addReport(
  388. 'Goals_EcommerceReports', 'Goals_ProductSKU', 'Goals.getItemsSku', $ecommerceCustomParams);
  389. $goalReportsByDimension->addReport(
  390. 'Goals_EcommerceReports', 'Goals_ProductName', 'Goals.getItemsName', $ecommerceCustomParams);
  391. $goalReportsByDimension->addReport(
  392. 'Goals_EcommerceReports', 'Goals_ProductCategory', 'Goals.getItemsCategory', $ecommerceCustomParams);
  393. $goalReportsByDimension->addReport(
  394. 'Goals_EcommerceReports', 'Goals_EcommerceLog', 'Goals.getEcommerceLog', $ecommerceCustomParams);
  395. }
  396. if ($conversions > 0) {
  397. // for non-Goals reports, we show the goals table
  398. $customParams = $ecommerceCustomParams + array('documentationForGoalsPage' => '1');
  399. if (Common::getRequestVar('idGoal', '') === '') // if no idGoal, use 0 for overview
  400. {
  401. $customParams['idGoal'] = '0'; // NOTE: Must be string! Otherwise Piwik_View_HtmlTable_Goals fails.
  402. }
  403. $allReports = Goals::getReportsWithGoalMetrics();
  404. foreach ($allReports as $category => $reports) {
  405. $categoryText = Piwik::translate('Goals_ViewGoalsBy', $category);
  406. foreach ($reports as $report) {
  407. if(empty($report['viewDataTable'])) {
  408. $report['viewDataTable'] = 'tableGoals';
  409. }
  410. $customParams['viewDataTable'] = $report['viewDataTable'];
  411. $goalReportsByDimension->addReport(
  412. $categoryText, $report['name'], $report['module'] . '.' . $report['action'], $customParams);
  413. }
  414. }
  415. }
  416. return $goalReportsByDimension->render();
  417. }
  418. //
  419. // Report rendering actions
  420. //
  421. public function getItemsSku()
  422. {
  423. return $this->renderReport(__FUNCTION__);
  424. }
  425. public function getItemsName()
  426. {
  427. return $this->renderReport(__FUNCTION__);
  428. }
  429. public function getItemsCategory()
  430. {
  431. return $this->renderReport(__FUNCTION__);
  432. }
  433. public function getVisitsUntilConversion()
  434. {
  435. return $this->renderReport(__FUNCTION__);
  436. }
  437. public function getDaysToConversion()
  438. {
  439. return $this->renderReport(__FUNCTION__);
  440. }
  441. }