How to write a Piwik Plugin

How to write a Piwik Plugin

Piwik offers a plugin architecture, that allows you to build plugins without modifying the Piwik core. This article will give an introduction to the Piwik plugin architecture and show you how to build your own plugin.

What can a plugin do for me?

A plugin can..

  • ..collect additional data that Piwik currently does not track.
  • ..define new Widgets to visualize currently available or new data.
  • ..define new or override existing menus to customize your Piwik
  • ..provide an API and automatically make data accessible in a number of formats

How does tracking work?

Before you can start writing your own plugin, you should know how Piwik tracks and processes data. The best way to start is by taking a look at the database layout of a fresh Piwik installation.

Piwik Database Schema

Tracking

Each time a visitor visits your Piwik enabled website the JavaScript will submit a set of basic data about your visitor and the page visited to the piwik.php script. The script takes the raw data and stores it in the database. Tracker plugins can modify or add data to it (e.g. the GeoIP plugin adds geolocation information to the raw data).

For each visitor a new entry is added to the _log_visit table. On each subsequent page visit the visited pages of that visitor are stored in _log_link_visit_action. The _log_action table contains an entry for each page that Piwik has tracked so far. The table is referenced by the _log_link_visit_action table.

If you define Goals for your websites in Piwik, they are stored in the _goal table. Every time one of those goals converts the conversion is stored in the _log_conversion table together with some data about the visitor and the page of the conversion.

Processing

The raw data that is produced during tracking cannot be displayed directly. It would be too much load for the database to compute all metrics on demand. Therefore Piwik processes the raw data for visualization. This process is called Archiving.

Archiving happens when you visit the Piwik interface. On high-traffic websites archiving should be done by a cronjob. Raw data is processed and stored into archive tables.

Archives

For each month that Piwik tracks data, it creates two tables in the database. They are named _archive_YEAR_MONTH and _archive_numeric_YEAR_MONTH and contain the processed data for that month.

The Piwik Documentation explains what those tables are used for:

The table archive_numeric_* is used to store plain numbers. The value field has a FLOAT type which means you can save integers and floats. For example, it is used to record the number of visitors over a given period and the number of distinct search engines keywords used.

The table archive_blob_* stores anything that is not a number. A BLOB is a binary data type that can contain anything from strings and compressed strings to serialized arrays and serialized objects. For example, it is used to store the search engine keywords that the visitors used over a given period and the visitors' browsers.

Plugins can now fetch the processed data from the archive tables and fill graphs with data or use the data in other ways.

The Plugin Architecture

Directory structure

Plugins are located in the plugins/ directory of your Piwik installation. Each plugin resides in its own subdirectory.

plugins/
|-- VisitFrequency
|   |-- API.php
|   |-- Controller.php
|   |-- templates
|   |   |-- index.tpl
|   |   `-- sparklines.tpl
|   `-- VisitFrequency.php

Most plugins consist of no more than three PHP files and a number of view templates.

Basic layout

The plugin system has a number of conventions that need to be followed in order to make a plugin work with Piwik. The first convention is, that a plugin has to have a PHP file with the same name as the directory it resides in. So if you build a VisitorForecast plugin, you need to create a directory called VisitorForecast with a VisitorForecast.php file.

Continuing with the idea of a VisitorForecast plugin, you need to create a class called Piwik_VisitorForecast that extends the Piwik_Plugin class:

<?php
class Piwik_VisitorForecast extends Piwik_Plugin { }

Piwik_Plugin is an abstract class with an abstract method called getInformation() that you need to implement. You can take a look at other plugins to see what the function has to return, or you just take a look at the abstract class in core/Plugin.php.

/**
* Returns the plugin details
* - 'description' => string        // 1-2 sentence description of the plugin
* - 'author' => string             // plugin author
* - 'author_homepage' => string    // author homepage URL (or email "mailto:youremail@example.org")
* - 'homepage' => string           // plugin homepage URL
* - 'license' => string            // plugin license
* - 'license_homepage' => string   // license homepage URL
* - 'version' => string            // plugin version number; examples and 3rd party plugins must not use Piwik_Version::VERSION; 3rd party plugins must increment the version number with each plugin release
* - 'translationAvailable' => bool // is there a translation file in plugins/your-plugin/lang/* ?
* - 'TrackerPlugin' => bool        // should we load this plugin during the stats logging process?
*
* @return array
*/
abstract public function getInformation();

The method must return an associative array with information about the plugin. An implementation for the VisitorForecast plugin could look like this:

