> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Conception du cycle de vie des services d'un package

> En analysant l'implémentation de bind, singleton et scoped dans Laravel 13, découvrez comment concevoir un package qui ne conserve pas d'état dans les workers de longue durée, et comment tester les frontières de cycle de vie.

Pour qu'un package fonctionne non seulement avec les requêtes HTTP classiques, mais aussi avec Octane ou les workers de file d'attente, il faut décider **pendant combien de temps et quelles données un service peut conserver**. Si vous stockez des données propres à une requête dans un singleton, un autre traitement risque de lire des données obsolètes.

Cette page examine l'implémentation du conteneur et du worker de file d'attente de Laravel Framework `v13.36.0`, ainsi que celle d'Octane, et relie les méthodes d'enregistrement de base du [conteneur de services](/fr/service-container) à la maintenance à long terme d'un package. Choisir `scoped()` ne suffit pas à réinitialiser toutes les références entre objets ni tous les états statiques.

## Choisir la méthode d'enregistrement selon la durée de conservation du service

| Enregistrement | Récupération classique via `make()` | Conception adaptée |
| - | - | - |
| `bind()` | Crée une instance à chaque résolution | Services pouvant être créés à chaque opération |
| `singleton()` | Réutilise l'instance déjà créée dans le même conteneur | Services ne conservant aucun état propre à une requête ou à un job |
| `scoped()` | Réutilise l'instance dans le même cycle de vie et supprime l'instance stockée à la frontière | État partagé au sein d'une requête ou d'un job |

Ce tableau compare les résolutions classiques. Vérifiez séparément les chemins particuliers, comme la résolution avec paramètres explicites ou le contextual binding.

Même avec `bind()`, si vous stockez le service récupéré dans une propriété d'un autre singleton, cette référence persiste. La méthode d'enregistrement est une règle de création et de réutilisation du conteneur : elle n'impose pas la recréation des objets conservés par le code appelant.

## scoped combine un enregistrement partagé et le suivi des clés à supprimer

`Container::scoped()`, tel que nous l'avons examiné, effectue les deux opérations suivantes :

1. Il enregistre la clé dans `scopedInstances`.
2. Il appelle `singleton()` pour l'enregistrer en tant que liaison partagée.

L'instance elle-même est stockée dans le même tableau `instances` que pour un singleton. `forgetScopedInstances()` appelle `unset()` sur les instances stockées pour les clés enregistrées. La logique de création de la liaison est conservée, ce qui permet de créer un nouvel objet lors de la prochaine résolution classique.

```mermaid theme={null}
flowchart TD
    A["Enregistrement scoped dans register()"] --> B["Enregistrement de la création et de la clé à supprimer"]
    B --> C["Premier make() dans le traitement A"]
    C --> D["Création et stockage de l'instance A"]
    D --> E["Même instance récupérée dans le traitement A"]
    E --> F["forgetScopedInstances()<br>à la frontière du cycle de vie"]
    F --> G["Suppression des instances stockées dans le conteneur"]
    G --> H["make() du traitement B crée l'instance B"]
```

Ce traitement ne réinitialise pas les propriétés de l'ancien objet et ne réécrit pas les références détenues par d'autres objets. Un ancien objet encore référencé reste utilisable.

### C'est l'environnement d'exécution qui crée la frontière

Le conteneur lui-même ne surveille pas le début des requêtes HTTP ou des jobs.

* **Worker de file d'attente** : `QueueServiceProvider` transmet au Worker un callback qui réinitialise la portée. `Worker::daemon()` l'appelle dans sa boucle avant de récupérer le job suivant, et ce callback appelle notamment `forgetScopedInstances()` et vide les instances résolues des Facades.
* **Octane** : la configuration officielle par défaut enregistre `FlushTemporaryContainerInstances` sur `OperationTerminated`. Cet écouteur appelle `forgetScopedInstances()`.
* **Commandes résidentes ou boucles personnalisées** : répéter simplement `make()` ne crée pas de frontière. Le cycle de vie doit être conçu côté infrastructure d'exécution.

<Warning>
  N'appelez pas `forgetScopedInstances()` de votre propre initiative depuis le code applicatif classique ou depuis le `boot()` d'un package. Cela affecte tous les services scoped du conteneur. De plus, tous les chemins d'exécution des jobs, y compris l'exécution synchrone, ne passent pas forcément par le même chemin de réinitialisation que le Worker résident.
</Warning>

## Rendre scoped l'état partagé au sein d'un traitement

Prenons l'exemple d'un package de livraison qui souhaite partager l'identifiant de destination au sein d'une requête ou d'un job. Au lieu de conserver directement la Request HTTP, il ne stocke que la valeur nécessaire.

