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

# Levensduur van packageservices ontwerpen

> Een analyse van de interne implementatie van bind, singleton en scoped in Laravel 13: packages ontwerpen die geen state meenemen in langlevende workers, en het testen van levenscyclusgrenzen.

Om een package niet alleen bij gewone HTTP-requests maar ook in Octane en queue workers bruikbaar te maken, moet je bepalen **hoe lang een service wat mag vasthouden**. Als je data per request in een singleton opslaat, kan een andere verwerking verouderde data te zien krijgen.

Op deze pagina bekijken we de implementatie van de container, de queue worker en Octane in Laravel Framework `v13.36.0`, en verbinden we de basisregistratiemethoden van de [service container](/nl/service-container) met het langetermijnonderhoud van packages. Alleen `scoped()` kiezen betekent niet dat alle verwijzingen tussen objecten en alle statische state worden gereset.

## Kies de registratiemethode op basis van de levensduur van de service

| Registratie | Ophalen met een normale `make()` | Geschikt ontwerp |
| - | - | - |
| `bind()` | Wordt bij elke resolutie aangemaakt | Services die per bewerking aangemaakt mogen worden |
| `singleton()` | Hergebruikt binnen dezelfde container de al aangemaakte instantie | Services die geen request- of jobspecifieke state vasthouden |
| `scoped()` | Hergebruikt binnen dezelfde levenscyclus en verwijdert de opgeslagen instantie bij de grens | State die binnen één request of één job wordt gedeeld |

Deze tabel vergelijkt normale resoluties. Controleer speciale paden, zoals resolutie met expliciete parameters of contextual binding, apart.

Ook bij `bind()` blijft de verwijzing bestaan als je de opgehaalde service opslaat in een property van een andere singleton. De registratiemethode is een regel van de container voor het aanmaken en hergebruiken, geen regel die objecten die de gebruiker vasthoudt geforceerd opnieuw aanmaakt.

## scoped combineert gedeelde registratie met het bijhouden van te verwijderen sleutels

De onderzochte `Container::scoped()` doet de volgende twee dingen:

1. De registratiesleutel vastleggen in `scopedInstances`.
2. `singleton()` aanroepen en registreren als gedeelde binding.

De instantie zelf wordt, net als bij een singleton, opgeslagen in `instances`. `forgetScopedInstances()` voert `unset()` uit op de opgeslagen instanties voor de vastgelegde sleutels. Omdat de aanmaaklogica van de binding blijft bestaan, kan bij de volgende normale resolutie een nieuw object worden aangemaakt.

```mermaid theme={null}
flowchart TD
    A["scoped registreren in register()"] --> B["Aanmaakwijze en te verwijderen sleutel vastleggen"]
    B --> C["Eerste make() in verwerking A"]
    C --> D["Instantie A aanmaken en opslaan"]
    D --> E["Binnen verwerking A dezelfde instantie ophalen"]
    E --> F["forgetScopedInstances()<br>bij de levenscyclusgrens"]
    F --> G["Opgeslagen instantie uit de container verwijderen"]
    G --> H["Instantie B aanmaken bij make() in verwerking B"]
```

Dit proces initialiseert geen properties van het oude object en herschrijft geen verwijzingen die andere objecten vasthouden. Een oud object waarnaar nog een verwijzing bestaat, kan nog steeds worden gebruikt.

### De uitvoeringsomgeving maakt de grens

De container zelf houdt niet in de gaten wanneer een HTTP-request of job begint.

* **Queue worker**: `QueueServiceProvider` geeft een callback die de scope reset door aan de Worker. `Worker::daemon()` roept deze aan in de lus voordat de volgende job wordt opgehaald, en binnen de callback worden onder andere `forgetScopedInstances()` en het wissen van de geresolvede instanties van Facades uitgevoerd.
* **Octane**: De officiële standaardconfiguratie registreert `FlushTemporaryContainerInstances` bij `OperationTerminated`. Deze listener roept `forgetScopedInstances()` aan.
* **Eigen langlopende commando's of lussen**: Simpelweg herhaaldelijk `make()` aanroepen vormt geen grens. De levenscyclus moet aan de kant van het uitvoeringsplatform worden ontworpen.