<?php
class Piwik_VisitorForecast extends Piwik_Plugin {
  public function getInformation() {
    return array(
      'description' => 'Provide a forecast of visits for the day',
      'author' => 'Your Name',
      'author_homepage' => 'http://yourwebsite.com',
      'homepage' => 'http://example.com',
      'license' => 'GPL v3 or later',
      'license_homepage' => 'http://www.gnu.org/licenses/gpl.html',
      'version' => '0.1',
      'translationAvailable' => false,
    );
  }
}

Translations

Piwik is available in over 45 languages! When you build your plugin you might want to utilize the internationalization features Piwik provides for you and provide translations for your custom plugin.

To tell Piwik that your plugin provides translations, you need to set 'translationAvailable' => true' in getInformation. Piwik will now look for translation strings in the lang/ subdirectory of the plugin. For each language you want to provide translations for create a php file with the ISO 639-1 alpha-2 name for that language.

Each language file consists of an associative array called $translations that maps a key to a translation. The keys are shared across all translation files, their translation changes depending on the language. By convention the keys are prefixed with the plugin name, so a translation file in the VisitorForecast plugin for English could look like this:

<?php
$translations = array(
  'VisitorForecast_PluginDescription' => 'Provide a forecast of visits for the day'
);

Try to give the translation keys descriptive names, so that translators have an easy job! Piwik provides a function called Piwik_Translate($key) that returns the translation for a given key, depending on the currently selected language of the user.

public function getInformation() {
  return array(
    'description' => Piwik_Translate('VisitorForecast_PluginDescription'),
    [..]
  );
}$

Hooks

Internally Piwik uses a PHP package called EventDispatcher. Plugins can hook a function to a predefined event, that gets called when the event is triggered. You can also define events so that other plugins create hooks for them.

If your plugin needs to register to any hooks, you need to implement the getListHooksRegistered method in your plugin class. The method must return an array, mapping the hook name to a callback function. Lets say, your plugin wants to be notified of each new visitor. Piwik has a hook called Tracker.newVisitorInformation that is triggered each time Piwik tracks a new visitor.

public function getListHooksRegistered()
{
  return array( 'Tracker.newVisitorInformation' => 'addVisitorInformation' );
}

