Aller au contenu
Accueil » [PHP] Les interfaces, c’est quoi et ça sert à quoi ?

[PHP] Les interfaces, c’est quoi et ça sert à quoi ?

Vous avez peut-être vu dans certains codes sources, ou entendu dans la bouche d’un de vos collègues le mot « Interface ». Sans savoir ce que c’est, vous vous dîtes que ça doit être un concept complexe, et qui n’est pas forcément utile en PHP ?

Détrompez-vous ! Lorsque l’on parle de POO (Programmation Orientée Objet, ou OPP en anglais), les interfaces sont des outils indispensables et très simples à prendre en main.

Voyons plutôt :

Qu’est-ce qu’une interface et pourquoi les utiliser ?

Avant de rentrer dans le vif du sujet – c’est à dire le code source – définissons ce qu’est une interface.

Une interface : kesako ?

Une interface est une classe, qui établit un contrat à suivre par d’autres classes. Toute seule, elle ne sert à rien. Mais d’autres classes vont pouvoir l’implémenter. On définit ainsi un cadre pour toutes les classes qui « adhéreront » de ce contrat. De fait, on saura que peu importe ce qu’elles contiennent, elles auront une structure et des méthodes communes : celles de l’interface.

On va définir dans une interface la liste des méthodes qu’une classe qui l’implémente devra override, afin que le-dit contrat soit respecté.

Quelques règles s’imposent de fait pour les interfaces :

  • Toutes les méthodes qui sont définies DOIVENT être implémentées dans la classe utilisant l’interface
  • Toutes les méthodes d’une interface doivent être publiques
  • Une classe peut implémenter plusieurs interfaces

À quoi ça va bien pouvoir me servir ?

Voici quelques-unes des principales raisons qui, de mon point de vue, rendent les interfaces très intéressantes à utiliser :

  • En utilisant une interface et lors de l’appel à une de ses méthodes, la classe appelante se souciera uniquement de l’interface de l’objet, et non des implémentations de ses méthodes. Vous pouvez donc modifier les implémentations de toutes les classes utilisant l’interface sans en affecter son fonctionnement.
  • Vous hiérarchiserez votre code, en ayant la possibilité d’avoir plusieurs classes, n’ayant au premier abord pas grand chose à voir, regroupées au sein d’une même implémentation. Vous gagnerez donc en stabilité : Si vous voulez modifier l’interface, vous être certains que toutes les implémentation suivront.
  • Une interface vous permet d’accéder à l’héritage multiple, étant donné qu’une classe peut implémenter plusieurs interfaces.

Comment les interfaces fonctionnent-elles ?

Une classe de type interface sera sans surprise déclarées en utilisant le mot clé interface. Quant à la classe qui l’implémente, elle devra utiliser le mot clé implement, puis spécifier l’interface à utiliser :

<?php

interface InterfaceClass
{
    public function doSomething(): string;
}

class ChildClass implements InterfaceClass
{
    public function doSomething(): string
    {
        return 'Hello guys!';
    }
}

$childClas = new ChildClass();
echo $childClas->doSomething();

Jusque là rien de sorcier, nous avons déclaré une méthode dans une interface, qui indique que le type de retour est du texte. La classe enfant l’implémente, et retourne le bon type. En jouant le code ci-dessus, on obtient donc :

Hello guys!

Si vous décidez de ne pas respecter les règles du contrat de l’interface, vous aurez une erreur. Par exemple, si vous n’implémentez pas toutes les méthodes de l’interface :

<?php

interface InterfaceClass
{
    public function doSomething(): string;
}

class ChildClass implements InterfaceClass
{
    public function doSomethingElse(): string
    {
        return 'Hello folks';
    }
}

$childClas = new ChildClass();
echo $childClas->doSomethingElse();

… vous aurez alors l’exception suivante :

Fatal error: Class ChildClass contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (InterfaceClass::doSomething)

Ou encore, si vous implémentez la bonne méthode, mais qu’elle ne correspond pas exactement à la signature déclarée en amont, par exemple comme ceci :

<?php

interface InterfaceClass
{
    public function doSomething(): string;
}

class ChildClass implements InterfaceClass
{
    public function doSomething(): int
    {
        return 1;
    }
}

