Le modèle objet de PHP (deuxième partie)

Youssef Chahir
GREYC — Université de Caen
En partie adapté du cours d'Alexandre Niveau et de Jean-Marc Lecarpentier
TODO: * traits * namespaces * stdclass ? * iterable * closure

Affichage debug d'un objet


<?php
class Titi {
    private 
bool $x false;
    protected 
string $y "";
    public 
$z null;

    function 
methode() { }
}

echo 
"---- var_export ----\n";
var_export(new Titi());

echo 
"---- print_r ----\n";
print_r(new Titi());

echo 
"---- var_dump ----\n";
var_dump(new Titi());
?>
---- var_export ----
Titi::__set_state(array(
   'x' => false,
   'y' => '',
   'z' => NULL,
))---- print_r ----
Titi Object
(
    [x:Titi:private] => 
    [y:protected] => 
    [z] => 
)
---- var_dump ----
/users/ensweb/www-prod/TW3/pres/oophp/demo/exPrintDebug.php:17:
class Titi#3 (3) {
  private bool $x =>
  bool(false)
  protected string $y =>
  string(0) ""
  public $z =>
  NULL
}

Mot clé final

  • final pour une méthode : la méthode ne peut pas être redéfinie dans une classe dérivée
  • final pour une classe : la classe ne peut pas être dérivée

<?php

class finalEx {
    private 
string $donnee "exemple";
  
    final function 
maj(): string {
        return 
strtoupper($this->donnee);
    }
}

final class 
finalExtend extends finalEx {
    
/*
     * Produira une erreur fatale :
     * Fatal error: Cannot override final method finalEx::maj()
     */
    /*
    public function maj(): string {
        return $this->donnee;
    }
    */
}

/*
 * Produira une erreur fatale :
 * Fatal error: Class erreur may not inherit from final class (finalExtend)
 */
/*
class erreur extends finalExtend {
    // impossible
}
*/

$class = new finalEx();
echo 
$class->maj();
echo 
"\n";

$classExtend = new finalExtend();
echo 
$classExtend->maj();

?>
EXEMPLE
EXEMPLE

Classe et méthode abstraite

  • Mot clé abstract
  • Une classe abstraite ne peut pas être instanciée
  • Une méthode abstraite définit sa signature, pas son implémentation
  • Une méthode abstraite doit être dérivée avec une visibilité supérieure

<?php

abstract class AbstractClass
{
    
// Force la classe étendue à définir cette méthode
    
abstract protected function getValue(): string;

    
// Méthode commune
    
public function printOut(): void
    
{
        echo 
$this->getValue();
    }
}

class 
ConcreteClass1 extends AbstractClass
{
    protected function 
getValue(): string
    
{
        return 
"ConcreteClass1";
    }
}

class 
ConcreteClass2 extends AbstractClass
{
    protected function 
getValue(): string
    
{
        return 
"ConcreteClass2";
    }
}

$class1 = new ConcreteClass1;
$class1->printOut();

$class2 = new ConcreteClass2;
$class2->printOut();

ConcreteClass1ConcreteClass2

Interfaces

  • Une interface fournit un plan général en spécifiant les méthodes publiques qu’une classe doit implémenter, sans impliquer la complexité et les détails de l’implémentation des méthodes.
  • Définir avec le mot clé interface
  • L’interface ne contient pas de propriétés ou de variables comme le cas dans une classe.
  • Une classe implémente une interface :
    class maClasse implements monInterface
  • Existence d'interfaces "internes" prédéfinies dans PHP5
    • Traversable dont le but est d’être l’interface de base de toutes les classes permettant de parcourir des objets
    • Iterator qui définit des signatures de méthodes pour les itérateurs
    • IteratorAggregate qui est une interface qu’on va pouvoir utiliser pour créer un itérateur externe
    • Throwable qui est l’interface de base pour la gestion des erreurs et des exceptions
    • ArrayAccess qui permet d’accéder aux objets de la même façon que pour les tableaux
    • Serializable qui permet de personnaliser la linéarisation d’objets.