public function addVisitorInformation($notification) {
{
     // we get the argument by reference associated with the hook
     $visitorInfo =& $notification->getNotificationObject();
     // get the referrer_url
     $url = $visitorInfo['referer_url'];
     // do something with the url
}

Callback functions can retrieve a notification object when the event is triggered. The content of the object depends on the hook. In this case the object is an array, containing the visitor information.

The Piwik Documentation has a list of hooks that describe when each hook is triggered and what type of object they provide.

Functions

A plugin can access a number of functions that are provided by Piwik to interact with the database, handle user access, add and modify menus and access GET/POST-request parameters. A maintained list of functions can be found in the Piwik documentation.

Controller

Piwik follows the Model-View-Controller pattern aiming at separating data presentation and data processing. All plugins follow the MVC pattern. We will later see that all data processing, if any, is done in the PluginName.php - effectively making it the model.

The controller, in a Piwik plugin has to be in a file called Controller.php. Again, by convention the controller class has to be named Piwik_PluginName_Controller and has to extend the Piwik_Controller class. All public methods in a controller can be called from the web.

class Piwik_VisitorForecast_Controller extends Piwik_Controller
{
  public function index() {
    $view = Piwik_View::factory('index');
    echo $view->render();
  }
}

When you visit your local Piwik installation you can render the page in Piwik by visiting index.php?module=VisitorForecast&action=index. Module in this case is the name of your plugin and the action the name of the method in the controller.

Views

Piwik uses the Smarty template engine to render views. Views are stored in the templates/ directory of your plugin and have the .tpl file extension.

There are several plugins for Smarty in Piwik. One being access to the translation function by which you can translate any key to its translation.

<h1>VisitorForecast</h1>
<p>{'VisitorForecast_PluginDescription'|translate}</p>

Views can be accessed in the controller using the Piwik_View class. Calling Piwik_View::factory('index') in your controller, you get an instance of your view in index.tpl. You can then set variables from the controller to make them available to the view. The render method renders out the template.

$view = Piwik_View::factory('index');
$view->myVariable = 12;
echo $view->render();

Inside the view you can access the variable by its name and render it with {$myVariable}.

API

All the data in Piwik is available through simple APIs. Plugins can extend the API to provide their data in various formats like XML, JSON, PHP or CSV.

To provide an API you need to create a file called API.php, with a class named Piwik_PluginName_API. All public methods in this class are exposed via Piwiks API.

The IPv6Usage plugin

Piwik supports IPv6 since version 1.4. While most people in 2012 are still using IPv4 the number of users having native IPv6 steadily increases. If your website is reachable over IPv6 it might be interesting to see how many visitors are actually browsing your website with it. We can use Piwik to determine which version of the Internet Protocol a visitor is using.

If you are not sure, whether you have IPv6 you can do a check online.

Prerequisites

Before we can start writing code, we need to install Piwik on our local machine. You can do this by installing PHP, MySQL and a webserver of your choice manually using your package manager (e.g. apt-get) and set them up properly.

A better alternative to start off quickly is using the Piwik Puppet Module + Vagrant. This will setup a preconfigured virtual machine and lets you start Piwik development instantly.

IPv6Usage.php

To start with the plugin we need to create the IPv6Usage.php, create a class called Piwik_IPv6Usage and extend from Piwik_Plugin. I will not show the implementation of getInformation here, as it is trivial to implement.

First we need to think about how we store our data. Since we want to track the IP version of each user, it is the best to add a field to the log_visit table. We can do this inside the install() method of our plugin, which gets automatically called when our plugin is activated the first time.

public function install()
{
  // add column location_ip_protocol in the visit table
  $query = "ALTER IGNORE TABLE `".Piwik_Common::prefixTable('log_visit')."` " .
                   "ADD `location_ip_protocol` TINYINT( 1 ) NULL ";

  // if the column already exist do not throw error. Could be installed twice...
  try {
    Piwik_Exec($query);
  }
  catch(Exception $e){
  }
}

These few lines will add a column called location_ip_protocol to the log_visit table. We will later just record a 4 if the user is on IPv4, and a 6 if the user is on IPv6.

Next thing we need to think about, is what kind of hooks we need to register to. In order to track new visits and determine their IP version, we need to register to Tracker.newVisitorInformation. We also want to process the data to display it in graphs, so we also need to register to the ArchiveProcessing_Day.compute and ArchiveProcessing_Period.compute hooks.

public function getListHooksRegistered()
{
  return array(
    'Tracker.newVisitorInformation' => 'logIPv6Info',
    'ArchiveProcessing_Day.compute' => 'archiveDay',
    'ArchiveProcessing_Period.compute' => 'archivePeriod',
    'WidgetsList.add' => 'addWidget',
    'Menu.add' => 'addMenu'
  );
}

We also want a separate menu entry to show off some graphs and define a widget to show the percentage of visitors using IPv6. For this Piwik defines two hooks called Menu.Add and WidgetList.add. We now need to define all callback methods and implement them.

public function addMenu()
{
  Piwik_AddMenu('General_Visitors', 'IPv6 Usage', array('module' => 'IPv6Usage', 'action' => 'index'));
}

Piwik_AddMenu adds a new menu entry. The first argument defines the name of the menu on the first level, the second is the label of the submenu, and the third argument an array of URL parameters. In this case, we should get a submenu labelled "IPv6Usage" that, when clicked, shows the index action of our controller (which we yet have to implement).

public function addWidget() {
  Piwik_AddWidget( 'General_Visitors', 'IPv6Usage_WidgetProtocolDescription', 'IPv6Usage', 'getIPv6UsageGraph');
}

Adding a widget is quite similar. The first argument is the category in which the widget will be listed, the second the title of the widget. Third and fourth argument are the name of the controller and the name of the method to be called.

Now we get to the interesting part of logging data to the database. For each visit, Piwik will call the logIPv6Info callback method. This method needs to determine the IP version the visitor is using, and set it in the visitorInfo array.

public function logIPv6Info($notification) {
  // retrieve the array of the visitors data from the notification object
  $visitorInfo =& $notification->getNotificationObject();
  // Fetch the users ip
  $ip = $visitorInfo['location_ip'];
  // Check the type of the IP (v4 or v6)
  $protocol = Piwik_IP::isIPv4($ip) ? 4 : 6;
  $visitorInfo['location_ip_protocol'] = $protocol;
}

The Piwik_IP class provides a convenience method to check whether an IP is in IPv4 format. We can assume that if the IP from location_ip is not an IPv4 address, that it is an IPv6 address. We modify the $visitorInfo array and set the value for location_ip_protocol. Piwik automatically converts that array into a database query and inserts all values into the log_visit table.

Archiving

The final step is to implement archiving of our data. The archiveDay callback method is called for each website and day that has to be processed. The notification object is an instance of Piwik_ArchiveProcessing. We need to fetch the number of visits for both protocol versions from the database and limit the query to only count visits in the date range that has to be archived.

public function archiveDay($notification)
{
  /* @var $archiveProcessing Piwik_ArchiveProcessing */
  $archiveProcessing = $notification->getNotificationObject();

  if(!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) return;

  $select = "location_ip_protocol, COUNT( location_ip_protocol ) as count";
  $from = "log_visit";

  $where = "log_visit.visit_last_action_time >= ?
                   AND log_visit.visit_last_action_time <= ?
                   AND log_visit.idsite = ?
                   AND location_ip_protocol IS NOT NULL
                   GROUP BY location_ip_protocol";

  $bind = array(
    $archiveProcessing->getStartDatetimeUTC(),
    $archiveProcessing->getEndDatetimeUTC(), 
    $archiveProcessing->idsite
  );

  $query = $archiveProcessing->getSegment()->getSelectQuery($select, $from, $where, $bind);
  $rowSet = $archiveProcessing->db->query($query['sql'], $query['bind']);

  $data = array(
    'IPv6Usage_IPv4' => 0,
    'IPv6Usage_IPv6' => 0
  );

  while($row = $rowSet->fetch())
  {
    $key = sprintf("%s%d", 'IPv6Usage_IPv', $row['location_ip_protocol']);
    $data[$key] = $row['count'];
  }

  foreach($data as $key => $value)
  {
    $archiveProcessing->insertNumericRecord($key, $value);
  }
}

After getting the numbers from the database we can call the method insertNumericRecord($key, $value) on the notification object to archive the values.

Controller.php

In the controller we want to do two things. We want to display a pie chart, showing us the proportion of visits by protocol and we want to render a basic view.

Rendering a basic view is very easy. You just need to instantiate your view template (we will come to that in a minute) and call the render() method on it.

public function index() {
  $view = Piwik_View::factory('index');
  $this->setPeriodVariablesView($view);
  $view->graphUsageEvolution = $this->getIPv6UsageEvolutionGraph( true, array('IPv6Usage_IPv4', 'IPv6Usage_IPv6') );
  echo $view->render();
}

The index template will render a graph, therefore we assign the rendered graph to a variable called graphUsageEvolution.

public function getIPv6UsageEvolutionGraph( $fetch = false, $columns = false)
{
  if(empty($columns))
  {
    $columns = Piwik_Common::getRequestVar('columns');
    $columns = Piwik::getArrayFromApiParameter($columns);
  }

  $documentation = Piwik_Translate('IPv6Usage_ProtocolUsageEvolution');

  // Note: if you edit this array, maybe edit the code below as well
  $selectableColumns = array(
    'IPv6Usage_IPv4',
    'IPv6Usage_IPv6',
    'nb_visits',
    'nb_uniq_visitors'
  );

  $view = $this->getLastUnitGraphAcrossPlugins($this->pluginName, __FUNCTION__, $columns,
                   $selectableColumns, $documentation);

  return $this->renderView($view, $fetch);
}

This method is a little more complex. It takes two arguments, the first is used in the call to renderView and if set to true will make it return the rendered view instead of printing it out. The second argument lets you filter the list of shown columns.

Now $selectableColumns is an array in which we specify which columns should be selectable in the graph. Finally the call to getLastUnitGraphAcrossPlugins creates the graph by calling API.get internally and selecting the columns that were requested.

Views

Right now, our plugin only has one simple template. It just renders the evolution graph that is created in the controller.

<h2>{'Referers_Evolution'|translate}</h2>
{$graphUsageEvolution}

Testing

Testing is a very important part of development. However testing a plugin in Piwik is not an easy task, as most of the time you will want to emulate visitors with certain settings on a page and make sure your plugin processes everything right.

I suggest you take a look at how other plugins are tested and try to come up with ways to test your plugin. You can also take a look at the VisitorGenerator plugin that comes with Piwik. It allows you to generate random visits and actions to fill up your database.

Conclusion

This article provided an extensive description of the Piwik plugin architecture and used the IPv6Usage plugin as an implementation example.

When developing your own plugin, the Piwik documentation will be of great help. If you are stuck with a problem a look at the implementation of other plugins can help a lot.
If you have any questions, feel free to leave a comment. I will update the documentation with your feedback.