PHP est un langage qui ne supporte pas l'héritage multiple depuis le début de sa pseudo-implémentation objet. L'héritage multiple apporte bien des soucis aux architectes logiciels car le point fort et le point faible de l'héritage multiple est le fait qu'il soit multiple.

Dans cet article, je vais vous montrer qu'il est possible de gérer plus ou moins dynamiquement l'héritage multiple grâce au concept d'agrégation, aux interfaces ainsi que les méthodes magiques de PHP.

Pour illustrer le plus simplement possible, je vous ai concocté un petit exemple concret qui permet de mettre en œuvre cet héritage multiple. Pour cela, il faut que vos classes implémentent toute une interface qui leur permettra d'avoir les même méthodes.

Le concept

La notion d'héritage est une spécialisation descendante des classes (analogiquement une généralisation montante) et permet de développer en respectant la méthode DRY (Don't Repeat Yourself) mais aussi de mettre en application les principaux concepts de la POO comme la surcharge, le polymorphisme ou les notions de constructeurs et de destructeurs.

Encore une fois, le langage PHP ne permet pas de mettre réellement en application certaines de ces notions car par exemple, la surcharge de méthodes au sein d'une même classe n'est pas possible. Mais cela le devient grâce à aux méthodes magiques __call() et __callStatic().

L'implémentation d'interfaces permet de mettre en commun des méthodes publiques de certaines classes. Cette pratique très utile oblige les classes qui implémentent les même interfaces à être homogène d'une certaine manière afin d'apporter une sécurité d'interaction avec l'environnement d'utilisation des classes.

Donc pour faire un mini-bilan, la notion d'héritage/généralisation se profile plutôt verticalement dans l'espace alors que l'implémentation d'interfaces se profile horizontalement.

Mise en œuvre

Pour mettre en place un pseudo-héritage multiple en PHP, nous allons créer un simple héritage d'une super-classe qui aura dans son bagage de méthodes des accesseurs permettant de gérer une pile de « classes dont la classe fille héritera ». Nous utiliserons donc les plus communes méthodes magiques.
Nous prendrons bien évidemment une version PHP 5.3 afin de bénéficier des dernières fonctionnalités.

Les classes héritées seront agrégées dans la classe mère et doivent impérativement implémenter une interface commune afin de certifier les même signatures de méthodes. Une fois les classes agrégées, la méthode call s'occupera d'appeler toutes les méthodes ayant le même nom de la méthode d'appel... Ainsi de suite, l'ordre d'appel des méthodes sera le même que l'ordre d'héritage.

Classe Abstract_Foo

<?php  
abstract class Abstract_Foo  
{
    protected $_inheritances = array();

    public function __isset($class_name)
    {
        return array_key_exists($class_name, $this->_inheritances);   
    }

    public function __get($class_name)
    {
        try {
            if (!isset($this->$class_name)) {
                throw new Exception($class_name
                    . ' does not exists!');
            }

            return $this->_inheritances[$class_name];

        } catch (Exception $e) {
            trigger_error($e->getMessage(), E_USER_WARNING);
            return false;
        } 
    }

    public function __set($class_name, $instance)
    {
        try {
            if (isset($this->$class_name)) {
                throw new Exception(get_class($this)
                    . ' already extends '
                    . $class_name);
            }

            if (!$instance instanceof $class_name) {
                throw new Exception('Unexpected instance of '
                    . get_class($instance)
                    . ' instead of '
                    . $class_name);
            }

            $this->_inheritances[$class_name] = $instance;

        } catch (Exception $e) {
            trigger_error($e->getMessage(), E_USER_WARNING);
        }
    }

    public function __unset($class_name)
    {
        unset($this->_inheritances[$class_name]);
    }

    public function call($method_name, $args = null)
    {
        try {
            foreach ($this->_inheritances as $obj) {
                if (!is_callable(array($obj, $method_name))) {
                    throw new Exception('Undefined method '
                        . get_class($obj)
                        . '::'
                        . $method_name
                        . '()');
                }
                $obj->$method_name($args);
            }
        } catch (Exception $e) {
            trigger_error($e->getMessage(), E_USER_WARNING);
        }
    }

    public function getApple()
    {
        var_dump(__METHOD__);
        $this->call(__FUNCTION__);
    }

    public function eatApple()
    {
        var_dump(__METHOD__);
        $this->call(__FUNCTION__);
    }
}
?>

Classe Foo

<?php  
class Foo  
    extends Abstract_Foo
    implements Interface_Frob
{
}
?>

Interface Interface_Frob

<?php  
interface Interface_Frob  
{
    public function getApple();
    public function eatApple();
}
?>

Classe Bar

<?php  
class Bar  
    implements Interface_Frob
{
    public function getApple()
    {
        var_dump(__METHOD__);
    }

    public function eatApple()
    {
        var_dump(__METHOD__);
    }
}
?>

Classe Baz

<?php  
class Baz  
    implements Interface_Frob
{
    public function getApple()
    {
        var_dump(__METHOD__);
    }

    public function eatApple()
    {
        var_dump(__METHOD__);
    }
}
?>

Page de test

<?php  
function __autoload($class_name) {  
    if (is_file($class_name . '.php')) {
        require_once $class_name . '.php';
    }
}

$foo = new Foo();
$foo->Bar = new Bar();
$foo->Baz = new Baz();
$foo->getApple();
$foo->eatApple();
?>

Résultat attendu

string 'Abstract_Foo::getApple' (length=22)  
string 'Bar::getApple' (length=13)  
string 'Baz::getApple' (length=13)  
string 'Abstract_Foo::eatApple' (length=22)  
string 'Bar::eatApple' (length=13)  
string 'Baz::eatApple' (length=13)  

De cette manière-ci, vous pouvez mettre en place un pseudo-héritage multiple pour vos applications sans trop se prendre la tête en attendant que Traits soit implémenté. Évidemment, Traits est bien plus poussé car ce sera inclut dans la structure du langage alors qu'ici, il ne s'agit que d'une bidouille orientée objet.

Vivement PHP 5.4 !


Joris Berthelot