![]() |
|
The More with symfony bookEmails |
|
You are currently reading "The More with symfony book" which is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.
realtime single_address spool none 
|
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License. |
por Fabien Potencier
Enviar emails con symfony es sencillo a la vez que potente, gracias al uso de la librería Swift Mailer. Aunque enviar emails con Swift Mailer es muy sencillo, symfony añade una capa adicional por encima para hacer que el envío de emails sea todavía más flexible y potente. Este capítulo explica todas las opciones a tu disposición.
symfony 1.3 incluye la versión 4.1 de Swift Mailer.
Symfony gestiona la creación y envío de emails a través de un objeto de tipo
mailer. Como la mayoría de objetos del núcleo de symfony, el objeto mailer
también es una factoría. Su comportamiento se configura mediante el archivo de
configuración factories.yml y siempre está disponible a través del objeto
que almacena el contexto:
$mailer = sfContext::getInstance()->getMailer();
Al contrario que el resto de factorías, el objeto mailer se carga e inicializa bajo demanda. Por tanto, si no lo utilizas no se penaliza el rendimiento de la aplicación.
Este tutorial explica la integración de Swift Mailer con symfony. Si quieres conocer todos los detalles de la librería Swift Mailer, puedes leer su propia documentación.
Obtener la instancia del objeto mailer en una acción es muy sencillo gracias
al atajo getMailer():
$mailer = $this->getMailer();
Enviar un email es tan sencillo como utilizar el método
sfAction::composeAndSend():
$this->getMailer()->composeAndSend( 'remitente@ejemplo.com', 'fabien@ejemplo.com', 'Asunto', 'Cuerpo' );
El método composeAndSend() utiliza cuatro argumentos:
from)to)Siempre que un método utilice una dirección de email como argumento, se puede indicar como cadena de texto o como array:
$direccion = 'fabien@ejemplo.com'; $direccion = array('fabien@ejemplo.com' => 'Fabien Potencier');
Obviamente puedes enviar un mismo email a varios destinatarios pasando como segundo argumento del método un array con todas las direcciones de email:
$para = array( 'destinatario1@ejemplo.com', 'destinatario2@ejemplo.com', ); $this->getMailer()->composeAndSend('remitente@ejemplo.com', $para, 'Asunto', 'Cuerpo'); $para = array( 'destinatario1@ejemplo.com' => 'Sr. Destinatario', 'destinatario2@ejemplo.com' => 'Sra. Destinataria', ); $this->getMailer()->composeAndSend('remitente@ejemplo.com', $para, 'Asunto', 'Cuerpo');
Si necesitas más flexibilidad, puedes hacer uso del método
sfAction::compose() para crear un mensaje, personalizarlo de la forma que
quieras y enviarlo después. Esta forma es útil por ejemplo cuando quieres
añadir un adjunto como se muestra a continuación:
// crear un objeto de tipo mensaje $mensaje = $this->getMailer() ->compose('remitente@ejemplo.com', 'fabien@ejemplo.com', 'Asunto', 'Cuerpo') ->attach(Swift_Attachment::fromPath('/ruta/hasta/el/archivo.zip')) ; // enviar el mensaje $this->getMailer()->send($mensaje);
Si necesitas aún más flexibilidad, puedes crear directamente el objeto del mensaje:
$mensaje = Swift_Message::newInstance() ->setFrom('remitente@ejemplo.com') ->setTo('destinatario@ejemplo.com') ->setSubject('Asunto') ->setBody('Cuerpo') ->attach(Swift_Attachment::fromPath('/ruta/hasta/el/archivo.zip')) ; $this->getMailer()->send($mensaje);
Si quieres saberlo todo sobre cómo crear mensajes, puedes leer las secciones "Creando mensajes" y "Cabeceras de los mensajes" de la documentación oficial de Swift Mailer.
Enviar los emails desde las acciones permite aprovechar fácilmente todas las características de los componentes y de los elementos parciales.
$mensaje->setBody($this->getPartial('nombre_del_parcial', $argumentos));
Como el objeto mailer también es una factoría, se puede modificar su
comportamiento mediante el archivo de configuración factories.yml. Su
configuración por defecto es la siguiente:
mailer:
class: sfMailer
param:
logging: %SF_LOGGING_ENABLED%
charset: %SF_CHARSET%
delivery_strategy: realtime
transport:
class: Swift_SmtpTransport
param:
host: localhost
port: 25
encryption: ~
username: ~
password: ~
Cuando se crea una nueva aplicación, el archivo factories.yml local redefine
la configuración anterior para establecer unos valores más apropiados en los
entornos prod, env y test:
test:
mailer:
param:
delivery_strategy: none
dev:
mailer:
param:
delivery_strategy: none
La estrategia o mecanismo de envío es una de las características más útiles de
la integración de Swift Mailer con symfony. La estrategia de envío permite
indicar a symfony la forma en la que se envían los mensajes y se puede
configurar mediante la opción delivery_strategy del archivo de
configuración factories.yml. La estrategia modifica el comportamiento del
método send()|sfMailer::send(). Por defecto se dispone de cuatro
estrategias diferentes, que cubren todas las necesidades habituales:
realtime: los mensajes se envían en tiempo real.single_address: los mensajes se envían a la dirección de correo electrónico indicada.spool: los mensajes se guardan en una cola de envío.none: los mensajes no se envían y simplemente se ignoran.realtimeLa estrategia realtime es la que se utiliza por defecto y la más sencilla de
configurar porque no se debe hacer nada especial.
Los emails se envían mediante el transporte configurado en la sección
transport del archivo de configuración factories.yml (la siguiente sección
explica cómo configurar un transporte para email).
single_addressUtilizando la estrategia single_address, todos los mensajes se envían a una
dirección de correo electrónico configurada en la opción delivery_address.
Esta estrategia es muy útil en el entorno de desarrollo para no enviar los emails a los usuarios reales pero al mismo tiempo permitir que el programador pueda comprobar con su lector de correo el aspecto y contenido de los emails enviados.
Si quieres comprobar las direcciones
para,ccybccoriginales, están disponibles en las cabecerasX-Swift-To,X-Swift-CcyX-Swift-Bccrespectivamente.
Los emails se envían mediante el mismo transporte que utiliza la estrategia
realtime.
spoolLa estrategia spool hace que todos los mensajes se almacenen en una cola.
Esta es la mejor estrategia para el entorno de producción, ya que las peticiones web no tienen que esperar a que se envíen los emails.
La clase spool se configura mediante la opción spool_class. Symfony
incluye por defecto tres clases de este tipo:
Swift_FileSpool: los mensajes se guardan en el sistema de archivos.Swift_DoctrineSpool: los mensajes se guardan en un modelo de Doctrine.Swift_PropelSpool: los mensajes se guardan en un modelo de Propel.Cuando se instancia la clase, se pasa como argumento de su constructor la
opción spool_arguments. A continuación se indican las opciones disponibles
para los tipos de colas incluidos por defecto:
Swift_FileSpool:
Swift_DoctrineSpool:
El modelo de Doctrine en el que se guardan los mensajes (por defecto es
MailMessage)
El nombre de la columna utilizada para guardar el mensaje (por defecto
es message)
El método que se invoca para obtener los mensajes a enviar (opcional). Como argumento de este método se le pasan las opciones de la cola.
Swift_PropelSpool:
El modelo de Propel en el que se guardan los mensajes (por defecto es
MailMessage)
El nombre de la columna utilizada para guardar el mensaje (por defecto
es message)
El método que se invoca para obtener los mensajes a enviar (opcional). Como argumento de este método se le pasan las opciones de la cola.
Seguidamente se muestra la configuración típica de una cola de Doctrine:
# Configuración del esquema en schema.yml
MailMessage:
actAs: { Timestampable: ~ }
columns:
message: { type: clob, notnull: true }
# configuración en factories.yml
mailer:
class: sfMailer
param:
delivery_strategy: spool
spool_class: Swift_DoctrineSpool
spool_arguments: [ MailMessage, message, getSpooledMessages ]
Y la misma configuración de antes para una cola de Propel:
# Configuración del esquema en schema.yml
mail_message:
message: { type: clob, required: true }
created_at: ~
# configuración en factories.yml
dev:
mailer:
param:
delivery_strategy: spool
spool_class: Swift_PropelSpool
spool_arguments: [ MailMessage, message, getSpooledMessages ]
Para enviar todos los mensajes almacenados en la cola, puedes emplear la tarea
project:send-emails (esta tarea es completamente independiente del tipo de
cola y de sus opciones):
$ php symfony project:send-emails
La tarea
project:send-emailsrequiere como argumentos el nombre de una aplicación y un entorno.
Cuando se utiliza la tarea project:send-emails, los mensajes se envían con
el mismo transporte utilizado por la estrategia realtime.
La tarea
project:send-emailsse puede ejecutar en cualquier máquina, no necesariamente en la misma en la que se creó el mensaje. Esto es así porque todo se guarda en el objeto del mensaje, incluso los archivos adjuntos.
El funcionamiento interno de las colas es muy sencillo. Los emails se envían sin controlar los errores que se pueden producir, es decir, como si hubieran sido enviados con la estrategia
realtime. Obviamente puedes extender las clases de las colas para incluir tu propia lógica de gestión de errores.
La tarea project:send-emails admite opcionalmente las siguientes dos opciones:
message-limit: limita el número de mensajes que se envían.
time-limit: limita (en segundos) el tiempo empleado en enviar los mensajes.
Las dos opciones también se pueden combinar:
$ php symfony project:send-emails --message-limit=10 --time-limit=20
El comando anterior deja de enviar mensajes cuando se envían 10 mensajes o cuando transcurren 20 segundos.
Aun cuando hagas uso de la estrategia spool, puede que tengas que enviar un
mensaje de forma inmediata sin almacenarlo en la cola de mensajes. Para ello,
puedes hacer uso de un método especial del mailer llamado
sendNextImmediately():
$this->getMailer()->sendNextImmediately()->send($mensaje);
En el ejemplo anterior, el $mensaje no se guardará en la cola y se enviará
inmediatamente. Como su propio nombre indica, el método
sendNextImmediately() solamente afecta al siguiente mensaje que se envía.
El método
sendNextImmediately()no produce ningún efecto especial cuando la estrategia de envío no esspool.
noneEsta estrategia es muy útil en el entorno de desarrollo para no enviar emails a ningún usuario real. Los mensajes están disponibles en la barra de depuración web (más adelante se explica la sección del mailer en la barra de depuración web).
También se trata de la mejor estrategia para el entorno de pruebas, donde el
objeto sfTesterMailer permite la introspección de los mensajes sin tener que
enviarlos realmente (como se explica más adelante en la sección de pruebas).
Los mensajes de correo electrónico realmente se envían a través de un
transporte configurado en el archivo de configuración factories.yml. La
configuración por defecto hace uso del servidor SMTP de la máquina local:
transport:
class: Swift_SmtpTransport
param:
host: localhost
port: 25
encryption: ~
username: ~
password: ~
Swift Mailer incluye tres tipos diferentes de clases de transporte:
Swift_SmtpTransport: hace uso de un servidor SMTP para enviar los
mensajes.
Swift_SendmailTransport: emplea sendmail para enviar los mensajes.
Swift_MailTransport: utiliza la función mail() de PHP para enviar
los mensajes.
La sección "Tipos de transporte" de la documentación oficial de Swift Mailer describe todo lo necesitas saber sobre las clases anteriores y sobre sus parámetros.
Enviar un email desde una tarea es muy similar a enviar un email desde una
acción, ya que el sistema de tareas también proporciona un método getMailer().
Cuando se crea el mailer, la tarea utiliza la configuración actual, por lo que
si quieres hacer uso de la configuración de una aplicación específica, debes
incluir la opción --application (el capítulo dedicado a las tareas tiene más
información sobre esta opción).
La tarea utiliza la misma configuración que los controladores, por lo que si
quieres forzar el envío de los mensajes cuando se utiliza la estrategia spool
puedes emplear el método sendNextImmediately():
$this->getMailer()->sendNextImmediately()->send($mensaje);
Depurar el envío de emails siempre ha sido una pesadilla. Con symfony la depuración es muy sencilla gracias a la barra de depuración web.
Directamente desde el navegador se pueden ver fácilmente cuántos mensajes ha enviado la acción que se ha ejecutado:

