PHP Conference Nagoya 2025

Objets paresseux

Un objet paresseux est un objet dont l'initialisation est différée jusqu'à ce que son état soit observé ou modifié. Quelques exemples d'utilisation incluent des composants d'injection de dépendances qui fournissent des services paresseux entièrement initialisés uniquement si nécessaire, des ORMs fournissant des entités paresseuses qui s'hydratent depuis la base de données uniquement lorsqu'elles sont accédées, ou un analyseur JSON qui retarde l'analyse jusqu'à ce que les éléments soient accédés.

Deux stratégies d'objets paresseux sont prises en charge : les objets fantômes (Ghost) et les proxys virtuels (Virtual Proxies), ci-après appelés "fantômes paresseux" et "proxys paresseux". Dans les deux stratégies, l'objet paresseux est attaché à un initialiseur ou une fabrique qui est appelé automatiquement lorsque son état est observé ou modifié pour la première fois. D'un point de vue d'abstraction, les objets paresseux sont indiscernables des non-paresseux : ils peuvent être utilisés sans savoir qu'ils sont paresseux, ce qui permet de les passer et de les utiliser par du code qui n'est pas conscient de la paresse. Les proxys paresseux sont également transparents, mais il faut faire attention lorsque leur identité est utilisée, car le proxy et son instance réelle ont des identités différentes.

Création d'objets paresseux

Il est possible de créer une instance paresseuse de n'importe quelle classe définie par l'utilisateur ou de la classe stdClass (d'autres classes internes ne sont pas prises en charge), ou de réinitialiser une instance de ces classes pour la rendre paresseuse. Les points d'entrée pour créer un objet paresseux sont les méthodes ReflectionClass::newLazyGhost() et ReflectionClass::newLazyProxy().

Les deux méthodes acceptent une fonction qui est appelée lorsque l'objet nécessite une initialisation. Le comportement attendu de la fonction varie en fonction de la stratégie utilisée, comme décrit dans la documentation de référence de chaque méthode.

Exemple #1 Création d'un fantôme paresseux

<?php
class Example
{
public function
__construct(public int $prop)
{
echo
__METHOD__, "\n";
}
}

$reflector = new ReflectionClass(Example::class);
$lazyObject = $reflector->newLazyGhost(function (Example $object) {
// Initialise l'objet sur place
$object->__construct(1);
});

var_dump($lazyObject);
var_dump(get_class($lazyObject));

// Déclenche l'initialisation
var_dump($lazyObject->prop);
?>

L'exemple ci-dessus va afficher :

lazy ghost object(Example)#3 (0) {
["prop"]=>
uninitialized(int)
}
string(7) "Example"
Example::__construct
int(1)

Exemple #2 Création d'un proxy paresseux

<?php
class Example
{
public function
__construct(public int $prop)
{
echo
__METHOD__, "\n";
}
}

$reflector = new ReflectionClass(Example::class);
$lazyObject = $reflector->newLazyProxy(function (Example $object) {
// Crée et retourne l'instance réelle
return new Example(1);
});

var_dump($lazyObject);
var_dump(get_class($lazyObject));

// Déclenche l'initialisation
var_dump($lazyObject->prop);
?>

L'exemple ci-dessus va afficher :

lazy proxy object(Example)#3 (0) {
  ["prop"]=>
  uninitialized(int)
}
string(7) "Example"
Example::__construct
int(1)

N'importe quel accès à des propriétés d'un objet paresseux déclenche son initialisation (y compris via ReflectionProperty). Cependant, certaines propriétés peuvent être connues à l'avance et ne devraient pas déclencher l'initialisation lorsqu'elles sont accédées :

Exemple #3 Initialisation des propriétés de manière impatiente

<?php
class BlogPost
{
public function
__construct(
private
int $id,
private
string $title,
private
string $content,
) { }
}

$reflector = new ReflectionClass(BlogPost::class);

$post = $reflector->newLazyGhost(function ($post) {
$data = fetch_from_store($post->id);
$post->__construct($data['id'], $data['title'], $data['content']);
});

// Sans cette ligne, l'appel suivant à ReflectionProperty::setValue() déclencherait
// l'initialisation.
$reflector->getProperty('id')->skipLazyInitialization($post);
$reflector->getProperty('id')->setValue($post, 123);

// Egalement, on peut utiliser ceci directement :
$reflector->getProperty('id')->setRawValueWithoutLazyInitialization($post, 123);

// L'identifiant peut être accédé sans déclencher l'initialisation
var_dump($post->id);
?>

Les méthodes ReflectionProperty::skipLazyInitialization() et ReflectionProperty::setRawValueWithoutLazyInitialization() offrent des moyens de contourner l'initialisation paresseuse lors de l'accès à une propriété.

A propos des stratégies d'objets paresseux

Les fantômes paresseux sont des objets qui s'initialisent sur place et, une fois initialisés, sont indiscernables d'un objet qui n'était jamais paresseux. Cette stratégie est adaptée lorsque nous contrôlons à la fois l'instanciation et l'initialisation de l'objet et est inadaptée si l'une de ces opérations est gérée par une autre partie.

Les proxys paresseux, une fois initialisés, agissent comme des proxys vers une instance réelle : toute opération sur un proxy paresseux initialisé est transmise à l'instance réelle. La création de l'instance réelle peut être déléguée à une autre partie, ce qui rend cette stratégie utile dans les cas où les fantômes paresseux sont inadaptés. Bien que les proxys paresseux soient presque aussi transparents que les fantômes paresseux, il faut faire attention lorsque leur identité est utilisée, car le proxy et son instance réelle ont des identités distinctes.