$childClas = new ChildClass();
echo $childClas->doSomething();

… vous aurez alors cette exception :

Fatal error: Declaration of ChildClass::doSomething(): int must be compatible with InterfaceClass::doSomething(): string

Toutes ces règles permettent d’assurer l’intégrité de vos classes entre-elles, pour qu’ainsi, toutes les classes qui implémentent vos interfaces soient cohérentes et maintenables !

Cas d’utilisation ?

C’est bien beau tout ça, mais jusqu’ici nous avons vu la définition des interfaces, et comment les utiliser via un exemple, mais ce n’est pas très parlant. Est-ce que ce concept est vraiment utile au sein d’un projet ?

Tout à fait ! (sans surprise 😁) Imaginons que vous souhaitiez créer un logger, pour gérer vos erreurs. On pourrait imaginer que le système commun collecterait les données, les enregistreraient. Seulement, chaque système a sa façon spécifique pour récupérer ses messages d’erreurs. Vous voyez où je veux en venir ? Allez, je vous donne l’exemple commenté :

On commence par créer une interface Logger, avec 2 méthodes assez explicites :

<?php

// Those 2 methods must be implemented in each class that implements this interface
interface Logger
{
    public function getLogs(): array;

    public function saveLogs(array $logs): void;
}

Ensuite, on va créer une première classe implémentant cette interface : WebLogger.

<?php

class WebLogger implements Logger
{
    public function getLogs(): array
    {
        // get logs file. Let's say it is stored as raw to this location
        $logLocation = '/var/www/log/apache2/error.log';
        $logs = file_get_contents($logLocation);

        // return wanted format
        return json_decode($logs);
    }

    public function saveLogs(array $logs): void
    {
        // let's loop into our logs and store them where we want, for instance a file
        $logLocation = '/etc/log/apache.log';
        file_put_contents(
            filename: $logLocation,
            data: json_encode($logs),
        );
    }
}

Puis, créons une deuxième classe implémentant cette même interface : SqlLogger.

<?php

class SqlLogger implements Logger
{
    private PDO $pdo;

    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    public function getLogs(): array
    {
        // get logs file. Let's say it is stored as raw to this location
        $logLocation = '/var/www/log/mysql/error.log';
        $logs = file_get_contents($logLocation);

        // return wanted format
        return json_decode($logs);
    }

    public function saveLogs(array $logs): void
    {
        // let's loop into our logs and store them where we want, for instance a database table
        foreach ($logs as $log) {
            $sql = 'INSERT INTO web_logs(date, message) VALUES (:date, :message)';
            $query = $this->pdo->prepare($sql);
            $query->execute([
                'date' => new DateTimeImmutable(),
                'message' => $log
            ]);
        }
    }
}

Ces deux classes fonctionnent d’une manière tout à fait différentes, mais sont pourtant similaires dans leur construction. Comme elles implémentent les même méthodes, on va pouvoir les utiliser exactement de la même façon :

<?php

use Logger;
use WebLogger;
use SqlLogger;

// initialize the PDO, that will be used to initialize the SqlLogger object
$pdo = new PDO(
    dsn: 'my-host',
    username: 'username',
    password: 'password',
);

$webLogger = new WebLogger();
$sqlLogger = new SqlLogger($pdo);

foreach ([$webLogger, $sqlLogger] as $logger) {
    // $logger will either be WebLogger or SqlLogger instance, so will always be Logger instance
    assert($logger instanceof Logger);

    // wo whatever the object, they implement the same class, then wa can call the same methods
    $logger->saveLogs(
        logs: $logger->getLogs()
    );
}

Et voilà, les deux instances de Logger seront bien reconnues en tant que telles, et fonctionneront exactement de la même manière.

Conclusion

Je pense que vous avez compris l’idée, on peut implémenter autant d’enfant de Logger que l’on veut, et appeler les méthodes getLogs() et saveLogs() indépendamment de leur implémentation propre. On s’abstrait donc totalement du fonctionnement, tant que le contrat décris par l’interface est respecté.

Il est également possible d’étendre d’une classe abstraite en utilisant des interfaces, pour avoir accès à encore plus de fonctionnalités sur l’héritage des classes. La seule limite sera votre imagination !

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *