How to write a plugin
Introduction
Piwik offers a powerful plugin architecture, which allows you to add new features without modifying the core source code. This documentation aims to explain how to build a new "advanced" plugin.
I will illustrate the process with a concrete example: the GeoIP plugin, which allows to get locations details (continent, country, city, latitude, longitude) of the visitors, using the MaxMind GeoIP database.
Prerequisites
There are a few things you may need to know about the Piwik architecture in order to be able to create a more advanced plugin: there is a good overview of the database structure, along with a list of the available hooks. Here are some Piwik global concepts you have to understand:
- Whenever a user visits your website, a javascript will transfer a set of basic data about your visitor to the piwik.php script: page visited, ip, browser info, operating system info, screen resolution info, etc. At this point, the data is stored in the DB in raw format: no processing takes place.
- When you visit the Piwik interface (or if you have setup a crontab), this raw data is processed and the results are stored in tables. This process is called "Archiving".
- Finally, the data is retrieved from these archive tables, and displayed in the dashboard and the other pages.
Note that this data is also available via powerful (yet simple) web services.
What you can do with a plugin
- You can store additional raw data. For example, in the GeoIP plugin, we store new information about the user: country, city, continent, latitude, longitude.
- You can compute additional archived statistics, based on the raw data.
- You can automatically make your statistics available through many APIs in many formats.
- You can also do interface things such as "add a menu", "add a widget", etc. More information on functions you can call from a plugin.
- You can build whatever you want. If your have some ideas about plugins and you would know if it's possible to do in piwik, just contact us with a precise description and we will help you.
- See the list of hooks that you can trigger on.
The Plugin architecture
- Piwik follows the MVC pattern, with which you are maybe familiar, aiming at separating data presentation and data processing. The plugins follow this pattern, with a model, where is processed the logic; a controller, making the appropriate model calls depending on the request; and views, to display the data.
- Piwik provides hooking facilities: it is possible to register a function in association with an event. This is how you interact with the application from the plugins: you define your functions, and you register them to the events with which you want them to be associated.
Let's now see how to put all that in practice.
The files you have to create in the plugin
Your plugin files must be stored in a folder called after your plugin name, in the /plugins/ folder.
To make it easy and have a skeleton with which to start, you can simply take the existing /plugins/ExamplePlugin/ and copy it in /plugins/YourPluginName/
A plugin consists of the files described below.
YourPluginName.php
The Piwik_YourPluginName class
This class extends the Piwik_Plugin class, and contains the business logic of your plugin.
You have to implement at least one method: getInformation(), which returns the name, description, author, etc. of the plugin.
All the other methods: install(), uninstall(), getListHookRegistered(), getListRequiredPlugins() are optional.
For example, in the GeoIP plugin, some columns (location_geoip_country, location_geoip_city, location_geoip_continent...) need to be added to the log_visit table for the plugin to work. These columns need to be removed when uninstalling the plugin. We would therefore have to implement the install() and uninstall() method in order to do these SQL Queries.
Then, if you want to hook on some actions, you need to define when. In the getListHookRegistered() method, you return an associative array with is a pair of (key, value) = (hook name, function name to trigger).
The triggered functions
For example, in the GeoIP plugin, the function getListHookRegistered() returns
array( 'LogStats.newVisitorInformation' => 'logGeoIPInfo', 'ArchiveProcessing_Day.compute' => 'archiveDay', 'ArchiveProcessing_Period.compute' => 'archiveMonth' )
So when a new visitor comes, which triggers the event LogStats.newVisitorInformation, I want to trigger the method logGeoIPInfo() inside Piwik_GeoIp class.
Then, when Piwik launches the archiving process, I want to archive the GeoIp information: country, city and continent. I do that in the archiveDay() and archiveMonth() methods, which will be triggered on the hook ArchiveProcessing_Day.compute and the hook ArchiveProcessing_Period.compute. We won't discuss here the implementation of these "archiving" methods, you can lookup the source code of any of the plugins that does trigger on the archive and see how it works. The code of the classes and function used should be well documented.
A triggered function will always be called with a parameter $notification.
You can then call $notification.getNotificationObject() which returns the object that may have been posted with the event. This depends on the event to which the function was registered. See the hooks and the optional objects posted with each hook.
Example of logGeoIPInfo()
This function, hooked on the LogStats.newVisitorInformation event, simply adds additional elements to the user information array. This array will be then saved in the log_visit database by the core Piwik process.
// retrieve the array of the visitors data from the notification object $visitorInfo =& $notification->getNotificationObject();
We retrieve our specific GeoIp details using the GeoIp database
$locationInfo = $this->getLocationInfo($visitorInfo['location_ip']);
Finally, we update the $visitorInfo user info array
$visitorInfo['location_geoip_country'] = $locationInfo['country_code_2']; ...
The modification we made to this array will be accessible from the Core piwik files, therefore the new 'location_geoip_country' field value will be recorded in the log_visit table in the database along with the default value.
We modified the behaviour of how we save data about a visitor without modifying any piwik file'''
This is the beauty of an event based software.
Widget function calls
In order to make you plugin output available from the menu, and / or integrated to the dashboard, you need to call the corresponding functions:
if(function_exists('Piwik_AddWidget'))
{
Piwik_AddWidget( 'GeoIP', 'getGeoIPContinent', 'Visitor continents (GeoIP)');
Piwik_AddWidget( 'GeoIP', 'getGeoIPCountry', ' GeoIP Visitor countries (GeoIP)');
Piwik_AddMenu('Visitors', 'Locations (GeoIP)', array('module' => 'GeoIP'));
}
Piwik_AddWidget allows to display your plugin output in the dashboard. It takes and take 3 parameters:
- the plugin name
- the plugin section name, which must correspond to a controller function (see the Controller section below) - there can be several sections in one module. For example here, we ve got one section for the continent, one for the country.
- the label for the section.
Piwik_AddMenu allows to add your plugin to the piwik menu. It takes also 3 parameters:
- the menu category in which it falls
- the label for the menu item
- a one-element array, of which the key seems to be 'module' (I am not sure what it corresponds to), and the value is the name of the plugin.
Note: I don't believe the widget and menu registration will be managed this way forever
Views
Now that we've seen how to process our data (first by adding logs in the log table, then by archiving them), we would like to make them available to the world. There are three files used in this purpose:
- the view, which actually display the data
- the controller, which create the view
- the API, which build the data structure
The view is a Smarty view. As such, it accepts placeHolder to put php variables. These variables are defined in the Controller. This view is the page which is displayed when you click on your plugin name from the menu.
The view for the geoIP plugin is fairly simple:
{postEvent name="template_headerGeoIPCountry"}
<script type="text/javascript" src="plugins/Home/templates/sparkline.js"></script>
<h2>Country (GeoIP)</h2>
{$dataTableCountry}
<h2>Continent (GeoIP)</h2>
{$dataTableContinent}
<p><img class="sparkline" src="{$urlSparklineCountries}" /> <span><strong>{$numberDistinctCountries} </strong> distinct countries</span></p>
{postEvent name="template_footerGeoIPCountry"}
I am not sure yet what the postEvent calls are for. The rest is rather simple: the {$dataTableCountry}, {$dataTableContinent}, {$urlSparklineCountries} and {$numberDistinctCountries} are place holders for variables defined in the Controller; the rest is simple html.
Controller.php
Serve the view. The index() function create the view, and set its place holder. e.g.
$view = new Piwik_View('GeoIP/index.tpl');
/* User Country */
$view->urlSparklineCountries = $this->getUrlSparkline('getLastDistinctCountriesGraph');
$view->numberDistinctCountries = $this->getNumberOfDistinctCountries(true);
$view->dataTableCountry = $this->getGeoIPCountry(true);
$view->dataTableContinent = $this->getGeoIPContinent(true);
echo $view->render();
This function call subfunctions, in particular $this->getGeoIPCountry(true) and $this->getGeoIPContinent(true), which are in charge of providing the data structure we want to display.
The key part of the function getGeoIPCountry(true) is the following line:
$view->init( 'GeoIP', __FUNCTION__, 'GeoIP.getGeoIPCountry', 'getCitiesFromCountryId' );
As far as I understood, the arguments of that init function are the following:
- the plugin name
- the function name from which we call the init function
- the Plugin.function name to call to get the data structure. This function is actually taken from the API file if I get it properly (see below)
- the subfunction name (taken from the Controller ?), in case the data to display has a nested structure - which is the case here: countries and cities are nested.
This $view->init() will create an html data structure to be displayed on the view; on which you can specify few parameters:
- whether you want a search box: e.g. $view->disableSearchBox();
- whether you want to exclude low population: e.g. $view->disableExcludeLowPopulation();
- which columns of the datatable you want to display: e.g $view->setColumnsToDisplay( array(0,1) );
...
API.php
As explained above, this class is used to extract the data to be displayed, and put it in a structure called dataTable.
Let's see the getGeoIPCountry() function, which returns the datatable containing the statistics on the countries and cities visited during the period passed as argument:
The process to build datatable is the following:
- The data is retrieved from the archive
$archive = Piwik_Archive::build($idSite, $period, $date );
$dataTable = $archive->getDataTable('GeoIP_cityByCountry');
- The datatable can be modified to put additional information, with the use of filter and callback functions: (presently defined globally at the end of the API file)
$dataTable->queueFilter('Piwik_DataTable_Filter_ColumnCallbackAddDetail', array('label', 'code', create_function('$label', 'return $label;')));
$dataTable->queueFilter('Piwik_DataTable_Filter_ColumnCallbackAddDetail', array('label', 'logo', 'Piwik_geoip_getFlagFromCode'));
$dataTable->queueFilter('Piwik_DataTable_Filter_ColumnCallbackReplace', array('label', 'Piwik_geoip_CountryTranslate'));
$dataTable->queueFilter('Piwik_DataTable_Filter_ReplaceColumnNames');
$dataTable->queueFilter('Piwik_DataTable_Filter_AddConstantDetail', array('logoWidth', 18));
$dataTable->queueFilter('Piwik_DataTable_Filter_AddConstantDetail', array('logoHeight', 12));
- then the datatable is returned
return $dataTable;
Side files
You may need additional files for your plugin to actually work. For example, I needed the maxmind libraries to get the actual user location information. These are put in the GeoIP/libs folder.
Configuration
For your plugin to work, it needs to be installed. If you do NOT interact with the logging event (that is, if you are not listening at the 'LogStats.newVisitorInformation' event), you can install your plugin simply from the admin interface (available at /?module=PluginsAdmin) However, if you do interact with the logging event, you need to edit the config/global.ini.php file and to append your plugin under the [Plugins_LogStats] line: [Plugins_LogStats] Plugins_LogStats[] = YourPlugin
Testing
Writing your plugin is great; testing it is better - but can be a bit tricky.
There may be few tips of help:
- You can generate random data - this is available from the admin menu
- You can define to archive data at each access to the statistic board - by default this is disabled for performance reason, but can be enabled in the
config/global.ini.php by setting always_archive_data = false
- By default, a user make a new entry in the log table only if its previous visit is older than 30 minutes. If you want to test your plugin by fetching your tracked website without waiting 30 minutes for a new entry to be generated, you can change this setting in the file:
modules/LogStats.php by changing the VISIT_STANDARD_LENGTH constant: const VISIT_STANDARD_LENGTH = 1800; => const VISIT_STANDARD_LENGTH = 5;
Conclusion
I hope this documentation was useful - if you have any questions, feel free to contact me at mikael dot letang at gmail dot com. I'll try to update the documentation with your feedback.
