PHP orienté objet : les nouvelles possibilités du constructeur en PHP 8

Présentation et utilisation des concepts suivants : promotion de propriétés, DTO et readonly

→ Corrigé du Challenge : Vegeta combat ses ennemis

Pour passer en revue les possibilités des constructeurs en PHP orienté objet, on va s’appuyer sur le challenge DBZ_1. Un challenge débutant, déjà corrigé pour passer en revue le while en PHP, qui va nous permettre de créer des objets intéressants.

Au programme :

Objets et propriétés avant PHP 8

Dans ce challenge, je dois faire évoluer Végéta selon 2 critères : sa force et son niveau. Je peux donc créer 2 propriétés. Seule la force est initialisé via le constructeur puisque le niveau démarre toujours à 1.

Avant PHP 8, je peux coder la classe de cette façon :

// Avant PHP 8
class Vegeta
{
    private int $force;
    private int $level = 1;

    public function __construct(int $force)
    {
        $this->force = $force;
    }
}

La promotion de propriétés en PHP 8

PHP 8 a introduit une nouvelle notion, la promotion de propriété. Cela permet de gérer à la fois la déclaration de la propriété et ce qui se passe dans le constructeur : le paramètre, et l’attribution de la valeur de la propriété.

Voici comment l’utiliser :

// A partir de PHP 8
class Vegeta
{
    private int $level = 1;

    public function __construct(
        private int $force,
    ) {}
}

La déclaration de $level n’a (pour le moment…) pas changé.

Mais la déclaration de $force est passée dans le constructeur, avec cette syntaxe où est déclaré dans les paramètres du constructeur :

  • La visibilité
  • Le type
  • Le nom

C’est le fait de déclarer la visibilité qui va enclencher la promotion de la propriété. La définition du type est facultative mais elle est fortement conseillé pour respecter le typage en PHP et ses avantages.

Toutes les spécificités de la promotion de propriétés en PHP 8

Valeur par défaut

Il est tout à fait possible de définir une valeur par défaut. On pourrait alors intégrer $level dans le constructeur :

// A partir de PHP 8
class Vegeta
{
    public function __construct(
        private int $force,
        private int $level = 1
    ) {}
}

Utilisation des paramètres nommés

Comme pour une fonction ou une méthode, les paramètres nommés peuvent être utilisés lors de la création de l’objet. Pour l’exemple, on rajoute une troisième propriété $name :

class Vegeta
{
    public function __construct(
        private int $force,
        private int $level = 1,
        private string $name = 'Vegeta'
    ) {

    }
}

$vegetaJunior = new Vegeta(
    name: 'Vegeta Jr.',
    force: 1,
);

Les fonctionnalités des paramètres nommés s’appliquent de la même façon.

Corps du constructeur

Les propriétés peuvent être utilisées tout de suite dans le corps du constructeur. Dans ce challenge, il est question d’une puissance, qui est le produit du niveau et de la force. On peut calculer cette puissance à chaque fois qu’on en a besoin ou la calculer une fois et la stocker dans une propriété. On aurait alors quelque comme ça :

class Vegeta
{
    public int $power;

    public function __construct(
        private int $force,
        private int $level = 1,
    ) {
        $this->power = $this->force * $level;
    }
}

Il est possible d’utiliser la valeur de la propriété avec ou sans $this. Le code ci-dessus est pour illustrer cela mais je te conseille de ne garder qu’une seule façon de le faire. Personnellement, je préfère celle avec $this pour visualiser très clairement qu’on travaille avec une propriété.

Usage de PHPDoc

La PHPDoc peut être intégrée dans la déclaration des paramètres. Pour l’exemple, un nouvel objet Fight, qui va prendre en paramètre 1 Vegeta et 1 tableau d’Ennemis :

class Fight
{
    public function __construct(
        private Vegeta $vegeta,
        /**
         * @property Ennemy[] $ennemis
         */
        private array $ennemis
    ) {}
}

Si besoin, tu peux te référer à cet article qui traite de PHPStan et de la déclaration des tableaux via la PHPDoc.

DTO et readonly

