1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
|
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugin\Dimension;
use Piwik\CacheId;
use Piwik\Cache as PiwikCache;
use Piwik\Columns\Dimension;
use Piwik\Common;
use Piwik\Db;
use Piwik\Plugin\Manager as PluginManager;
use Piwik\Plugin\Segment;
use Piwik\Tracker\Request;
use Piwik\Tracker\Visitor;
use Piwik\Tracker\Action;
use Piwik\Tracker;
use Piwik\Plugin;
use Exception;
/**
* Defines a new visit dimension that records any visit related information during tracking.
*
* You can record any visit information by implementing one of the following events: {@link onNewVisit()},
* {@link onExistingVisit()}, {@link onConvertedVisit()} or {@link onAnyGoalConversion()}. By defining a
* {@link $columnName} and {@link $columnType} a new column will be created in the database (table `log_visit`)
* automatically and the values you return in the previous mentioned events will be saved in this column.
*
* You can create a new dimension using the console command `./console generate:dimension`.
*
* @api
* @since 2.5.0
*/
abstract class VisitDimension extends Dimension
{
const INSTALLER_PREFIX = 'log_visit.';
private $tableName = 'log_visit';
/**
* Installs the visit 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_visit` MySQL table.
*
* Example:
* ```
public function install()
{
$changes = parent::install();
$changes['log_visit'][] = "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_visit' => array("ADD COLUMN `$this->columnName` $this->columnType", "ADD INDEX ...")
);
```
* @api
*/
public function install()
{
if (empty($this->columnType) || empty($this->columnName)) {
return array();
}
$changes = array(
$this->tableName => array("ADD COLUMN `$this->columnName` $this->columnType")
);
if ($this->isHandlingLogConversion()) {
$changes['log_conversion'] = array("ADD COLUMN `$this->columnName` $this->columnType");
}
return $changes;
}
/**
* @see ActionDimension::update()
* @param array $conversionColumns An array of currently installed columns in the conversion table.
* @return array
* @ignore
*/
public function update($conversionColumns)
{
if (!$this->columnType) {
return array();
}
$changes = array();
$changes[$this->tableName] = array("MODIFY COLUMN `$this->columnName` $this->columnType");
$handlingConversion = $this->isHandlingLogConversion();
$hasConversionColumn = array_key_exists($this->columnName, $conversionColumns);
if ($hasConversionColumn && $handlingConversion) {
$changes['log_conversion'] = array("MODIFY COLUMN `$this->columnName` $this->columnType");
} elseif (!$hasConversionColumn && $handlingConversion) {
$changes['log_conversion'] = array("ADD COLUMN `$this->columnName` $this->columnType");
} elseif ($hasConversionColumn && !$handlingConversion) {
$changes['log_conversion'] = array("DROP COLUMN `$this->columnName`");
}
return $changes;
}
/**
* @see ActionDimension::getVersion()
* @return string
* @ignore
*/
public function getVersion()
{
return $this->columnType . $this->isHandlingLogConversion();
}
private function isHandlingLogConversion()
{
if (empty($this->columnName) || empty($this->columnType)) {
return false;
}
return $this->hasImplementedEvent('onAnyGoalConversion');
}
/**
* 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)) {
return;
}
try {
$sql = "ALTER TABLE `" . Common::prefixTable($this->tableName) . "` DROP COLUMN `$this->columnName`";
Db::exec($sql);
} catch (Exception $e) {
if (!Db::get()->isErrNo($e, '1091')) {
throw $e;
}
}
try {
if (!$this->isHandlingLogConversion()) {
return;
}
$sql = "ALTER TABLE `" . Common::prefixTable('log_conversion') . "` DROP COLUMN `$this->columnName`";
Db::exec($sql);
} catch (Exception $e) {
if (!Db::get()->isErrNo($e, '1091')) {
throw $e;
}
}
}
/**
* 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)
{
$sqlSegment = $segment->getSqlSegment();
if (!empty($this->columnName) && empty($sqlSegment)) {
$segment->setSqlSegment('log_visit.' . $this->columnName);
}
parent::addSegment($segment);
}
/**
* Sometimes you may want to make sure another dimension is executed before your dimension so you can persist
* this dimensions' value depending on the value of other dimensions. You can do this by defining an array of
* dimension names. If you access any value of any other column within your events, you should require them here.
* Otherwise those values may not be available.
* @return array
* @api
*/
public function getRequiredVisitFields()
{
return array();
}
/**
* The `onNewVisit` method is triggered when a new visitor is detected. This means you can define an initial
* value for this user here. By returning boolean `false` no value will be saved. Once the user makes another action
* the event "onExistingVisit" is executed. Meaning for each visitor this method is executed once.
*
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed|false
* @api
*/
public function onNewVisit(Request $request, Visitor $visitor, $action)
{
return false;
}
/**
* The `onExistingVisit` method is triggered when a visitor was recognized meaning it is not a new visitor.
* You can overwrite any previous value set by the event `onNewVisit` by implemting this event. By returning boolean
* `false` no value will be updated.
*
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed|false
* @api
*/
public function onExistingVisit(Request $request, Visitor $visitor, $action)
{
return false;
}
/**
* This event is executed shortly after `onNewVisit` or `onExistingVisit` in case the visitor converted a goal.
* Usually this event is not needed and you can simply remove this method therefore. An example would be for
* instance to persist the last converted action url. Return boolean `false` if you do not want to change the
* current value.
*
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed|false
* @api
*/
public function onConvertedVisit(Request $request, Visitor $visitor, $action)
{
return false;
}
/**
* By implementing this event you can persist a value to the `log_conversion` table in case a conversion happens.
* The persisted value will be logged along the conversion and will not be changed afterwards. This allows you to
* generate reports that shows for instance which url was called how often for a specific conversion. Once you
* implement this event and a $columnType is defined a column in the `log_conversion` MySQL table will be
* created automatically.
*
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed|false
* @api
*/
public function onAnyGoalConversion(Request $request, Visitor $visitor, $action)
{
return false;
}
/**
* This hook is executed by the tracker when determining if an action is the start of a new visit
* or part of an existing one. Derived classes can use it to force new visits based on dimension
* data.
*
* For example, the Campaign dimension in the Referrers plugin will force a new visit if the
* campaign information for the current action is different from the last.
*
* @param Request $request The current tracker request information.
* @param Visitor $visitor The information for the currently recognized visitor.
* @param Action|null $action The current action information (if any).
* @return bool Return true to force a visit, false if otherwise.
* @api
*/
public function shouldForceNewVisit(Request $request, Visitor $visitor, Action $action = null)
{
return false;
}
/**
* Get all visit dimensions that are defined by all activated plugins.
* @return VisitDimension[]
*/
public static function getAllDimensions()
{
$cacheId = CacheId::pluginAware('VisitDimensions');
$cache = PiwikCache::getTransientCache();
if (!$cache->contains($cacheId)) {
$plugins = PluginManager::getInstance()->getPluginsLoadedAndActivated();
$instances = array();
foreach ($plugins as $plugin) {
foreach (self::getDimensions($plugin) as $instance) {
$instances[] = $instance;
}
}
usort($instances, array('self', 'sortByRequiredFields'));
$cache->save($cacheId, $instances);
}
return $cache->fetch($cacheId);
}
/**
* @ignore
*/
public static function sortByRequiredFields($a, $b)
{
$fields = $a->getRequiredVisitFields();
if (empty($fields)) {
return -1;
}
if (in_array($b->columnName, $fields)) {
return 1;
}
return 0;
}
/**
* Get all visit dimensions that are defined by the given plugin.
* @param Plugin $plugin
* @return VisitDimension[]
* @ignore
*/
public static function getDimensions(Plugin $plugin)
{
$dimensions = $plugin->findMultipleComponents('Columns', '\\Piwik\\Plugin\\Dimension\\VisitDimension');
$instances = array();
foreach ($dimensions as $dimension) {
$instances[] = new $dimension();
}
return $instances;
}
}
|