```php theme={null}
<?php

namespace Acme\Courier;

final class DeliveryContext
{
    private ?string $destinationId = null;

    public function setDestinationId(string $destinationId): void
    {
        $this->destinationId = $destinationId;
    }

    public function destinationId(): ?string
    {
        return $this->destinationId;
    }
}
```

Dans le service provider du package, enregistrez un context vide, sans lire l'état de l'application au moment de la création.

```php theme={null}
<?php

namespace Acme\Courier;

use Illuminate\Support\ServiceProvider;

class CourierServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->scoped(DeliveryContext::class);
    }
}
```

Dans le contrôleur ou la méthode `handle()` d'un job de l'application, définissez l'identifiant après avoir validé l'entrée et vérifié les autorisations. Les autres services qui résolvent ce type dans le même cycle de vie peuvent alors accéder au même context.

```php theme={null}
$context = app(\Acme\Courier\DeliveryContext::class);
$context->setDestinationId($authorizedDestinationId);
```

Ce context n'est pas un mécanisme d'autorisation. Pouvoir définir un identifiant ne signifie pas que l'accès à cette destination est autorisé. De plus, `scoped()` n'assure aucune persistance en base de données ou dans un cache externe. Les valeurs à transmettre au job suivant doivent être passées explicitement dans les données du job.

<Info>
  Avec Octane, les méthodes `register()` et `boot()` des service providers s'exécutent au démarrage initial du worker. Ne concevez pas votre package pour stocker l'utilisateur courant ou la destination dans le context depuis `boot()`. Définissez les valeurs propres à chaque traitement une fois celui-ci commencé.
</Info>

## Les références d'un singleton vers un service scoped ne sont pas mises à jour automatiquement

L'enregistrement suivant ne fonctionne pas comme prévu, même si seul le context est scoped.

```php theme={null}
$this->app->singleton(DeliveryService::class, function ($app) {
    return new DeliveryService($app->make(DeliveryContext::class));
});
```

Si `DeliveryService` stocke le context dans une propriété dans son constructeur, il conserve le premier context créé. Même si le conteneur supprime le context stocké, la propriété du `DeliveryService` déjà créé ne change pas.

Choisissez la solution selon la responsabilité du service.

1. **Passer les valeurs nécessaires à chaque traitement en argument de méthode**. Si aucun état ne doit être partagé, c'est la solution la plus simple.
2. **Rendre aussi scoped le service qui conserve la référence**. `DeliveryService` et le context sont alors tous deux recréés à chaque traitement.
3. **Passer un resolver qui résout le service au moment de l'utilisation**. Si le singleton doit être conservé, récupérez le service nécessaire depuis le conteneur courant à chaque fois.

Dans le troisième exemple, au lieu de capturer l'ancien context dans une closure, on le résout au moment de l'appel.

```php theme={null}
use Illuminate\Container\Container;

$this->app->singleton(DeliveryService::class, function () {
    return new DeliveryService(
        fn () => Container::getInstance()->make(DeliveryContext::class),
    );
});
```

Dans cet exemple, `DeliveryService` doit recevoir une `Closure` dans son constructeur et être modifié pour exposer une **API qui l'appelle à chaque opération**. Appeler le resolver une seule fois dans le constructeur et en stocker le résultat ne résout pas le problème. Il s'agit d'un exemple de conception qui applique le container resolver de la documentation officielle d'Octane à un context propre au package.

### Vérifier aussi les états que scoped ne supprime pas

`scoped()` ne supprime pas automatiquement les états suivants :

* les valeurs ajoutées aux propriétés statiques propres au package ;
* les références au context stockées dans un autre singleton ou une variable globale ;
* « l'utilisateur courant » ou « la destination courante » conservés à l'intérieur d'un singleton ;
* les références conservées par des Facades ou des couches de cache personnalisées.

Distinguez la réinitialisation effectuée par l'infrastructure d'exécution standard de celle des états propres au package. Réduire le nombre d'emplacements qui conservent un état par traitement est plus simple que de maintenir une logique de nettoyage personnalisée pour chaque environnement d'exécution.

## Écrire des tests qui franchissent une frontière dans le même conteneur

Si chaque test HTTP crée simplement une nouvelle application, vous risquez de passer à côté des problèmes liés à la réutilisation du même conteneur par un processus résident. Commencez par vérifier, au niveau du conteneur seul, le partage au sein d'un même traitement et la recréation après la frontière.

Les tests suivants utilisent PHPUnit. Le premier test utilise `forgetScopedInstances()`, comme les tests officiels du Container, pour vérifier l'effet de la frontière sans démarrer un Worker complet.

