# Dependency injection en de service container

Hoe Laravel je classes bouwt: auto resolving, bind, singleton en scoped, interfaces binden aan een implementatie, en wanneer een interface de moeite loont.

Multimedia & Creatieve Technologie · 3.1 The Agency · Zelfstudie · versie 1.0.0 · 2026-09-30

Tags: laravel, php, architecture, design-patterns

Slides: <https://slides.mctantwerp.be/nl/the-agency/04-service-container>

<!-- slide 2 -->

## Agenda

1. Dependency injection
2. De service container
3. Bindings
4. Interfaces en contracts
5. Contextual binding
6. Wanneer een interface

> **Toelichting:** De eerste twee hoofdstukken gaan over het idee, de volgende drie over de tools van Laravel, en het laatste over wanneer je ze niet moet gebruiken. Dat laatste hoofdstuk is het belangrijkste voor je dagelijkse werk: te veel abstractie kost meer tijd dan te weinig.

<!-- slide 3 -->

## Na deze les kan je

- Uitleggen wat dependency injection is, en waarom new in een service een probleem wordt
- Voorspellen wat de container zelf kan bouwen, en de twee foutmeldingen lezen als het niet lukt
- Kiezen tussen bind, singleton, scoped en instance, en uitleggen wat een singleton onder PHP-FPM en Octane betekent
- Een interface aan een implementatie binden, per omgeving, met een provider of met #\[Bind\]
- Contextual binding en attributes als #\[Config\] en #\[Storage\] gebruiken
- Beslissen wanneer een interface de moeite loont en wanneer het ceremonie is

> **Toelichting:** Gebruik deze lijst na de les als checklist. Kan je een punt niet uitleggen aan een klasgenoot, ga dan terug naar dat hoofdstuk. De huistaak op het einde raakt vooral punt 4 en 6.

<!-- slide 4 -->

## 1. Dependency injection

> **Toelichting:** Dependency injection is het idee, de container is het gereedschap. Je kan het idee toepassen zonder Laravel, en dat is precies waar dit hoofdstuk begint. Zo zie je wat de container voor je oplost voor je hem gebruikt.

<!-- slide 5 -->

### Zelf bouwen

```php
class ReminderService
{
    public function remind(Invoice $invoice): void
    {
        $sms = new TwilioSmsSender('AC81f0c2e4', 'live-token-3f9a');

        $sms->send(
            to: $invoice->client->phone,
            message: "Factuur {$invoice->number} staat nog open.",
        );
    }
}
```

Een ReminderService die zelf zijn sms-client maakt. Dat werkt, tot je hem wil testen of van provider wil wisselen.

> **Toelichting:** Het voorbeeld gebruikt een verzonnen TwilioSmsSender, geen echte SDK. Twilio heeft een eigen PHP-package met een andere API. In een echt project zou je rond zo'n SDK een eigen class schrijven, precies zoals deze, zodat de rest van je app de SDK niet rechtstreeks kent.

<!-- slide 6 -->

### Wat er mis is

- De token staat in de code, en dus in git.
- Elke test die remind() oproept, stuurt een echte sms naar een echte klant.
- Een andere provider betekent: elke new TwilioSmsSender in je project gaan zoeken.
- Aan de buitenkant zie je niet dat ReminderService een sms-client nodig heeft. Dat merk je pas als hij iets verstuurt.

> **Toelichting:** Het laatste punt heet een verborgen dependency. Wie ReminderService gebruikt, ziet aan de constructor niets, en ontdekt pas in productie dat er een sms-token nodig is. Dat soort verrassingen maakt code moeilijk om over te nemen.

<!-- slide 7 -->

### Meegeven

```php
class ReminderService
{
    public function __construct(
        private TwilioSmsSender $sms,
    ) {}

    public function remind(Invoice $invoice): void
    {
        $this->sms->send($invoice->client->phone, "Factuur {$invoice->number} staat nog open.");
    }
}
```

1. De constructor vraagt wat de class nodig heeft. `private` maakt er meteen een property van.
2. De methode gebruikt `$this->sms`. Hoe die gebouwd werd, weet ReminderService niet meer.

> **Toelichting:** Constructor promotion bestaat sinds PHP 8.0. Vroeger schreef je een property, een constructorparameter en een toewijzing $this->sms = $sms. Het resultaat is hetzelfde, alleen korter. readonly ervoor, private readonly TwilioSmsSender $sms, zorgt dat niemand de property later nog vervangt.

<!-- slide 8 -->

### Dependency injection

