PHP : self versus static
Accueil Tags Recherche22 Octobre 2019
PHP : self versus static
Quelle est la différence entre self et static, et comment les utiliser ?Cet article s’inscrit dans un dossier sur les concepts PHP et l’architecture de code, commencé avec un précédent article sur les traits.
Introduction
Si vous avez déjà fait un peu de POO, vous êtes censés connaître les fonctions et propriétés statiques qui dépendent d’une classe, mais pas d’une instance. Un exemple couramment vu dans les tutoriels est celui du compteur d’instances :
class Mother
{
protected static int $counter = 0;
public function __construct()
{
self::$counter++;
}
public static function count(): int
{
return self::$counter;
}
}
new Mother();
new Mother();
new Mother();
print Mother::count(); // 3
Dans le code ci-dessus, on constate l’utilisation du mot-clé self
pour accéder à la propriété statique. Certains d’entre-vous auraient peut-être utilisé le mot-clé static
à la place, ce qui aurait parfaitement fonctionné dans ce cas précis.
Mais alors quelle différence ?
Les limites de self
Poussons un peu plus loin l’exemple précédent et ajoutons de l’héritage :
class Mother
{
protected static int $counter = 0;
public function __construct()
{
self::$counter++;
}
public static function count(): int
{
return self::$counter;
}
}
class Daughter extends Mother
{
protected static int $counter = 0;
}
On redéfini le compteur dans la classe fille afin de dissocier les instances de Mother
de celles de Daughter
dans nos comptes, puis on teste :
new Mother();
new Mother();
new Mother();
new Daughter();
print Mother::count(); // 4
print Daughter::count(); // 4
On constate que les deux compteurs ont la même valeur, mais voici ce qu’il se passe réellement : on utilise le même compteur. Qu’on utilise Mother
ou Daughter
, tous les appels finissent par remonter jusqu’à la fonction Mother::count()
et dans cette classe, self
est une référence à elle-même : Mother
.
Une solution serait de redéfinir les deux fonctions dans la classe fille Daughter
, mais on perd dans ce cas absolument tout intérêt à utiliser de l’héritage.
La notion de “late static binding”
PHP implémente (depuis sa version 5.3.0) le late static binding, ou “résolution statique à la volée” en français. Le principe est de résoudre la classe d’appel au moment de l’exécution : on cherche à trouver la classe active lors de l’appel, et non pas celle dans laquelle est définie la fonction (ou propriété) statique appelée.
Dans des termes plus techniques, le site php.net précise que le mot-clé static
comme contient une référence à la classe active lors du “dernier appel non transmis”.
Un “appel transmis” est un appel statique déclenché par
self::
,parent::
,static::
ou, tout en haut de la hiérarchie des classes,forward_static_call()
.
Lorsque vous utiliser l’appel ci-dessous, c’est un appel non transmis car il s’effectue sur une classe :
print Daughter::count();
Comme il s’agit d’une fonction héritée, le contenu de cette fonction dans le contexte de la classe fille est implicitement le suivant :
public static function count(): int
{
return parent::count();
}
Ce second appel est un appel transmis car il s’effectue sur le mot-clé parent
. La classe active (Daughter
) est bien différente de la classe de définition (Mother
), et c’est là que le mot-clé static
va être utile.
self
versus static
Pour résumé ce que nous venons de voir :
self
permet d’accéder à la classe dans laquelle ce mot-clé est écritstatic
permet d’accéder à la classe active lors de l’exécution
En reprenant toujours le même exemple, pour avoir deux compteurs différents il faudrait procéder comme ceci (on change simplement tous les self::$counter
en static::$counter
) :
class Mother
{
protected static int $counter = 0;
public function __construct()
{
static::$counter++;
}
public static function count(): int
{
return static::$counter;
}
}
class Daughter extends Mother
{
protected static int $counter = 0;
}
new Mother();
new Mother();
new Mother();
new Daughter();
print Mother::count(); // 3
print Daughter::count(); // 1
L’utilisation du mot-clé static
permettra de faire appel au compteur de la classe Daughter
: la classe active, celle que nous utilisons vraiment.
get_called_class()
Il est bon de noter que PHP 5.3.0 introduit en même temps la fonction get_called_class()
qui permet de récupérer le nom de cette classe active.
Cas pratique
En pratique, j’utilise beaucoup et principalement le late static binding avec des classes abstraites.
Prenons un nouvel d’exemple, avec une architecture de classes sœurs définissant des APIs. Suivant le principe DRY (Don’t Repeat Yourself), je vais chercher à rendre mon code le plus abstrait possible et à le centraliser dans une classe mère (généralement abstraite), afin de simplifier au maximum la création de futures classes d’APIs. Je pourrais avoir une architecture de ce type :
src/
Api/
AbstractApi.php
Customers.php
Orders.php
Products.php
Pour chacune de mes APIs et partant du principe que j’ai mis en place un routeur, je vais aller définir son namespace et sa racine (l’URL sur laquelle je pourrais appeler cette API). Pour les besoin de cet exemple, je ne vais évidemment utiliser que des fonctions statiques :
abstract class AbstractApi
{
const VERSION = 'v1';
}
class Customers extends AbstractApi
{
protected static function namespace(): string
{
return 'clients';
}
public static function root(): string
{
return sprintf('api/%s/%s', self::VERSION, self::namespace());
}
}
class Orders extends AbstractApi
{
protected static function namespace(): string
{
return 'orders';
}
public static function root(): string
{
return sprintf('api/%s/%s', self::VERSION, self::namespace());
}
}
class Products extends AbstractApi
{
protected static function namespace(): string
{
return 'products';
}
public static function root(): string
{
return sprintf('api/%s/%s', self::VERSION, self::namespace());
}
}
Je me rend vite compte de deux problèmes :
- la fonction
root()
est toujours identique, mais je ne peux pas la définir dansAbstractApi
à cause de l’utilisation deself::namespace()
- la plupart du temps
namespace()
renvoie le nom de la classe en minuscules (sauf pourCustomers
où j’ai voulu utiliserclients
pour une raison quelconque)
Pour améliorer tout ça, je déplace donc ces deux fonctions vers la classe mère, en veillant bien à utiliser le mot-clé static
pour que les surcharges éventuelles des classes filles soient appelées :
abstract class AbstractApi
{
const VERSION = 'v1';
protected static function namespace(): string
{
return strtolower(get_class_name());
}
public static function root(): string
{
return sprintf('api/%s/%s', self::VERSION, static::namespace());
}
}
class Customers extends AbstractApi
{
protected static function namespace(): string
{
return 'clients';
}
}
class Orders extends AbstractApi {}
class Products extends AbstractApi {}
Et voilà !
print Customers::root(); // api/v1/clients
print Orders::root(); // api/v1/orders
print Products::root(); // api/v1/products
Tout est propre, aucune ligne n’est répétée, et la création d’une nouvelle API est simplifiée sans enlever la possibilité de surcharge.
Conclusion
Retenez bien la différence entre les deux mots-clés :
self
: la classe où ce mot-clé est écrit (classe “courante”)static
: la classe appelée pendant l’exécution (class “active”)
La grande majorité du temps vous n’aurez besoin que de self
, mais dès que vous voulez organiser proprement votre code (ou faire un peu de refactoring), vous tomberez forcément sur des situations où vous aurez besoin du mot-clé static
pour aller plus loin.