# De Laravel lifecycle

Wat er gebeurt tussen de url in de browser en de response: index.php, de kernel, service providers, middleware, router en terminate.

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

Tags: laravel, php, architecture

Slides: <https://slides.mctantwerp.be/nl/the-agency/03-lifecycle>

<!-- slide 2 -->

## Agenda

1. De weg van een request
2. Bootstrap
3. Service providers
4. Middleware
5. Router en controller
6. Na de response
7. Facades
8. Andere ingangen

> **Toelichting:** De hoofdstukken volgen de weg van één request, van de eerste regel tot na de response. Facades en de andere ingangen staan op het einde, omdat je ze pas snapt als je die weg kent. De volgende les, over de service container, bouwt op hoofdstuk 3 en 7.

<!-- slide 3 -->

## Na deze les kan je

- De weg van een request door Laravel uittekenen, van public/index.php tot terminate()
- Kiezen of code in register() of boot() van een service provider hoort, en uitleggen waarom
- Een middleware schrijven die voor én na de controller werkt, en ze op de juiste plaats registreren
- Werk na de response doen met defer() of een terminable middleware
- Uitleggen wat Cache::get() echt doet, en waarom een queue worker je nieuwe code niet ziet

> **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 de eerste vier punten.

<!-- slide 4 -->

## 1. De weg van een request

> **Toelichting:** Bijna elke vreemde fout in Laravel wordt duidelijk als je weet waar in de weg van een request je zit. Een user die null is, een sessie die leeg is, een header die ontbreekt: het antwoord is meestal dat de code te vroeg of op de verkeerde plek draait.

<!-- slide 5 -->

**Eén ingang.**

De homepage, een formulier, een webhook van Mollie: alles komt binnen in `public/index.php`. Er is geen PHP-bestand per pagina zoals in je eerste jaar. De webserver stuurt alles door één deur, en Laravel beslist daarna wat er gebeurt. Dat heet een front controller.

> **Toelichting:** Het front controller pattern is niet uniek voor Laravel: Symfony, Rails en de meeste moderne frameworks werken zo. Het grote voordeel is dat alles wat voor elke request moet gebeuren, zoals sessies en beveiliging, op één plek zit. In het eerste jaar moest elk PHP-bestand zelf session_start() oproepen.

<!-- slide 6 -->

### Van request tot response

Volg de request stap voor stap. Kies daarna een ander scenario en kijk welke stappen wegvallen, en waar de request omkeert.

Een request door Laravel, stap voor stap: public/index.php → bootstrap/app.php → HTTP kernel → Service providers → Global middleware → Router → Route middleware → Controller → Route middleware → Global middleware → send() → terminate().
- Alles gaat goed (GET /projecten/12): 200 OK.
- Niet ingelogd (GET /projecten/12): 302 naar /login. auth vindt geen user en gooit een AuthenticationException. De exception handler maakt er een redirect naar /login van. De controller draait nooit.
- Route bestaat niet (GET /projectn): 404 Not Found. Geen route past. De handler rendert een 404, zonder dat de web group gedraaid heeft: geen sessie, dus ook geen ingelogde user op je 404-pagina.
- Validatie faalt (POST /projecten): 302 terug, met errors. Laravel resolvet eerst de parameters van store(). De Form Request valideert, faalt en gooit een ValidationException. Je methode begint er nooit aan.

> **Toelichting:** Kijk na elke scenario naar de lijst links: doorstreepte stappen zijn stappen die niet draaien. Dat een exception bij een gast of bij validatie toch netjes terug door de middleware gaat, komt doordat Laravel de exception in elke laag opvangt en er meteen een response van maakt. Daarom kan een middleware na $next ook op een foutpagina nog een header zetten.

<!-- slide 7 -->

### Wat de scenario's tonen

- Middleware kan een request tegenhouden. Bij een gast draait de controller nooit, maar de redirect gaat wel terug door de lagen erbuiten.
- Past er geen route, dan draait de web group niet. Geen sessie, dus op je 404-pagina is niemand ingelogd.
- Een Form Request valideert voor je methode begint. Faalt de validatie, dan kom je nooit in de body van store().
- terminate() draait altijd, ook na een fout of een redirect. Een goede plek om te loggen.

> **Toelichting:** Het derde punt verklaart een klassieke vraag: waarom zie ik mijn dd() in de controller niet? Als de validatie faalt, bereik je de body van je methode nooit. Zet je dd() dan in de Form Request, of kijk in de sessie naar de errors.

<!-- slide 8 -->

### public/index.php

```php
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
    require $maintenance;
}

require __DIR__.'/../vendor/autoload.php';

$app = require_once __DIR__.'/../bootstrap/app.php';
$app->handleRequest(Request::capture());
```

1. Staat de app in onderhoud (`php artisan down`), dan stopt het hier, nog voor Composer laadt.
2. De Composer autoloader. Vanaf nu vindt PHP elke class zonder dat je iets moet requiren.
3. `bootstrap/app.php` maakt de app, en `handleRequest()` stuurt de request erdoor.

> **Toelichting:** De maintenance-check staat voor de autoloader zodat een onderhoudspagina zelfs werkt als je vendor-map net half vervangen is tijdens een deploy. Met php artisan down --secret=een-geheim kan jij via een geheime url toch binnen, terwijl de rest de onderhoudspagina ziet.

<!-- slide 9 -->

### LARAVEL_START

```php
define('LARAVEL_START', microtime(true));
```

De eerste regel van `public/index.php`, nog voor alles hierboven. Hij onthoudt wanneer de request binnenkwam. Trek hem af van `microtime(true)` op het einde, en je weet hoe lang de hele request duurde. Dat doe je in de huistaak.

> **Toelichting:** microtime(true) geeft de tijd in seconden, met microseconden achter de komma. Laravel gebruikt LARAVEL_START zelf ook, bijvoorbeeld om trage requests te melden met whenRequestLifecycleIsLongerThan() op de kernel. Debugbar toont dezelfde totale tijd.

<!-- slide 10 -->

### Alles gaat via public