- Een class maakt zijn dependencies niet zelf, hij krijgt ze.
- Meestal via de constructor, soms via een methode.
- Je ziet in de constructor wat een class nodig heeft, zonder de code te lezen.
- In een test geef je een fake mee, en er vertrekt geen echte sms.
- James Shore noemde het ["a 25-dollar term for a 5-cent concept"](https://www.jamesshore.com/v2/blog/2006/dependency-injection-demystified). Het klinkt als architectuur, het is een parameter.

> **Toelichting:** Setter injection bestaat ook: een methode setSms() die je na het maken oproept. Dat zie je zelden in Laravel, want dan kan het object bestaan zonder zijn dependency, en krijg je een fout op het moment dat je hem gebruikt. Met de constructor is een object altijd compleet.

<!-- slide 9 -->

### Met de hand

```php
$reminders = new ReminderService(
    new TwilioSmsSender(
        config('services.twilio.sid'),
        config('services.twilio.token'),
    ),
);

$reminders->remind($invoice);
```

Dit is dependency injection zonder hulp, en het werkt. Het probleem is schaal: elke plek die een `ReminderService` nodig heeft, moet weten hoe je een `TwilioSmsSender` bouwt en waar de token staat. Dat werk neemt de container over.

> **Toelichting:** In kleine scripts of in een test is dit een prima manier van werken. Je ziet meteen alles wat er gebouwd wordt, zonder magie. Het is ook hoe een test er vaak uitziet als je een class los test zonder Laravel: new met een fake erin.

<!-- slide 10 -->

### Welke versie gebruikt dependency injection?

- A. new TwilioSmsSender() in de methode
- B. TwilioSmsSender $sms in de constructor
- C. app(TwilioSmsSender::class) in de methode
- D. Een static TwilioSmsSender::send()

**Antwoord:** B. TwilioSmsSender $sms in de constructor

Alleen de constructor maakt de dependency zichtbaar en vervangbaar. app() in de methode is verleidelijk, want de container bouwt het object wel, maar de dependency zit weer verstopt in de code. Dat heet service location. new en een static call zitten allebei vast aan één class.

> **Toelichting:** Service location is niet verboden. In code die niet door de container gebouwd wordt, zoals een Blade-view of een helperfunctie, is app() soms de enige optie. In een class die je zelf schrijft, is de constructor bijna altijd beter.

<!-- slide 11 -->

## 2. De service container

> **Toelichting:** De container is het hart van Laravel: bijna elk object dat Laravel voor je maakt, komt eruit. Dit hoofdstuk toont wat hij zelf kan, zonder dat je iets configureert, en waar hij stopt.

<!-- slide 12 -->

### De service container

- Eén object dat weet hoe je classes gebouwd worden.
- Het leest met reflection de constructor van een class, en bouwt eerst wat daarin staat.
- Controllers, middleware, listeners, jobs en commands komen er allemaal uit. Daarom kan je daar type-hinten.
- Jij type-hint, de container bouwt.
- [Docs: Service Container](https://laravel.com/docs/13.x/container)

> **Toelichting:** Reflection is een deel van PHP waarmee code andere code kan inspecteren, zoals de parameters van een constructor en hun types. De container gebruikt ReflectionClass om te zien wat een class nodig heeft. Dat is ook waarom type hints hier verplicht zijn: zonder type weet de container niet wat hij moet bouwen.

<!-- slide 13 -->

### De app is de container

```php
$container = app();

$reminders = app(ReminderService::class);
```

De `$app` uit `bootstrap/app.php` is de container: `Application` extends `Container`. `app()` zonder argument geeft hem, `app(X::class)` vraagt hem een X te bouwen. In een service provider heet hij `$this->app`.

> **Toelichting:** Er is ook een helper resolve(), die hetzelfde doet als app(X::class). En in een service provider heb je $this->app. Allemaal dezelfde container, drie manieren om hem te bereiken.

<!-- slide 14 -->

### Auto resolving

```php
class ReminderService
{
    public function __construct(
        private InvoiceFormatter $formatter,
    ) {}
}

class InvoiceFormatter
{
    public function __construct(
        private MoneyFormatter $money,
    ) {}
}
```

- `app(ReminderService::class)`
- Die wil een InvoiceFormatter: eerst die bouwen.
- Die wil een MoneyFormatter: eerst die bouwen.
- MoneyFormatter heeft niets nodig: new MoneyFormatter.
- Terug naar boven, tot ReminderService klaar is.

> **Toelichting:** De container onthoudt niets tussen deze calls: zonder binding bouwt hij elke keer een nieuwe ReminderService, met een nieuwe InvoiceFormatter en een nieuwe MoneyFormatter. Wil je dat anders, dan heb je een binding nodig, zie hoofdstuk 3.

<!-- slide 15 -->

### Zonder configuratie

Zolang elke constructor alleen andere classes vraagt, lost de container het zelf op, hoe diep de boom ook is. Je hebt geen enkele regel configuratie nodig. Bij de meeste classes in een Laravel-project is dat zo.

> [!TIP]
> Een class zonder constructor, of met alleen classes als parameter, hoef je nooit te registreren. Type-hint hem en klaar.

> **Toelichting:** Parameters met een standaardwaarde, zoals int $retries = 3, vult de container zelf in met die standaardwaarde. Een nullable parameter zonder standaardwaarde krijgt null. Alleen een verplichte string of int zonder waarde houdt hem tegen.

<!-- slide 16 -->

### Waar het stopt

#### Een string

```php
public function __construct(
    private string $token,
) {}
```

Unresolvable dependency resolving \[Parameter #0 \[ <required> string $token \]\]

#### Een interface

```php
public function __construct(
    private SmsSender $sms,
) {}
```

Target \[App\\Contracts\\SmsSender\] is not instantiable while building \[App\\Services\\ReminderService\].

> **Toelichting:** Bij een lange boom zie je in de melding de hele weg: while building [A, B, C]. Lees ze van achter naar voor om te zien waar de ketting brak. De exception heet BindingResolutionException en komt uit Illuminate\Contracts\Container.

<!-- slide 17 -->

### Waarom het stopt

- Een string of een int kan de container niet verzinnen. Welke token? Welke url?
- Een interface kan je niet met new maken, en de container gaat niet zoeken welke class hem implementeert.
- In beide gevallen moet jij het de container vertellen, met een binding. Dat is hoofdstuk 3.

> [!NOTE]
> Allebei zijn het een `BindingResolutionException`. Lees de melding tot het einde: ze zegt welke parameter in welke class het probleem is.

> **Toelichting:** Een class die je niet zelf geschreven hebt, zoals een SDK uit een package, heeft vaak strings in zijn constructor. Die kan je dus nooit zomaar type-hinten. Dat is geen fout van de container: het is een signaal dat je die class zelf moet leren bouwen, met een binding.

<!-- slide 18 -->

### Constructor en method injection

```php
public function __construct(
    private ReminderService $reminders,
) {}

public function store(Request $request, Invoice $invoice): RedirectResponse
{
    $this->reminders->remind($invoice);

    return back()->with('status', 'Herinnering verstuurd');
}
```

1. Constructor injection: voor wat de hele controller gebruikt.
2. Method injection: voor wat alleen deze actie nodig heeft. Werkt ook in `handle()` van een job.

> **Toelichting:** App::call([$object, 'methode']) roept een methode op en vult de parameters in via de container. Dat is wat Laravel achter de schermen doet bij controllers, jobs en listeners. Je gebruikt het zelf zelden, maar het verklaart waarom method injection daar werkt en in je eigen methodes niet.

<!-- slide 19 -->

### Een service vraagt string $apiKey in zijn constructor, zonder binding. Wat gebeurt er als je hem injecteert?

- A. De container geeft een lege string
- B. Een BindingResolutionException
- C. Laravel zoekt API_KEY in .env
- D. Het werkt tot je $apiKey gebruikt

**Antwoord:** B. Een BindingResolutionException

Unresolvable dependency: de container kan geen string verzinnen, en hij gokt ook niet. Laravel zoekt niet in .env op basis van de naam van je parameter, hoe logisch dat ook zou zijn. Geef hem een binding met een closure, of zet #\[Config('services.x.key')\] op de parameter.

> **Toelichting:** Laravel zoekt niet in .env op naam, en dat is een bewuste keuze: config hoort in config/, zodat config:cache werkt. #[Config('services.x.key')] is de korte weg naar dezelfde waarde, via het config-bestand.

<!-- slide 20 -->

## 3. Bindings

> **Toelichting:** Bindings zijn de manier om de container te vertellen wat hij niet zelf kan raden: hoe een class met strings gebouwd wordt, welke implementatie bij een interface hoort, en hoe lang een object mag leven. Die laatste vraag is de belangrijkste van het hoofdstuk.

<!-- slide 21 -->

### bind

*app/Providers/AppServiceProvider.php*

```php
public function register(): void
{
    $this->app->bind(TwilioSmsSender::class, fn () => new TwilioSmsSender(
        sid: config('services.twilio.sid'),
        token: config('services.twilio.token'),
    ));
}
```

1. Elke keer dat iemand een TwilioSmsSender vraagt, draait deze closure opnieuw.
2. `config()` staat in de closure. De waarde wordt pas gelezen als iemand het object vraagt.

> **Toelichting:** bindIf(), singletonIf() en scopedIf() zetten alleen een binding als er nog geen is. Packages gebruiken dat, zodat jouw eigen binding in je app voorrang krijgt. De closure krijgt ook de container zelf mee als argument, handig om er andere objecten uit te halen.

<!-- slide 22 -->

### Vier soorten

| Methode | Wat je krijgt | Gebruik het voor |
| --- | --- | --- |
| `bind` | Bij elke resolve een nieuw object | De standaard, services zonder state |
| `singleton` | Eén object, zolang het proces leeft | Iets dat duur is om te bouwen, zonder data van een user |
| `scoped` | Eén object per request of per job | State die bij één request hoort: het huidige team, de tenant |
| `instance` | Een object dat je zelf al gemaakt hebt | Tests, of een object van buiten Laravel |

[Docs: binding basics](https://laravel.com/docs/13.x/container#binding-basics)

> **Toelichting:** Een binding zet je altijd in register(), zoals je in de les over de lifecycle zag. In een provider met veel eenvoudige bindings kan je ook de properties $bindings en $singletons gebruiken, een array van interface naar class. Laravel zet ze dan zelf.

<!-- slide 23 -->

### Zelfde object

Kies een binding en waar je app draait, en kijk welk object elke resolve teruggeeft.

Twee keer app(Cart::class) in request 1, twee keer in request 2, met elk type binding:
- bind: altijd een nieuw object.
- singleton en instance: één object per proces. Onder PHP-FPM is elke request een nieuw proces, onder Octane of een queue worker delen de requests hetzelfde object.
- scoped: één object per request of job, overal.

> **Toelichting:** Het lab houdt het eenvoudig met twee requests. In werkelijkheid draait Octane meerdere workers naast elkaar, en elke worker heeft zijn eigen container. Een singleton bestaat dan één keer per worker, niet één keer voor de hele server.

<!-- slide 24 -->

### Wat het lab toont

- bind geeft altijd een nieuw object, ook twee keer in dezelfde request.
- Onder PHP-FPM boot elke request de app opnieuw. Een singleton leeft dan één request, net als scoped.
- Onder Octane of in een queue worker blijft het proces draaien, en de singleton met hem. Request 2 krijgt het object van request 1.
- Handig voor iets duurs zonder state. Gevaarlijk voor alles wat bij één user hoort: zie de les over design patterns.

> **Toelichting:** Een queue worker gedraagt zich zoals Octane, maar met jobs in plaats van requests. Scoped bindings worden ook tussen jobs opgekuist, singletons niet. Daarom gelden de regels uit de les over design patterns ook voor jobs.

<!-- slide 25 -->

### singleton in de praktijk

```php
$this->app->singleton(ExchangeRates::class, fn () => new ExchangeRates(
    apiKey: config('services.rates.key'),
));
```

ExchangeRates haalt wisselkoersen op en houdt daarvoor een HTTP-client bij. Duur om te bouwen, en niets erin hoort bij één user. Een goede kandidaat: elke plek in de request krijgt hetzelfde object.

> **Toelichting:** Wanneer is iets duur om te bouwen? Een HTTP-client met een verbinding, een object dat een groot config-bestand inleest, een SDK die bij het starten een token ophaalt. Een object met twee properties is niet duur: daar wint een singleton niets.

<!-- slide 26 -->

### scoped in de praktijk

```php
$this->app->scoped(CurrentTeam::class, fn () => new CurrentTeam(
    auth()->user()?->currentTeam,
));
```

Het team van de ingelogde user hoort bij één request. `scoped` bouwt het één keer per request, en Octane en de queue worker gooien het weg voor de volgende. Onder PHP-FPM gedraagt het zich als een singleton, onder Octane veilig.

> **Toelichting:** In een project zonder Octane lijkt scoped overbodig, omdat singleton hetzelfde doet. Kies toch scoped voor alles wat bij een user hoort: dan is je code klaar voor de dag dat iemand Octane aanzet, of dat de class in een queue job gebruikt wordt.

<!-- slide 27 -->

### Attributes

*app/Services/ExchangeRates.php*

```php
use Illuminate\Container\Attributes\Config;
use Illuminate\Container\Attributes\Singleton;

#[Singleton]
class ExchangeRates
{
    public function __construct(
        #[Config('services.rates.key')] private string $apiKey,
    ) {}
}
```

1. `#[Singleton]` doet hetzelfde als `singleton()` in een provider, maar staat bij de class.
2. `#[Config]` vult de string in uit config. Er is geen regel in een provider nodig.

> **Toelichting:** #[Scoped] werkt net als #[Singleton], maar voor één request of job. Er bestaat ook een attribute #[Give] om op een parameter een specifieke class te vragen. Attributes staan bij de class, bindings in een provider: kies één stijl per project, zodat iedereen weet waar hij moet zoeken.

<!-- slide 28 -->

### Wisselen in een test

*tests/Feature/RemindersTest.php*

```php
it('stuurt een herinnering', function () {
    $invoice = Invoice::factory()->create();

    $this->mock(TwilioSmsSender::class, function (MockInterface $mock) {
        $mock->expects('send');
    });

    $this->post(route('reminders.store', $invoice))->assertRedirect();
});
```

1. `$this->mock()` maakt een nep-TwilioSmsSender en zet die met `instance()` in de container.
2. De controller vraagt een TwilioSmsSender en krijgt de mock. Er vertrekt geen sms.

> **Toelichting:** $this->mock() geeft de mock ook terug, zodat je na de request nog kan controleren wat er opgeroepen werd. Met $this->partialMock() vervang je maar een paar methodes en laat je de rest echt draaien. De les over fakes en mocks gaat dieper op mocking in.

<!-- slide 29 -->

### Cart is een singleton. Onder PHP-FPM doen twee klanten na elkaar een request. Krijgen ze hetzelfde Cart-object?

- A. Ja, dat is wat singleton betekent
- B. Nee, elke request is een nieuw proces
- C. Alleen als ze dezelfde sessie hebben
- D. Alleen als de config gecached is

**Antwoord:** B. Nee, elke request is een nieuw proces

Onder PHP-FPM boot elke request de app opnieuw, dus ook de container, en een singleton leeft één request. 'Dat is wat singleton betekent' klinkt juist, maar singleton betekent één object per container, niet één voor de hele server. Onder Octane of in een queue worker blijft de container wel staan, en dan klopt het antwoord ja.

> **Toelichting:** Octane staat voor veel projecten nog niet aan, en daarom duikt deze fout vaak pas op bij een migratie naar Octane. Test je code met een queue job, dan zie je het gedrag van een blijvend proces ook zonder Octane.

<!-- slide 30 -->

### Een service zonder state, goedkoop om te bouwen, met alleen classes in zijn constructor. Hoe registreer je hem?

- A. Met bind in een provider
- B. Met singleton, dat is sneller
- C. Niet, de container bouwt hem zelf
- D. Met instance in boot()

**Antwoord:** C. Niet, de container bouwt hem zelf

Auto resolving doet het werk: type-hint hem en je krijgt een nieuw object, net als met bind. Een bind-regel toevoegen is dus overbodig. Singleton lijkt sneller, maar voor iets goedkoops win je niets, en je riskeert state die blijft hangen onder Octane.

> **Toelichting:** Een binding die niets toevoegt, is ruis: wie de provider leest, denkt dat er iets speciaals aan de class is. Registreer alleen wat de container niet zelf kan, of wat een andere levensduur nodig heeft.

<!-- slide 31 -->

## 4. Interfaces en contracts

> **Toelichting:** Interfaces zijn het deel van dit onderwerp waar je het meest mee kan winnen, en het meest mee kan overdrijven. Dit hoofdstuk toont de winst, het laatste hoofdstuk de prijs.

<!-- slide 32 -->

### Een interface

```php
interface SmsSender
{
    public function send(string $to, string $message): void;
}

class LogSmsSender implements SmsSender
{
    public function send(string $to, string $message): void
    {
        Log::info("SMS naar {$to}: {$message}");
    }
}
```

1. De interface zegt wat een sms-sender kan, zonder één regel code.
2. LogSmsSender belooft `send()` te hebben. Lokaal zie je in de log wat er verstuurd zou zijn.

> **Toelichting:** Een interface kan geen properties hebben en geen code, alleen methodes en constanten. Wil je gedeelde code tussen implementaties, dan heb je een abstracte class of een trait nodig. In Laravel zie je vaak een interface met een trait ernaast, zoals ShouldQueue met de trait Queueable.

<!-- slide 33 -->

### Twee implementaties

```php
class TwilioSmsSender implements SmsSender { /* de echte API */ }

class LogSmsSender implements SmsSender { /* schrijft naar de log */ }
```

Allebei beloven ze `send()`. Code die `SmsSender` vraagt, werkt met allebei, en weet niet welke hij krijgt. Dat beslist de container, en dus beslis jij het op één plek.

> **Toelichting:** Een map app/Contracts voor je eigen interfaces is een Laravel-gewoonte, naar het voorbeeld van Illuminate\Contracts. Niet verplicht: sommige teams zetten de interface naast de implementaties, bijvoorbeeld in app/Sms.

<!-- slide 34 -->

### Binden

#### AppServiceProvider

```php
public function register(): void
{
    $this->app->bind(
        SmsSender::class,
        TwilioSmsSender::class,
    );
}
```

Vraagt iemand een SmsSender, geef dan een TwilioSmsSender.

#### ReminderService

```php
public function __construct(
    private SmsSender $sms,
) {}
```

ReminderService vraagt de interface. Welke provider erachter zit, weet hij niet meer.

> **Toelichting:** bind met twee class-namen is een verkorte vorm. De container bouwt de TwilioSmsSender zelf, met auto resolving of een eigen binding voor die class. Zo kan je de interface-binding en de manier waarop de class gebouwd wordt, los van elkaar houden.

<!-- slide 35 -->

### Per omgeving

*app/Contracts/SmsSender.php*

```php
#[Bind(TwilioSmsSender::class)]
#[Bind(LogSmsSender::class, environments: ['local', 'testing'])]
interface SmsSender
{
    public function send(string $to, string $message): void;
}
```

1. Standaard geeft de container een TwilioSmsSender.
2. Lokaal en in tests krijg je LogSmsSender. Tijdens het ontwikkelen vertrekt er geen enkele sms.

Geen regel in een provider nodig. [Docs: Bind attribute](https://laravel.com/docs/13.x/container#bind-attribute)

> **Toelichting:** Een #[BindWhen]-attribute bestaat ook, voor een binding die van een voorwaarde afhangt, zoals een feature flag. Die vraagt PHP 8.5 of hoger. De attributes worden gelezen in de volgorde waarin ze staan, dus de eerste die past wint.

<!-- slide 36 -->

### Wat je wint

- Lokaal en in tests gaat er nooit een echte sms weg.
- Stel: halverwege het project kiest de klant een goedkopere sms-provider. Met de interface is dat een nieuwe class en één regel. Zonder ga je elke new en elke static call zoeken.
- Een klant met een eigen provider: dezelfde interface, een andere binding.
- De controller en de service kennen alleen `SmsSender`.

> **Toelichting:** In een klantproject is het derde punt vaak de doorslag: klant A heeft een contract met de ene provider, klant B met een andere. Zelfde code, andere binding per klant. Met een manager uit de les over design patterns kan je zelfs per bericht kiezen.

<!-- slide 37 -->

### Laravel's eigen contracts

| Contract | Facade |
| --- | --- |
| `Illuminate\Contracts\Cache\Repository` | `Cache` |
| `Illuminate\Contracts\Mail\Mailer` | `Mail` |
| `Illuminate\Contracts\Filesystem\Filesystem` | `Storage::disk()` |
| `Illuminate\Contracts\Queue\Queue` | `Queue` |
| `Illuminate\Contracts\Hashing\Hasher` | `Hash` |

Achter elke facade zit een interface. Type-hint ze en je krijgt wat in config staat: redis of file, s3 of local. [Docs: Contracts](https://laravel.com/docs/13.x/contracts)

> **Toelichting:** Contracts zijn vooral nuttig in packages die met Laravel samenwerken zonder de volledige framework-code te vereisen. In je eigen app is een facade meestal even goed, zoals je in de les over de lifecycle zag. Kies wat je team leesbaar vindt.

<!-- slide 38 -->

### Je vergeet de binding voor SmsSender. Wat zegt Laravel?

- A. Target \[App\\Contracts\\SmsSender\] is not instantiable
- B. Class SmsSender not found
- C. Niets, het neemt de eerste class die de interface implementeert
- D. Niets, tot je send() oproept

**Antwoord:** A. Target \[App\\Contracts\\SmsSender\] is not instantiable

Een interface kan je niet met new maken, dus de container stopt met 'not instantiable'. Hij gaat niet zoeken naar classes die de interface implementeren: welke zou hij moeten kiezen als er twee zijn? 'Class not found' krijg je alleen als de interface zelf niet bestaat.

> **Toelichting:** Class not found krijg je als de interface zelf niet bestaat, bijvoorbeeld door een tikfout in de namespace of een vergeten use-statement. Dat is een andere fout en een ander probleem: controleer dan eerst je imports.

<!-- slide 39 -->

## 5. Contextual binding

> **Toelichting:** Contextual binding lost een specifiek probleem op: hetzelfde type, maar een andere waarde afhankelijk van wie het vraagt. Je komt het vooral tegen bij disks, loggers en databaseconnecties.

<!-- slide 40 -->

### Twee classes, twee disks

Facturen moeten op een private disk, avatars op de publieke. Beide controllers vragen een Filesystem.

```php
$this->app->when(InvoicePdfController::class)
    ->needs(Filesystem::class)
    ->give(fn () => Storage::disk('invoices'));

$this->app->when(AvatarsController::class)
    ->needs(Filesystem::class)
    ->give(fn () => Storage::disk('public'));
```

1. Vraagt InvoicePdfController een Filesystem, dan krijgt hij de private disk.
2. AvatarsController krijgt de publieke disk. Zelfde type, andere implementatie.

> **Toelichting:** needs() kan ook een primitive nemen: needs('$apiKey')->give('...') of ->giveConfig('services.x.key'). Zo geef je een string mee aan één class zonder een volledige closure te schrijven voor de hele class.

<!-- slide 41 -->

### Met een attribute

```php
class InvoicePdfController
{
    public function __construct(
        #[Storage('invoices')] private Filesystem $disk,
        #[Config('app.timezone')] private string $timezone,
    ) {}
}
```

1. Dezelfde contextual binding, maar op de parameter zelf. Geen regel in een provider.
2. `#[Config]` werkt ook voor strings, dus ook voor het probleem uit hoofdstuk 2.

> **Toelichting:** Een eigen contextual attribute kan je ook maken: een class die ContextualAttribute implementeert, met een static resolve()-methode. De docs tonen hoe je zo de Config-attribute zelf zou schrijven. Handig als je in een project vaak hetzelfde type waarde injecteert, zoals het huidige team.

<!-- slide 42 -->

### Welke attributes er zijn

- `#[Storage('s3')]` geeft een disk, `#[Config('app.timezone')]` een waarde uit config.
- `#[Cache('redis')]`, `#[DB('mysql')]` en `#[Log('daily')]` geven een cache store, een databaseconnectie en een logkanaal.
- `#[CurrentUser] User $user` geeft de ingelogde gebruiker, ook in een route of een controller-methode.
- De volledige lijst: [Contextual attributes](https://laravel.com/docs/13.x/container#contextual-attributes).

> **Toelichting:** Zet een route met #[CurrentUser] achter de auth-middleware, zoals het voorbeeld in de docs doet: zonder ingelogde user is er niets om in te vullen. De andere attributes werken ook in de constructor van een job of een listener, overal waar de container bouwt.

<!-- slide 43 -->

### Twee controllers vragen een Filesystem, maar moeten elk een andere disk krijgen. Wat gebruik je?

- A. Contextual binding of een #\[Storage\]-attribute
- B. Een singleton per disk
- C. Twee aparte interfaces
- D. instance() in elke controller

**Antwoord:** A. Contextual binding of een #\[Storage\]-attribute

Contextual binding is precies hiervoor: hetzelfde type, een andere waarde afhankelijk van wie erom vraagt. Twee interfaces lijken netjes, maar het is twee keer hetzelfde contract, alleen om de container iets te vertellen. Een attribute zegt het korter.

> **Toelichting:** Het antwoord met instance() is ook verleidelijk, maar instance() zet één object voor iedereen die dat type vraagt, niet per controller. Het zou dus beide controllers dezelfde disk geven.

<!-- slide 44 -->

## 6. Wanneer een interface

> **Toelichting:** Dit hoofdstuk gaat over een keuze die je in elk project maakt. Er is geen regel die altijd klopt, wel een vraag die bijna altijd helpt: welke tweede implementatie bestaat er, of komt er zeker?

<!-- slide 45 -->

### Ceremonie

- Een UserRepositoryInterface met precies één EloquentUserRepository erachter.
- Een interface voor elke service, "voor later". Later komt zelden.
- Elke wijziging raakt dan twee bestanden, en Go to definition brengt je naar de interface in plaats van naar de code.

> **Toelichting:** Het repository pattern boven Eloquent is het bekendste voorbeeld van dit soort ceremonie. De les over application architecture gaat erop in, en toont ook wanneer een repository wel zin heeft, bijvoorbeeld bij een externe API of een oude database.

<!-- slide 46 -->

### Loont

- Externe diensten die kunnen wisselen: sms, betalingen, pdf, een boekhoudpakket.
- Twee implementaties tegelijk: per klant, per land, per omgeving.
- Een nep-implementatie voor lokaal, zoals LogSmsSender.
- Code die je in een package deelt, zodat anderen hun eigen implementatie kunnen binden.

> **Toelichting:** De laatste reden, packages, is waarom Laravel zelf zoveel interfaces heeft: het framework moet werken met implementaties die de makers nooit gezien hebben. Jouw klantproject heeft die eis bijna nooit.

<!-- slide 47 -->

### Mocken is geen reden

```php
$this->mock(ReminderService::class, function (MockInterface $mock) {
    $mock->expects('remind');
});
```

Een veelgehoorde reden voor een interface: anders kan ik niet mocken. In Laravel klopt dat niet. `$this->mock()` werkt op een gewone class, zolang die niet `final` is. Je hebt een interface pas nodig als er echt twee implementaties zijn.

> **Toelichting:** Een final class kan je niet mocken met Mockery, omdat een mock een subclass maakt. Sommige teams maken alle classes final voor strengere code. Dan heb je voor wat je wil mocken wel een interface nodig, of je laat final weg op die classes.

<!-- slide 48 -->

**Begin zonder interface.**

Voeg er een toe op de dag dat er een tweede implementatie komt. PhpStorm doet die refactor, Extract Interface, in een minuut.

> **Toelichting:** Extract Interface zit in PhpStorm onder Refactor. Het maakt een interface van de publieke methodes van een class en kan de type hints in je code meteen aanpassen. In VS Code gaat het met de hand, maar het blijft een klein werk voor één class.

<!-- slide 49 -->

### Stel: veertig interfaces

Stel: je neemt een project over met een interface voor elke service, elk met precies één implementatie. Elke nieuwe methode schrijf je twee keer. Wie de code leest, springt telkens eerst naar de interface. En niemand weet nog welke interface ooit een tweede implementatie moest krijgen.

> [!TIP]
> Vraag bij elke interface: welke tweede implementatie bestaat er, of komt er zeker? Geen antwoord, dan is het ceremonie.

> **Toelichting:** Een scenario zoals dit ontstaat zelden bewust. Meestal begon iemand met goede bedoelingen, of volgde een tutorial uit een ander framework. Opkuisen kan stap voor stap: vervang telkens één interface met één implementatie door de class zelf, en draai je tests.

<!-- slide 50 -->

### Waar loont een interface het meest?

- A. Een ProjectService die alleen Eloquent gebruikt
- B. Een betaalprovider die per klant kan verschillen
- C. Een helper die datums formatteert
- D. Een controller

**Antwoord:** B. Een betaalprovider die per klant kan verschillen

Twee of meer implementaties, of een externe dienst die kan wisselen: daar verdient een interface zich terug. De ProjectService is de verleidelijke keuze omdat hij belangrijk voelt, maar met één implementatie is de interface een extra bestand zonder winst. Testen kan ook zonder.

> **Toelichting:** Een ProjectService voelt belangrijk, en belangrijk voelt als een reden voor abstractie. Maar de vraag is niet hoe belangrijk een class is, wel of er een tweede implementatie komt. Voor een betaalprovider die per klant verschilt, is het antwoord ja.

<!-- slide 51 -->

### Opdracht: Wissel van sms-provider

In je Agency-project of een nieuw Laravel-project.

1. Maak een interface `App\Contracts\SmsSender` met `send(string $to, string $message): void`.
2. Schrijf `LogSmsSender` en `HttpSmsSender`. Die tweede doet een POST naar een url uit `config/services.php`. Test hem met [webhook.site](https://webhook.site).
3. Bind lokaal `LogSmsSender` en in productie `HttpSmsSender`, met `#[Bind]` op de interface, en injecteer SmsSender in een ReminderService.
4. Klaar als je van implementatie wisselt door alleen `APP_ENV` aan te passen, en een Pest-test met `$this->mock()` slaagt zonder dat er een request vertrekt.

Vastgelopen? Slide 35 (per omgeving), 28 (wisselen in een test) en de docs over de [service container](https://laravel.com/docs/13.x/container).

> **Toelichting:** Begin met de interface en LogSmsSender, en test met een route die de ReminderService oproept. Kijk in storage/logs/laravel.log of het bericht er staat. Voor HttpSmsSender gebruik je de Http-facade: Http::post($url, [...]). Webhook.site toont elke request die binnenkomt.

## Begrippen

- **Dependency**: Een object dat een class nodig heeft om zijn werk te doen, zoals een sms-client in een ReminderService. (slide 5)
- **Dependency injection**: Een class maakt zijn dependencies niet zelf met new, hij krijgt ze binnen, meestal via de constructor. Zo zie je wat hij nodig heeft en geef je in een test een fake mee. (slide 4)
- **Constructor injection**: Dependencies als parameters van de constructor, met constructor promotion meteen een property. De standaard in Laravel. (slide 7)
- **Constructor promotion**: private of public voor een constructorparameter zetten, zodat PHP er meteen een property van maakt. Scheelt een property en een toewijzing per dependency. (slide 7)
- **Method injection**: Een dependency als parameter van een methode die Laravel zelf oproept, zoals een controller-actie of handle() van een job. Zelf doen kan met App::call(). (slide 18)
- **Service location**: app(X::class) midden in je code in plaats van X in de constructor. De container bouwt het wel, maar de dependency zit weer verstopt. (slide 10)
- **Service container**: Het object dat weet hoe je classes gebouwd worden en ze voor je maakt. In Laravel is de applicatie zelf de container: Application extends Container. (slide 11)
- **Auto resolving**: De container leest met reflection de constructor van een class en bouwt eerst wat daarin staat, recursief. Voor classes met alleen andere classes als parameter heb je geen configuratie nodig. (slide 14)
- **BindingResolutionException**: De fout als de container iets niet kan bouwen: een string zonder waarde (Unresolvable dependency) of een interface zonder binding (Target is not instantiable). (slide 16)
- **Binding**: Een regel in de container die zegt hoe een class of interface gebouwd wordt. Je zet ze in register() van een service provider, of met een attribute. (slide 21)
- **bind**: Een binding waarbij elke resolve een nieuw object oplevert. De standaard. (slide 21)
- **singleton**: Een binding die één object bouwt en dat daarna altijd teruggeeft, zolang het proces leeft. Onder PHP-FPM is dat één request, onder Octane of in een queue worker veel langer. (slide 22)
- **scoped**: Een binding die één object per request of per job bouwt. Octane en de queue worker gooien het weg voor de volgende request of job. (slide 22)
- **instance**: Een object dat je zelf al gemaakt hebt en in de container zet. Vooral in tests, $this->mock() doet het achter de schermen. (slide 22)
- **#\[Singleton\]**: Een attribute op een class of interface dat de container zegt: maar één keer bouwen. Hetzelfde als singleton() in een provider. #\[Scoped\] bestaat ook. (slide 27)
- **Interface**: Een lijst methodes zonder code. Een class die de interface implementeert, belooft die methodes te hebben. Code die de interface type-hint, werkt met elke implementatie. (slide 32)
- **#\[Bind\]**: Een attribute op een interface dat zegt welke class de container moet geven, eventueel per omgeving met environments. (slide 35)
- **Contract**: Laravel's naam voor de interfaces van zijn eigen services, in Illuminate\\Contracts. Mailer, Cache Repository, Filesystem: elke facade heeft er een achter zich. (slide 37)
- **Contextual binding**: Een binding die afhangt van wie erom vraagt: when(X::class)->needs(Filesystem::class)->give(...). Zo krijgt de ene controller een andere disk dan de andere. (slide 40)
- **Contextual attribute**: Een attribute op een parameter dat de container zegt wat hij moet geven, zoals #\[Storage('invoices')\], #\[Config('app.timezone')\] of #\[CurrentUser\]. (slide 41)
- **Mock**: Een nep-object dat zich voordoet als een echte class en controleert hoe het gebruikt wordt. Met $this->mock() zet je het in de container, dus Laravel geeft het overal waar die class gevraagd wordt. (slide 28)

## Links en bronnen

### In de presentatie

- [Dependency Injection Demystified](https://www.jamesshore.com/v2/blog/2006/dependency-injection-demystified) (slide 8): James Shore, 2006: "a 25-dollar term for a 5-cent concept"
- [Service Container](https://laravel.com/docs/13.x/container) (slide 12, slide 51): bind, singleton, scoped, instance, contextual binding en de attributes
- [Docs: binding basics](https://laravel.com/docs/13.x/container#binding-basics) (slide 22)
- [Docs: Bind attribute](https://laravel.com/docs/13.x/container#bind-attribute) (slide 35)
- [Contracts](https://laravel.com/docs/13.x/contracts) (slide 37): De interfaces van Laravel zelf, en contracts tegenover facades
- [Contextual attributes](https://laravel.com/docs/13.x/container#contextual-attributes) (slide 42)
- [webhook.site](https://webhook.site) (slide 51)

### Bronnen

- [Service Providers](https://laravel.com/docs/13.x/providers): Waar bindings thuishoren
- [Mocking](https://laravel.com/docs/13.x/mocking): $this->mock() en $this->instance() in een test
- [Octane: dependency injection](https://laravel.com/docs/13.x/octane#dependency-injection-and-octane): Waarom een singleton onder Octane blijft leven
- [Illuminate\\Container\\Container](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Container/Container.php): De foutmeldingen bij een primitive of een interface zonder binding
