v13.39.0. Para el envío básico, consulta Envío de correo, y para preparar el provider, Desarrollo de paquetes.
Separa el nombre del mailer del nombre del transport
Mail::mailer('receipts') lee mail.mailers.receipts y llama al proceso de creación correspondiente a transport. MailManager::build() pasa ese Transport al Mailer de Laravel y, en la resolución normal por nombre, también configura el remitente global y otros valores.
Los creadores personalizados se eligen antes que los Transports integrados. No sobrescribas smtp ni ses; usa un nombre de transport propio del paquete.
Construye el flujo de registro sin envíos externos
Primero comprueba la configuración y el flujo de resolución, no la API de destino. Ensrc/Transport/MemoryTransport.php del paquete Acme\Postbox, crea una clase que herede del ArrayTransport estándar.
ArrayTransport::send() guarda el SentMessage de Symfony en una Collection interna. Puedes consultarlo con messages() y borrar los registros de esa instancia con flush(). La etiqueta sirve para comprobar que la configuración llegó al proceso de creación.
Regístralo en el Manager de carga diferida
La documentación oficial llama aMail::extend() en boot(). En un paquete puedes combinarlo con un hook posterior a la resolución del servicio para no generar el Manager innecesariamente en aplicaciones que no usan correo.
Crea src/PostboxServiceProvider.php.
- El
MailServiceProviderde Laravel es un provider diferido y registramail.managercomo singleton. La facadeMailtambién resuelve esta clave. callAfterResolving()espera a la resolución posterior si aún no se ha resuelto, y aplica la extensión de inmediato a un Manager ya resuelto. No reemplaza los Mailers que ya se hayan generado.- Evita convertir sin más en
DeferrableProvidertu propio provider que registra la extensión. Así te aseguras de que no se solicite el Transport propio antes de registrar el hook. - Este ejemplo registra el hook en su propio
boot(). No garantiza un orden de arranque en el que otro provider use el mailer propio antes. Diseña el orden de registro y no envíes correo dentro deregister().
No confundas los argumentos de extend con los de otros Managers
MailManager::createSymfonyTransport() llama al creador personalizado con un único argumento: el array de configuración. No son los dos argumentos, aplicación y configuración, de Cache::extend(). El valor devuelto es un Transport que implementa TransportInterface, no un Mailer de Laravel ni el resultado de un envío.
Además, MailManager::extend() solo guarda el closure y no lo vuelve a vincular al Manager. El ejemplo usa closures static y argumentos explícitos para no depender de cómo se interprete $this.
Si necesitas un cliente para una API externa, aplica la configuración en este proceso de creación y pásalo al Transport. No envíes nada en el momento del registro, y diseña la solución para que, cuando haya varios mailers con el mismo nombre de transport, no se mezclen sus configuraciones ni sus destinos.
Añade los mailers a la aplicación que usa el paquete
Añade estos dos elementos al arraymailers existente en config/mail.php. El paquete no debe sobrescribir el valor predeterminado del usuario ni el array mailers completo.
MAIL_MAILER es el nombre del mailer, receipts, no el nombre del transport. Si lo especificas de forma individual, puedes usarlo sin cambiar el valor predeterminado existente.
raw() es un ejemplo para comprobar el flujo de envío síncrono sin preparar una vista. En el envío normal con Mailables, el Mailer también genera el cuerpo y demás contenido y luego lo entrega al mismo Transport.
Límites al crear un Transport de entrega real
Para una implementación que entregue a un servicio externo, puedes seguir la documentación oficial: heredar deSymfony\Component\Mailer\Transport\AbstractTransport e implementar doSend(SentMessage $message): void y __toString(): string. El método de registro anterior se puede usar tal cual.
Mailer::sendSymfonyMessage() de Laravel llama al send() del Transport. Si un listener de MessageSending cancela el envío o el Transport devuelve null, el send() de Laravel tampoco devuelve un resultado de envío. No consideres que la entrega fue correcta solo porque no hubo excepciones. Aunque se devuelva un SentMessage, eso no garantiza que haya llegado al buzón del destinatario.
Distingue el cambio de configuración de la nueva resolución del mailer
MailManager retiene cada Mailer y su Transport por nombre de mailer. Cambiar la configuración o volver a registrar extend() no actualiza los objetos ya generados.
$same es el mismo Mailer que $before. En $after, obtenido después de purge('receipts'), el Transport se genera a partir de la nueva configuración.
La implementación revisada de
purge() solo elimina la referencia del array interno del Manager. No reemplaza el antiguo $before ni los servicios a los que ya se haya inyectado ese Mailer. Tampoco cierra explícitamente las conexiones externas.
Los mailers padre, como failover, también se retienen por separado
failover y roundrobin leen la configuración de los nombres de mailer hijos, generan los Transports directamente y los incorporan al Transport padre. No comparten la instancia Mail::mailer() de los hijos.
Por eso, aunque cambies la configuración de un hijo y llames a purge('receipts'), el Transport interno del mailer padre ya resuelto no se actualiza. También hay que volver a resolver el mailer padre afectado. Ni siquiera forgetMailers() puede reemplazar las referencias al mailer padre que ya conserva el código que lo usa.
En producción, incluye la actualización de la caché de configuración y el reinicio de los workers de larga duración en el procedimiento de despliegue habitual. No pienses en la nueva resolución como una actualización simultánea de todos los procesos, y consulta también El ciclo de vida de los servicios.
Prueba el flujo de creación real, no solo Mail::fake
Mail::fake() sirve para registrar el envío y la puesta en cola de Mailables. MailFake::mailer() no resuelve el Mailer real: registra el nombre del mailer y devuelve el propio fake. raw() tampoco realiza ningún envío. Las aserciones de envío de Mailables pueden pasar aunque el Transport propio no esté registrado.
En el entorno de pruebas del paquete, haz pasar el flujo por el Manager y el Transport reales sin usar fake, como en el siguiente ejemplo. Como el Transport de este ejemplo no envía nada al exterior, puedes comprobar el flujo de registro sin riesgo de entregas reales. Se carga primero un provider auxiliar para verificar también el caso en que el Manager ya está resuelto.
Fuentes primarias consultadas
- Documentación oficial de Laravel: Transports personalizados de correo y pruebas (en el momento de la revisión)
- Documentación oficial de Laravel: desarrollo de paquetes
- MailManager: creación, retención de mailers, purge y Transports compuestos
- MailServiceProvider: binding del MailManager
- ServiceProvider: callAfterResolving
- Mailer: envío al Transport y eventos
- ArrayTransport: registro de mensajes
- Facade Mail y MailFake
- MailFake: selección del mailer y registro de Mailables
- Pruebas oficiales de Laravel: verificación del envío con Mailer y ArrayTransport