Le modèle objet de PHP (première partie)

Youssef Chahir
GREYC — Université de Caen
En partie adapté du cours d'Alexandre Niveau et de Jean-Marc Lecarpentier
TODO:
  • changer les exemples, qui sont encore compliqués pour être vraiment utilisables en CM
  • Carre extends Rectangle : mauvais exemple; trouver un mieux, ou parler explicitement des problèmes (dépend de l'API — si les instances sont immutables ça ne pose pas de problème)

Programmation impérative

  • Orientée autour de la définition de fonctions ;
  • Les données sont des variables stockées dans le main ou globales ;
  • Inconvénients :
    • les données sont stockées de manière approximative
    • l’utilisation du code peut être complexe ;
    • la modification d’une fonction peut engendrer de lourdes conséquences.

Programmation Orientée Objet

  • la protection des données en les encapsulant au sein de structures de plus haut niveau (les objets) ;
  • la ré-utilisation du code ;
  • la facilité d’utilisation des objets

Objet - instance et classe

Un objet est un ensemble de variables (appelées attributs) et de fonctions (appelées méthodes).
  • Un objet est un concept (qui peut être vu comme un moule).
  • Une instance est une implémentation de ce concept (un produit du moule).
    Exemple : Aristote est un Être Humain ⇒ Être Humain est objet et Aristote est une instance.
  • Une classe est le code représentant un objet.
Le paradigme objet est defini par 3 points :
  • Encapsulation : C'est une méthode permettant de rassembler les données et les fonctions dans une même structure (l’objet). Pour cela, on va ”interdire” l’accès direct aux données et cacher l’implémentation des fonctions associées à l’objet.
    • Propriétés : caractéristiques de l’objet (appelées « attributs» en Java). Ce sont des variables d'instance
    • Méthodes : actions qui s’appliquent à l’objet
  • Héritage : l’objet descend d’un autre, plus général, dont il va hériter des attributs et méthodes
  • Polymorphisme : Capacité d’un ensemble d’objets à exécuter des méthodes de même nom, mais dont le comportement est propre à chacune des versions
Parmi les méthodes, on retrouve :
  • Constructeur : appelé lors de la création d’une instance et initialise l’instance. — il DOIT être nommé __construct(...)
  • Destructeur : appelé lors de la destruction de l’objet pour libérer la mémoire. ——- il DOIT être nommé __destruct().
  • Accesseur : permet d’accéder en lecture ou en écriture à un attribut.

Une classe en PHP

  • Définition et utilisation d'une classe :
    
    <?php

    class Personne {
        
    // Attributs de la classe
        
    private string $prenom;
        private 
    string $nom;
        private 
    int $age;

        
    // Constructeur
        
    public function __construct(string $prenomstring $nomint $age) {
            
    // Initialisation ou on passe par les setters
            
    $this->setPrenom($prenom);
            
    $this->setNom($nom);
            
    $this->setAge($age);
        }

        
    // Destructeur
        
    public function __destruct() {
            
    // Du code à exécuter
            
    print "Destruction de " __CLASS__ {$this->nom}\n";
        }

        
    // Méthode toString
        
    public function __toString(): string {
            return 
    "[$this->prenom$this->nom$this->age]";
        }

        
    // Getters
        
    public function getPrenom(): string {
            return 
    $this->prenom;
        }

        public function 
    getNom(): string {
            return 
    $this->nom;
        }

        public function 
    getAge(): int {
            return 
    $this->age;
        }

        
    // Setters
        
    public function setPrenom(string $prenom): void {
            
    $this->prenom $prenom;
        }

        public function 
    setNom(string $nom): void {
            
    $this->nom $nom;
        }

        public function 
    setAge(int $age): void {
            
    $this->age $age;
        }
    }

    // Test
    // Création d'objets Personne
    $p1 = new Personne("Paul""Hervé"48);
    $p2 = new Personne("Jules""Savary"20);

    // Identité de cette personne
    print "personne1 = $p1\n";

    // On change l'âge
    $p1->setAge(24);

    // Identité de la personne
    print "personne1 = $p1\n";
    print 
    "personne2 = $p2\n";

    ?>

Pièges des objets

Il faut obligatoirement utiliser $this dans une classe pour faire référence aux propriétés et méthodes
<?php
class Toto {
    public 
int $x 3;
    function 
afficherX() : void {
        echo 
$x;  // (au lieu de $this->x)
    
}
}
$toto = new Toto();
$toto->afficherX();  // lève une Notice: undefined variable $x
?>
Lors de la déclaration de la visibilité des propriétés, on leur met des $, mais pas ailleurs. Cependant, mettre un $ n'est pas une erreur de syntaxe, ça fait juste autre chose que ce qu'on voudrait…
<?php

class Toto {
 public 
int $x 3;
}

$toto = new Toto();

// Affiche 3
echo $toto->x."\n";

// Lève une Notice: undefined variable $x
// Affiche 3
echo $toto->{'x'}."\n";

$variable "x";

// Affiche 3 : $toto->$variable est interprété comme $toto->x !
echo $toto->{$variable}."\n";

?>
Un foreach sur un objet parcourt ses propriétés :
 <?php
 
class Toto {
  public 
int $x 3;
  public 
string $y 'coucou';
 }
 
$toto = new Toto();
 foreach (
$toto as $k => $v) {
      echo 
"cle $k, valeur $v\n";
 }
 
// résultat :
 //     cle x, valeur 3
 //     cle y, valeur coucou
 
?>

Constructeur, destructeur, conversion en chaîne

  • Fonction __construct()
  • Fonction __destruct() : appelée lorsque l'instance est détruite (utilisation de unset, ou fin de l'exécution du script)
  • Fonction __toString() : appelée lorsque l'objet doit être converti en chaîne de caractères
  • Attention au double underscore du constructeur, crée des bugs difficiles à trouver !