Cycle de vie des objets paresseux

Les objets peuvent être rendus paresseux lors de l'instanciation en utilisant ReflectionClass::newLazyGhost() ou ReflectionClass::newLazyProxy(), ou après l'instanciation en utilisant ReflectionClass::resetAsLazyGhost() ou ReflectionClass::resetAsLazyProxy(). Ensuite, un objet paresseux peut être initialisé par l'une des opérations suivantes :

Comme les objets paresseux sont initialisés lorsque toutes leurs propriétés sont marquées non-paresseuses, les méthodes ci-dessus ne marqueront pas un objet comme paresseux si aucune propriété ne peut être marquée comme paresseuse.

Déclencheurs d'initialisation

Les objets paresseux sont conçus pour être entièrement transparents pour leurs consommateurs, de sorte que les opérations normales qui observent ou modifient l'état de l'objet déclencheront automatiquement l'initialisation avant que l'opération ne soit effectuée. Cela inclut, mais sans s'y limiter, les opérations suivantes :

Les appels de méthodes qui n'accèdent pas à l'état de l'objet ne déclencheront pas l'initialisation. De même, les interactions avec l'objet qui invoquent des méthodes magiques ou des fonctions de crochet ne déclencheront pas l'initialisation si ces méthodes ou fonctions n'accèdent pas à l'état de l'objet.

Opérations non déclenchantes

Les méthodes ou opérations spécifiques suivantes permettent d'accéder ou de modifier des objets paresseux sans déclencher l'initialisation :

Séquence d'initialisation

Cette section décrit la séquence d'opérations effectuées lorsqu'une initialisation est déclenchée, en fonction de la stratégie utilisée.

Objets fantômes

Après l'initialisation, l'objet est indiscernable d'un objet qui n'a jamais été paresseux.

Objets proxys

  • L'objet est marqué comme non-paresseux.
  • Contrairement aux objets fantômes, les propriétés de l'objet ne sont pas modifiées à cette étape.
  • La fonction fabrique est appelée avec l'objet comme premier paramètre et doit retourner une instance non-paresseuse d'une classe compatible (voir ReflectionClass::newLazyProxy()).
  • L'instance retournée est appelée instance réelle et est attachée au proxy.
  • Les valeurs des propriétés du proxy sont jetées comme si unset() est appelé.

Après l'initialisation, l'accès à n'importe quelle propriété sur le proxy donnera le même résultat que l'accès à la propriété correspondante sur l'instance réelle ; tous les accès aux propriétés sur le proxy sont transmis à l'instance réelle, y compris les propriétés déclarées, dynamiques, inexistantes, ou les propriétés marquées avec ReflectionProperty::skipLazyInitialization() ou ReflectionProperty::setRawValueWithoutLazyInitialization().

L'objet proxy lui-même n'est pas remplacé ou substitué par l'instance réelle.

Tandis que la fabrique reçoit le proxy comme premier paramètre, il n'est pas attendu qu'elle le modifie (les modifications sont autorisées mais seront perdues lors de l'étape finale d'initialisation). Cependant, le proxy peut être utilisé pour des décisions basées sur les valeurs des propriétés initialisées, la classe, l'objet lui-même, ou son identité. Par exemple, l'initialiseur pourrait utiliser la valeur d'une propriété initialisée lors de la création de l'instance réelle.

Comportement commun

La portée et le contexte $this de la fonction d'initialisation ou de la fabrique restent inchangés, et les contraintes de visibilité habituelles s'appliquent.

Après une initialisation réussie, la fonction d'initialisation ou la fabrique n'est plus référencée par l'objet et peut être libérée si elle n'a pas d'autres références.

Si l'initialisation lève une exception, l'état de l'objet est rétabli à son état pré-initialisation et l'objet est à nouveau marqué comme paresseux. En d'autres termes, tous les effets sur l'objet lui-même sont annulés. Les autres effets secondaires, tels que les effets sur d'autres objets, ne sont pas annulés. Cela empêche l'exposition d'une instance partiellement initialisée en cas d'échec.

Clonage

Cloner un objet paresseux déclenche son initialisation avant que le clone ne soit créé, résultant en un objet initialisé.

Pour les objets proxis, le proxy et son instance réelle sont clonés, et le clone du proxy est retourné. La méthode __clone est appelée sur l'instance réelle, pas sur le proxy. Le proxy cloné et l'instance réelle clonée sont liés comme ils le sont pendant l'initialisation, donc les accès au clone du proxy sont transmis au clone de l'instance réele.

Ce comportement garantit que le clone et l'objet original maintiennent des états séparés. Les modifications apportées à l'objet original ou à l'état de son initialisateur après le clonage n'affectent pas le clone. Cloner à la fois le proxy et son instance réelle, plutôt que de retourner un clone de l'instance réelle seule, garantit que l'opération de clonage retourne systématiquement un objet de la même classe.

Destructeurs

Pour les objets paresseux, le destructeur n'est appelé que si l'objet a été initialisé. Pour les proxys, le destructeur n'est appelé que sur l'instance réelle, si elle existe.

Les méthodes ReflectionClass::resetAsLazyGhost() et ReflectionClass::resetAsLazyProxy() peuvent invoquer le destructeur de l'objet réinitialisé.

add a note

User Contributed Notes

There are no user contributed notes for this page.
To Top