Et bien si.

Il avait tort, complètement et irrémédiablement tort.

Google m'a bien renvoyé quelques résultats, mais la plupart pointent directement ou indirectement sur la version 0.2 d'une classe écrite en PHP 4 qui remonte à 2005...

J'ai donc envisagé dans un second temps de faire effectuer le travail par le navigateur, via un script en JavaScript tel que celui-ci, mais j'ai vite abandonné l'idée.

Je souhaitais en effet pouvoir distribuer mon graphique via une simple URL, à la manière d'une image, et cette solution ne le permettait pas.

Je me suis donc mis à concevoir une classe PHP me permettant de faire de la génération de sparklines.

Mon premier réflexe, en aficionados de la programmation orientée objet, a été de concevoir ma classe sparkline de la manière suivante :

...
namespace svneeg;

class sparkline
{
protected $width = 0;
protected $height = 0;
protected $foregroundColor = array(90, 90, 90);
protected $backgroundColor = array(255, 255, 255);
protected $gridColor = array(240, 240, 255);
protected $axisColor = array(0, 0, 102);

public function __construct($width, $height)
{
...
}

public function setWidth($width)
{
...
}

public function setHeight($height)
{
...
}

public function setForegroundColor($r, $g, $b)
{
...
}

public function setBackgroundColor($r, $g, $b)
{
...
}

public function render($data)
{
...
}

public function save(array $data, $file)
{
...
}

...
}
...

Je m'étais dit, sans trop y réfléchir, qu'il me suffirait de dériver cette classe en fonction de mes besoins pour obtenir de nouveaux types de graphiques plus ou moins complexes tel que ceux présentés au début de ce billet.

Cependant, à y réfléchir dans le détail, j'avais fait de la merde.

En effet, cette classe est très peu évolutive, comme le démontre ces quelques exemples :

  • Si nous souhaitons sauvegarder le graphique dans autre chose qu'un fichier ou sous un autre format il faut dériver la classe et ajouter le code correspondant.
  • Si nous souhaitons dessiner une courbe dans un autre style, il faut dériver la classe et ajouter le code correspondant.
  • Si nous souhaitons ajouter au graphique un élément, comme une légende ou un axe d'ordonnées ou d'abscisse, il faut dériver la classe et ajouter le code correspondant.
  • Si nous souhaitons ajouter au graphique un élément, il n'est pas possible de le dessiner à la profondeur de son choix, par exemple entre le fond de l'image et la courbe.
  • Si nous voulons ajouter à la fois un nouveau format de sauvegarde et un nouvel élément graphique, c'est le bordel il faut dériver la classe et potentiellement faire de la duplication de code.

Bref, la solution de l'héritage manque cruellement de souplesse pour faire évoluer cette classe, d'autant que PHP ne supporte pas l'héritage multiple.

Mais pourquoi est-ce que je me retrouve dans cette situation ?

Le problème vient du fait que j'ai donné bien trop de responsabilités à ma classe.

Elle prend en effet en charge :

  1. La définition des dimensions de l'image finale,
  2. La définition du style des éléments graphiques qui compose l'image finale tel que la courbe, les axes, la grille et le fond,
  3. La génération de l'image finale,
  4. La sauvegarde de l'image finale.

Or, à la base, une sparkline est une image disposant d'une hauteur et d'une largeur, et qui contient une courbe, éventuellement un ou plusieurs axes, un fond, etc.

Ce n'est donc pas de son ressort de prendre en charge la définition du style des éléments graphiques, leur rendu, ou bien encore sa propre sauvegarde.

Il est donc nécessaire de déléguer ces tâches à des classes annexes, et donc de faire de la composition, en terme de programmation orientée objet.

Le dessin de chaque élément graphique doit donc être délégué à une instance d'une classe dédiée dérivée d'une classe layer qui prendra en charge à la fois la définition du style de l'élément ainsi que son rendu.

De même, la sauvegarde du graphique doit être assurée par une ou plusieurs instances d'une classe dédiée dérivée d'une classe writer afin de pouvoir enregistrer l'image dans le format de son choix, voir même dans plusieurs formats.

Le code de la classe sparkline est donc devenu ceci :

...
namespace svneeg;

class sparkline implements \iteratorAggregate, \countable
{
...
protected $width = 0;
protected $height = 0;
protected $layers = array();
protected $writers = array();

public function __construct($width, $height, array $data)
{
...
}

public function __destruct()
{
...
}

public function setWidth($width)
{
...
}

public function setHeight($height)
{
...
}

public function addLayer(\svneeg\sparkline\layer $layer)
{
...
}

public function addWriter(\svneeg\sparkline\writer $writer)
{
...
}

public function render()
{
...
foreach ($this->layers as $layerNumber => $layer)
{
$this->merge($layerNumber, $layer->draw($this));
}

return $this;
}

public function write()
{
...
foreach ($this->writers as $writerNumber => $writer)
{
$writer->write($data);
}

return $this;
}

...
}
...

Pour ajouter un élément au graphique, il n'est donc plus nécessaire de dériver la classe sparkline, il suffit de définir un nouveau type de calque à partir de la classe abstraite layer.

De plus, avec cette nouvelle version, il est possible de dessiner un élément graphique à la profondeur de son choix, en fonction de l'ordre dans lequel les calques sont ajoutés au graphique :

	...
$sparkline
->addLayer(new layers\background\solid(0, 60, 0))
->addLayer(new layers\grid\vertical(0, 90, 0)) // dessiné au dessus du fond
->addLayer(new layers\lines\line()) // dessiné au dessus de la grille verticale
->addLayer(new layers\axis\y(0, 255, 0)) // dessiné au dessus de la courbe
// pour dessiner l'axe des y en dessous de la courbe, il suffit d'ajouter le calque correspondant avant celui de la courbe.
;
...

La même philosophie a été appliquée en ce qui concerne la sauvegarde de l'image :

	...
$sparkline
->addWriter(new writers\png(config\directories\tmp . '/' . config\sparkline\name))
->addWriter(new writers\jpeg(config\directories\tmp . '/' . config\sparkline\name))
->addWriter(new writers\http\png())
;
...

La composition est donc une solution élégante qui permet d'avoir un code souple, évolutif et surtout simple car le périmètre fonctionnel de chaque classe est strictement limité à une tâche précise.

En conséquence, il y a beaucoup moins de risques d'erreurs lors du développement et l'évolutivité et la maintenabilité du code est grandement augmentée par rapport à une solution qui nécéssite le recours à l'héritage.

Et pour ceux qui sont intéressés par le code, justement, le code de la classe sparkline dans sa dernière version est disponible dans mon dépôt svn.

Évidement, il ne fonctionne qu'avec au minimum la version 5.3 de PHP, vu qu'il met en oeuvre les espaces de nommage.