<?php

class Point {
    public 
int $x;
    public 
int $y;

    public function 
__construct(int $xint $y) {
        
$this->$x;
        
$this->$y;
    }

    public function 
__toString(): string {
        return 
"({$this->x},{$this->y})";
    }
}

$p = new Point(32);
echo 
$p."\n";

?>

(3,2)


Méthodes statiques en PHP :

  • Dans certains cas, il est très pratique d’accéder aux méthodes et aux propriétés en termes de classe plutôt qu’en objet.
  • Cela peut être fait à l’aide du mot clé « static ».
  • Toute méthode déclarée comme static est accessible sans la création d’un objet.
  • Les fonctions statiques sont associées à la classe, pas une instance de classe.
  • Ils sont autorisés à accéder uniquement aux méthodes statiques et aux variables statiques.
  • Pour ajouter une méthode statique à la classe, le mot clé « static » est utilisé.
 <?php

 
class Compteur {
     public static 
int $count;

     public static function 
getCount(): int {
         return 
self::$count++;
     }
 }

 
Compteur::$count 1;

 for (
$i 0$i 10; ++$i) {
     echo 
'La valeur suivante est : ' Compteur::getCount() . "<br>";
 }

 
?>
 La valeur suivante est : 1
La valeur suivante est : 2
La valeur suivante est : 3
La valeur suivante est : 4
La valeur suivante est : 5
La valeur suivante est : 6
La valeur suivante est : 7
La valeur suivante est : 8
La valeur suivante est : 9
La valeur suivante est : 10

Membres statiques

  • Utilisation du mot clé static
  • Une propriété statique a la même valeur pour toutes les instances de la classe
  • Une méthode statique peut être appelée sans avoir instancié la classe
  • Différences avec Java :
    • pas la même syntaxe pour l'accès aux propriétés et méthodes statiques : double deux-points à la place de la flèche
    • on n'appelle pas les propriétés et méthodes statiques avec $this, mais avec le mot-clef self, qui représente « la classe de l'instance courante »

<?php

class Toto {
    private static 
int $compteur 0;
    private 
string $contenu;

    public function 
__construct(string $contenu) {
        
$this->contenu $contenu;
        
self::$compteur++;
    }

    public function 
printInstanceStats(): string {
        
// Utilisation de $this->dummy au lieu de $this->compteur
        
return $this->contenu " et " self::$compteur "\n";
    }

    public static function 
printClassStats(): string {
        
// Commenté pour éviter une fatal error
        // $this->dummy = "toto";
        
return "Il y a actuellement " self::$compteur " instances de la classe Toto\n";
    }

    public function 
__destruct() {
        echo 
"Destruction d'une instance\n";
        
self::$compteur--;
    }
}

echo 
Toto::printClassStats();

$A = new Toto("instance A");
echo 
"\$this->compteur n'est pas défini => donne une chaine vide et une Notice\n";
echo 
"A : " $A->printInstanceStats();
echo 
Toto::printClassStats();

$B = new Toto("instance B");
echo 
"\$this->compteur n'est pas défini => donne une chaine vide et une Notice\n";
echo 
"B : " $B->printInstanceStats();
echo 
Toto::printClassStats();

echo 
"unset(\$A)\n";
unset(
$A);
echo 
Toto::printClassStats();