Différences entre les interfaces et les classes abstraites sont :
  • Une interface ne peut contenir que les signatures des méthodes ainsi qu’éventuellement des constantes mais pas de propriétés. Cela est dû au fait qu’aucune implémentation n’est faite dans une interface : une interface n’est véritablement qu’un plan ;
  • Une classe ne peut pas étendre plusieurs autres classes à cause des problèmes d’héritage. En revanche, une classe peut tout à fait implémenter plusieurs interfaces.

Exemple


<?php

interface Abonne {
    public const 
ABONNEMENT 15;
    public function 
getNom(): string;
    public function 
setPrixAbo(): void;
    public function 
getPrixAbo(): float;
}

class 
Etudiant implements Abonne {
    protected 
string $nom;
    protected 
string $persopass;
    protected 
string $region;
    protected 
float $prix_abo;

    public function 
__construct(string $nstring $pstring $r) {
        
$this->nom $n;
        
$this->persopass $p;
        
$this->region $r;
    }

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

    public function 
getPrixAbo(): float {
        return 
$this->prix_abo;
    }

    public function 
setPrixAbo(): void {
        if (
$this->region === 'Normandie') {
            
$this->prix_abo Abonne::ABONNEMENT 2;
        } else {
            
$this->prix_abo Abonne::ABONNEMENT;
        }
    }
}
/*
    class Etudiant implements Abonne, Stagiaire {
        // code...
      }
    class Salarie implements Abonne{
        // code...
    }
*/
$valentin = new Etudiant('Pierre''abcdef''Normandie');
$sophie = new Etudiant('Sophie''123456''Bretagne');
$gael = new Etudiant('Gael''flotri''Occitanie');

$valentin->setPrixAbo();
$sophie->setPrixAbo();
$gael->setPrixAbo();

echo 
'Prix de l\'abonnement pour ' $valentin->getNom() . ' : ' $valentin->getPrixAbo() . '<br>';
echo 
'Prix de l\'abonnement pour ' $sophie->getNom() . ' : ' $sophie->getPrixAbo() . '<br>';
echo 
'Prix de l\'abonnement pour ' $gael->getNom() . ' : ' $gael->getPrixAbo();

?>

Prix de l'abonnement pour Pierre : 7.5
Prix de l'abonnement pour Sophie : 15
Prix de l'abonnement pour Gael : 15
Une classe peut implémenter plusieurs interfaces, séparées par des virgules.

Type Nullable

Dans PHP 8, l'introduction des types nullables offre une flexibilité supplémentaire lors de la déclaration des types pour les paramètres de fonction, les valeurs de retour et les propriétés de classe. Un type nullable est déclaré en ajoutant un point d'interrogation (?) avant le type.
    //Types Nullable pour les Valeurs de Retour
    function diviser(?float $a, ?float $b): ?float {
        if ($b === 0.0) {
            return null;
        }
        return $a / $b;
    }

    // Exemple d'utilisation
    $resultat = diviser(10.0, 2.0);
    
    //Types Nullable pour les Paramètres de Fonction
    function exemple(?string $parametre) {
        if ($parametre === null) {
            echo "Le paramètre est null.";
        } else {
            echo "Le paramètre est : " . $parametre;
        }
    }

    // Exemple d'utilisation
    exemple("valeur");
    exemple(null);
    
    //Types Nullable pour les Propriétés de Classe
    class Exemple {
        public ?string $propriete;

        public function __construct(?string $valeur) {
            $this->propriete = $valeur;
        }
    }

    // Exemple d'utilisation
    $objet = new Exemple("valeur");
    echo $objet->propriete;
    

Espaces de noms

Les namespaces permettent de définir un nom de package que l'on pourra ensuite charger de manière automatique gràce à un autoloading.