<Warning>
  Roep `forgetScopedInstances()` niet op eigen initiatief aan vanuit gewone applicatielogica of vanuit `boot()` van een package. Dit beïnvloedt alle scoped services in de hele container. Bovendien doorlopen niet alle jobpaden, inclusief synchrone uitvoering, noodzakelijkerwijs hetzelfde resetpad als een langlopende Worker.
</Warning>

## Maak state die binnen een verwerking wordt gedeeld scoped

Neem als voorbeeld een bezorgpackage dat binnen één request of job de identifier van de bestemming wil delen. In plaats van de HTTP-Request direct vast te houden, slaan we alleen de benodigde waarde op.

```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;
    }
}
```

In de service provider van het package registreer je een lege context, zonder bij het aanmaken de state van de gebruiker te lezen.

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

namespace Acme\Courier;

use Illuminate\Support\ServiceProvider;

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

In een controller of in `handle()` van een job in de gebruikende applicatie stel je de identifier in nadat de invoer is gevalideerd en de rechten zijn gecontroleerd. Andere services die dit type binnen dezelfde levenscyclus resolven, zien dezelfde context.

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

Deze context is geen autorisatiemechanisme. Dat een identifier kan worden ingesteld, betekent nog niet dat toegang tot die bestemming is toegestaan. Bovendien is `scoped()` geen persistentie naar een database of externe cache. Waarden die aan de volgende job moeten worden doorgegeven, geef je expliciet mee als jobdata.

<Info>
  In Octane worden `register()` en `boot()` van service providers uitgevoerd bij de initiële start van de worker. Ontwerp niet zo dat de huidige gebruiker of bestemming in `boot()` in de context wordt opgeslagen. Stel waarden per verwerking pas in nadat de verwerking is begonnen.
</Info>

## Verwijzingen van een singleton naar scoped worden niet automatisch bijgewerkt

Bij de volgende registratie werkt het niet zoals bedoeld als je alleen de context scoped maakt.

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

Als `DeliveryService` de context in de constructor in een property opslaat, blijft hij de eerst aangemaakte context vasthouden. Ook als de opgeslagen context uit de container wordt verwijderd, veranderen de properties van de al aangemaakte `DeliveryService` niet.

Kies een oplossing die past bij de verantwoordelijkheid van de service.

1. **Geef de per verwerking benodigde waarden mee als methodeargumenten**. Als er geen state gedeeld hoeft te worden, is dit het eenvoudigst.
2. **Maak ook de vasthoudende kant scoped**. Zowel `DeliveryService` als de context worden per verwerking opnieuw aangemaakt.
3. **Geef een resolver mee die bij gebruik resolvet**. Als de singleton behouden moet blijven, haal je de benodigde service telkens op uit de huidige container.

In het derde voorbeeld wordt de oude context niet in de closure ingesloten, maar wordt hij op het moment van aanroepen geresolved.

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

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

De `DeliveryService` in dit voorbeeld moet worden aangepast zodat hij in de constructor een `Closure` ontvangt en een **API wordt die deze bij elke bewerking aanroept**. Als je de resolver in de constructor maar één keer aanroept en het resultaat opslaat, is het probleem niet opgelost. Dit is een ontwerpvoorbeeld waarbij de container resolver uit de officiële Octane-documentatie wordt toegepast op een packagespecifieke context.

### Controleer ook state die scoped niet wist

`scoped()` verwijdert de volgende state niet automatisch:

* Waarden die zijn toegevoegd aan eigen statische properties van het package
* Verwijzingen naar de context die zijn opgeslagen in een andere singleton of in globale variabelen
* Een "huidige gebruiker" of "huidige bestemming" die in een singleton wordt vastgehouden
* Verwijzingen die worden vastgehouden door eigen Facades of cachelagen

Maak onderscheid tussen resets door het standaard uitvoeringsplatform en resets van packagespecifieke state. Het aantal plekken waar state per verwerking wordt vastgehouden verminderen is eenvoudiger dan in elke uitvoeringsomgeving eigen wislogica te onderhouden.