unset(
$B);
echo 
Toto::printClassStats();
?>

Il y a actuellement 0 instances de la classe Toto
$this->compteur n'est pas défini => donne une chaine vide et une Notice
A : instance A et 1
Il y a actuellement 1 instances de la classe Toto
$this->compteur n'est pas défini => donne une chaine vide et une Notice
B : instance B et 2
Il y a actuellement 2 instances de la classe Toto
unset($A)
Destruction d'une instance
Il y a actuellement 1 instances de la classe Toto
Destruction d'une instance
Il y a actuellement 0 instances de la classe Toto


Constante d'un objet

  • Définition avec const
  • Comme pour les propriétés statiques, accès avec ::

<?php

class MyClass {
    const 
ma_constante 'toto';
    
    public function 
showConstant(): void {
        echo 
"Voici ma constante: " self::ma_constante "\n";
    }
    
    public function 
chgeConstant(): void {
        
// En PHP 8, une constante ne peut pas être modifiée après sa définition.
        // Cela générera une erreur fatale.
        // self::ma_constante = "test d'erreur";
    
}
}

$obj = new MyClass();

$obj->showConstant();

echo 
"MyClass::ma_constante : " MyClass::ma_constante "\n";  // Fonctionne
// Les constantes de classe ne peuvent pas être accédées via une instance
//echo "\$obj->ma_constante : " . $obj->ma_constante . "\n";  // Ne fonctionne pas

// Vous pouvez accéder à une constante de classe via une instance en utilisant ::
echo "\$obj::ma_constante : " $obj::ma_constante "\n";
?>

Dérivation

L'héritage en PHP permet de créer une nouvelle classe qui héritera des propriétés et des méthodes d'une classe parent et qui pourra, si on le souhaite, redéfinir certaines propriétés et méthodes.
  • Définition d'une classe dérivée avec extends
    Pour faire hériter une classe il suffit d'utiliser le mot clef extends après le nom de la classe :
    
    <?php

    class Rectangle {
        protected 
    float $longueur;
        protected 
    float $largeur;

        public function 
    __construct(float $longueurfloat $largeur) {
            
    $this->longueur $longueur;
            
    $this->largeur $largeur;
        }
    }

    class 
    Carre extends Rectangle {
        public function 
    __construct(float $cote) {
            
    parent::__construct($cote$cote);
        }
    }

    ?>
  • Contrairement à Java, constructeurs et destructeurs sont hérités : si non définis dans une sous-classe, ce sont ceux du parent qui sont utilisés
  • En revanche, s'ils sont redéfinis, ils n'appellent pas automatiquement ceux du parent : il faut le faire explicitement avec parent::__construct()

Late static binding

  • Il existe une alternative à self : static
  • Permet de résoudre « tardivement » une référence à un champ statique
  • static::toto n'est pas forcément le toto de la classe courante, mais celui de la classe qui a effectivement été instanciée à l'exécution
  • voir la doc
  • permet de faire de l'héritage sur les méthodes/propriétés statiques… Utile par exemple pour faire des factory methods statiques

Visibilité des membres

  • 3 niveaux :
    • public : accessible de partout
    • protected : accès limité à la classe et ses classes dérivées
    • private : accès seulement dans la classe

<?php

class MyClass
{
    public 
string $public 'Public ';
    protected 
string $protected 'Protected ';
    private 
string $private 'Private ';

    public function 
printHello(): void
    
{
        echo 
"$this->public $this->protected $this->private\n";
    }
}

$obj = new MyClass();
echo 
$obj->public// Fonctionne
//echo $obj->protected; // Erreur fatale
//echo $obj->private; // Erreur fatale
$obj->printHello(); // Affiche Public, Protected et Private


// Classe dérivée
class MyClass2 extends MyClass
{
    
// On peut redéclarer la méthode publique et protégée, mais pas la privée
    
protected string $protected 'Protected2 ';

    public function 
printHello(): void
    
{
        echo 
"$this->public $this->protected $this->private\n";
    }
}

$obj2 = new MyClass2();
echo 
$obj2->public// Fonctionne
//echo $obj2->private; // Indéfini (propriété privée)
//echo $obj2->protected; // Erreur fatale
$obj2->printHello(); // Affiche Public, Protected2

?>
Public Public  Protected  Private 
Public Public  Protected2  

Encapsulation des données

  • Données membres non visibles de l'extérieur
  • Utilisation d'accesseurs et de mutateurs

<?php

class Rectangle {
    protected 
float $longueur;
    protected 
float $largeur;

