Accueil > Non classé > Classes proxy en PHP : l’AOP des pauvres

Classes proxy en PHP : l’AOP des pauvres

Introduction

Dans cet article, nous allons voir comment mettre en œuvre le design pattern Proxy en PHP pour faire par exemple de l’AOP. Nous ne couvrirons qu’une petite partie du vaste domaine qu’est la programmation par aspects, pour des raisons de simplicité et de longueur d’article.

Commençons par la classique citation Wikipedia :

La programmation orientée aspect (POA, en anglais aspect-oriented programming – AOP) est un paradigme de programmation qui permet de séparer les considérations techniques (aspect en anglais) des descriptions métier dans une application

L’utilisation de l’AOP permet ainsi de factoriser du code transverse tel que le logging des paramètres passés à une méthode, ou encore l’exécution d’une méthode au sein d’une transaction. On peut ainsi se concentrer sur le code métier dans la méthode.

Nous allons mettre en œuvre une implémentation basique d’AOP en PHP. Je sais, le domaine d’expertise d’Excilys est bien le Java ;).

Architecture

Le but de cet exemple est d’être le moins intrusif possible. Dans le meilleur des cas, il faudrait que l’on puisse exécuter le code avec ou sans AOP sans avoir à le modifier. Comme je suis d’humeur flemmarde aujourd’hui (oui, c’est dimanche), je vais quand même être un peu plus souple sur cette règle. Nous allons faire en sorte de pouvoir utiliser ou non l’AOP en modifiant une seule ligne de code (l’instanciation de la classe). Bien sûr, dans une vraie application l’injection de dépendances ferait que je n’aurais même pas eu à écrire ce paragraphe.

Définissons quelques termes qui seront utilisés par la suite :

  • Un pointcut est un endroit dans le code où nous allons utiliser l’AOP pour ajouter un comportement (par exemple l’entrée dans une méthode). Notez toutefois que l’AOP permet dans certains cas de remplacer le comportement d’une méthode.
  • Un advice est le comportement qui sera ajouté à un pointcut (par exemple afficher le nom de la méthode appelée).
  • Un aspect est la combinaison d’un ou plusieurs pointcut(s) et advice(s) (afficher le nom de la méthode appelée lorsque l’on entre dedans).

Pour rester simple, nos pointcuts pourront être placés au début ou à la fin d’une méthode (et pas autour, par exemple). Ils contiendront l’advice, qui sera en fait un callback (donc une fonction ou une méthode de classe). Chaque pointcut pourra être associé à une ou plusieurs méthodes, et sera défini par :

  • un nom de méthode exact,
  • un joker ‘*’ pour « n’importe quelle méthode »,
  • ou une expression régulière , par exemple « findAll.* »

Pour pouvoir prendre en compte ces pointcuts et appeler les advices au moment voulu, nous allons proxifier l’instance sous AOPéïne, et tout bonnement intercepter tous les appels à ses méthodes. Pour cela, rien de tel qu’une bonne vieille composition. Notre classe Proxy va contenir l’instance dans un attribut, et chaque appel de méthode sera détecté grâce à la méthode magique __call(). Elle est exécutée à chaque fois que l’on tente d’appeler une méthode non définie dans une classe (souvenez-vous, PHP est un langage dynamique :)).

Comme Proxy ne définit (presque) aucune méthode particulière, tous les appels passeront par __call(), ce qui nous permettra d’exécuter les advices puis la « vraie » méthode proxifiée.

Implémentation

Passons maintenant au code. Voici la classe Pointcut :


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
<?php

namespace Pikwyx;

/**
 * A Pointcut is a place in a method where behavior will be added.
 *
 * @author bastien
 */

class Pointcut {
    const BEFORE = 1;
    const AFTER = 2;

    /** The location of this Pointcut: at the begining or at the end of a method */
    private $when;
    /** The behavior to add to the method */
    private $callback;
    /** The method name or pattern to which the Pointcut is applied */
    private $functionPattern;
    /** A flag indicating if $functionPattern is indeed a pattern */
    private $isRegexp;

