config->show_limit_control = true; * $view->config->translations['myFancyMetric'] = "My Fancy Metric"; * // ... * return $view->render(); * } * * **Using {@link Piwik\Plugin\Controller::renderReport}** * * First, a controller method that displays a single report: * * public function myReport() * { * return $this->renderReport(__FUNCTION__);` * } * * Then the event handler for the {@hook ViewDataTable.configure} event: * * public function configureViewDataTable(ViewDataTable $view) * { * switch ($view->requestConfig->apiMethodToRequestDataTable) { * case 'MyPlugin.myReport': * $view->config->show_limit_control = true; * $view->config->translations['myFancyMetric'] = "My Fancy Metric"; * // ... * break; * } * } * * **Using custom configuration objects in a new visualization** * * class MyVisualizationConfig extends Piwik\ViewDataTable\Config * { * public $my_new_property = true; * } * * class MyVisualizationRequestConfig extends Piwik\ViewDataTable\RequestConfig * { * public $my_new_property = false; * } * * class MyVisualization extends Piwik\Plugin\ViewDataTable * { * public static function getDefaultConfig() * { * return new MyVisualizationConfig(); * } * * public static function getDefaultRequestConfig() * { * return new MyVisualizationRequestConfig(); * } * } * * * @api */ abstract class ViewDataTable implements ViewInterface { const ID = ''; /** * DataTable loaded from the API for this ViewDataTable. * * @var DataTable */ protected $dataTable = null; /** * Contains display properties for this visualization. * * @var \Piwik\ViewDataTable\Config */ public $config; /** * Contains request properties for this visualization. * * @var \Piwik\ViewDataTable\RequestConfig */ public $requestConfig; /** * @var ViewDataTableRequest */ protected $request; private $isComparing = null; /** * Constructor. Initializes display and request properties to their default values. * Posts the {@hook ViewDataTable.configure} event which plugins can use to configure the * way reports are displayed. */ public function __construct($controllerAction, $apiMethodToRequestDataTable, $overrideParams = array()) { if (strpos($controllerAction, '.') === false) { $controllerName = ''; $controllerAction = ''; } else { list($controllerName, $controllerAction) = explode('.', $controllerAction); } $this->requestConfig = static::getDefaultRequestConfig(); $this->config = static::getDefaultConfig(); $this->config->subtable_controller_action = $controllerAction; $this->config->setController($controllerName, $controllerAction); $this->request = new ViewDataTableRequest($this->requestConfig); $this->requestConfig->idSubtable = Common::getRequestVar('idSubtable', false, 'int'); $this->config->self_url = Request::getBaseReportUrl($controllerName, $controllerAction); $this->requestConfig->apiMethodToRequestDataTable = $apiMethodToRequestDataTable; $report = ReportsProvider::factory($this->requestConfig->getApiModuleToRequest(), $this->requestConfig->getApiMethodToRequest()); if (!empty($report)) { /** @var Report $report */ $subtable = $report->getActionToLoadSubTables(); if (!empty($subtable)) { $this->config->subtable_controller_action = $subtable; } $this->config->show_goals = $report->hasGoalMetrics(); $relatedReports = $report->getRelatedReports(); if (!empty($relatedReports)) { foreach ($relatedReports as $relatedReport) { if (!$relatedReport) { continue; } $relatedReportName = $relatedReport->getName(); $this->config->addRelatedReport($relatedReport->getModule() . '.' . $relatedReport->getAction(), $relatedReportName); } } $metrics = $report->getMetrics(); if (!empty($metrics)) { $this->config->addTranslations($metrics); } $processedMetrics = $report->getProcessedMetrics(); if (!empty($processedMetrics)) { $this->config->addTranslations($processedMetrics); } $this->config->title = $report->getName(); $report->configureView($this); } /** * Triggered during {@link ViewDataTable} construction. Subscribers should customize * the view based on the report that is being displayed. * * This event is triggered before view configuration properties are overwritten by saved settings or request * parameters. Use this to define default values. * * Plugins that define their own reports must subscribe to this event in order to * specify how the Piwik UI should display the report. * * **Example** * * // event handler * public function configureViewDataTable(ViewDataTable $view) * { * switch ($view->requestConfig->apiMethodToRequestDataTable) { * case 'VisitTime.getVisitInformationPerServerTime': * $view->config->enable_sort = true; * $view->requestConfig->filter_limit = 10; * break; * } * } * * @param ViewDataTable $view The instance to configure. */ Piwik::postEvent('ViewDataTable.configure', array($this)); $this->assignRelatedReportsTitle(); $this->config->show_footer_icons = (false == $this->requestConfig->idSubtable); // the exclude low population threshold value is sometimes obtained by requesting data. // to avoid issuing unnecessary requests when display properties are determined by metadata, // we allow it to be a closure. if (isset($this->requestConfig->filter_excludelowpop_value) && $this->requestConfig->filter_excludelowpop_value instanceof \Closure ) { $function = $this->requestConfig->filter_excludelowpop_value; $this->requestConfig->filter_excludelowpop_value = $function(); } $this->overrideViewPropertiesWithParams($overrideParams); $this->overrideViewPropertiesWithQueryParams(); /** * Triggered after {@link ViewDataTable} construction. Subscribers should customize * the view based on the report that is being displayed. * * This event is triggered after all view configuration values have been overwritten by saved settings or * request parameters. Use this if you need to work with the final configuration values. * * Plugins that define their own reports can subscribe to this event in order to * specify how the Piwik UI should display the report. * * **Example** * * // event handler * public function configureViewDataTableEnd(ViewDataTable $view) * { * if ($view->requestConfig->apiMethodToRequestDataTable == 'VisitTime.getVisitInformationPerServerTime' * && $view->requestConfig->flat == 1) { * $view->config->show_header_message = 'You are viewing this report flattened'; * } * } * * @param ViewDataTable $view The instance to configure. */ Piwik::postEvent('ViewDataTable.configure.end', array($this)); } private function assignRelatedReportsTitle() { if (!empty($this->config->related_reports_title)) { // title already assigned by a plugin return; } if (count($this->config->related_reports) == 1) { $this->config->related_reports_title = Piwik::translate('General_RelatedReport') . ':'; } else { $this->config->related_reports_title = Piwik::translate('General_RelatedReports') . ':'; } } /** * Returns the default config instance. * * Visualizations that define their own display properties should override this method and * return an instance of their new {@link Piwik\ViewDataTable\Config} descendant. * * See the last example {@link ViewDataTable here} for more information. * * @return \Piwik\ViewDataTable\Config */ public static function getDefaultConfig() { return new VizConfig(); } /** * Returns the default request config instance. * * Visualizations that define their own request properties should override this method and * return an instance of their new {@link Piwik\ViewDataTable\RequestConfig} descendant. * * See the last example {@link ViewDataTable here} for more information. * * @return \Piwik\ViewDataTable\RequestConfig */ public static function getDefaultRequestConfig() { return new VizRequest(); } protected function loadDataTableFromAPI() { if (!is_null($this->dataTable)) { // data table is already there // this happens when setDataTable has been used return $this->dataTable; } $extraParams = []; if ($this->isComparing()) { $extraParams['compare'] = '1'; } $this->dataTable = $this->request->loadDataTableFromAPI($extraParams); return $this->dataTable; } /** * Returns the viewDataTable ID for this DataTable visualization. * * Derived classes should not override this method. They should instead declare a const ID field * with the viewDataTable ID. * * @throws \Exception * @return string */ public static function getViewDataTableId() { $id = static::ID; if (empty($id)) { $message = sprintf('ViewDataTable %s does not define an ID. Set the ID constant to fix this issue', get_called_class()); throw new \Exception($message); } return $id; } /** * Returns `true` if this instance's or any of its ancestors' viewDataTable IDs equals the supplied ID, * `false` if otherwise. * * Can be used to test whether a ViewDataTable object is an instance of a certain visualization or not, * without having to know where that visualization is. * * @param string $viewDataTableId The viewDataTable ID to check for, eg, `'table'`. * @return bool */ public function isViewDataTableId($viewDataTableId) { $myIds = ViewDataTableManager::getIdsWithInheritance(get_called_class()); return in_array($viewDataTableId, $myIds); } /** * Returns the DataTable loaded from the API. * * @return DataTable * @throws \Exception if not yet loaded. */ public function getDataTable() { if (is_null($this->dataTable)) { throw new \Exception("The DataTable object has not yet been created"); } return $this->dataTable; } /** * To prevent calling an API multiple times, the DataTable can be set directly. * It won't be loaded from the API in this case. * * @param DataTable $dataTable The DataTable to use. * @return void */ public function setDataTable($dataTable) { $this->dataTable = $dataTable; } /** * Checks that the API returned a normal DataTable (as opposed to DataTable\Map) * @throws \Exception * @return void */ protected function checkStandardDataTable() { Piwik::checkObjectTypeIs($this->dataTable, array('\Piwik\DataTable')); } /** * Requests all needed data and renders the view. * * @return string The result of rendering. */ public function render() { return ''; } protected function getDefaultDataTableCssClass() { return 'dataTableViz' . Piwik::getUnnamespacedClassName(get_class($this)); } /** * Returns the list of view properties that can be overridden by query parameters. * * @return array */ protected function getOverridableProperties() { return array_merge($this->config->overridableProperties, $this->requestConfig->overridableProperties); } private function overrideViewPropertiesWithQueryParams() { $properties = $this->getOverridableProperties(); foreach ($properties as $name) { if (property_exists($this->requestConfig, $name)) { $this->requestConfig->$name = $this->getPropertyFromQueryParam($name, $this->requestConfig->$name); } elseif (property_exists($this->config, $name)) { $this->config->$name = $this->getPropertyFromQueryParam($name, $this->config->$name); } } // handle special 'columns' query parameter $columns = Common::getRequestVar('columns', false); if (false !== $columns) { $this->config->columns_to_display = Piwik::getArrayFromApiParameter($columns); array_unshift($this->config->columns_to_display, 'label'); } } protected function getPropertyFromQueryParam($name, $defaultValue) { $type = is_numeric($defaultValue) ? 'int' : null; $value = Common::getRequestVar($name, $defaultValue, $type); // convert comma separated values to arrays if needed if (is_array($defaultValue)) { $value = Piwik::getArrayFromApiParameter($value); } return $value; } /** * Returns `true` if this instance will request a single DataTable, `false` if requesting * more than one. * * @return bool */ public function isRequestingSingleDataTable() { $requestArray = $this->request->getRequestArray() + $_GET + $_POST; $date = Common::getRequestVar('date', null, 'string', $requestArray); $period = Common::getRequestVar('period', null, 'string', $requestArray); $idSite = Common::getRequestVar('idSite', null, 'string', $requestArray); if (Period::isMultiplePeriod($date, $period) || strpos($idSite, ',') !== false || $idSite == 'all' ) { return false; } return true; } /** * Returns `true` if this visualization can display some type of data or not. * * New visualization classes should override this method if they can only visualize certain * types of data. The evolution graph visualization, for example, can only visualize * sets of DataTables. If the API method used results in a single DataTable, the evolution * graph footer icon should not be displayed. * * @param ViewDataTable $view Contains the API request being checked. * @return bool */ public static function canDisplayViewDataTable(ViewDataTable $view) { return $view->config->show_all_views_icons; } private function overrideViewPropertiesWithParams($overrideParams) { if (empty($overrideParams)) { return; } foreach ($overrideParams as $key => $value) { if (property_exists($this->requestConfig, $key)) { $this->requestConfig->$key = $value; } elseif (property_exists($this->config, $key)) { $this->config->$key = $value; } elseif ($key != 'enable_filter_excludelowpop') { $this->config->custom_parameters[$key] = $value; } } } /** * Display a meaningful error message when any invalid parameter is being set. * * @param $overrideParams * @throws */ public function throwWhenSettingNonOverridableParameter($overrideParams) { $nonOverridableParams = $this->getNonOverridableParams($overrideParams); if(count($nonOverridableParams) > 0) { throw new \Exception(sprintf( "Setting parameters %s is not allowed. Please report this bug to the Matomo team.", implode(" and ", $nonOverridableParams) )); } } /** * @param $overrideParams * @return array */ public function getNonOverridableParams($overrideParams) { $paramsCannotBeOverridden = array(); foreach ($overrideParams as $paramName => $paramValue) { if (property_exists($this->requestConfig, $paramName)) { $allowedParams = $this->requestConfig->overridableProperties; } elseif (property_exists($this->config, $paramName)) { $allowedParams = $this->config->overridableProperties; } else { // setting Config.custom_parameters is always allowed continue; } if (!in_array($paramName, $allowedParams)) { $paramsCannotBeOverridden[] = $paramName; } } return $paramsCannotBeOverridden; } /** * Returns true if both this current visualization supports comparison, and if comparison query parameters * are present in the URL. * * @return bool */ public function isComparing() { if (!$this->supportsComparison() || $this->config->disable_comparison ) { return false; } $request = $this->request->getRequestArray(); $request = ApiRequest::getRequestArrayFromString($request); $result = DataComparisonFilter::isCompareParamsPresent($request); return $result; } /** * Implementations should override this method if they support a special comparison view. By * default, it is assumed visualizations do not support comparison. * * @return bool */ public function supportsComparison() { return false; } public function getRequestArray() { $requestArray = $this->request->getRequestArray(); return ApiRequest::getRequestArrayFromString($requestArray); } }