    public function 
__construct(float $longueurfloat $largeur) {
        
$this->longueur $longueur;
        
$this->largeur $largeur;
    }

    
// Accesseurs

    // Getters
    
public function getLongueur(): float {
        return 
$this->longueur;
    }

    public function 
getLargeur(): float {
        return 
$this->largeur;
    }

    
// Setters
    
public function setLongueur(float $longueur): void {
        
$this->longueur $longueur;
    }

    public function 
setLargeur(float $largeur): void {
        
$this->largeur $largeur;
    }
}

class 
Carre extends Rectangle {
    public function 
__construct(float $cote) {
        
parent::__construct($cote$cote);
    }

    public function 
setLongueur(float $cote): void {
        
parent::setLongueur($cote);
        
parent::setLargeur($cote);
    }

    public function 
setLargeur(float $cote): void {
        
parent::setLongueur($cote);
        
parent::setLargeur($cote);
    }
}

echo 
"Définition d'un rectangle 5x2 :\n";
$R = new Rectangle(52);
// echo $R->longueur; produit Fatal error
// Utiliser les accesseurs :
echo "Longueur : " $R->getLongueur() . "\n";
echo 
"Largeur : " $R->getLargeur() . "\n";

echo 
"Définition d'un carré de côté 4\n";
$C = new Carre(4);
echo 
"Côté :  " $C->getLargeur() . "\n";

echo 
"Changement du côté : \n";
$C->setLargeur(8);
echo 
"Côté :  " $C->getLargeur() . "\n";

?>
Définition d'un rectangle 5x2 :
Longueur : 5
Largeur : 2
Définition d'un carré de côté 4
Côté :  4
Changement du côté : 
Côté :  8

Au passage, noter le mot-clef parent pour appeler une méthode (ou le constructeur) de la superclasse

