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

# Mailtransports voor packages registreren en testen

> Ontleed de MailManager van Laravel 13 en verifieer het registreren van transports vanuit een package, het doorgeven van configuratie, het vasthouden en opnieuw resolven van Mailers en verzendpaden die je met fake niet kunt controleren.

Om vanuit een package een eigen manier van e-mail versturen aan te bieden, registreer je bij Laravel **de logica die een Symfony-Transport aanmaakt**. Je hoeft Mailables of de Mailer van Laravel niet opnieuw te bouwen.

Deze pagina is gecontroleerd aan de hand van de implementatie van Laravel Framework `v13.39.0`. Zie [E-mail versturen](https://kawax.biz/nl/mail.md) voor de basis van het versturen en [Packageontwikkeling](https://kawax.biz/nl/advanced/package-development.md) voor het voorbereiden van de provider.

## Scheid de Mailernaam van de Transportnaam

| Element | Voorbeeld op deze pagina | Rol |
| - | - | - |
| Transportnaam | `acme-memory-mail` | De aanmaakmethode die je met `extend()` registreert |
| Mailernaam | `receipts` / `alerts` | De configuratienaam in `mail.mailers` en de identifier van de aangemaakte Mailer |
| Symfony Transport | `MemoryTransport` | Het bezorgen van de e-mail. In dit voorbeeld alleen vastleggen in het geheugen |
| Laravel Mailer | `Illuminate\Mail\Mailer` | Genereren van de inhoud, verzendevents, doorgeven aan de Transport enzovoort |

`Mail::mailer('receipts')` leest `mail.mailers.receipts` en roept de aanmaakfunctie aan die bij `transport` hoort. `MailManager::build()` geeft die Transport door aan de Mailer van Laravel en stelt bij normaal resolven op naam ook onder meer de globale afzender in.

```mermaid theme={null}
flowchart TD
    A["Mail::mailer('receipts')"] --> B{"Is receipts al geresolved?"}
    B -->|Ja| C["Vastgehouden Mailer retourneren"]
    B -->|Nee| D["mail.mailers.receipts inlezen"]
    D --> E["Aanmaakfunctie voor acme-memory-mail<br>enige argument is de configuratie-array"]
    E --> F["Symfony Transport retourneren"]
    F --> G["Laravel-Mailer aanmaken en<br>vasthouden onder de naam receipts"]
    G --> H["Bij verzenden Transport::send() aanroepen"]
    C --> H
```

Eigen aanmaakfuncties krijgen voorrang op de ingebouwde Transports. Overschrijf `smtp` of `ses` dus niet, maar gebruik een packagespecifieke Transportnaam.

## Bouw het registratiepad zonder externe verzending

Controleer eerst de configuratie en het resolvepad in plaats van de API van de bezorgdienst. Maak in `src/Transport/MemoryTransport.php` van het package `Acme\Postbox` een klasse die de standaard `ArrayTransport` uitbreidt.

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

namespace Acme\Postbox\Transport;

use Illuminate\Mail\Transport\ArrayTransport;

class MemoryTransport extends ArrayTransport
{
    public function __construct(
        private readonly string $label,
    ) {
        parent::__construct();
    }

    public function __toString(): string
    {
        return 'acme-memory-mail:'.$this->label;
    }
}
```

`ArrayTransport::send()` slaat de `SentMessage` van Symfony op in een interne Collection. Je kunt die bekijken met `messages()` en met `flush()` wis je de vastgelegde berichten van die instantie. Het label dient om te controleren dat de configuratie de aanmaakfunctie heeft bereikt.

> [!WARNING]
>
> Dit voorbeeld bezorgt geen e-mail naar buiten. Het is bedoeld om registratie en testen te leren. De in-memory registratie wordt niet tussen processen gedeeld of opgeslagen en bevestigt ook niet dat een bericht de ontvanger heeft bereikt. Als je alleen in het geheugen wilt vastleggen, volstaat de ingebouwde `array`-Transport.

### Registreer bij de lazy geladen Manager

De officiële documentatie roept `Mail::extend()` aan in `boot()`. In een package kun je dit combineren met een [hook na het resolven van een service](https://kawax.biz/nl/advanced/package-service-resolution.md), zodat applicaties die geen e-mail gebruiken de Manager niet onnodig aanmaken.

Maak `src/PostboxServiceProvider.php` aan.

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

namespace Acme\Postbox;

use Acme\Postbox\Transport\MemoryTransport;
use Illuminate\Mail\MailManager;
use Illuminate\Support\ServiceProvider;
use Symfony\Component\Mailer\Transport\TransportInterface;

class PostboxServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->callAfterResolving('mail.manager', static function (
            MailManager $manager,
        ): void {
            $manager->extend('acme-memory-mail', static function (
                array $config,
            ): TransportInterface {
                return new MemoryTransport($config['label'] ?? 'mail');
            });
        });
    }
}
```

Laad de provider bijvoorbeeld via [package auto-discovery](https://kawax.biz/nl/advanced/package-discovery.md).

* De `MailServiceProvider` van Laravel is een deferred provider en registreert `mail.manager` als singleton. De `Mail`-facade resolvet ook deze key.
* `callAfterResolving()` wacht op een latere resolve als de Manager nog niet is geresolved, en past de uitbreiding direct toe op een al geresolvede Manager. **Al aangemaakte Mailers worden hiermee niet vervangen.**
* Maak je eigen provider die de uitbreiding registreert niet zomaar een `DeferrableProvider`. Zorg ervoor dat je eigen Transport niet wordt opgevraagd voordat de hook is geregistreerd.
* Dit voorbeeld registreert de hook in de eigen `boot()`. Een opstartvolgorde waarin andere providers de eigen Mailer al eerder gebruiken, wordt niet gegarandeerd. Ontwerp de registratievolgorde en verstuur geen e-mail binnen `register()`.

### Verwar de argumenten van extend niet met die van andere Managers

`MailManager::createSymfonyTransport()` roept een eigen aanmaakfunctie aan met **slechts één configuratie-array**. Het zijn niet de twee argumenten applicatie en configuratie zoals bij `Cache::extend()`. De returnwaarde is een Transport die `TransportInterface` implementeert, geen Laravel-Mailer of verzendresultaat.

Bovendien slaat `MailManager::extend()` de closure alleen op en bindt die niet opnieuw aan de Manager. Het voorbeeld gebruikt een statische closure en expliciete argumenten, zodat het niet afhankelijk is van hoe `$this` wordt geïnterpreteerd.

Heb je een client voor een externe API nodig, pas dan in deze aanmaakfunctie de configuratie toe en geef die door aan de Transport. Verstuur niets op het moment van registreren, en ontwerp het zo dat configuratie en bestemmingen niet door elkaar raken wanneer meerdere Mailers dezelfde Transportnaam gebruiken.

### Voeg Mailers toe in de gebruikende applicatie

Voeg de volgende twee toe aan de bestaande `mailers`-array in `config/mail.php`. Overschrijf vanuit het package niet de standaardwaarde of de volledige `mailers` van de gebruiker.

```php theme={null}
'receipts' => [
    'transport' => 'acme-memory-mail',
    'label' => 'receipts-v1',
],

'alerts' => [
    'transport' => 'acme-memory-mail',
    'label' => 'alerts',
],
```

Wil je het als standaard gebruiken, dan geef je in `MAIL_MAILER` niet de Transportnaam op, maar **de Mailernaam `receipts`**. Met een expliciete keuze per aanroep kun je het gebruiken zonder de bestaande standaard te wijzigen.

```php theme={null}
use Illuminate\Mail\Message;
use Illuminate\Support\Facades\Mail;

Mail::mailer('receipts')->raw('Receipt body', static function (Message $message): void {
    $message->from('noreply@example.com')
        ->to('reader@example.com')
        ->subject('Receipt');
});

$transport = Mail::mailer('receipts')->getSymfonyTransport();

$recorded = $transport->messages()->sole();
```

Deze `raw()` is een voorbeeld om het synchrone verzendpad te controleren zonder een view voor te bereiden. Ook bij normaal versturen met een Mailable genereert de Mailer eerst de inhoud en geeft die daarna door aan dezelfde Transport.

## Grenzen bij het bouwen van een echte bezorg-Transport

Voor een implementatie die naar een externe dienst bezorgt, kun je volgens de officiële documentatie `Symfony\Component\Mailer\Transport\AbstractTransport` uitbreiden en `doSend(SentMessage $message): void` en `__toString(): string` implementeren. De registratiemethode hierboven kun je ongewijzigd gebruiken.

| Wat je controleert | Verantwoordelijkheid van het package |
| - | - |
| Bericht en ontvangers | Niet alleen de tekst, maar ook HTML, bijlagen, CC/BCC en de ontvangers uit de Envelope correct doorgeven aan de API van de bezorgdienst |
| Fouten | API-fouten niet als succes inslikken, maar doorgeven aan de aanroeper |
| Gescheiden configuratie | Het endpoint en de inloggegevens per Mailer toepassen op de Transport |
| Stringrepresentatie | Geen inloggegevens of e-mailinhoud opnemen in `__toString()` |
| Testen | Naast tests voor registratie, configuratie en opnieuw resolven ook de omzetting naar de API en de foutafhandeling verifiëren |

`Mailer::sendSymfonyMessage()` van Laravel roept `send()` van de Transport aan. Als een `MessageSending`-listener het verzenden afbreekt of de Transport `null` retourneert, retourneert ook `send()` van Laravel geen verzendresultaat. **Beschouw het uitblijven van een exception niet als enig bewijs van geslaagde bezorging.** Ook als er een `SentMessage` terugkomt, is dat geen garantie dat het bericht de mailbox van de ontvanger heeft bereikt.

## Onderscheid configuratiewijzigingen van het opnieuw resolven van Mailers

`MailManager` houdt Mailers en hun Transport per Mailernaam vast. Alleen de configuratie wijzigen of `extend()` opnieuw registreren werkt al aangemaakte objecten niet bij.

```php theme={null}
use Illuminate\Support\Facades\Mail;

$before = Mail::mailer('receipts');

config(['mail.mailers.receipts.label' => 'receipts-v2']);

$same = Mail::mailer('receipts');

Mail::purge('receipts');

$after = Mail::mailer('receipts');
```

`$same` is dezelfde Mailer als `$before`. Bij `$after`, die na `purge('receipts')` wordt opgehaald, wordt de Transport aangemaakt op basis van de nieuwe configuratie.

| Bewerking | Doel |
| - | - |
| `Mail::purge('receipts')` | Verwijdert de referentie naar de opgegeven Mailer die de Manager vasthoudt |
| `Mail::forgetMailers()` | Verwijdert de referenties naar alle Mailers die de Manager vasthoudt. Geregistreerde uitbreidingen blijven behouden |
| `$transport->flush()` | Wist de vastgelegde berichten die de MemoryTransport uit dit voorbeeld vasthoudt |

De gecontroleerde implementatie van `purge()` verwijdert alleen de referentie uit de interne array van de Manager. De oude `$before` en services waarin die Mailer al is geïnjecteerd, worden niet vervangen. Het sluit ook niet expliciet externe verbindingen af.

### Bovenliggende Mailers zoals failover worden apart vastgehouden

`failover` en `roundrobin` lezen de configuratie van de onderliggende Mailernamen, **maken de Transports rechtstreeks aan** en nemen ze op in de Transport van de bovenliggende Mailer. Ze delen geen instanties van `Mail::mailer()` van de onderliggende Mailers.

Wijzig je daarom de configuratie van een onderliggende Mailer en roep je `purge('receipts')` aan, dan wordt de interne Transport van een al geresolvede bovenliggende Mailer niet bijgewerkt. Ook de betreffende bovenliggende Mailer moet opnieuw worden geresolved. Zelfs `forgetMailers()` kan referenties naar bovenliggende Mailers die de gebruikende code al vasthoudt niet vervangen.

Neem in productie het vernieuwen van de configuratiecache en het herstarten van langlevende workers op in je normale deployproces. Zie opnieuw resolven niet als een gelijktijdige update van alle processen, en lees ook [Levensduur van services](https://kawax.biz/nl/advanced/package-service-lifetimes.md).

## Test niet alleen met Mail::fake, maar ook het echte aanmaakpad

`Mail::fake()` dient om het versturen en in de queue plaatsen van Mailables vast te leggen. `MailFake::mailer()` resolvet geen echte Mailer, maar legt de Mailernaam vast en retourneert de fake zelf. Ook `raw()` verstuurt niets. Assertions voor verzonden Mailables kunnen dus slagen, zelfs als je eigen Transport niet is geregistreerd.

Laat in de [testomgeving van het package](https://kawax.biz/nl/advanced/package-testing.md) zoals hieronder de echte Manager en Transport doorlopen, **zonder fake**. De Transport in dit voorbeeld verstuurt niets naar buiten, zodat je het registratiepad zonder risico op echte bezorging kunt controleren. Er wordt eerst een hulpprovider geladen, zodat ook het geval van een al geresolvede Manager wordt geverifieerd.

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

namespace Acme\Postbox\Tests;

use Acme\Postbox\PostboxServiceProvider;
use Acme\Postbox\Transport\MemoryTransport;
use Illuminate\Mail\Message;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\ServiceProvider;
use Orchestra\Testbench\TestCase;
use Symfony\Component\Mime\Email;

class MailTransportTest extends TestCase
{
    protected function getPackageProviders($app): array
    {
        return [EarlyMailManagerProvider::class, PostboxServiceProvider::class];
    }

    protected function defineEnvironment($app): void
    {
        $app['config']->set('mail.default', 'receipts');
        $app['config']->set('mail.from', [
            'address' => 'noreply@example.com',
            'name' => 'Postbox',
        ]);
        $app['config']->set('mail.mailers.receipts', [
            'transport' => 'acme-memory-mail',
            'label' => 'receipts-v1',
        ]);
        $app['config']->set('mail.mailers.alerts', [
            'transport' => 'acme-memory-mail',
            'label' => 'alerts',
        ]);
    }

    public function test_registers_on_an_already_resolved_manager(): void
    {
        $this->assertSame(
            $this->app->make('postbox.test.early-manager'),
            $this->app->make('mail.manager'),
        );

        $transport = Mail::mailer('receipts')->getSymfonyTransport();

        $this->assertInstanceOf(MemoryTransport::class, $transport);
        $this->assertSame('acme-memory-mail:receipts-v1', (string) $transport);
    }

    public function test_message_reaches_the_transport(): void
    {
        $mailer = Mail::mailer('receipts');

        $sent = $mailer->raw('Receipt body', static function (Message $message): void {
            $message->to('reader@example.com')->subject('Receipt');
        });

        $transport = $mailer->getSymfonyTransport();

        $this->assertNotNull($sent);
        $this->assertCount(1, $transport->messages());

        $recorded = $transport->messages()->sole();
        $email = $recorded->getOriginalMessage();

        $this->assertInstanceOf(Email::class, $email);
        $this->assertSame('Receipt', $email->getSubject());
        $this->assertSame('Receipt body', $email->getTextBody());
        $this->assertSame('noreply@example.com', $email->getFrom()[0]->getAddress());
        $this->assertSame(
            'reader@example.com',
            $recorded->getEnvelope()->getRecipients()[0]->getAddress(),
        );
    }

    public function test_mailer_names_have_separate_transports(): void
    {
        $receipts = Mail::mailer('receipts');
        $alerts = Mail::mailer('alerts');

        $this->assertNotSame($receipts->getSymfonyTransport(), $alerts->getSymfonyTransport());
        $this->assertSame('acme-memory-mail:alerts', (string) $alerts->getSymfonyTransport());

        $receipts->raw('Receipt body', static function (Message $message): void {
            $message->to('reader@example.com')->subject('Receipt');
        });

        $this->assertCount(1, $receipts->getSymfonyTransport()->messages());
        $this->assertCount(0, $alerts->getSymfonyTransport()->messages());
    }

    public function test_config_change_requires_purging_the_mailer(): void
    {
        $before = Mail::mailer('receipts');

        config(['mail.mailers.receipts.label' => 'receipts-v2']);

        $this->assertSame($before, Mail::mailer('receipts'));
        $this->assertSame('acme-memory-mail:receipts-v1', (string) $before->getSymfonyTransport());

        Mail::purge('receipts');

        $after = Mail::mailer('receipts');

        $this->assertNotSame($before, $after);
        $this->assertSame('acme-memory-mail:receipts-v2', (string) $after->getSymfonyTransport());
        $this->assertSame('acme-memory-mail:receipts-v1', (string) $before->getSymfonyTransport());
    }
}

class EarlyMailManagerProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->app->instance('postbox.test.early-manager', $this->app->make('mail.manager'));
    }
}
```

Deze test controleert de grenzen van registratie, configuratie, synchroon versturen en vastgehouden objecten. Verifieer bij een echte bezorg-Transport daarnaast de payload en foutresponses met een mock of fake van de API-client. Als je versturen via de queue aanbiedt, scheid dan ook de tests voor [Queues](https://kawax.biz/nl/queues.md) en controleer dat de provider ook aan de kant van de worker wordt geladen.

## Geraadpleegde primaire bronnen

* [Officiële Laravel-documentatie: eigen Transports en testen voor e-mail (op het moment van controle)](https://github.com/laravel/docs/blob/960eedf0911155ad16326ade49c0ea5d18eda0b8/mail.md#custom-transports)
* [Officiële Laravel-documentatie: packageontwikkeling](https://github.com/laravel/docs/blob/13.x/packages.md)
* [MailManager: aanmaken, vasthouden van Mailers, purge en samengestelde Transports](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Mail/MailManager.php)
* [MailServiceProvider: binding van de MailManager](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Mail/MailServiceProvider.php)
* [ServiceProvider: callAfterResolving](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Support/ServiceProvider.php)
* [Mailer: versturen naar de Transport en events](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Mail/Mailer.php)
* [ArrayTransport: vastleggen van berichten](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Mail/Transport/ArrayTransport.php)
* [Mail-facade en MailFake](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Support/Facades/Mail.php)
* [MailFake: Mailerselectie en vastleggen van Mailables](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Support/Testing/Fakes/MailFake.php)
* [Officiële Laravel-tests: verifiëren van verzending met Mailer en ArrayTransport](https://github.com/laravel/framework/blob/v13.39.0/tests/Mail/MailMailerTest.php)


## Related topics

- [E-mail versturen](https://kawax.biz/nl/mail.md)
- [Geavanceerde onderwerpen](https://kawax.biz/nl/advanced/index.md)
- [Event listeners van een package registreren en cachen](https://kawax.biz/nl/advanced/package-events.md)
- [Storage-drivers registreren in een package en opnieuw resolven](https://kawax.biz/nl/advanced/package-filesystem-drivers.md)
- [Artisan-commands van een package registreren en afhankelijkheden resolven](https://kawax.biz/nl/advanced/package-commands.md)


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