Le problème
Dans votre script est présent une classe s'occupant de la gestion d'un module. Lors d'une action précise, vous exécutez une ou plusieurs instructions. Celles-ci n'ont qu'une chose en commun : le fait qu'elles soient appelées car telle action s'est produite. Elles n'ont rien d'autre en commun, elles sont un peu foutues dans la méthode
« parce qu'il faut bien les appeler et qu'on sait pas où les mettre ». Il est intéressant dans ce cas-là de séparer les différentes actions effectuées lorsque telle action survient. Pour cela, nous allons regarder du côté du pattern Observer.
Le principe est simple : vous avez une classe observée et une ou plusieurs autre(s) classe(s) qui l'observe(nt). Lorsque telle action survient, vous allez prévenir toutes les classes qui l'observent. Nous allons, pour une raison d'homogénéité, utiliser les interfaces prédéfinies de la
SPL. Il s'agit d'une librairie standard qui est fournie d'office avec PHP. Elle contient différentes classes, fonctions, interfaces, etc. Vous vous en êtes déjà servi en utilisant
spl_autoload_register().
Bref, regardons plutôt ce qui nous intéresse, à savoir deux interfaces :
SplSubject et
SplObserver.
La première interface,
SplSubject, est l'interface implémentée par l'objet observé. Elle contient trois méthodes :
- attach (SplObserver $observer) : méthode appelée pour ajouter une classe observatrice à notre classe observée ;
- detach (SplObserver $observer) : méthode appelée pour supprimer une classe observatrice ;
- notify() : méthode appelée lorsqu'on aura besoin de prévenir toutes les classes observatrices que quelque chose s'est produit.
L'interface
SplObserver est l'interface implémentée par les différents observateurs. Elle ne contient qu'une seule méthode qui est celle appelée par la classe observée dans la méthode
notify() : il s'agit de
update ( (SplSubject $subject)).
Voici un diagramme mettant en œuvre ce design pattern :
On va maintenant imaginer le code correspondant au diagramme. Commençons par la classe observée :
Code : PHP - Classe observée 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 | <?php
class Observee implements SplSubject
{
// Ceci est le tableau qui va contenir tous les objets qui nous observent
protected $observers = array();
// Dès que cet attribut changera on notifiera les classes observatrices
protected $nom;
public function attach (SplObserver $observer)
{
$this->observers[] = $observer;
}
public function detach (SplObserver $observer)
{
if (is_int ($key = array_search ($observer, $this->observers, true)))
unset ($this->observers[$key]);
}
public function notify()
{
foreach ($this->observers as $observer)
$observer->update ($this);
}
public function getNom()
{
return $this->nom;
}
public function setNom ($nom)
{
$this->nom = $nom;
$this->notify();
}
}
?>
|
Vous pouvez constater la présence du nom des interfaces en guise d'argument. Cela veut dire que cet argument doit implémenter l'interface spécifiée.
Voici les deux classes observatrices :
Code : PHP - Classes observatrices 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | <?php
class Observer1 implements SplObserver
{
public function update (SplSubject $obj)
{
echo __CLASS__, ' a été notifié ! Nouvelle valeur de l\'attribut <strong>nom</strong> : ', $obj->getNom();
}
}
class Observer2 implements SplObserver
{
public function update (SplSubject $obj)
{
echo __CLASS__, ' a été notifié ! Nouvelle valeur de l\'attribut <strong>nom</strong> : ', $obj->getNom();
}
}
?>
|
Ces deux classes font exactement la même chose, ce n'était qu'à titre d'exemple basique que je vous ai donné ça, histoire que vous voyez la syntaxe de base lors de l'utilisation du pattern Observer.
Pour tester nos classes, vous pouvez utiliser ce bout de code :
Code : PHP | <?php
$o = new Observee;
$o->attach(new Observer1); // Ajout d'un observateur
$o->attach(new Observer2); // Ajout d'un autre observateur
$o->setNom('Victor'); // On modifie le nom pour voir si les classes observatrices ont bien été notifiées
?>
|
Vous pouvez voir qu'ajouter des classes observatrices de cette façon peut être assez long si on en a 5 ou 6. Il y a une petite technique qui consiste à pouvoir obtenir ce genre de code :
Code : PHP | <?php
$o = new Observee;
$o->attach(new Observer1)
->attach(new Observer2)
->attach(new Observer3)
->attach(new Observer4)
->attach(new Observer5);
$o->setNom('Victor'); // On modifie le nom pour voir si les classes observatrices ont bien été notifiées
?>
|
Pour effectuer ce genre de manœuvres, la méthode
attach() doit retourner l'instance qui l'a appelé (en d'autres termes, elle doit retourner
$this).
Exemple concret
Regardons un exemple concret à présent. Nous allons imaginer que vous ayez, dans votre script, une classe gérant les erreurs générées par PHP. Lorsqu'une erreur est générée, vous aimeriez qu'il se passe deux choses :
- Que l'erreur soit enregistrée en BDD ;
- Que l'erreur vous soit envoyée par mail.
Pour cela, vous pensez donc coder une classe comportant une méthode attrapant l'erreur et effectuant les deux opérations ci-dessus. Grave erreur ! Ceci est surtout à ne pas faire : votre classe est chargée
d'intercepter les erreurs, et non de les
gérer ! Ce sera à d'autres classes de s'en occuper : ces classes vont observer la classe gérant l'erreur et une fois notifiée, elles vont effectuer l'action pour laquelle elles ont été conçues. Vous voyez un peu la tête qu'aura le script ?
Rappel : pour intercepter les erreurs, il vous faut utiliser set_error_handler(). Pour faire en sorte que la fonction de callback appelée lors de la génération d'une erreur soit une méthode d'une classe, passez un tableau à deux entrées en premier argument. La première entrée est l'objet sur lequel vous allez appeler la méthode, et la seconde est le nom de la méthode.
Vous êtes capables de le faire tout seul. Voici la correction :
Secret (cliquez pour afficher)
ErrorHandler : classe gérant les erreurs
Code : PHP 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 | <?php
class ErrorHandler implements SplSubject
{
// Ceci est le tableau qui va contenir tous les objets qui nous observent
protected $observers = array();
// Attribut qui va contenir notre erreur formatée
protected $formatedError;
public function attach (SplObserver $observer)
{
$this->observers[] = $observer;
return $this;
}
public function detach (SplObserver $observer)
{
if (is_int ($key = array_search ($observer, $this->observers, true)))
unset ($this->observers[$key]);
}
public function getFormatedError()
{
return $this->formatedError;
}
public function notify()
{
foreach ($this->observers as $observer)
$observer->update ($this);
}
public function error ($errno, $errstr, $errfile, $errline)
{
$this->formatedError = '[' . $errno . '] ' . $errstr . "\n" . 'Fichier : ' . $errfile . ' (ligne ' . $errline . ')';
$this->notify();
}
}
?>
|
MailSender : classe s'occupant d'envoyer les mails
Code : PHP 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | <?php
class MailSender implements SplObserver
{
protected $mail;
public function __construct ($mail)
{
if (preg_match('`^[a-z0-9._-]+@[a-z0-9._-]{2,}\.[a-z]{2,4}$`', $mail))
$this->mail = $mail;
}
public function update (SplSubject $obj)
{
mail ($this->mail, 'Erreur détectée !', 'Une erreur a été détectée sur le site. Voici les informations de celle-ci : ' . "\n" . $obj->getFormatedError());
}
}
?>
|
BDDWriter : classe s'occupant de l'enregistrement en BDD
Code : PHP 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | <?php
class BDDWriter implements SplObserver
{
protected $db;
public function __construct (PDO $db)
{
$this->db = $db;
}
public function update (SplSubject $obj)
{
$q = $this->db->prepare('INSERT INTO erreurs SET erreur = :erreur');
$q->bindValue(':erreur', $obj->getFormatedError());
$q->execute();
}
}
?>
|
Testons notre code !
Code : PHP | <?php
$o = new ErrorHandler; // Nous créons un nouveau gestionnaire d'erreur
$db = PDOFactory::getMysqlConnexion();
$o->attach(new MailSender('login@fai.tld'))
->attach(new BDDWriter($db));
set_error_handler (array ($o, 'error')); // Ce sera par la méthode error() de la classe ErrorHandler que les erreurs doivent être traitées
5 / 0; // Générons une erreur
?>
|
Pfiou, ça en fait du code ! Je ne sais pas si vous vous en rendez compte, mais ce qu'on vient de créer là est une
excellente manière de coder. Nous venons de séparer notre code comme il se doit et nous pourrons le modifier aisément car les différentes actions ont été séparées avec logique.