## Schrijf tests die grenzen overschrijden binnen dezelfde container

Als HTTP-tests alleen elke keer een nieuwe applicatie aanmaken, kun je problemen missen waarbij een langlopend proces dezelfde container blijft gebruiken. Controleer eerst op containerniveau het delen binnen dezelfde verwerking en het opnieuw aanmaken na de grens.

De volgende tests zijn in PHPUnit-formaat. De eerste test gebruikt, net als de officiële Container-tests, `forgetScopedInstances()` en controleert het effect van de grens zonder de hele Worker te starten.

```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));
    }
}
```

De tweede test legt het gedrag vast dat verwijzingen blijven bestaan. Voeg aan je echte package ook tests toe die controleren dat met de gekozen oplossing een nieuwe context of nieuwe waarde wordt gebruikt.

| Controle bij updates | Verwacht resultaat |
| - | - |
| Context meerdere keren resolven binnen dezelfde verwerking | Dezelfde instantie en de ingestelde waarde worden gedeeld |
| Resolven over een grens heen binnen dezelfde container | Een nieuwe instantie zonder de vorige waarde |
| Singleton gebruikt de context | Slaat geen oude context op en gebruikt de huidige waarde |
| Opeenvolgende Octane-requests uitvoeren in de gebruikende applicatie | Het tweede request neemt de bestemming enzovoort van het eerste niet over |
| Opeenvolgende jobs uitvoeren in een langlopende queue worker | Instellingen per job zijn van elkaar gescheiden |
| Laravel of Octane bijwerken | Het aanroeppad van de reset en de configuratie van de gebruikende applicatie opnieuw controleren |

Unittests op containerniveau verifiëren niet het werkelijke uitvoeringspad van de Octane-sandbox of de queue worker. Je hebt ook integratietests nodig die opeenvolgende verwerkingen uitvoeren in de uitvoeringsomgevingen waarvoor je ondersteuning claimt. Bij deploys die de registratiemethode of de service-implementatie wijzigen, herlaad je de langlopende processen om de nieuwe code toe te passen.

## Gerelateerde pagina's

<Columns cols={2}>
  <Card title="Hooks na het resolven van packageservices" icon="puzzle-piece" href="/nl/advanced/package-service-resolution">
    Bekijk afterResolving, het ophalen van gedeelde instanties en de verschillen in configuratie bij herhaalde resolutie.
  </Card>

  <Card title="Laravel Octane" icon="bolt" href="/nl/octane">
    Bekijk aandachtspunten voor het vasthouden van de Request en de container, en voor het beheer van langlopende processen.
  </Card>
</Columns>

## Geraadpleegde primaire bronnen

* [Officiële Laravel-documentatie: Binding scoped singletons](https://github.com/laravel/docs/blob/13.x/container.md#binding-scoped-singletons)
* [Officiële Laravel-documentatie: Dependency injection and Octane](https://github.com/laravel/docs/blob/13.x/octane.md#dependency-injection-and-octane)
* [Container: scoped(), singleton(), forgetScopedInstances() en resolve()](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Container/Container.php)
* [ContainerTest: delen en opnieuw aanmaken met scoped](https://github.com/laravel/framework/blob/v13.36.0/tests/Container/ContainerTest.php)
* [QueueServiceProvider: resetScope die aan de Worker wordt doorgegeven](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Queue/QueueServiceProvider.php)
* [Worker: scope resetten in 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: standaardlisteners van OperationTerminated](https://github.com/laravel/octane/blob/ecc9254e6d79cd607f191015f634f9be944e2267/config/octane.php)


## Related topics

- [Laravel-packages ontwikkelen](/nl/advanced/package-development.md)
- [Geavanceerde onderwerpen](/nl/advanced/index.md)
- [Hooks na het resolven van packageservices](/nl/advanced/package-service-resolution.md)
- [Ontwikkelgids voor apps met de engine-API - VOICEVOX for Laravel](/nl/packages/laravel-voicevox/app-guide.md)
- [Packagecaches integreren in optimize](/nl/advanced/package-optimization.md)


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