    function __construct($when, $functionPattern, $callback) {
        $this->when = $when;
        $this->functionPattern = $functionPattern;
        $this->callback = $callback;

        $this->isRegexp = !preg_match('/\w+/', $functionPattern);
    }

    /**
     * Determines if the current Pointcut should be used for the given function.
     *
     * @param string $functionName the function which is called
     * @return boolean true if this Pointcut matches the function name, false otherwise
     */

    public function matches($functionName) {
        if ($this->isRegexp) {
            if ($this->functionPattern === '*') {
                return true;
            } else {
                return preg_match($this->functionPattern, $functionName);
            }
        } else {
            return $functionName === $this->functionPattern;
        }
    }

    public function getWhen() {
        return $this->when;
    }

    public function getCallback() {
        return $this->callback;
    }
}
?>

Pour nous faciliter la vie, la méthode matches a été ajoutée pour déterminer si le Pointcut courant doit être utilisé pour la méthode en cours d’appel.

Voici à quoi ressemble la seconde classe, Proxy :


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
<?php

namespace Pikwyx;

require_once 'Pointcut.php';

/**
 * A class which wraps an instance of any class and allows to add behavior
 * before or after calling methods on this instance.
 *
 * @author bastien
 */

class Proxy {

    /** The instance on which we want to add behavior */
    private $proxifiedClass;
    /** An array of pointcuts to be placed before method calls */
    private $beforePointcuts = array();
    /** An array of pointcuts to be placed after method calls */
    private $afterPointcuts = array();

    function __construct($proxifiedClass) {
        $this->proxifiedClass = $proxifiedClass;
    }

    /**
     * Allows to add logic before a method is called on the instance.
     *
     * @param string $functionName the method on which to add behavior
     * @param callback $callback the behavior to add
     */

    public function doBefore($functionName, $callback) {
        $pointcut = new Pointcut(Pointcut::BEFORE, $functionName, $callback);
        $this->beforePointcuts[] = $pointcut;
    }

    /**
     * Allows to add logic after a method is called on the instance.
     *
     * @param string $functionName the method on which to add behavior
     * @param callback $callback the behavior to add
     */

    public function doAfter($functionName, $callback) {
        $pointcut = new Pointcut(Pointcut::AFTER, $functionName, $callback);
        $this->afterPointcuts[] = $pointcut;
    }

    /**
     * Magic function that intercepts any call to methods to allow advices to
     * be called.
     *
     * @param string $name the method name
     * @param mixed $arguments arguments to pass to the method
     */

    public function __call($name, $arguments) {
        $this->runAdvices($this->beforePointcuts, $name);

        if (is_array($arguments)) {
            call_user_func_array(array($this->proxifiedClass, $name), $arguments);
        } else {
            call_user_func(array($this->proxifiedClass, $name), $arguments);
        }

        $this->runAdvices($this->afterPointcuts, $name);
    }

    /**
     * Run the advices contained in the given list of pointcuts if they match
     * the given method name.
     *
     * @param array $pointcuts a list of Pointcut to match to the method
     * @param string $methodName the method which is called on the proxified class
     */

    private function runAdvices($pointcuts, $methodName) {
        foreach ($pointcuts as $pointcut) {
            /* @var $pointcut Pointcut */
            if ($pointcut->matches($methodName)) {
                call_user_func($pointcut->getCallback());
            }
        }
    }
}
?>

Les méthodes doBefore() et doAfter() permettent d’ajouter des pointcuts avant ou après une méthode de la classe proxifiée. Par la suite, lors de chaque appel, __call() sera exécutée, et utilisera ces pointcuts avant et après avoir appelé la méthode proxifiée.

Nous utilisons massivement call_user_func [1], qui est une fonction native de PHP permettant d’appeler une méthode par son nom contenu dans une chaine de caractères. Nous utilisons son premier argument sous forme de tableau, contenant également la référence vers l’objet sur lequel la méthode sera appelée.

Le « main » ressemblera donc à ceci :


1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