```php theme={null}
<?php

namespace Acme\Courier\Tests;

use Acme\Courier\DeliveryContext;
use Illuminate\Container\Container;
use PHPUnit\Framework\TestCase;

class ServiceLifetimeTest extends TestCase
{
    public function test_context_is_shared_then_recreated(): void
    {
        $container = new Container;
        $container->scoped(DeliveryContext::class);

        $first = $container->make(DeliveryContext::class);
        $first->setDestinationId('destination-a');

        $this->assertSame($first, $container->make(DeliveryContext::class));
        $this->assertSame('destination-a', $first->destinationId());

        $container->forgetScopedInstances();

        $second = $container->make(DeliveryContext::class);
        $this->assertNotSame($first, $second);
        $this->assertNull($second->destinationId());
    }

    public function test_singleton_keeps_a_captured_scoped_dependency(): void
    {
        $container = new Container;
        $container->scoped(DeliveryContext::class);
        $container->singleton('courier.holder', function ($app) {
            return (object) ['context' => $app->make(DeliveryContext::class)];
        });

        $holder = $container->make('courier.holder');
        $oldContext = $holder->context;

        $container->forgetScopedInstances();

        $this->assertSame($holder, $container->make('courier.holder'));
        $this->assertSame($oldContext, $holder->context);
        $this->assertNotSame($oldContext, $container->make(DeliveryContext::class));
    }
}
```

Le second test documente le comportement selon lequel les références persistent. Dans un vrai package, ajoutez aussi des tests vérifiant que la solution retenue utilise bien un nouveau context ou les valeurs actuelles.

| Vérification lors des mises à jour | Résultat attendu |
| - | - |
| Résolution répétée du context dans le même traitement | La même instance et la valeur définie sont partagées |
| Résolution de part et d'autre d'une frontière dans le même conteneur | Nouvelle instance, sans la valeur précédente |
| Utilisation du context par un singleton | L'ancien context n'est pas stocké et la valeur actuelle est utilisée |
| Requêtes Octane successives dans l'application | La deuxième ne reprend pas la destination ou autre de la première |
| Jobs successifs dans un worker de file d'attente résident | Les valeurs définies sont isolées pour chaque job |
| Mise à jour de Laravel ou d'Octane | Revérifier le chemin d'appel de la réinitialisation et la configuration de l'application |

Les tests unitaires du conteneur ne vérifient pas le chemin d'exécution réel de la sandbox d'Octane ou du worker de file d'attente. Des tests d'intégration exécutant des traitements successifs dans les environnements que vous déclarez prendre en charge sont également nécessaires. Lors d'un déploiement modifiant les méthodes d'enregistrement ou l'implémentation des services, rechargez les processus résidents pour appliquer le nouveau code.

## Pages connexes

<Columns cols={2}>
  <Card title="Hooks de résolution des services d'un package" icon="puzzle-piece" href="/fr/advanced/package-service-resolution">
    Découvrez afterResolving, la récupération des instances partagées et les différences de configuration lors des nouvelles résolutions.
  </Card>

  <Card title="Laravel Octane" icon="bolt" href="/fr/octane">
    Découvrez la conservation de la Request et du conteneur, ainsi que les points d'attention liés à l'exploitation des processus résidents.
  </Card>
</Columns>

## Sources primaires consultées

* [Documentation officielle de Laravel : Binding scoped singletons](https://github.com/laravel/docs/blob/13.x/container.md#binding-scoped-singletons)
* [Documentation officielle de Laravel : Dependency injection and Octane](https://github.com/laravel/docs/blob/13.x/octane.md#dependency-injection-and-octane)
* [Container : scoped(), singleton(), forgetScopedInstances(), resolve()](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Container/Container.php)
* [ContainerTest : partage et recréation des services scoped](https://github.com/laravel/framework/blob/v13.36.0/tests/Container/ContainerTest.php)
* [QueueServiceProvider : resetScope transmis au Worker](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Queue/QueueServiceProvider.php)
* [Worker : réinitialisation de la portée dans daemon()](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Queue/Worker.php)
* [Octane : FlushTemporaryContainerInstances](https://github.com/laravel/octane/blob/ecc9254e6d79cd607f191015f634f9be944e2267/src/Listeners/FlushTemporaryContainerInstances.php)
* [Octane : écouteurs par défaut de OperationTerminated](https://github.com/laravel/octane/blob/ecc9254e6d79cd607f191015f634f9be944e2267/config/octane.php)


## Related topics

- [Développement de packages Laravel](/fr/advanced/package-development.md)
- [Sujets avancés](/fr/advanced/index.md)
- [Hooks de résolution des services d'un package](/fr/advanced/package-service-resolution.md)
- [Enregistrement et cache des routes d'un package](/fr/advanced/package-routes.md)
- [Cycle de vie d'une requête](/fr/lifecycle.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.