Exceptions : principe

  • Quand on rencontre un problème dans son programme, au lieu de simplement l'arrêter, on envoie une exception
  • L'envoi d'une exception arrête l'exécution du code en cours
  • L'intérêt est qu'il est possible de récupérer des exceptions qui ont été envoyées par une partie de son programme, afin de traiter la source du problème (si c'est possible)
  • Une exception non récupérée remonte à la fonction appelante, qui elle-même peut avoir mis en place une récupération, etc.
  • Si l'exception remonte « jusqu'en haut », le programme s'arrête avec une
    Fatal error: Uncaught exception

Renvoyer une exception

  • throw new Exception("Houston, we have a problem");
  • Classe Exception prédefinie dans PHP5
  • Méthodes ("final") :
    • getMessage() : message d'erreur de l'exception
    • getCode() : code d'erreur de l'exception
    • getFile() : fichier dans lequel l'exception a été renvoyée
    • getLine() : n° de ligne où l'exception a été renvoyée
    • getTrace() : tableau contenant la trace
    • getTraceAsString() : chaîne de caractères avec la trace

Essayer du code

  • Pour pouvoir récupérer des exceptions, il faut placer le code PHP à exécuter dans un bloc try
  • Tout envoi d'exception dans ce bloc interrompt l'exécution…
  • … mais le programme passe alors à la gestion de l'exception, qui suit

Attraper l'exception

  • Un bloc try doit être suivi d'un bloc catch
  • Ce bloc "attrape" les exceptions éventuellement qui ont été renvoyées dans le bloc try
  • 
    <?php

    try {
        
    // Code à exécuter
        
    division(10);
    } catch (
    Exception $e) {
        echo 
    "Message : " $e->getMessage() . "<br/>\n";
        echo 
    "Code : " $e->getCode() . "<br/>\n";
    }

    function 
    division(float $afloat $b): float {
        if (
    $b == 0) {
            throw new 
    Exception("Erreur division par zéro", -1);
        } else {
            return 
    $a $b;
        }
    }
    ?>

    Message : Erreur division par zéro
    Code : -1

Personnaliser ses exceptions

  • Dériver la classe Exception
  • Permet de créer différents types d'exceptions en utilisant plusieurs blocs catch
  • Une exception non gérée dans un bloc catch est passée au bloc suivant
<?php
class MonException extends Exception{
  
// Ici on peut rajouter de nouvelles méthodes
}
?>
Et on peut ensuite utiliser cette class pour lever une erreur
throw new MonException();

Catch multiple

On peut faire plusieurs catch pour attrapper les différentes erreurs possibles et exécuter un code différent suivant les cas. Un catch (Exception $e) capturera toutes les erreurs car toutes nos classes héritent de cette classe là.
try {
 // Un code qui lève une erreur
 } catch (MonException $e) {
 // Une exception spécifique à MonException
 } catch (PDOException $e) {
 // Une exception spécifique à PDOException (liée à la base de données)
 } catch (Exception $e) {
 // Une exception plus générale
 }

Polymorphisme en php

Le polymorphisme est le fait qu'une classe mère peut prendre plusieurs formes (poly : plusieurs, morphisme : forme).

<?php

abstract class Forme {
    abstract public function 
aire(): float;
}

class 
Point extends Forme {
    protected 
float $x;
    protected 
float $y;

    public function 
__construct(float $xfloat $y) {
        
$this->$x;
        
$this->$y;
    }

    public function 
getX(): float {
        return 
$this->x;
    }

    public function 
getY(): float {
        return 
$this->y;
    }

    public function 
aire(): float {
        return 
0// Un point n'a pas d'aire
    
}
}

class 
Ligne extends Forme {
    protected 
Point $point1;
    protected 
Point $point2;

    public function 
__construct(Point $point1Point $point2) {
        
$this->point1 $point1;
        
$this->point2 $point2;
    }

    public function 
aire(): float {
        return 
0// Une ligne n'a pas d'aire
    
}
}

class 
Triangle extends Forme {
    protected 
Point $sommet1;
    protected 
Point $sommet2;
    protected 
Point $sommet3;

    public function 
__construct(Point $sommet1Point $sommet2Point $sommet3) {
        
$this->sommet1 $sommet1;
        
$this->sommet2 $sommet2;
        
$this->sommet3 $sommet3;
    }

    public function 
aire(): float {
        
// Calcul de l'aire d'un triangle (exemple simple, non réaliste)
        
$x1 $this->sommet1->getX();
        
$y1 $this->sommet1->getY();
        
$x2 $this->sommet2->getX();
        
$y2 $this->sommet2->getY();
        
$x3 $this->sommet3->getX();
        
$y3 $this->sommet3->getY();

        
// Formule de l'aire d'un triangle
        
return abs(($x1 * ($y2 $y3) + $x2 * ($y3 $y1) + $x3 * ($y1 $y2)) / 2);
    }
}

class 
Rectangle extends Forme {
    protected 
Point $coinSuperieurGauche;
    protected 
Point $coinInferieurDroit;

    public function 
__construct(Point $coinSuperieurGauchePoint $coinInferieurDroit) {
        
$this->coinSuperieurGauche $coinSuperieurGauche;
        
$this->coinInferieurDroit $coinInferieurDroit;
    }

    public function 
aire(): float {
        
// Calcul de l'aire d'un rectangle (exemple simple, non réaliste)
        
$x1 $this->coinSuperieurGauche->getX();
        
$y1 $this->coinSuperieurGauche->getY();
        
$x2 $this->coinInferieurDroit->getX();
        
$y2 $this->coinInferieurDroit->getY();

        return 
abs(($x2 $x1) * ($y1 $y2));
    }
}

class 
Carre extends Rectangle {
    public function 
__construct(Point $coinSuperieurGauchefloat $cote) {
        
$coinInferieurDroit = new Point($coinSuperieurGauche->getX() + $cote$coinSuperieurGauche->getY() - $cote);
        
parent::__construct($coinSuperieurGauche$coinInferieurDroit);
    }
}

class 
Cercle extends Forme {
    protected 
Point $centre;
    protected 
float $rayon;

    public function 
__construct(Point $centrefloat $rayon) {
        
$this->centre $centre;
        
$this->rayon $rayon;
    }

    public function 
aire(): float {
        
// Calcul de l'aire d'un cercle (exemple simple, non réaliste)
        
return M_PI $this->rayon $this->rayon;
    }
}

// Fonction générique utilisant le polymorphisme
function afficherAire(Forme $forme): void {
    echo 
"Aire : " $forme->aire() . "\n";
}

// Utilisation du polymorphisme
$sommet1 = new Point(12);
$sommet2 = new Point(34);
$sommet3 = new Point(56);

$ligne = new Ligne($sommet1$sommet2);

$triangle = new Triangle($sommet1$sommet2$sommet3);
$rectangle = new Rectangle($sommet1$sommet3);

// Création d'un nouvel objet Point pour représenter le coin supérieur gauche du carré
$coinSuperieurGaucheCarre = new Point(78);
$carre = new Carre($coinSuperieurGaucheCarre10);

$cercle = new Cercle($sommet15);

afficherAire($sommet1);
afficherAire($ligne);
afficherAire($triangle);
afficherAire($rectangle);
afficherAire($carre);
afficherAire($cercle);

?>