metricId)) { return $this->metricId; } $id = $this->getId(); return str_replace(array('.', ' ', '-'), '_', strtolower($id)); } /** * Installs the action dimension in case it is not installed yet. The installation is already implemented based on * the {@link $columnName} and {@link $columnType}. If you want to perform additional actions beside adding the * column to the database - for instance adding an index - you can overwrite this method. We recommend to call * this parent method to get the minimum required actions and then add further custom actions since this makes sure * the column will be installed correctly. We also recommend to change the default install behavior only if really * needed. FYI: We do not directly execute those alter table statements here as we group them together with several * other alter table statements do execute those changes in one step which results in a faster installation. The * column will be added to the `log_link_visit_action` MySQL table. * * Example: * ``` public function install() { $changes = parent::install(); $changes['log_link_visit_action'][] = "ADD INDEX index_idsite_servertime ( idsite, server_time )"; return $changes; } ``` * * @return array An array containing the table name as key and an array of MySQL alter table statements that should * be executed on the given table. Example: * ``` array( 'log_link_visit_action' => array("ADD COLUMN `$this->columnName` $this->columnType", "ADD INDEX ...") ); ``` * @api */ public function install() { if (empty($this->columnName) || empty($this->columnType) || empty($this->dbTableName)) { return array(); } // TODO if table does not exist, create it with a primary key, but at this point we cannot really create it // cause we need to show the query in the UI first and user needs to be able to create table manually. // we cannot return something like "create table " here as it would be returned for each table etc. // we need to do this in column updater etc! return array( $this->dbTableName => array("ADD COLUMN `$this->columnName` $this->columnType") ); } /** * Updates the action dimension in case the {@link $columnType} has changed. The update is already implemented based * on the {@link $columnName} and {@link $columnType}. This method is intended not to overwritten by plugin * developers as it is only supposed to make sure the column has the correct type. Adding additional custom "alter * table" actions would not really work since they would be executed with every {@link $columnType} change. So * adding an index here would be executed whenever the columnType changes resulting in an error if the index already * exists. If an index needs to be added after the first version is released a plugin update class should be * created since this makes sure it is only executed once. * * @return array An array containing the table name as key and an array of MySQL alter table statements that should * be executed on the given table. Example: * ``` array( 'log_link_visit_action' => array("MODIFY COLUMN `$this->columnName` $this->columnType", "DROP COLUMN ...") ); ``` * @ignore */ public function update() { if (empty($this->columnName) || empty($this->columnType) || empty($this->dbTableName)) { return array(); } return array( $this->dbTableName => array("MODIFY COLUMN `$this->columnName` $this->columnType") ); } /** * Uninstalls the dimension if a {@link $columnName} and {@link columnType} is set. In case you perform any custom * actions during {@link install()} - for instance adding an index - you should make sure to undo those actions by * overwriting this method. Make sure to call this parent method to make sure the uninstallation of the column * will be done. * @throws Exception * @api */ public function uninstall() { if (empty($this->columnName) || empty($this->columnType) || empty($this->dbTableName)) { return; } try { $sql = "ALTER TABLE `" . Common::prefixTable($this->dbTableName) . "` DROP COLUMN `$this->columnName`"; Db::exec($sql); } catch (Exception $e) { if (!Db::get()->isErrNo($e, '1091')) { throw $e; } } } /** * Returns the ID of the category (typically a translation key). * @return string */ public function getCategoryId() { return $this->category; } /** * Returns the translated name of this dimension which is typically in singular. * * @return string */ public function getName() { if (!empty($this->nameSingular)) { return Piwik::translate($this->nameSingular); } return $this->nameSingular; } /** * Returns a translated name in plural for this dimension. * @return string * @api since Piwik 3.2.0 */ public function getNamePlural() { if (!empty($this->namePlural)) { return Piwik::translate($this->namePlural); } return $this->getName(); } /** * Defines whether an anonymous user is allowed to view this dimension * @return bool * @api since Piwik 3.2.0 */ public function isAnonymousAllowed() { return $this->allowAnonymous; } /** * Sets (overwrites) the SQL segment * @param $segment * @api since Piwik 3.2.0 */ public function setSqlSegment($segment) { $this->sqlSegment = $segment; } /** * Sets (overwrites the dimension type) * @param $type * @api since Piwik 3.2.0 */ public function setType($type) { $this->type = $type; } /** * A dimension should group values by using this method. Otherwise the same row may appear several times. * * @param mixed $value * @param int $idSite * @return mixed * @api since Piwik 3.2.0 */ public function groupValue($value, $idSite) { switch ($this->type) { case Dimension::TYPE_URL: return str_replace(array('http://', 'https://'), '', $value); case Dimension::TYPE_BOOL: return !empty($value) ? '1' : '0'; case Dimension::TYPE_DURATION_MS: return number_format($value / 1000, 2); // because we divide we need to group them and cannot do this in formatting step } return $value; } /** * Formats the dimension value. By default, the dimension is formatted based on the set dimension type. * * @param mixed $value * @param int $idSite * @param Formatter $formatter * @return mixed * @api since Piwik 3.2.0 */ public function formatValue($value, $idSite, Formatter $formatter) { switch ($this->type) { case Dimension::TYPE_BOOL: if (empty($value)) { return Piwik::translate('General_No'); } return Piwik::translate('General_Yes'); case Dimension::TYPE_ENUM: $values = $this->getEnumColumnValues(); if (isset($values[$value])) { return $values[$value]; } break; case Dimension::TYPE_MONEY: return $formatter->getPrettyMoney($value, $idSite); case Dimension::TYPE_FLOAT: return $formatter->getPrettyNumber((float) $value, $precision = 2); case Dimension::TYPE_NUMBER: return $formatter->getPrettyNumber($value); case Dimension::TYPE_DURATION_S: return $formatter->getPrettyTimeFromSeconds($value, $displayAsSentence = false); case Dimension::TYPE_DURATION_MS: return $formatter->getPrettyTimeFromSeconds($value, $displayAsSentence = true); case Dimension::TYPE_PERCENT: return $formatter->getPrettyPercentFromQuotient($value); case Dimension::TYPE_BYTE: return $formatter->getPrettySizeFromBytes($value); } return $value; } /** * Overwrite this method to configure segments. To do so just create an instance of a {@link \Piwik\Plugin\Segment} * class, configure it and call the {@link addSegment()} method. You can add one or more segments for this * dimension. Example: * * ``` * $segment = new Segment(); * $segment->setSegment('exitPageUrl'); * $segment->setName('Actions_ColumnExitPageURL'); * $segment->setCategory('General_Visit'); * $this->addSegment($segment); * ``` */ protected function configureSegments() { if ($this->segmentName && $this->category && ($this->sqlSegment || ($this->columnName && $this->dbTableName)) && $this->nameSingular) { $segment = new Segment(); $this->addSegment($segment); } } /** * Configures metrics for this dimension. * * For certain dimension types, some metrics will be added automatically. * * @param MetricsList $metricsList * @param DimensionMetricFactory $dimensionMetricFactory */ public function configureMetrics(MetricsList $metricsList, DimensionMetricFactory $dimensionMetricFactory) { if ($this->getMetricId() && $this->dbTableName && $this->columnName && $this->getNamePlural()) { if (in_array($this->getType(), array(self::TYPE_DATETIME, self::TYPE_DATE, self::TYPE_TIME, self::TYPE_TIMESTAMP))) { // we do not generate any metrics from these types return; } elseif (in_array($this->getType(), array(self::TYPE_URL, self::TYPE_TEXT, self::TYPE_BINARY, self::TYPE_ENUM))) { $metric = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_UNIQUE); $metricsList->addMetric($metric); } elseif (in_array($this->getType(), array(self::TYPE_BOOL))) { $metric = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_SUM); $metricsList->addMetric($metric); } else { $metric = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_SUM); $metricsList->addMetric($metric); $metric = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_MAX); $metricsList->addMetric($metric); } } } /** * Check whether a dimension has overwritten a specific method. * @param $method * @return bool * @ignore */ public function hasImplementedEvent($method) { $method = new \ReflectionMethod($this, $method); $declaringClass = $method->getDeclaringClass(); return 0 === strpos($declaringClass->name, 'Piwik\Plugins'); } /** * Adds a new segment. It automatically sets the SQL segment depending on the column name in case none is set * already. * @see \Piwik\Columns\Dimension::addSegment() * @param Segment $segment * @api */ protected function addSegment(Segment $segment) { if (!$segment->getSegment() && $this->segmentName) { $segment->setSegment($this->segmentName); } if (!$segment->getType()) { $metricTypes = array(self::TYPE_NUMBER, self::TYPE_FLOAT, self::TYPE_MONEY, self::TYPE_DURATION_S, self::TYPE_DURATION_MS); if (in_array($this->getType(), $metricTypes, $strict = true)) { $segment->setType(Segment::TYPE_METRIC); } else { $segment->setType(Segment::TYPE_DIMENSION); } } if (!$segment->getCategoryId() && $this->category) { $segment->setCategory($this->category); } if (!$segment->getName() && $this->nameSingular) { $segment->setName($this->nameSingular); } $sqlSegment = $segment->getSqlSegment(); if (empty($sqlSegment) && !$segment->getUnionOfSegments()) { if (!empty($this->sqlSegment)) { $segment->setSqlSegment($this->sqlSegment); } elseif ($this->dbTableName && $this->columnName) { $segment->setSqlSegment($this->dbTableName . '.' . $this->columnName); } else { throw new Exception('Segment cannot be added because no sql segment is set'); } } if (!$this->suggestedValuesCallback) { // we can generate effecient value callback for enums automatically $enum = $this->getEnumColumnValues(); if (!empty($enum)) { $this->suggestedValuesCallback = function ($idSite, $maxValuesToReturn) use ($enum) { $values = array_values($enum); return array_slice($values, 0, $maxValuesToReturn); }; } } if (!$this->acceptValues) { // we can generate accept values for enums automatically $enum = $this->getEnumColumnValues(); if (!empty($enum)) { $enumValues = array_values($enum); $enumValues = array_slice($enumValues, 0, 20); $this->acceptValues = 'Eg. ' . implode(', ', $enumValues); }; } if ($this->acceptValues && !$segment->getAcceptValues()) { $segment->setAcceptedValues($this->acceptValues); } if (!$this->sqlFilterValue && !$segment->getSqlFilter() && !$segment->getSqlFilterValue()) { // no sql filter configured, we try to configure automatically for enums $enum = $this->getEnumColumnValues(); if (!empty($enum)) { $this->sqlFilterValue = function ($value, $sqlSegmentName) use ($enum) { if (isset($enum[$value])) { return $value; } $id = array_search($value, $enum); if ($id === false) { $id = array_search(strtolower(trim(urldecode($value))), $enum); if ($id === false) { throw new \Exception("Invalid '$sqlSegmentName' segment value $value"); } } return $id; }; }; } if ($this->suggestedValuesCallback && !$segment->getSuggestedValuesCallback()) { $segment->setSuggestedValuesCallback($this->suggestedValuesCallback); } if ($this->sqlFilterValue && !$segment->getSqlFilterValue()) { $segment->setSqlFilterValue($this->sqlFilterValue); } if ($this->sqlFilter && !$segment->getSqlFilter()) { $segment->setSqlFilter($this->sqlFilter); } if (!$this->allowAnonymous) { $segment->setRequiresAtLeastViewAccess(true); } $this->segments[] = $segment; } /** * Get the list of configured segments. * @return Segment[] * @ignore */ public function getSegments() { if (empty($this->segments)) { $this->configureSegments(); } return $this->segments; } /** * Returns the name of the segment that this dimension defines * @return string * @api since Piwik 3.2.0 */ public function getSegmentName() { return $this->segmentName; } /** * Get the name of the dimension column. * @return string * @ignore */ public function getColumnName() { return $this->columnName; } /** * Returns a sql segment expression for this dimension. * @return string * @api since Piwik 3.2.0 */ public function getSqlSegment() { if (!empty($this->sqlSegment)) { return $this->sqlSegment; } if ($this->dbTableName && $this->columnName) { return $this->dbTableName . '.' . $this->columnName; } } /** * Check whether the dimension has a column type configured * @return bool * @ignore */ public function hasColumnType() { return !empty($this->columnType); } /** * Returns the name of the database table this dimension belongs to. * @return string * @api since Piwik 3.2.0 */ public function getDbTableName() { return $this->dbTableName; } /** * Returns a unique string ID for this dimension. The ID is built using the namespaced class name * of the dimension, but is modified to be more human readable. * * @return string eg, `"Referrers.Keywords"` * @throws Exception if the plugin and simple class name of this instance cannot be determined. * This would only happen if the dimension is located in the wrong directory. * @api */ public function getId() { $className = get_class($this); return $this->generateIdFromClass($className); } /** * @param string $className * @return string * @throws Exception * @ignore */ protected function generateIdFromClass($className) { // parse plugin name & dimension name $regex = "/Piwik\\\\Plugins\\\\([^\\\\]+)\\\\" . self::COMPONENT_SUBNAMESPACE . "\\\\([^\\\\]+)/"; if (!preg_match($regex, $className, $matches)) { throw new Exception("'$className' is located in the wrong directory."); } $pluginName = $matches[1]; $dimensionName = $matches[2]; return $pluginName . '.' . $dimensionName; } /** * Gets an instance of all available visit, action and conversion dimension. * @return Dimension[] */ public static function getAllDimensions() { $cacheId = CacheId::siteAware(CacheId::pluginAware('AllDimensions')); $cache = PiwikCache::getTransientCache(); if (!$cache->contains($cacheId)) { $plugins = PluginManager::getInstance()->getPluginsLoadedAndActivated(); $instances = array(); /** * Triggered to add new dimensions that cannot be picked up automatically by the platform. * This is useful if the plugin allows a user to create reports / dimensions dynamically. For example * CustomDimensions or CustomVariables. There are a variable number of dimensions in this case and it * wouldn't be really possible to create a report file for one of these dimensions as it is not known * how many Custom Dimensions will exist. * * **Example** * * public function addDimension(&$dimensions) * { * $dimensions[] = new MyCustomDimension(); * } * * @param Dimension[] $reports An array of dimensions */ Piwik::postEvent('Dimension.addDimensions', array(&$instances)); foreach ($plugins as $plugin) { foreach (self::getDimensions($plugin) as $instance) { $instances[] = $instance; } } /** * Triggered to filter / restrict dimensions. * * **Example** * * public function filterDimensions(&$dimensions) * { * foreach ($dimensions as $index => $dimension) { * if ($dimension->getName() === 'Page URL') {} * unset($dimensions[$index]); // remove this dimension * } * } * } * * @param Dimension[] $dimensions An array of dimensions */ Piwik::postEvent('Dimension.filterDimensions', array(&$instances)); $cache->save($cacheId, $instances); } return $cache->fetch($cacheId); } public static function getDimensions(Plugin $plugin) { $columns = $plugin->findMultipleComponents('Columns', '\\Piwik\\Columns\\Dimension'); $instances = array(); foreach ($columns as $colum) { $instances[] = new $colum(); } return $instances; } /** * Creates a Dimension instance from a string ID (see {@link getId()}). * * @param string $dimensionId See {@link getId()}. * @return Dimension|null The created instance or null if there is no Dimension for * $dimensionId or if the plugin that contains the Dimension is * not loaded. * @api * @deprecated Please use DimensionsProvider::factory instead */ public static function factory($dimensionId) { list($module, $dimension) = explode('.', $dimensionId); return ComponentFactory::factory($module, $dimension, __CLASS__); } /** * Returns the name of the plugin that contains this Dimension. * * @return string * @throws Exception if the Dimension is not located within a Plugin module. * @api */ public function getModule() { $id = $this->getId(); if (empty($id)) { throw new Exception("Invalid dimension ID: '$id'."); } $parts = explode('.', $id); return reset($parts); } /** * Returns the type of the dimension which defines what kind of value this dimension stores. * @return string * @api since Piwik 3.2.0 */ public function getType() { if (!empty($this->type)) { return $this->type; } if ($this->getDbColumnJoin()) { // best guess return self::TYPE_TEXT; } if ($this->getEnumColumnValues()) { // best guess return self::TYPE_ENUM; } if (!empty($this->columnType)) { // best guess $type = strtolower($this->columnType); if (strpos($type, 'datetime') !== false) { return self::TYPE_DATETIME; } elseif (strpos($type, 'timestamp') !== false) { return self::TYPE_TIMESTAMP; } elseif (strpos($type, 'date') !== false) { return self::TYPE_DATE; } elseif (strpos($type, 'time') !== false) { return self::TYPE_TIME; } elseif (strpos($type, 'float') !== false) { return self::TYPE_FLOAT; } elseif (strpos($type, 'decimal') !== false) { return self::TYPE_FLOAT; } elseif (strpos($type, 'int') !== false) { return self::TYPE_NUMBER; } elseif (strpos($type, 'binary') !== false) { return self::TYPE_BINARY; } } return self::TYPE_TEXT; } /** * Get the version of the dimension which is used for update checks. * @return string * @ignore */ public function getVersion() { return $this->columnType; } }