Pour déclarer un namespace, il faut utiliser la commande namespace suivi du nom du namespace à créer.

On prendra l'habitude de créer nos classes dans un namespace pour éviter les collisions ! Par exemple, nous allons placer notre class Exemple dans un namespace MonNamespace.

Exemple

<?php   
namespace Tutoriel\MonNamespace;
        
function 
strlen()
{
    echo 
'Hello world ! <br/>';
}

strlen();
echo \
strlen('Hello world !');


  
?>
Hello world ! 
13

Héritage vs composition

Un principe important en POO est de favoriser la composition plutôt que l'héritage pour mutualiser du code

En effet l'héritage a des conséquences pénibles, notamment le fait qu'à partir du moment où on a sous-classé une classe, il devient difficile de la faire évoluer (tout changement a des conséquences sur les classes filles)

cela contrevient à la modularisation du code — les classes sont trop dépendantes les unes des autres, alors que le but de la POO est plutôt d'essayer de créer des « boîtes noires » dont le comportement interne n'a pas d'influence à l'extérieur

En utilisant la composition, on garde l'indépendance entre les classes

le prix à payer est une syntaxe un peu plus lourde dans la classe, et l'obligation parfois d'écrire des méthodes de « délégation »

ex autom cell: comportement en fonction de la règle, en fonction de l'affichage voulu

Traits

Les traits de PHP sont (principalement) un moyen de faire de la composition, sans les inconvénients