require_once 'Pikwyx/Proxy.php';
require_once 'Foo.php';
require_once 'FooAdvice.php';

use Pikwyx\Proxy;

$foo = new Proxy(new Foo);
$fooAdvice = new FooAdvice();
$foo->doBefore('doSomeStuff', array($fooAdvice, 'onBefore'));
$foo->doAfter('doSomeStuff', array($fooAdvice, 'onAfter'));

$foo->doSomeStuff('AOP');

Avec :


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Foo {
    public function doSomeStuff($param) {
        echo "Hey, I'm doing some stuff with $param<br/>";
    }
}

class FooAdvice {
    public function onBefore() {
        echo "I'm just before the method call!<br/>";
    }

    public function onAfter() {
        echo "I'm just after the method call!<br/>";
    }
}

On a ainsi ajouté deux pointcuts de « logging », avant et après doSomeStuff(). Le résultat produit par ce script est sans surprise :


1
2
3
I'm just before the method call!
Hey, I'm doing some stuff with AOP
I'm just after the method call!

Pour ne plus utiliser l’AOP, il suffit de remplacer $foo = new Proxy(new Foo); par $foo = new Foo;.

Et après ?

Pour aller plus loin et écrire encore moins de code, il serait possible d’apporter les améliorations suivantes :

  • Ajouter un pointcut entourant l’appel à une méthode, permettant par exemple de faire un try/catch.
  • Configuration des pointcuts dans un fichier XML (validé par une XSD), et récupération de l’instance du Proxy via une factory qui va se charger de lire la configuration et d’instancier automatiquement les Pointcuts.
  • Utiliser un framework d’injection de dépendances pour ne pas avoir à changer une ligne de code pour activer ou non l’AOP ;).
  • Faire en sorte que la classe proxifiée puisse être reconnue comme étant une instance de Foo et non de Proxy (dans le cas où l’objet est passé à une méthode ayant des paramètres typés). C’est LE problème majeur de cette implémentation.
  • Intégrer les advices directement dans le code dans une version en cache de la classe Foo (ce qui pourrait corriger le point précédent), et mettre en place un autoloader qui permettra de créer/mettre à jour la version cachée si besoin, ou sinon de la renvoyer directement. On s’approcherait ainsi du bytecode weaving d’AspectJ.

Conclusion

Avec deux classes nous avons réussi à faire une ébauche d’AOP en PHP, ne nécessitant que peu de modifications dans du code existant. Même si l’approche est assez maladroite sur certains points (notamment le fait que l’instance de Proxy ne soit pas reconnue comme étant un objet de type Foo), avec quelques développements supplémentaires il serait possible d’obtenir des résultats rapidement intéressants. Reste à savoir si l’AOP est réellement utile dans une application PHP.

[1] : non, cette fonction ne permet pas de téléphoner à l’utilisateur nommé “func”…

Code source

Share
Categories: Non classé Tags: , ,
  1. 12/06/2010 à 20:10 | #1

    Hi Bastien,

    I found about your article using Google search for PHP and AOP and read it using Google Translate. Few days ago I have pushed my own implementation of some AOP principles for the PHP. It is released under GNU LGPLv3 and available in PointcutPHP project repository at Gitorious http://gitorious.org/pointcutphp

  2. snoozer
    05/11/2012 à 18:25 | #2

    autrement pour faire du vrai AOP tu peut aussi utilisé l’extension php AOP que tu trouvera sur PECL !

  3. Thibaud
    20/12/2013 à 20:27 | #3

    Merci pour l’article et l’implémentation, seul caveat, elle n’est pas compatible avec des libs externes qui utilisent le type-hinting, et impose de ne pas en utiliser soi-même… Dommage :(

  4. Thibaud
    20/12/2013 à 20:28 | #4

    @Thibaud

    Ooops, j’avais lu trop vite… désolé.

  5. 18/08/2017 à 06:25 | #5

    I am genuinely happy to read this website posts which includes lots of
    useful facts, thanks for providing these data.

  1. 18/11/2015 à 13:21 | #1