- De document root van je site is `public/`, niet de map van je project. Dat stel je in bij je webserver of je hosting.
- Bestaat het bestand in public (een afbeelding, je gebuilde css), dan stuurt de webserver dat bestand. PHP komt er niet aan te pas.
- Al de rest gaat naar `index.php`, en dus naar Laravel.
- Stel dat de document root op de projectmap staat: dan kan iedereen `agency.be/.env` downloaden, met je databasewachtwoord en je Mollie key. Check het bij elke nieuwe server.

> **Toelichting:** Herd en Forge zetten de document root voor jou goed. Bij shared hosting met cPanel moet je het vaak zelf doen, en staat hij standaard op public_html. Een veelgebruikte oplossing is de inhoud van public in public_html zetten en de rest van het project een map hoger, buiten de publieke map.

<!-- slide 11 -->

## 2. Bootstrap

> **Toelichting:** Bootstrap is het deel waar je het minst aan komt, en daarom het deel dat je het snelst vergeet. Maar in bootstrap/app.php registreer je middleware en pas je foutafhandeling aan, dus je moet er de weg vinden.

<!-- slide 12 -->

### bootstrap/app.php

```php
return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware): void {
        //
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        //
    })->create();
```

Zo ziet hij eruit in een nieuw Laravel 13-project. De lege closures vul je zelf in.

> **Toelichting:** Kijk in je eigen project naar dit bestand. Staat er een api-regel in withRouting, dan heeft iemand php artisan install:api gedraaid. De health-route op /up geeft een 200 als de app boot, en hostingplatformen gebruiken die om te checken of je deploy gelukt is.

<!-- slide 13 -->

### Wat elke methode doet

| Methode | Wat het doet |
| --- | --- |
| `configure()` | Maakt de applicatie. Die is meteen de service container, waar de volgende les over gaat. |
| `withRouting()` | Welke route-bestanden geladen worden, en een health check op /up voor je hosting. |
| `withMiddleware()` | Middleware toevoegen, groepen aanpassen, aliases geven. Zie hoofdstuk 4. |
| `withExceptions()` | Hoe fouten gelogd en getoond worden. |
| `create()` | Geeft de afgewerkte Application terug aan index.php. |