DTO ? Cet acronyme signifie Data Transfert Object. C’est un objet simple qui sert à encapsuler des données et les transmettre de manière structurée entre les différentes parties d’une application. Au lieu d’utiliser de simples variables ou de structurer les données avec un array, on va utiliser un objet. Cet objet ne contiendra aucune logique métier. Les données qu’il contient ne peuvent pas être modifiées.

Pour continuer le corrigé du challenge, on va créer un objet Ennemy qui a une seule propriété power (puissance).

Avant PHP 8, on pouvait créer ce DTO de cette façon :

class EnnemyOld
{
    private int $power;

    public function __construct(int $power)
    {
        $this->power = $power;
    }

    public function getPower(): int
    {
        return $this->power;
    }
}

PHP 8.1 a introduit la notion de readonly, cela permet de déclarer des propriétés qui ne peuvent être que lues, et surtout initialisées une seule fois. L’écriture est complètement simplifiée :

class Ennemy
{
    public function __construct(
        public readonly int $power
    ) {}
}

La propriété a été passée en public. On pourra donc l’appeler en dehors de l’objet, sans getteur. Par contre :

$e = new Ennemy(4);
echo $e->power; // Tout est OK

$e->power = 5; // Déclenche une erreur fatale

Normalement ton IDE te mettra en garde dès l’écriture de cette ligne 😉

PHP 8.2 a introduit la notion de readonly, mais au niveau de la classe ! De cette façon, toutes les propriétés sont automatiquement en readonly. Voici ce que ça donne :

readonly class Ennemy
{
    public function __construct(
        public int $power
    ) {}
}

Le code complet des 2 classes

Avant de continuer, si tu n’as pas résolu le challenge, tu peux tenter de le résoudre en utilisant les concepts vus jusqu’ici : challenge de programmation DBZ_1.

Pour résoudre ce challenge, je vais volontairement créer le plus de méthodes possibles pour découper le code en plusieurs « petites » opérations.

La puissance de végéta sera calculée puis stockée. Puis recalculé seulement si nécessaire.

Dans certaines méthodes de Vegeta, je m’appuie sur le typage pour préciser qu’une instance d’Ennemy est attendue comme paramètre.

Ennemy.php


declare(strict_types=1);

namespace Challenges\DBZ_1;

readonly class Ennemy
{
    public function __construct(
        public int $power
    ) {}
}

Vegeta.php

declare(strict_types=1);

namespace Challenges\DBZ_1;
class Vegeta
{
    public int $power;

    public function __construct(
        public int $force,
        public int $level = 1
    ) {
        $this->calculPower();
    }

    public function calculPower(): void
    {
        $this->power = $this->force * $this->level;
    }

    public function fight(Ennemy $ennemy): void
    {
        $this->controlPowerAndLevel($ennemy);
        $this->upForce($ennemy);
    }

    public function controlPowerAndLevel(Ennemy $ennemy): void
    {
        while ($this->power < $ennemy->power) {
            $this->upLevel();
        }
    }

    public function upLevel(): void
    {
        $this->level++;
        $this->calculPower();
    }

    public function upForce(Ennemy $ennemy): void
    {
        $this->force += (int) floor($ennemy->power / 10);
        $this->calculPower();
    }
}

Programme principal

// Données issues du challenge
$ennemis = [327, 792, 814, 993];
$force_vegeta = 160;

// Un petit array_map pour transformer le tableau de puissance en tableau d'Ennemy
$ennemis = array_map(function($power) {
    return new Ennemy($power);
}, $ennemis);

// Instance de Vegeta
$vegeta = new Vegeta($force_vegeta);

// Vegeta affronte chaque ennemi
foreach ($ennemis as $ennemy) {
    $vegeta->fight($ennemy);
}

Si tu n’es pas familier de array_map, tu peux consulter notre guide complet de array_map 😉

N’hésite pas à chercher un challenge de programmation pour mettre tout ça en oeuvre.


Qui a codé ce superbe contenu ?

Keep learning

Autres contenus à découvrir


Ta newsletter chaque mois

Corrigés, challenges, actualités, veille technique... aucun spam.