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

# Enregistrer et tester des transports de mail dans un package

> Analysez le MailManager de Laravel 13 pour comprendre l'enregistrement d'un transport depuis un package, la transmission de la configuration, la conservation et la nouvelle résolution des Mailers, ainsi que les chemins d'envoi que le fake ne permet pas de vérifier.

Pour fournir une méthode d'envoi de mail propre depuis un package, enregistrez auprès de Laravel **la création d'un Transport Symfony**. Il n'est pas nécessaire de réécrire les Mailables ni le Mailer de Laravel.

Cette page a été vérifiée sur l'implémentation de Laravel Framework `v13.39.0`. Pour l'envoi de base, consultez [Envoi d'e-mails](https://kawax.biz/fr/mail.md), et pour préparer le provider, consultez [Développement de packages](https://kawax.biz/fr/advanced/package-development.md).

## Distinguer le nom du Mailer du nom du Transport

| Élément | Exemple de cette page | Rôle |
| - | - | - |
| Nom du Transport | `acme-memory-mail` | La méthode de création enregistrée avec `extend()` |
| Nom du Mailer | `receipts` / `alerts` | Le nom de configuration dans `mail.mailers` et l'identifiant du Mailer généré |
| Transport Symfony | `MemoryTransport` | Le traitement de livraison des mails. Dans cet exemple, un simple enregistrement en mémoire |
| Mailer Laravel | `Illuminate\Mail\Mailer` | Génération du contenu, événements d'envoi, transmission au Transport, etc. |

`Mail::mailer('receipts')` lit `mail.mailers.receipts` et appelle la création correspondant à `transport`. `MailManager::build()` transmet ce Transport au Mailer de Laravel et, lors d'une résolution par nom classique, configure aussi l'expéditeur global, entre autres.

```mermaid theme={null}
flowchart TD
    A["Mail::mailer('receipts')"] --> B{"receipts déjà résolu ?"}
    B -->|Oui| C["Retourner le Mailer conservé"]
    B -->|Non| D["Lire mail.mailers.receipts"]
    D --> E["Création pour acme-memory-mail<br>seul argument : le tableau de configuration"]
    E --> F["Retourner un Transport Symfony"]
    F --> G["Créer le Mailer de Laravel et<br>le conserver sous le nom receipts"]
    G --> H["Appeler Transport::send() à l'envoi"]
    C --> H
```

Les créations personnalisées sont choisies avant les Transports intégrés. N'écrasez pas `smtp` ou `ses` : utilisez un nom de Transport propre au package.

## Construire le chemin d'enregistrement sans envoi externe

Commencez par vérifier la configuration et le chemin de résolution plutôt que l'API de destination. Dans `src/Transport/MemoryTransport.php` du package `Acme\Postbox`, créez une classe qui hérite de l'`ArrayTransport` standard.

```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()` enregistre le `SentMessage` de Symfony dans une Collection interne. Vous pouvez le consulter avec `messages()` et effacer les enregistrements de cette instance avec `flush()`. Le label sert à vérifier que la configuration atteint bien la création.

> [!WARNING]
>
> Cet exemple ne livre aucun mail vers l'extérieur. Il sert uniquement à apprendre l'enregistrement et les tests. Les enregistrements en mémoire ne sont ni partagés entre processus ni persistés, et ne confirment pas la réception par le destinataire. Si votre seul objectif est d'enregistrer en mémoire, le Transport intégré `array` suffit.

### Enregistrer sur un Manager chargé de manière différée

La documentation officielle appelle `Mail::extend()` dans `boot()`. Dans un package, vous pouvez combiner cela avec les [hooks après résolution de service](https://kawax.biz/fr/advanced/package-service-resolution.md) afin d'éviter de créer inutilement le Manager dans les applications qui n'utilisent pas le mail.

Créez `src/PostboxServiceProvider.php`.

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

Chargez le provider via la [découverte automatique de packages](https://kawax.biz/fr/advanced/package-discovery.md) ou un mécanisme équivalent.

* Le `MailServiceProvider` de Laravel est un provider différé qui enregistre `mail.manager` en singleton. La façade `Mail` résout également cette clé.
* `callAfterResolving()` attend la résolution ultérieure si le Manager n'est pas encore résolu, et applique immédiatement l'extension à un Manager déjà résolu. **Il ne remplace pas les Mailers déjà générés.**
* Évitez de rendre simplement `DeferrableProvider` votre propre provider qui enregistre l'extension. Faites en sorte que le Transport personnalisé ne soit pas demandé avant l'enregistrement du hook.
* Cet exemple enregistre le hook dans son propre `boot()`. Il ne garantit pas un ordre de démarrage où un autre provider utiliserait le Mailer personnalisé plus tôt. Concevez l'ordre d'enregistrement et n'envoyez pas de mail dans `register()`.

### Ne pas confondre les arguments d'extend avec ceux d'autres Managers

`MailManager::createSymfonyTransport()` appelle la création personnalisée avec **un seul argument : le tableau de configuration**. Ce ne sont pas les deux arguments application et configuration de `Cache::extend()`. La valeur de retour est un Transport implémentant `TransportInterface`, et non un Mailer de Laravel ni un résultat d'envoi.

De plus, `MailManager::extend()` se contente de conserver la closure et ne la relie pas au Manager. L'exemple utilise une closure statique et des arguments explicites afin de ne pas dépendre de l'interprétation de `$this`.

Si vous avez besoin d'un client pour une API externe, appliquez la configuration dans cette création et transmettez-le au Transport. N'envoyez rien au moment de l'enregistrement, et concevez le code de façon à ne pas mélanger configurations et destinations lorsque plusieurs Mailers partagent le même nom de Transport.

### Ajouter les Mailers dans l'application cliente

Ajoutez les deux entrées suivantes au tableau `mailers` existant de `config/mail.php`. Le package ne doit pas écraser la valeur par défaut de l'utilisateur ni l'ensemble de `mailers`.

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

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

Pour en faire la valeur par défaut, indiquez dans `MAIL_MAILER` **le nom du Mailer `receipts`**, et non le nom du Transport. En le désignant explicitement, vous pouvez l'utiliser sans modifier la valeur par défaut existante.

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

Ce `raw()` est un exemple permettant de vérifier le chemin d'envoi synchrone sans préparer de vue. Lors d'un envoi classique avec un Mailable, le Mailer génère le contenu puis le transmet au même Transport.

## Limites lors de la création d'un vrai Transport de livraison

Pour une implémentation qui livre vers un service externe, vous pouvez, conformément à la documentation officielle, hériter de `Symfony\Component\Mailer\Transport\AbstractTransport` et implémenter `doSend(SentMessage $message): void` et `__toString(): string`. La méthode d'enregistrement ci-dessus reste identique.

| Point à vérifier | Responsabilité du package |
| - | - |
| Message et destinataires | Transmettre correctement à l'API de destination non seulement le corps, mais aussi le HTML, les pièces jointes, CC et BCC, et les destinataires de l'Envelope |
| Erreurs | Ne pas masquer un échec de l'API en succès, mais le propager à l'appelant |
| Isolation de la configuration | Appliquer au Transport la destination et les identifiants propres à chaque Mailer |
| Représentation textuelle | Ne pas inclure d'identifiants ni de contenu de mail dans `__toString()` |
| Tests | En plus des tests d'enregistrement, de configuration et de nouvelle résolution, vérifier la conversion vers l'API et la gestion des erreurs |

`Mailer::sendSymfonyMessage()` de Laravel appelle `send()` sur le Transport. Si un listener `MessageSending` annule l'envoi ou si le Transport retourne `null`, le `send()` de Laravel ne retourne pas non plus de résultat d'envoi. **Ne vous fiez pas à la seule absence d'exception pour juger qu'une livraison a réussi.** Même si un `SentMessage` est retourné, cela ne garantit pas que le mail a atteint la boîte du destinataire.

## Distinguer changement de configuration et nouvelle résolution du Mailer

`MailManager` conserve le Mailer et son Transport par nom de Mailer. Un changement de configuration ou un nouvel enregistrement via `extend()` ne met pas à jour les objets déjà générés.

```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` est le même Mailer que `$before`. Pour `$after`, obtenu après `purge('receipts')`, le Transport est créé à partir de la nouvelle configuration.

| Opération | Cible |
| - | - |
| `Mail::purge('receipts')` | Retire la référence au Mailer indiqué conservée par le Manager |
| `Mail::forgetMailers()` | Retire les références à tous les Mailers conservés par le Manager. Les extensions enregistrées sont conservées |
| `$transport->flush()` | Efface les messages enregistrés par le MemoryTransport de cet exemple |

L'implémentation de `purge()` examinée se contente de retirer la référence du tableau interne du Manager. L'ancien `$before` et les services dans lesquels ce Mailer a déjà été injecté ne sont pas remplacés. Ce n'est pas non plus une opération qui ferme explicitement les connexions externes.

### Les Mailers parents comme failover sont aussi conservés séparément

`failover` et `roundrobin` lisent la configuration des noms de Mailers enfants, **créent directement les Transports** et les intègrent dans le Transport parent. Ils ne partagent pas les instances `Mail::mailer()` des enfants.

Par conséquent, même si vous modifiez la configuration d'un enfant et appelez `purge('receipts')`, le Transport interne d'un Mailer parent déjà résolu n'est pas mis à jour. Il faut aussi résoudre à nouveau le Mailer parent concerné. Même `forgetMailers()` ne peut pas remplacer les références au Mailer parent déjà conservées par le code appelant.

En production, intégrez la mise à jour du cache de configuration et le redémarrage des workers de longue durée dans votre procédure de déploiement habituelle. Ne considérez pas la nouvelle résolution comme une mise à jour globale de tous les processus, et consultez également le [cycle de vie des services](https://kawax.biz/fr/advanced/package-service-lifetimes.md).

## Tester le vrai chemin de création, pas seulement Mail::fake

`Mail::fake()` sert à enregistrer l'envoi et la mise en queue des Mailables. `MailFake::mailer()` ne résout pas de vrai Mailer : il enregistre le nom du Mailer et retourne le fake lui-même. `raw()` n'effectue pas non plus d'envoi. Les assertions d'envoi de Mailables peuvent donc réussir même sans Transport personnalisé enregistré.

Dans votre [environnement de test de package](https://kawax.biz/fr/advanced/package-testing.md), faites passer les appels par le vrai Manager et le vrai Transport, **sans fake**, comme ci-dessous. Le Transport de cet exemple n'envoie rien vers l'extérieur, ce qui permet de vérifier le chemin d'enregistrement sans risque de livraison réelle. Un provider auxiliaire est chargé en premier pour vérifier aussi le cas où le Manager est déjà résolu.

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

Ce test vérifie les limites de l'enregistrement, de la configuration, de l'envoi synchrone et des objets conservés. Pour un vrai Transport de livraison, vérifiez séparément la charge utile et les réponses d'échec à l'aide d'un mock ou d'un fake du client API. Si vous proposez l'envoi via queue, séparez aussi les tests des [queues](https://kawax.biz/fr/queues.md) et vérifiez que le provider est bien chargé côté worker.

## Sources primaires consultées

* [Documentation officielle de Laravel : Transports personnalisés et tests des mails (au moment de la vérification)](https://github.com/laravel/docs/blob/960eedf0911155ad16326ade49c0ea5d18eda0b8/mail.md#custom-transports)
* [Documentation officielle de Laravel : Développement de packages](https://github.com/laravel/docs/blob/13.x/packages.md)
* [MailManager : création, conservation des Mailers, purge et Transports composites](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Mail/MailManager.php)
* [MailServiceProvider : liaison du 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 : envoi vers le Transport et événements](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Mail/Mailer.php)
* [ArrayTransport : enregistrement des messages](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Mail/Transport/ArrayTransport.php)
* [Façade Mail et MailFake](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Support/Facades/Mail.php)
* [MailFake : sélection du Mailer et enregistrement des Mailables](https://github.com/laravel/framework/blob/v13.39.0/src/Illuminate/Support/Testing/Fakes/MailFake.php)
* [Tests officiels de Laravel : vérification de l'envoi avec Mailer et ArrayTransport](https://github.com/laravel/framework/blob/v13.39.0/tests/Mail/MailMailerTest.php)


## Related topics

- [Envoi d'e-mails](https://kawax.biz/fr/mail.md)
- [Sujets avancés](https://kawax.biz/fr/advanced/index.md)
- [Enregistrer des drivers Storage dans un package et les résoudre à nouveau](https://kawax.biz/fr/advanced/package-filesystem-drivers.md)
- [Cache d'un package et intégration à optimize](https://kawax.biz/fr/advanced/package-optimization.md)
- [Tester des packages Laravel avec Orchestra Testbench](https://kawax.biz/fr/advanced/package-testing.md)


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