Meer in de docs: [Request Lifecycle](https://laravel.com/docs/13.x/lifecycle).

> **Toelichting:** withRouting() kan ook een then-closure krijgen om extra route-bestanden te laden, bijvoorbeeld routes/admin.php met een eigen prefix. withExceptions() is de plek om een exception te negeren in je logs of om een eigen foutpagina te tonen voor één type fout.

<!-- slide 14 -->

### Oude projecten

In een agency werk je ook aan projecten van jaren geleden. Laravel 11 en later draaien die oude structuur gewoon, dus veel projecten zijn nooit omgezet. Weet waar je moet zoeken.

| Wat | Laravel 10 en ouder | Laravel 11 en later |
| --- | --- | --- |
| Middleware registreren | `app/Http/Kernel.php` | `withMiddleware()` |
| Exceptions | `app/Exceptions/Handler.php` | `withExceptions()` |
| Routes laden | `RouteServiceProvider` | `withRouting()` |
| Jouw providers | `providers` in `config/app.php` | `bootstrap/providers.php` |
| Scheduler | `app/Console/Kernel.php` | `routes/console.php` |

> **Toelichting:** Laravel 11 kwam uit in maart 2024. Een project van voor die datum heeft bijna zeker de oude structuur. Upgraden kan, maar het hoeft niet: Laravel Shift is een betaalde dienst die het automatisch doet, en de upgrade guide in de docs beschrijft elke stap.

<!-- slide 15 -->

### De kernel

`handle()` van de HTTP kernel krijgt een `Request` en geeft een `Response` terug. Voor hij iets met de request doet, draait hij zijn bootstrappers, in deze volgorde:

1. **.env**: Environment variables laden
2. **config**: Alles uit config/ inlezen
3. **Exceptions**: Error handling aanzetten
4. **Facades**: Cache::, Route:: en co werken
5. **register()**: Op elke provider
6. **boot()**: Op elke provider

Je komt aan geen van deze classes. Maar de volgorde verklaart fouten: een facade in een config-bestand faalt, want config wordt ingelezen voor facades bestaan.

> **Toelichting:** De bootstrappers zijn classes in Illuminate\Foundation\Bootstrap. De console kernel voor Artisan heeft bijna dezelfde lijst, met één extra stap die een nep-request aanmaakt, zodat url() ook in een commando werkt. Je kan de lijst zelf lezen in vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php.

<!-- slide 16 -->

### Gecachte config

```console
$ php artisan config:cache
INFO  Configuration cached successfully.
```

- Alle config komt in één bestand in bootstrap/cache. Snel, maar vanaf dan leest Laravel .env niet meer.
- `env()` geeft dan alleen nog echte environment variables van de server, en dat is meestal `null`.
- Forge, Cloud en `php artisan optimize` doen dit bij elke deploy. Lokaal merk je er dus niets van, in productie wel.
- Daarom: `env()` alleen in `config/`, en overal elders `config()`. [Docs](https://laravel.com/docs/13.x/configuration#configuration-caching)

> **Toelichting:** php artisan config:clear verwijdert de cache weer. Lokaal wil je meestal geen cache, anders moet je na elke wijziging in .env opnieuw cachen. Een snelle check of de cache aan staat: kijk of bootstrap/cache/config.php bestaat.

<!-- slide 17 -->

### Je voegt MOLLIE_KEY toe aan .env op de server. config('services.mollie.key') blijft null. Wat is de oorzaak?

- A. De server moet herstarten
- B. De config is gecached, dus .env wordt niet gelezen
- C. Mollie keys mogen niet in .env
- D. Je moet composer dump-autoload draaien

**Antwoord:** B. De config is gecached, dus .env wordt niet gelezen

Na config:cache leest Laravel .env niet meer, tot je de cache opnieuw bouwt met php artisan config:cache of opnieuw deployt. Een herstart van de server helpt niet: het cachebestand in bootstrap/cache blijft gewoon staan. dump-autoload gaat over classes vinden, niet over config.

> **Toelichting:** Een variant van dezelfde fout: een package-config die je nooit gepubliceerd hebt, leest ook env() in zijn eigen config-bestand, en dat blijft werken. Het gaat alleen mis als jouw eigen code env() gebruikt buiten config/. Larastan waarschuwt daar ook voor.

<!-- slide 18 -->

## 3. Service providers

> **Toelichting:** Service providers zijn de plek waar een package zich in je app inplugt, en waar jij de app instelt voor hij requests krijgt. Als je begrijpt wat een provider doet, begrijp je ook wat er gebeurt als je composer require van een package doet en het meteen werkt.

<!-- slide 19 -->

### Service providers

- Een service provider is een class die een stuk van de app klaarzet, nog voor er één request binnenkomt.
- Laravel zelf heeft er tientallen: voor de database, de queue, mail, validatie en routing.
- Voor je eigen project volstaat meestal `AppServiceProvider`. Een aparte provider maak je als een onderdeel groot genoeg wordt, zoals alles rond facturatie.
- Alles over providers: [de docs](https://laravel.com/docs/13.x/providers).

```console
$ php artisan make:provider BillingServiceProvider
```

> **Toelichting:** Deferred providers zijn een optimalisatie: een provider die alleen bindings zet, kan DeferrableProvider implementeren, en wordt dan pas geladen als iemand één van zijn services vraagt. Laravel doet dat zelf voor tientallen van zijn eigen providers. In je eigen project heb je het zelden nodig.

<!-- slide 20 -->

### bootstrap/providers.php

```php
return [
    AppServiceProvider::class,
    BillingServiceProvider::class,
];
```

`make:provider` zet je nieuwe provider hier zelf bij. Providers van packages staan hier niet: een package zet de zijne in `extra.laravel.providers` van zijn `composer.json`, en Laravel vindt ze vanzelf. Dat heet package discovery.

> **Toelichting:** Wil je een package niet automatisch laten laden, dan zet je het in extra.laravel.dont-discover van je eigen composer.json. Handig voor een debug-package dat je alleen onder bepaalde voorwaarden wil registreren. php artisan package:discover bouwt de lijst opnieuw op.

<!-- slide 21 -->

### register en boot

#### register()

- Alleen bindings in de container: bind, singleton, scoped.
- Draait op alle providers, voor één enkele boot() begint.
- Gebruik hier geen andere services. Die zijn misschien nog niet geregistreerd.

#### boot()

- Draait als alle providers geregistreerd zijn.
- Event listeners, view composers, macro's, Gate-regels.
- Instellingen zoals `Model::preventLazyLoading()`.
- Mag alles uit de container gebruiken.

> **Toelichting:** Een fout die je vaak ziet: een Gate::define() of een Event::listen() in register(). Soms werkt dat toevallig, omdat de service al bestond. Tot een ander package de volgorde van de providers verandert, en je een fout krijgt die niets met jouw wijziging lijkt te maken te hebben.

<!-- slide 22 -->

### Waarom twee fases

In `register()` weet je niet welke andere providers al geladen zijn. Pas als elke provider zijn bindings heeft gezet, is de container compleet. Daarom zet Laravel `boot()` erachter: daar bestaat alles zeker, en mag je elke service gebruiken.

> [!TIP]
> Twijfel je? Staat er `bind`, `singleton` of `scoped`: `register()`. Al de rest: `boot()`.

> **Toelichting:** Laravel roept eerst register() op elke provider op, in de volgorde van de lijst, en daarna boot() op elke provider. boot() mag ook dependencies vragen in zijn parameters, zoals boot(ResponseFactory $response): de container vult ze in.

<!-- slide 23 -->

### AppServiceProvider

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

public function boot(): void
{
    Model::preventLazyLoading(! $this->app->isProduction());
}
```

1. In `register()` alleen een binding. De closure draait pas als iemand `ExchangeRates` vraagt, dus `config()` mag erin.
2. In `boot()` een instelling voor Eloquent. Die service bestaat nu zeker.

> **Toelichting:** Model::preventLazyLoading() gooit een fout als je een relatie lazy laadt, zodat je N+1-problemen lokaal meteen ziet. In productie staat het uit, zodat een vergeten with() geen crash wordt maar alleen een trage pagina. De les over Eloquent-queries gaat er dieper op in.

<!-- slide 24 -->

### Geen queries in een provider

#### Draait altijd

```php
public function boot(): void
{
    View::share('categories', Category::all());
}
```

Een query bij elke request, elk Artisan-commando en elke queue job. Ook als geen enkele pagina de categorieën toont.

#### Draait als het nodig is

```php
public function boot(): void
{
    View::composer('layouts.nav', function (View $view) {
        $view->with('categories', Category::all());
    });
}
```

Een view composer draait alleen als die ene view gerenderd wordt.

> **Toelichting:** View::composer() kan ook een array van views krijgen, of een wildcard zoals partials.*. Voor een composer die groter wordt, maak je een eigen class met een compose()-methode. Die class komt uit de container, dus je kan er dependencies in vragen.

<!-- slide 25 -->

### Waarom dat misloopt

Stel: je zet de `View::share()` van de vorige slide in `boot()`. Lokaal werkt alles. Op een nieuwe server draai je `php artisan migrate`, en dat faalt: `boot()` draait ook voor Artisan, en de tabel `categories` bestaat nog niet.

> [!WARNING]
> Zelfs `composer install` kan dan falen, want na de install boot `package:discover` de app. Hou providers vrij van queries.

> **Toelichting:** Twijfel je of iets in een provider een query doet, draai dan php artisan migrate:fresh op een lege database. Werkt dat, dan boot je app zonder tabellen. Sommige teams zetten dat als stap in hun CI.

<!-- slide 26 -->

### Waar registreer je een view composer of een event listener?

- A. In register() van een provider
- B. In boot() van een provider
- C. In public/index.php
- D. In config/app.php

**Antwoord:** B. In boot() van een provider

In boot() is de container compleet, dus de View- en Event-services bestaan zeker. register() lijkt ook te kunnen, want het is ook een methode van de provider, maar daar mag je alleen bindings zetten: de services die je nodig hebt, zijn er misschien nog niet.

> **Toelichting:** register() is niet helemaal fout voor een listener, en daarom is het een verleidelijk antwoord. Maar Laravel garandeert niet dat de Event-service al klaarstaat op dat moment. In boot() is die garantie er wel, en de docs zeggen het ook zo.

<!-- slide 27 -->

## 4. Middleware

> **Toelichting:** Middleware is de plek waar je regels zet die voor veel routes tegelijk gelden: inloggen, rechten, taal, headers. Wie middleware begrijpt, schrijft die regels één keer in plaats van in elke controller.

<!-- slide 28 -->

### Een ui

global middleware

web group

auth

Controller

- De request gaat door elke laag naar binnen, van buiten naar binnen.
- Elke laag mag stoppen en zelf een response teruggeven. De lagen erbinnen draaien dan niet.
- De response gaat door dezelfde lagen terug, in omgekeerde volgorde.
- Alles over middleware: [de docs](https://laravel.com/docs/13.x/middleware).

> **Toelichting:** In de broncode heet dit een pipeline: de kernel stuurt de request door een lijst middleware met Illuminate\Pipeline\Pipeline. Elke middleware krijgt een closure $next die de rest van de lijst voorstelt. Dat verklaart waarom de volgorde zo belangrijk is.

<!-- slide 29 -->

### Voor en na de controller

```php
public function handle(Request $request, Closure $next): Response
{
    app()->setLocale($request->user()?->locale ?? 'nl');

    $response = $next($request);

    $response->headers->set('Content-Language', app()->getLocale());

    return $response;
}
```

1. Voor `$next`: de request is op weg naar binnen. De taal staat juist voor de controller draait.
2. `$next($request)` is de rest van de ui: de volgende middleware, en uiteindelijk je controller.
3. Na `$next`: de response is op weg naar buiten. Je voegt een header toe en geeft hem terug.

> **Toelichting:** Een middleware moet altijd een Response teruggeven. Vergeet je return $response, dan krijg je een fout over een null-response. $request->user() werkt hier omdat SetLocale in de web group na StartSession draait: vroeger in de ui is er nog geen sessie, en dus geen user.

<!-- slide 30 -->

### Een middleware maken

```console
$ php artisan make:middleware SetLocale
```

Dat maakt `app/Http/Middleware/SetLocale.php` met een lege `handle()`. Daar komt de code van de vorige slide in. Een middleware draait pas als je ze ergens registreert: global, in een groep of op een route.

> **Toelichting:** Een middleware kan ook parameters krijgen, bijvoorbeeld role:admin op een route. Die komen als extra argumenten na $next in handle(). De docs over middleware parameters tonen hoe.

<!-- slide 31 -->

### Registreren

*bootstrap/app.php*

```php
->withMiddleware(function (Middleware $middleware): void {
    $middleware->web(append: [
        SetLocale::class,
    ]);

    $middleware->alias([
        'active' => EnsureUserIsActive::class,
    ]);
})
```

1. `web(append: ...)` zet SetLocale achteraan in de web group, dus op elke route in routes/web.php.
2. Een alias geeft een middleware een korte naam, zodat je op een route gewoon `active` schrijft.

> **Toelichting:** In Laravel 10 en ouder stond dit in app/Http/Kernel.php, in de arrays middleware, middlewareGroups en middlewareAliases. $middleware->append() voegt toe aan de global middleware, prepend() zet ze vooraan.

<!-- slide 32 -->

### Op een route

*routes/web.php*

```php
Route::get('facturen', [InvoicesController::class, 'index'])->middleware(['auth', 'active']);
```

Middleware op een route draait na de web group, in de volgorde van de array: eerst `auth`, dan `active`. Die volgorde is logisch: of een user actief is, kan je pas checken als je weet wie hij is.

> **Toelichting:** Middleware op een groep routes zet je met Route::middleware(['auth', 'active'])->group(...). Eén route uit zo'n groep uitsluiten kan met withoutMiddleware(), maar alleen voor route middleware, niet voor global.

<!-- slide 33 -->

### Drie plaatsen

| Waar | Hoe | Draait op |
| --- | --- | --- |
| Global | `$middleware->append(…)` | Elke request, ook een 404 en de API |
| Group | `$middleware->web(append: […])` | Elke route in routes/web.php |
| Route | `->middleware('auth')` | Alleen die route of die groep routes |

> [!TIP]
> Zet middleware zo diep als het kan. Een header die alleen je dashboard nodig heeft, hoort niet in global: daar draait hij ook op elke API-call en elke 404.

> **Toelichting:** Global middleware op een API-route kan verrassend zijn: een middleware die een sessie verwacht, crasht daar, want de api-groep heeft geen sessie. Nog een reden om zo diep mogelijk te registreren.

<!-- slide 34 -->

### De web group

| Middleware | Wat ze doet |
| --- | --- |
| `EncryptCookies` | Ontsleutelt je cookies op de weg naar binnen, versleutelt ze op de weg naar buiten |
| `StartSession` | Start de sessie, en slaat ze op na de controller |
| `ShareErrorsFromSession` | Zet `$errors` klaar voor je Blade views |
| `PreventRequestForgery` | De CSRF-check op POST, PUT en DELETE. Faalt hij: 419. |
| `SubstituteBindings` | Maakt van `{project}` in de url een `Project`-model |

Laravel zet deze groep op elke route in `routes/web.php`. In oudere projecten heet de CSRF-check nog `VerifyCsrfToken`.

> **Toelichting:** SubstituteBindings zit in de web group en in de api group. Daarom werkt route model binding in allebei. Een 419-fout betekent bijna altijd dat het formulier geen @csrf heeft, of dat de sessie verlopen is terwijl het formulier openstond.

<!-- slide 35 -->

### De auth-middleware stopt een gast. Wat gebeurt er met de controller?

- A. Hij draait, maar zonder user
- B. Hij draait nooit
- C. Hij draait na de redirect
- D. Dat hangt af van de route

**Antwoord:** B. Hij draait nooit

De request komt nooit voorbij auth, dus de controller draait niet. Wie denkt dat hij toch draait zonder user, verwart het met een route zonder auth: daar geeft auth()->user() null. De redirect naar /login gaat wel terug door de buitenste lagen, zodat de sessie nog opgeslagen wordt.

> **Toelichting:** Achter de schermen gooit auth een AuthenticationException, en de exception handler maakt er een redirect naar de route login van. Bestaat die route niet in je project, dan krijg je een fout over een ontbrekende route login. Bij een request die JSON verwacht, krijg je een 401 in plaats van een redirect.

<!-- slide 36 -->

### Welke middleware draait er bij een url waar geen route op past?

- A. Alleen de global middleware
- B. De web group
- C. De middleware van de dichtstbijzijnde route
- D. Geen enkele

**Antwoord:** A. Alleen de global middleware

Global middleware draait voor de router, dus altijd. De web group hangt aan de routes in routes/web.php, en zonder route is er geen groep. Daarom is er op een 404-pagina geen sessie.

> **Toelichting:** Dat verklaart ook waarom een 404 op een onbekende url sneller is dan een 404 door een onbestaand model: er draait minder. Voor een API maakt het niet uit, maar voor een Blade-layout met een ingelogde user wel.

<!-- slide 37 -->

## 5. Router en controller

> **Toelichting:** De router is waar je url's en je code aan elkaar hangen. Hier leer je vooral wat er gebeurt als die koppeling niet lukt, want dat is het moment waarop je gaat debuggen.

<!-- slide 38 -->

### De router

- De router zoekt de eerste route die past op methode en url.
- Past er geen, dan wordt het een 404, en de route middleware draait nooit.
- Past er wel een, dan draait eerst de middleware van die route, pas daarna je controller.
- Route model binding gebeurt in `SubstituteBindings`, een middleware. Een onbestaand id geeft dus ook een 404, maar dan wel met sessie.

> **Toelichting:** Met php artisan route:list zie je alle routes met hun middleware. Twee routes die op dezelfde url passen, is een klassieke fout: de eerste in het bestand wint, dus zet een route met een vaste waarde, zoals projecten/nieuw, voor een route met een parameter zoals projecten/{project}.

<!-- slide 39 -->

### Een 404 met sessie

*onderaan routes/web.php*

```php
Route::fallback(function () {
    return response()->view('errors.404', status: 404);
});
```

Op je gewone 404-pagina is niemand ingelogd, dus een menu met de naam van de user blijft leeg. Een fallback-route vangt alles op waar geen andere route op past. Omdat ze in `routes/web.php` staat, draait de web group wel. [Docs](https://laravel.com/docs/13.x/routing#fallback-routes)

> **Toelichting:** Je kan ook alleen een view errors/404.blade.php maken, dan gebruikt Laravel die voor elke 404. Dat is genoeg zolang de pagina geen ingelogde user nodig heeft. Pas voor een menu met de naam van de user heb je de fallback-route nodig.

<!-- slide 40 -->

### Wat je returnt

| Je controller returnt | Laravel maakt ervan |
| --- | --- |
| `view('projects.show', …)` | 200 met HTML |
| Een array, een model of een collection | 200 met JSON |
| `redirect()->route('projects.index')` | 302 met een Location-header |
| `abort(403)` | Een exception, en de handler maakt er een 403-pagina van |

Wat je ook returnt, er komt een `Response`-object uit. Daarom kan een middleware na `$next` altijd een header toevoegen.

> **Toelichting:** Een Responsable-object is een vijfde optie: een eigen class met een methode toResponse(), die Laravel omzet zoals een view. API Resources werken zo. De les over API development gaat daar verder op in.

<!-- slide 41 -->

### De controller komt uit de container

```php
public function store(
    StoreInvoiceRequest $request,
    InvoiceNumberGenerator $numbers,
): RedirectResponse {
    $invoice = Invoice::create([
        ...$request->validated(),
        'number' => $numbers->next(),
    ]);

    return redirect()->route('invoices.show', $invoice);
}
```

1. Laravel maakt je controller niet met `new`, de container doet het. Wat je type-hint, krijg je.
2. Als deze regel draait, is de Form Request al gevalideerd. Hoe dat werkt: de les over de container.

> **Toelichting:** Een Form Request valideert al tijdens het resolven: de container maakt het object, en Laravel roept meteen de validatie op. Daarom staat de validatie niet in je methode en werkt het toch. Hoe de container objecten bouwt, is de volgende les.

<!-- slide 42 -->

## 6. Na de response

> **Toelichting:** Het werk na de response is een manier om je app sneller te laten voelen zonder queue. Het is ook de plek waar je logt en meet, omdat je daar de hele request en de response kent.

<!-- slide 43 -->

### send en terminate

- `send()` stuurt headers en body naar de browser.
- Daarna roept Laravel `terminate()` op, op de kernel.
- Dan draaien terminable middleware, `defer()`-callbacks en `app()->terminating()`-callbacks.
- Met PHP-FPM, wat Herd, Forge en de meeste servers gebruiken, is de verbinding met de browser dan al dicht. De gebruiker wacht er niet op.
- Met `php artisan serve` wacht de browser wel. Lokaal zie je het verschil dus niet altijd.

> **Toelichting:** fastcgi_finish_request() is de PHP-functie die de verbinding met de browser sluit, en Symfony roept ze op in send(). Daarom werkt het alleen met PHP-FPM. Octane en FrankenPHP hebben hun eigen manier om na de response verder te werken.

<!-- slide 44 -->

### defer()

```php
use function Illuminate\Support\defer;

public function store(StoreOrderRequest $request): RedirectResponse
{
    $order = Order::create($request->validated());

    defer(fn () => Http::post(config('services.crm.webhook'), $order->toArray()));

    return redirect()->route('orders.show', $order);
}
```

1. `defer` is een functie, geen facade. Je importeert ze met `use function`.
2. De closure draait na `send()`. De klant ziet zijn redirect meteen, de call naar het CRM volgt.

> **Toelichting:** defer() bestaat sinds Laravel 11. Met een tweede argument geef je een naam mee, en met defer()->forget('naam') annuleer je hem weer. In een test draaien deferred functions pas op het einde, tenzij je $this->withoutDefer() oproept.

<!-- slide 45 -->

### defer() of een queue

#### defer()

- Voor iets kleins dat niet kritisch is: een ping naar een CRM, een statistiek.
- Draait alleen bij een geslaagde response. Bij een 4xx of 5xx niet, tenzij je `->always()` toevoegt.
- Mislukt het, dan is het weg. Er is geen retry.
- [Docs over defer()](https://laravel.com/docs/13.x/helpers#deferred-functions)

#### Queue

- Voor alles wat opnieuw moet kunnen: mails, facturen, betalingen.
- Retries, backoff en een lijst met mislukte jobs.
- Meer in de les over queues.

> **Toelichting:** Een queue vraagt een worker en een driver, zoals de database of Redis. Voor kleine projecten zonder worker is defer() een goede tussenstap. Groeit het werk of moet het betrouwbaar zijn, dan stap je over op een queue.

<!-- slide 46 -->

### Terminable middleware

```php
public function handle(Request $request, Closure $next): Response
{
    return $next($request);
}

public function terminate(Request $request, Response $response): void
{
    $seconds = round(microtime(true) - LARAVEL_START, 3);

    Log::info("{$request->path()} in {$seconds}s");
}
```

1. `handle()` laat de request gewoon door. Een middleware heeft er altijd een nodig.
2. `terminate()` draait na `send()`. `LARAVEL_START` komt uit index.php, dus je meet de hele request.

> **Toelichting:** Laravel roept terminate() alleen op voor middleware die in de kernel geregistreerd is: global of via een route. Een middleware die je handmatig in een controller zou aanroepen, krijgt geen terminate().

<!-- slide 47 -->

### Een nieuwe instance

Voor `terminate()` maakt Laravel een nieuwe instance van je middleware. Een property die je in `handle()` zette, is dan weg. Moet je iets doorgeven van `handle()` naar `terminate()`, registreer de middleware dan als singleton:

*AppServiceProvider::register()*

```php
$this->app->singleton(LogResponseTime::class);
```

[Docs over terminable middleware](https://laravel.com/docs/13.x/middleware#terminable-middleware)

> **Toelichting:** Zonder singleton kan je ook de starttijd op de request zelf zetten, met $request->attributes->set(). De request in terminate() is dezelfde als in handle(), dus je leest hem daar gewoon terug.

<!-- slide 48 -->

### Wanneer draait een defer()-callback?

- A. Voor de controller
- B. Na de response, als die geslaagd is
- C. In een queue worker
- D. Alleen in productie

**Antwoord:** B. Na de response, als die geslaagd is

Na send(), tijdens terminate, en alleen bij een response zonder 4xx of 5xx, tenzij je ->always() toevoegt. Het lijkt op een queue omdat het later draait, maar er komt geen worker aan te pas: het gebeurt in hetzelfde PHP-proces, en een fout wordt niet opnieuw geprobeerd.

> **Toelichting:** Een alternatief voor terminable middleware is app()->terminating() in een service provider. Die callback draait voor elke request en elk Artisan-commando, dus check eerst of je in een HTTP-request zit als je iets over de url wil loggen.

<!-- slide 49 -->

## 7. Facades

> **Toelichting:** Facades zie je overal in Laravel-code, in de docs, in packages en in tutorials. Weten dat het geen echte static methods zijn, verklaart waarom je ze kan faken in een test, en waarom ze toch in de container zitten.

<!-- slide 50 -->

### Hoe een facade werkt

`Cache::get('rates')` lijkt een static method. Dat is het niet: de class `Cache` heeft geen methode `get`. Dit gebeurt er:

1. **`Cache::get('rates')`**: Je roept een static method op die niet bestaat
2. **`__callStatic()`**: De Facade-class vangt elke onbekende static call op
3. **`app('cache')`**: getFacadeAccessor() zegt onder welke naam het object in de container zit
4. **`->get('rates')`**: De call gaat naar dat object, de CacheManager

> **Toelichting:** __callStatic() is een magic method van PHP die draait wanneer je een static methode oproept die niet bestaat. Laravel onthoudt het object na de eerste keer, dus de container wordt niet bij elke call opnieuw gevraagd.

<!-- slide 51 -->

### De Cache facade

*Illuminate\\Support\\Facades\\Cache*

```php
class Cache extends Facade
{
    protected static function getFacadeAccessor(): string
    {
        return 'cache';
    }
}
```

Dit is de hele class, zonder docblocks. Elke facade in Laravel ziet er zo uit. Kijk zelf in `vendor/laravel/framework/src/Illuminate/Support/Facades`. [Docs over facades](https://laravel.com/docs/13.x/facades)

> **Toelichting:** In je editor springt Go to definition op Cache::get() vaak naar niets, omdat de methode niet bestaat. Het package barryvdh/laravel-ide-helper genereert een bestand met alle facade-methodes, zodat je editor ze toch kent. De @method-docblocks bovenaan elke facade-class dienen voor hetzelfde.

<!-- slide 52 -->

### Drie keer hetzelfde

| Stijl | Code |
| --- | --- |
| Facade | `Cache::get('rates')` |
| Helper | `cache('rates')` |
| Injectie | `Repository $cache` in de constructor, dan `$this->cache->get('rates')` |

Alle drie halen hetzelfde object uit de container. Er is geen snelheidsverschil, en je kan ze alle drie vervangen in een test.

> **Toelichting:** Een real-time facade maakt van elke class een facade: importeer Facades\App\Services\ExchangeRates in plaats van App\Services\ExchangeRates, en je kan ExchangeRates::get() schrijven. Handig in code die je niet kan injecteren, maar het verstopt de dependency, dus gebruik het spaarzaam.

<!-- slide 53 -->

### Een facade in een test

```php
test('toont de wisselkoers', function () {
    Cache::shouldReceive('get')
        ->with('rates')
        ->andReturn(['USD' => 1.08]);

    $this->get('/koersen')->assertSee('1.08');
});
```

Omdat een facade de call doorgeeft aan de container, kan Laravel er in een test een nep-object tussen zetten. Een echte static method kan je niet vervangen, een facade wel. Meer in de les over fakes en mocks.

> **Toelichting:** Voor Mail, Queue, Event, Notification en Storage heeft Laravel fakes, zoals Mail::fake(). Die zijn handiger dan shouldReceive(), omdat je achteraf kan controleren wat er verstuurd is. De les over fakes en mocks gaat daarover.

<!-- slide 54 -->

### Facade of injectie

- Facades zijn prima. De docs en de meeste packages gebruiken ze.
- Het risico: tien facades in één class zie je niet, tien parameters in een constructor wel. De docs noemen dat scope creep.
- Wordt een class te groot, dan is dat een teken om hem te splitsen, niet om dependencies te verstoppen.
- Een goede vuistregel: facades voor Laravel zelf (Cache, Log, Storage), injectie voor je eigen services. Zie de les over de service container.

> **Toelichting:** Een signaal dat een class te groot wordt: je scrollt om de lijst use-statements te zien. Tel de facades en de dependencies samen. Zijn het er meer dan vijf of zes, dan doet de class waarschijnlijk meer dan één ding.

<!-- slide 55 -->

### De Cache-class heeft geen methode get(). Waarom werkt Cache::get('rates') toch?

- A. PHP maakt de methode aan bij de eerste call
- B. \_\_callStatic() geeft de call door aan het object dat onder cache in de container zit
- C. Laravel genereert de methode bij composer install
- D. get() staat in een trait van Cache

**Antwoord:** B. \_\_callStatic() geeft de call door aan het object dat onder cache in de container zit

De Facade-class vangt elke onbekende static call op met \_\_callStatic(), vraagt de container om het object met de naam uit getFacadeAccessor(), en roept daar get() op. Er wordt niets gegenereerd: het is gewoon PHP-magie die op het moment zelf draait.

> **Toelichting:** Laravel genereert niets: het werkt op het moment zelf, bij elke call. De trait-optie is verleidelijk omdat traits in Laravel veel voorkomen, maar kijk zelf in de class Cache, er staat echt alleen getFacadeAccessor() in.

<!-- slide 56 -->

## 8. Andere ingangen

> **Toelichting:** Niet alles wat je bouwt is een pagina. Commando's, geplande taken en queue jobs gebruiken dezelfde app, maar niet dezelfde weg. Wie die verschillen kent, vermijdt de fout waarbij code in de browser werkt en in een job crasht.

<!-- slide 57 -->

### Niet alles is een request

| Ingang | Start met | Wat er anders is |
| --- | --- | --- |
| Browser | `public/index.php` | Middleware, router en een sessie |
| Artisan | `artisan` | Geen middleware, geen sessie, geen ingelogde user |
| Scheduler | `php artisan schedule:run` | Een Artisan-commando dat cron elke minuut start |
| Queue worker | `php artisan queue:work` | Boot één keer, en verwerkt daarna job na job |

In een Artisan-commando geeft `auth()->user()` dus `null`. Er is niemand ingelogd.

> **Toelichting:** De scheduler heeft een eigen les. Voor nu: de crontab op de server roept elke minuut php artisan schedule:run op, en Laravel beslist welke taken er dat moment moeten draaien. Elke run is een nieuw proces, dus daar speelt het probleem van de queue worker niet.

<!-- slide 58 -->

### De console kernel

*artisan*

```php
$app = require_once __DIR__.'/bootstrap/app.php';

$status = $app->handleCommand(new ArgvInput);

exit($status);
```

Het bestand `artisan` in je project lijkt op `index.php`: dezelfde app uit `bootstrap/app.php`, dezelfde providers. Alleen gaat het naar de console kernel, zonder middleware en zonder router.

> **Toelichting:** routes/console.php, dat in withRouting() stond, is waar je eigen Artisan-commando's met een closure en je scheduler staan. Commando's als class staan in app/Console/Commands, en Laravel vindt ze vanzelf.

<!-- slide 59 -->

### Processen die blijven draaien

- Een request of een Artisan-commando boot de app, doet zijn werk en stopt.
- Een queue worker boot één keer en blijft draaien, soms dagen aan een stuk.
- Nieuwe code ziet hij pas na `php artisan queue:restart`. Zet dat in je deploy.
- Static variabelen en singletons leven van job tot job. Octane doet hetzelfde met requests: zie de les over design patterns.
- [Docs: queue workers en deployment](https://laravel.com/docs/13.x/queues#queue-workers-and-deployment)

> **Toelichting:** Laravel Horizon, voor Redis-queues, en Forge herstarten je workers bij een deploy als je het zo instelt. Lokaal met php artisan queue:work moet je hem zelf herstarten na een wijziging, of php artisan queue:listen gebruiken, die de app voor elke job opnieuw boot.

<!-- slide 60 -->

### Je deployt een nieuwe mailtemplate, maar mails uit de queue gebruiken nog de oude. Waarom?

- A. De view cache is niet geleegd
- B. De queue worker draait nog met de oude code
- C. Mails worden gecached door de mailprovider
- D. config:cache is vergeten

**Antwoord:** B. De queue worker draait nog met de oude code

Een queue worker boot één keer en houdt de code in het geheugen, dus na elke deploy: php artisan queue:restart. De view cache lijkt een logische verdachte, maar een deploy bouwt die meestal opnieuw, en de web-requests tonen de nieuwe template wel. Alleen de worker loopt achter.

> **Toelichting:** De view cache is een logische verdachte, en php artisan view:clear kan geen kwaad. Maar als je nieuwe template wel werkt in de browser en niet in de mail uit de queue, weet je dat het aan het proces ligt en niet aan de cache.

<!-- slide 61 -->

### Opdracht: Volg een request

In een Laravel 13-project, lokaal met Herd.

1. Maak met `make:middleware` een middleware die een header `X-Request-Id` met een uuid op elke response zet, en registreer ze global.
2. Voeg in `boot()` van AppServiceProvider een `app()->terminating()`-callback toe die de url en de duur logt, met `LARAVEL_START`.
3. Maak een route met een `defer()` die iets logt, en kijk in `storage/logs/laravel.log` in welke volgorde alles draait.
4. Klaar als elke response de header heeft, en de log per request een regel met de duur toont, ook bij een 404.

Vastgelopen? Kijk terug naar slide 29 (voor en na de controller), 31 (registreren) en 44 (defer), en naar de docs over [middleware](https://laravel.com/docs/13.x/middleware).

> **Toelichting:** Begin met de middleware: make:middleware, dan in handle() de response van $next ophalen en $response->headers->set() met Str::uuid(). Registreer hem met $middleware->append() in bootstrap/app.php. Test met de network tab van je browser of met curl -I op je lokale url.

## Begrippen

- **Request lifecycle**: De weg die elke request aflegt: index.php, bootstrap, service providers, middleware, router, controller, en dezelfde middleware terug naar buiten. Wie die weg kent, weet waar een bug zit. (slide 6)
- **Front controller**: Eén bestand dat elke request binnenkrijgt en beslist wat ermee gebeurt. Bij Laravel is dat public/index.php. (slide 5)
- **public/index.php**: De enige ingang voor HTTP requests. Laadt de Composer autoloader, haalt de app uit bootstrap/app.php en roept handleRequest() op. (slide 8)
- **Document root**: De map die de webserver publiek maakt. Bij Laravel is dat public/, zodat .env, app/ en vendor/ nooit via een url bereikbaar zijn. (slide 10)
- **bootstrap/app.php**: Maakt de applicatie met Application::configure() en stelt routing, middleware en exceptions in. Vervangt sinds Laravel 11 de oude Kernel-classes en de RouteServiceProvider. (slide 12)
- **HTTP kernel**: De class die een Request binnenkrijgt en een Response teruggeeft. Draait eerst de bootstrappers en stuurt de request dan door de global middleware naar de router. (slide 15)
- **Bootstrappers**: De vaste stappen van de kernel voor elke request: .env laden, config inlezen, error handling, facades, providers registreren en booten. (slide 15)
- **Service provider**: Een class die een stuk van de app klaarzet voor er een request binnenkomt: bindings in de container, listeners, macro's. Jouw eigen providers staan in bootstrap/providers.php. (slide 18)
- **register()**: De methode van een service provider waar je alleen bindings in de container zet. Draait op alle providers voor één enkele boot() begint. (slide 21)
- **boot()**: De methode van een service provider die draait als alle providers geregistreerd zijn. Hier mag je andere services gebruiken: listeners, view composers, Model::preventLazyLoading(). (slide 21)
- **Package discovery**: Packages zetten hun provider in extra.laravel.providers van hun composer.json. Laravel pikt die zelf op, dus je hoeft ze nergens te registreren. (slide 20)
- **Middleware**: Een laag rond je app die elke request doorlaat, aanpast of tegenhoudt met $next($request). Code voor $next draait voor de controller, code erna op de weg terug. (slide 28)
- **Global middleware**: Middleware die op elke request draait, ook als er geen route past. Je voegt ze toe met $middleware->append() in bootstrap/app.php. (slide 33)
- **Middleware group**: Een lijst middleware onder één naam. Laravel zet de web group op elke route in routes/web.php: cookies, sessie, CSRF en route model binding. (slide 34)
- **SubstituteBindings**: De middleware uit de web group die {project} in je url omzet naar een Project-model. Zonder die middleware geen route model binding. (slide 34)
- **Route::fallback()**: Een route die alles opvangt waar geen andere route op past. Omdat ze in routes/web.php staat, draait de web group wel, en is de user op je 404-pagina ingelogd. (slide 39)
- **Terminable middleware**: Middleware met een terminate()-methode, die draait nadat de response verstuurd is. Laravel maakt er een nieuwe instance voor, tenzij je de middleware als singleton registreert. (slide 46)
- **defer()**: Een helper die een closure uitvoert na de response, zonder queue. Draait alleen bij een geslaagde response, tenzij je ->always() toevoegt. (slide 44)
- **Facade**: Een class met een static interface naar een object uit de container. Cache::get() roept via \_\_callStatic() de get() op van wat onder de naam cache in de container zit. (slide 50)
- **getFacadeAccessor()**: De enige methode van een facade: ze geeft de naam terug waaronder het echte object in de container zit, zoals cache of mailer. (slide 51)
- **config:cache**: Artisan-commando dat alle config in één bestand zet. Daarna laadt Laravel .env niet meer, dus env() buiten config/ geeft null. (slide 16)
- **Long-lived process**: Een proces dat Laravel één keer boot en daarna veel werk doet, zoals een queue worker of Octane. Het ziet nieuwe code pas na een herstart. (slide 59)

## Links en bronnen

### In de presentatie

- [Request Lifecycle](https://laravel.com/docs/13.x/lifecycle) (slide 13): Het officiële overzicht, van index.php tot send()
- [Configuration caching](https://laravel.com/docs/13.x/configuration#configuration-caching) (slide 16): Waarom env() alleen in config/ hoort
- [Service Providers](https://laravel.com/docs/13.x/providers) (slide 19): register tegenover boot, bootstrap/providers.php
- [Middleware](https://laravel.com/docs/13.x/middleware) (slide 28, slide 61): Before en after, groups, aliases, terminable middleware
- [Fallback routes](https://laravel.com/docs/13.x/routing#fallback-routes) (slide 39): Een 404 die wel door de web group gaat
- [Deferred functions](https://laravel.com/docs/13.x/helpers#deferred-functions) (slide 45): defer() en ->always()
- [Docs over terminable middleware](https://laravel.com/docs/13.x/middleware#terminable-middleware) (slide 47)
- [Facades](https://laravel.com/docs/13.x/facades) (slide 51): getFacadeAccessor, scope creep, shouldReceive
- [Queue workers and deployment](https://laravel.com/docs/13.x/queues#queue-workers-and-deployment) (slide 59): Long-lived processen en queue:restart

### Bronnen

- [laravel/laravel op GitHub](https://github.com/laravel/laravel/tree/13.x): public/index.php, bootstrap/app.php en artisan uit de skeleton
- [Illuminate\\Foundation\\Http\\Kernel](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Foundation/Http/Kernel.php): De bootstrappers, de pipeline naar de router en terminate()