trait MonTrait {
    function uneMethodeUtile() {
}

class Toto {
    use MonTrait;
}
Dans l'exemple ci-dessus, la classe Toto contient la méthode uneMethodeUtile, comme si elle avait hérité de MonTrait, sauf qu'il n'y a pas eu d'héritage :
  • Toto peut très bien étendre une autre classe
  • Toto peut utiliser d'autres traits, pas de problème d'héritage multiple (en cas de conflit de nommage, il faut obligatoirement définir explicitement la méthode concernée dans Toto
  • On peut utiliser une même méthode dans plusieurs classes, sans qu'elles soient liées par le typage
Les inconvénients de la composition disparaissent, puisque tout se passe comme si la méthode était définie dans la classe :
  • syntaxe simple
  • pas besoin d'écrire des méthodes de délégation

Méthodes magiques

  • Méthodes __sleep et __wakeup : code à exécuter lors de la sérialisation et désérialisation d'un objet
  • Utile pour les ressources (connection BDD, ressource fichier)
  • Méthodes __get __set __call pour gérer la surcharge objet (voir le manuel)
  • à utiliser pour gérer les erreurs, pas « pour de vrai » ! peu efficace + code moins lisible, moins analysable par des outils + leaky abstraction, voir un exemple dans la doc

Linéarisation / Délinéarisation

En PHP, la linéarisation est effectuée nativement avec les deux fonctions suivantes :
<?php   
class Personne {
 public 
string $nom;
 public function 
__construct(string $nom) {
    
$this->nom $nom;
 }

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

 public function 
salut(Personne $autre): string {
    return 
"Ravi de vous rencontrer " $autre->nom ", <br/>"" Je suis " $this->nom "<br/>";
 }
}

// Création de deux objets
$youssef = new Personne("Youssef");
$jean = new Personne("Jean");
echo 
$youssef->salut($jean);
echo 
serialize($youssef);
echo 
"</br>";
$test unserialize(serialize($youssef));
echo 
"</br>";
echo (
$test->qui());

?>
Ravi de vous rencontrer Jean, 
Je suis Youssef
O:8:"Personne":1:{s:3:"nom";s:7:"Youssef";}

Youssef

Itérables

  • Les Itérables peuvent être utilisés comme type d'argument pour indiquer qu'une fonction a besoin d'un ensemble de valeurs
    • Un iterator doit avoir ces methodes:
    • current() : Renvoie l'élément courant
    • key() : Renvoie la clé associée à l'élément actuel .
    • next() : Element suivant
    • rewind() : 1er element
    • valid() : S'il ne pointe sur aucun élémenf

<?php
// Create an Iterator
class MyIterator implements Iterator {
  private array 
$items = [];
  private 
int $pointer 0;

  public function 
__construct(array $items) {
    
// array_values() makes sure that the keys are numbers
    
$this->items array_values($items);
  }

  public function 
current(): ?string {
    return 
$this->items[$this->pointer];
  }

  public function 
key() : int{
    return 
$this->pointer;
  }

  public function 
next() : void {
    
$this->pointer++;
  }

  public function 
rewind() : void {
    
$this->pointer 0;
  }

  public function 
valid() : bool {
    return 
$this->pointer count($this->items);
  }


}


function 
affiche(Traversable $myIterable): void{
   foreach(
$myIterable as $item) {
     echo 
$item "<br/>";
   }
}



// Use the iterator as an iterable
$iterator = new MyIterator(["a""b""c"]);
affiche($iterator);

?> 
a
b
c

Conventions de nommage en PHP

Les conventions de nommage sont un ensemble de «lignes directrices» définissant le cas à utiliser pour nommer les éléments de votre code. C'est à dire comment créer des noms de variables, de fonctions et classes en PHP ?
Elles ne sont que des lignes directrices (rien d'obligatoire mais juste une bonne pratique).
Ceci est fait pour garder votre code uniforme, ce qui le rend plus facile à suivre et à comprendre.
Les deux cas utilisés le plus souvent sont le Pascal Case et le Camel Case.
  • Pascal case -> NomEnPascalCase (aussi appelée Upper Camel Case)
    Pascal case c'est quand la première lettre de chaque mot commence par une majuscule sans espaces entre les mots.
  • Camel Case -> nomEnCamelCase (aussi appelé Lower Camel Case)
    Camel cas c'est la même chose, sauf que la première lettre du mot commence par une lettre minuscule.

Les bases et les généralités

  • Les mots clefs abstract ou static doivent passer avant la visibilité d'une variable de classe
  • Déclarez une classe par fichier
  • Les propriétés de classe avant les méthodes
  • Les méthodes dans cet ordre : publiques, protégées, privées.
  • Classe : MaClasse (Upper Camel Case) //Le nom de la classe doit indiquer l'objet créé (User, Article...)
  • Fichier (script tout bête sans classe dedans) : mon_script.php
  • Fichier de classe : MaClasse.php (Upper Camel Case)
  • Fonction : maFonction() (camel Case) //Le nom doit décrire le comportement de la fonction ou de la méthode
  • variable : $maVariable, $monArray, $maString, $monEntier (camel Case) Les noms doivent décrire les données dans la variable.
  • constante : NOM_DE_CONSTANTE (ALL_CAPS -> tout en majuscule). Il faut utiliser le signe '_' pour séparer les mots dans des constantes
  • bool :true, false en minscule
  • Une difficile : un fichier doit soit contenir des déclarations de fichiers, mais pas de code executable.
    • Entendre par là, vous pouvez déclarer des variables et des fonctions dans un fichier, mais rien qui ne s'exécute à l'inclusion de ce fichier
    • A l'exception du contenu des classes bien entendu

Autoload et espace de noms

  • Les espaces de noms et les classes doivent utiliser l'autoload
  • Il n'y a pas de limitation quant à la quantité de niveaux pour les espaces de nom
  • Dans l'espace de nom, chaque séparateur est converti en constante DIRECTORY_SEPARATOR
  • Même chose pour le caractère '_' dans le nom de la classe. Par contre, aucune signification dans l'espace de nom
  • L'espace de nom doit comporter l'extension .php quand il est chargé par le système
  • Les Use doivent apparaitre après les espaces de nom
  • Une ligne vide après la fin des déclarations de vos espaces de nom