Si pulsas sobre el icono del email, puedes visualizar todos los detalles de los mensajes enviados, como se muestra en la siguiente imagen.

Symfony también añade un mensaje en el archivo de log cada vez que se envía un email.
La integración de la librería no hubiera sido completa sin una forma sencilla
de realizar pruebas con mensajes de correo electrónico. Para facilitar las
pruebas con emails, symfony dispone de un tester denominado mailer (clase
sfMailerTester)
El método hasSent() prueba el número de mensajes enviados durante la
petición actual:
$browser-> get('/foo')-> with('mailer')-> hasSent(1) ;
El código anterior comprueba que la URL /foo solamente envía un email.
Se pueden realizar pruebas más detalladas con cada email enviado gracias a los
métodos checkHeader() y checkBody():
$browser-> get('/foo')-> with('mailer')->begin()-> hasSent(1)-> checkHeader('Asunto', '/Asunto/')-> checkBody('/Cuerpo/')-> end() ;
El segundo argumento de checkHeader() y el primero de checkBody() pueden
ser cualquiera de los siguientes elementos:
!) que no debe cumplir el valor comprobadoLas comprobaciones siempre se realizan sobre el primer mensaje enviado. Si se
han enviado varios mensajes, puedes elegir cual quieres comprobar mediante el
método withMessage().
$browser-> get('/foo')-> with('mailer')->begin()-> hasSent(2)-> withMessage('destinatario@ejemplo.com')-> checkHeader('Asunto', '/Asunto/')-> checkBody('/Cuerpo/')-> end() ;
El método withMessage() toma como primer argumento un destinatario. Si se
han enviado varios mensajes al mismo destinatario, también toma como segundo
argumento el mensaje que se quiere probar.
Por último, el método debug() muestra toda la información sobre los
mensajes enviados para detectar fácilmente la causa de los problemas:
$browser-> get('/foo')-> with('mailer')-> debug() ;
En la introducción de este capítulo se ha mostrado cómo enviar emails desde una acción. Esta es probablemente la forma más sencilla de enviar emails en una aplicación de symfony y la forma que debes utilizar cuando sólo tienes que enviar unos pocos mensajes.
No obstante, si tu aplicación maneja muchos tipos diferentes de mensajes de correo electrónico, probablemente debas utilizar una estrategia diferente.
Además, el uso de clases para los mensajes de correo electrónico significa que puedes reutilizar el mismo mensaje en diferentes aplicaciones, como por ejemplo en el frontend y en el backend.
Como los mensajes son objetos PHP normales, la forma más obvia de organizar tus mensajes consiste en crear una clase para cada uno de ellos:
// lib/email/ProjectConfirmationMessage.class.php class ProjectConfirmationMessage extends Swift_Message { public function __construct() { parent::__construct('Asunto', 'Cuerpo'); $this ->setFrom(array('aplicacion@ejemplo.com' => 'Robot de la Aplicación')) ->attach('...') ; } }
A continuación, enviar un mensaje desde una acción o desde cualquier otro punto de la aplicación es algo tan sencillo como instanciar la clase de mensaje adecuada:
$this->getMailer()->send(new ProjectConfirmationMessage());
Obviamente, puede ser muy útil crear una clase base que centralice todas las
cabeceras comunes, como por ejemplo la cabecera From, o la inclusión de la
misma firma en todos los mensajes:
// lib/email/ProjectConfirmationMessage.class.php class ProjectConfirmationMessage extends ProjectBaseMessage { public function __construct() { parent::__construct('Asunto', 'Cuerpo'); // cabeceras específicas, adjuntos, ... $this->attach('...'); } } // lib/email/ProjectBaseMessage.class.php class ProjectBaseMessage extends Swift_Message { public function __construct($asunto, $cuerpo) { $body .= <<<EOF -- Email enviado por el Robot de mi Aplicación EOF ; parent::__construct($asunto, $cuerpo); // añadir todas las cabeceras comunes $this->setFrom(array('aplicacion@ejemplo.com' => 'Robot de la Aplicación')); } }
Si un mensaje depende de algunos objetos del modelo, también puedes pasarlos como argumentos de su constructor:
// lib/email/ProjectConfirmationMessage.class.php class ProjectConfirmationMessage extends ProjectBaseMessage { public function __construct($usuario) { parent::__construct('Confirmación para '.$usuario->getNombre(), 'Cuerpo'); } }
Si no dispones de un servidor SMTP pero tienes una cuenta de correo electrónico de Gmail, utiliza la siguiente configuración para enviar los mensajes a través de los servidores de Google:
transport:
class: Swift_SmtpTransport
param:
host: smtp.gmail.com
port: 465
encryption: ssl
username: tu_nombre_de_usuario_de_gmail
password: tu_contrasena_de_gmail
Sustituye el valor de las opciones username y password por tus
credenciales de Gmail y ya puedes empezar a enviar emails.
Si necesitas una configuración mayor que la proporcionada por el archivo de
configuración factories.yml, puedes utilizar el evento mailer.configure
para personalizar todavía más el objeto mailer.
Puedes conectar tu aplicación con este evento directamente en la clase
ProjectConfiguration, tal y como se muestra a continuación:
class ProjectConfiguration extends sfProjectConfiguration { public function setup() { // ... $this->dispatcher->connect( 'mailer.configure', array($this, 'configurarMailer') ); } public function configurarMailer(sfEvent $event) { $mailer = $event->getSubject(); // hacer algo con el objeto mailer } }
La siguiente sección muestra un caso práctico de esta técnica.
Los plugins de Swift Mailer plugins requieren el uso del evento
mailer.configure explicado en la sección anterior:
public function configurarMailer(sfEvent $event) { $mailer = $event->getSubject(); $plugin = new Swift_Plugins_ThrottlerPlugin( 100, Swift_Plugins_ThrottlerPlugin::MESSAGES_PER_MINUTE ); $mailer->registerPlugin($plugin); }
La sección "Plugins" de la documentación oficial de Swift Mailer describe todas las características de los plugins incluidos en la librería.
El comportamiento por defecto de las colas es muy simple. Se seleccionan todos los emails de la cola de forma aleatoria y se envían.
Puedes configurar una cola para que se limite el tiempo (en segundos) dedicado al envío de los mensajes o para que se limite el número de mensajes a enviar:
$spool = $mailer->getSpool(); $spool->setMessageLimit(10); $spool->setTimeLimit(10);
En esta sección se explica cómo crear un mecanismo de prioridad para la cola y se muestra todo lo necesario para desarrollar nuestra propia lógica.
En primer lugar se añade en el esquema una columna de prioridad:
# para Propel
mail_message:
message: { type: clob, required: true }
created_at: ~
priority: { type: integer, default: 3 }
# para Doctrine
MailMessage:
actAs: { Timestampable: ~ }
columns:
message: { type: clob, notnull: true }
priority: { type: integer }
Cuando se envía un email, se establece la cabecera de prioridad (siendo 1
la máxima prioridad):
$mensaje = $this->getMailer() ->compose('remitente@ejemplo.com', 'destinatario@ejemplo.com', 'Asunto', 'Cuerpo') ->setPriority(1) ; $this->getMailer()->send($mensaje);
A continuación, redefine el método setMessage() por defecto para modificar
la prioridad del propio objeto MailMessage:
// para Propel class MailMessage extends BaseMailMessage { public function setMessage($mensaje) { $msg = unserialize($mensaje); $this->setPriority($msg->getPriority()); return parent::setMessage($mensaje); } } // para Doctrine class MailMessage extends BaseMailMessage { public function setMessage($mensaje) { $msg = unserialize($mensaje); $this->priority = $msg->getPriority(); return $this->_set('message', $mensaje); } }
Ten en cuenta que la cola serializa los mensajes, así que es necesario deserializar el mensaje antes de obtener su prioridad. A continuación es necesario crear un método que ordene los mensajes por prioridad:
// para Propel class MailMessagePeer extends BaseMailMessagePeer { static public function getSpooledMessages(Criteria $criteria) { $criteria->addAscendingOrderByColumn(self::PRIORITY); return self::doSelect($criteria); } // ... } // para Doctrine class MailMessageTable extends Doctrine_Table { public function getSpooledMessages() { return $this->createQuery('m') ->orderBy('m.priority') ; } // ... }
El último paso consiste en definir en el archivo de configuración
factories.yml el método que se invoca para obtener los mensajes de la cola:
spool_arguments: [ MailMessage, message, getSpooledMessages ]
Y eso es todo lo que hay que hacer. Ahora, cuando ejecutes la tarea
project:send-emails, los mensajes se enviarán de acuerdo a su prioridad.
Personalizando la cola con cualquier criterio
El ejemplo anterior emplea una cabecera estándar de los emails, la prioridad. Pero si quieres utilizar cualquier otro criterio, o si no quieres modificar el mensaje enviado, puedes guardar el criterio en una cabecera personalizada y borrarla antes de enviar el mensaje.
En primer lugar añade la cabecera personalizada al email que se va a enviar:
public function executeIndex() { $mensaje = $this->getMailer() ->compose('remitente@ejemplo.com', 'destinatario@ejemplo.com', 'Asunto', 'Cuerpo') ; $mensaje->getHeaders()->addTextHeader('X-Queue-Criteria', 'valor'); $this->getMailer()->send($mensaje); }A continuación, cuando se guarda el mensaje se obtiene el valor de esta cabecera y se eliminar inmediatamente:
public function setMessage($mensaje) { $msg = unserialize($mensaje); $cabeceras = $msg->getHeaders(); $criteria = $cabeceras->get('X-Queue-Criteria')->getFieldBody(); $this->setCriteria($criteria); $cabeceras->remove('X-Queue-Criteria'); return parent::_set('message', serialize($msg)); }
If you find a typo or an error, please register and open a ticket.
If you need support or have a technical question, please post to the official user mailing-list.