Emails
by Fabien Potencier
Sending ~emails~ with symfony is simple and powerful, thanks to the usage of the Swift Mailer library. Although ~Swift Mailer~ makes sending emails easy, symfony provides a thin wrapper on top of it to make sending emails even more flexible and powerful. This chapter will teach you how to put all the power at your disposal.
symfony 1.3 embeds Swift Mailer version 4.1.
Introduction
Email management in symfony is centered around a mailer object. And like many other core symfony objects, the mailer is a factory. It is configured in the factories.yml
configuration file, and always available via the context instance:
$mailer = sfContext::getInstance()->getMailer();
Unlike other factories, the mailer is loaded and initialized on demand. If you don't use it, there is no performance impact whatsoever.
This tutorial explains the Swift Mailer integration in symfony. If you want to learn the nitty-gritty details of the Swift Mailer library itself, refer to its dedicated documentation.
Sending Emails from an Action
From an action, retrieving the mailer instance is made simple with the getMailer()
shortcut method:
$mailer = $this->getMailer();
The Fastest Way
Sending an email is then as simple as using the ~sfMailer::composeAndSend()
~ method:
$this->getMailer()->composeAndSend( '[email protected]', '[email protected]', 'Subject', 'Body' );
The composeAndSend()
method takes four arguments:
- the sender email address (
from
); - the recipient email address(es) (
to
); - the subject of the message;
- the body of the message.
Whenever a method takes an email address as a parameter, you can pass a string or an array:
$address = '[email protected]'; $address = array('[email protected]' => 'Fabien Potencier');
Of course, you can send an email to several people at once by passing an array of emails as the second argument of the method:
$to = array( '[email protected]', '[email protected]', ); $this->getMailer()->composeAndSend('[email protected]', $to, 'Subject', 'Body'); $to = array( '[email protected]' => 'Mr Foo', '[email protected]' => 'Miss Bar', ); $this->getMailer()->composeAndSend('[email protected]', $to, 'Subject', 'Body');
The Flexible Way
If you need more flexibility, you can also use the ~sfMailer::compose()
~ method to create a message, customize it the way you want, and eventually send it. This is useful, for instance, when you need to add an ~attachment|email attachment~ as shown below:
// create a message object $message = $this->getMailer() ->compose('[email protected]', '[email protected]', 'Subject', 'Body') ->attach(Swift_Attachment::fromPath('/path/to/a/file.zip')) ; // send the message $this->getMailer()->send($message);
The Powerful Way
You can also create a message object directly for even more flexibility:
$message = Swift_Message::newInstance() ->setFrom('[email protected]') ->setTo('[email protected]') ->setSubject('Subject') ->setBody('Body') ->attach(Swift_Attachment::fromPath('/path/to/a/file.zip')) ; $this->getMailer()->send($message);
The "Creating Messages" and "Message Headers" sections of the Swift Mailer official documentation describe all you need to know about creating messages.
Using the Symfony View
Sending your emails from your actions allows you to leverage the power of partials and components quite easily.
$message->setBody($this->getPartial('partial_name', $arguments));
Configuration
As any other symfony factory, the mailer can be configured in the factories.yml
configuration file. The default configuration reads as follows:
---
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:
When creating a new application, the local factories.yml
configuration file overrides the default configuration with some sensible defaults for the prod
, env
, and test
environments:
---
test:
mailer:
param:
delivery_strategy: none
dev:
mailer:
param:
delivery_strategy: none
The Delivery Strategy
One of the most useful feature of the Swift Mailer integration in symfony is the delivery strategy. The delivery strategy allows you to tell symfony how to deliver email messages and is configured via the ~delivery_strategy
~ setting of factories.yml
. The strategy changes the way the ~send()
|sfMailer::send()
~ method behaves. Four strategies are available by default, which should suit all the common needs:
realtime
: Messages are sent in realtime.single_address
: Messages are sent to a single address.spool
: Messages are stored in a queue.none
: Messages are simply ignored.
The ~realtime
~ Strategy
The realtime
strategy is the default delivery strategy, and the easiest to setup as there is nothing special to do.
Email messages are sent via the transport configured in the transport
section of the factories.yml
configuration file (see the next section for more information about how to configure the mail transport).
The ~single_address
~ Strategy
With the single_address
strategy, all messages are sent to a single address, configured via the delivery_address
setting.
This strategy is really useful in the development environment to avoid sending messages to real users, but still allow the developer to check the rendered message in an email reader.
If you need to verify the original
to
,cc
, andbcc
recipients, they are available as values of the following headers:X-Swift-To
,X-Swift-Cc
, andX-Swift-Bcc
respectively.
Email messages are sent via the same email transport as the one used for the realtime
strategy.
The ~spool
~ Strategy
With the spool
strategy, messages are stored in a queue.
This is the best strategy for the production environment, as web requests do not wait for the emails to be sent.
The spool
class is configured with the ~spool_class
~ setting. By default, symfony comes bundled with three of them:
-
~
Swift_FileSpool
~: Messages are stored on the filesystem. -
~
Swift_DoctrineSpool
~: Messages are stored in a Doctrine model. -
~
Swift_PropelSpool
~: Messages are stored in a Propel model.
When the spool is instantiated, the ~spool_arguments
~ setting is used as the constructor arguments. Here are the options available for the built-in queues classes:
-
Swift_FileSpool
:- The absolute path of the queue directory (messages are stored in this directory)
-
Swift_DoctrineSpool
:-
The Doctrine model to use to store the messages (
MailMessage
by default) -
The column name to use for message storage (
message
by default) -
The method to call to retrieve the messages to send (optional). It receives the queue options as a argument.
-
-
Swift_PropelSpool
:-
The Propel model to use to store the messages (
MailMessage
by default) -
The column name to use for message storage (
message
by default) -
The method to call to retrieve the messages to send (optional). It receives the queue options as a argument.
-
Here is a classic configuration for a Doctrine spool:
#---
# configuration in factories.yml
mailer:
class: sfMailer
param:
delivery_strategy: spool
spool_class: Swift_DoctrineSpool
spool_arguments: [ MailMessage, message, getSpooledMessages ]
And the same configuration for a Propel spool:
#---
# configuration in factories.yml
dev:
mailer:
param:
delivery_strategy: spool
spool_class: Swift_PropelSpool
spool_arguments: [ MailMessage, message, getSpooledMessages ]
To send the message stored in a queue, you can use the ~project:send-emails
~ task (note that this task is totally independent of the queue implementation, and the options it takes):
$ php symfony project:send-emails
The
project:send-emails
task takes anapplication
andenv
options.
When calling the project:send-emails
task, email messages are sent via the same transport as the one used for the realtime
strategy.
Note that the
project:send-emails
task can be run on any machine, not necessarily on the machine that created the message. It works because everything is stored in the message object, even the file attachments.
The built-in implementation of the queues are very simple. They send emails without any error management, like they would have been sent if you have used the
realtime
strategy. Of course, the default queue classes can be extended to implement your own logic and error management.
The project:send-emails
task takes two optional options:
-
message-limit
: Limits the number of messages to sent. -
time-limit
: Limits the time spent to send messages (in seconds).
Both options can be combined:
$ php symfony project:send-emails --message-limit=10 --time-limit=20
The above command will stop sending messages when 10 messages are sent or after 20 seconds.
Even when using the spool
strategy, you might need to send a message immediately without storing it in the queue. This is possible by using the special sendNextImmediately()
method of the mailer:
$this->getMailer()->sendNextImmediately()->send($message);
In the previous example, the $message
won't be stored in the queue and will be sent immediately. As its name implies, the sendNextImmediately()
method only affects the very next message to be sent.
The
sendNextImmediately()
method has no special effect when the delivery strategy is notspool
.
The ~none
~ Strategy
This strategy is useful in the development environment to avoid emails to be sent to real users. Messages are still available in the web debug toolbar (more information in the section below about the mailer panel of the web debug toolbar).
It is also the best strategy for the test environment, where the sfTesterMailer
object allows you to introspect the messages without the need to actually send them (more information in the section below about testing).
The Mail Transport
Mail messages are actually sent by a transport. The transport is configured in the factories.yml
configuration file, and the default configuration uses the SMTP server of the local machine:
---
transport:
class: Swift_SmtpTransport
param:
host: localhost
port: 25
encryption:
username:
password:
Swift Mailer comes bundled with three different transport classes:
-
~
Swift_SmtpTransport
~: Uses a SMTP server to send messages. -
~
Swift_SendmailTransport
~: Usessendmail
to send messages. -
~
Swift_MailTransport
~: Uses the native PHPmail()
function to send messages.
The "Transport Types" section of the Swift Mailer official documentation describes all you need to know about the built-in transport classes and their different parameters.
Sending an Email from a Task
Sending an email from a task is quite similar to sending an email from an action, as the task system also provides a getMailer()
method.
When creating the mailer, the task system relies on the current configuration. So, if you want to use a configuration from a specific application, you must accept the --application
option (see the chapter on tasks for more information on this topic).
Notice that the task uses the same configuration as the controllers. So, if you want to force the delivery when the spool
strategy is used, use sendNextImmediately()
:
$this->getMailer()->sendNextImmediately()->send($message);
Debugging
Traditionally, debugging emails has been a nightmare. With symfony, it is very easy, thanks to the ~web debug toolbar~.
From the comfort of your browser, you can easily and rapidly see how many messages have been sent by the current action:
If you click on the email icon, the sent messages are displayed in the panel in their raw form as shown below.
Each time an email is sent, symfony also adds a message in the log.
Testing
Of course, the integration would not have been complete without a way to test mail messages. By default, symfony registers a mailer
tester (~sfMailerTester
~) to ease mail testing in functional tests.
The ~hasSent()
~ method tests the number of messages sent during the current request:
$browser-> get('/foo')-> with('mailer')-> hasSent(1) ;
The previous code checks that the /foo
URL sends only one email.
Each sent email can be further tested with the help of the ~checkHeader()
~ and ~checkBody()
~ methods:
$browser-> get('/foo')-> with('mailer')->begin()-> hasSent(1)-> checkHeader('Subject', '/Subject/')-> checkBody('/Body/')-> end() ;
The second argument of checkHeader()
and the first argument of checkBody()
can be one of the following:
-
a string to check an exact match;
-
a regular expression to check the value against it;
-
a negative regular expression (a regular expression starting with a
!
) to check that the value does not match.
By default, the checks are done on the first message sent. If several messages have been sent, you can choose the one you want to test with the ~withMessage()
~ method:
$browser-> get('/foo')-> with('mailer')->begin()-> hasSent(2)-> withMessage('[email protected]')-> checkHeader('Subject', '/Subject/')-> checkBody('/Body/')-> end() ;
The withMessage()
takes a recipient as its first argument. It also takes a second argument to indicate which message you want to test if several ones have been sent to the same recipient.
Last but not the least, the ~debug()
~ method dumps the sent messages to spot problems when a test fails:
$browser-> get('/foo')-> with('mailer')-> debug() ;
Email Messages as Classes
In this chapter's introduction, you have learnt how to send emails from an action. This is probably the easiest way to send emails in a symfony application and probably the best when you just need to send a few simple messages.
But when your application needs to manage a large number of different email messages, you should probably have a different strategy.
As an added bonus, using classes for email messages means that the same email message can be used in different applications; a frontend and a backend one for instance.
As messages are plain PHP objects, the obvious way to organize your messages is to create one class for each of them:
// lib/email/ProjectConfirmationMessage.class.php class ProjectConfirmationMessage extends Swift_Message { public function __construct() { parent::__construct('Subject', 'Body'); $this ->setFrom(array('[email protected]' => 'My App Bot')) ->attach('...') ; } }
Sending a message from an action, or from anywhere else for that matter, is simple a matter of instantiating the right message class:
$this->getMailer()->send(new ProjectConfirmationMessage());
Of course, adding a base class to centralize the shared headers like the From
header, or to add a common signature can be convenient:
// lib/email/ProjectConfirmationMessage.class.php class ProjectConfirmationMessage extends ProjectBaseMessage { public function __construct() { parent::__construct('Subject', 'Body'); // specific headers, attachments, ... $this->attach('...'); } } // lib/email/ProjectBaseMessage.class.php class ProjectBaseMessage extends Swift_Message { public function __construct($subject, $body) { $body .= <<<EOF -- Email sent by My App Bot EOF ; parent::__construct($subject, $body); // set all shared headers $this->setFrom(array('[email protected]' => 'My App Bot')); } }
If a message depends on some model objects, you can of course pass them as arguments to the constructor:
// lib/email/ProjectConfirmationMessage.class.php class ProjectConfirmationMessage extends ProjectBaseMessage { public function __construct($user) { parent::__construct('Confirmation for '.$user->getName(), 'Body'); } }
Recipes
Sending Emails via ~Gmail~
If you don't have an SMTP server but have a Gmail account, use the following configuration to use the Google servers to send and archive messages:
---
transport:
class: Swift_SmtpTransport
param:
host: smtp.gmail.com
port: 465
encryption: ssl
username: your_gmail_username_goes_here
password: your_gmail_password_goes_here
Replace the username
and password
with your Gmail credentials and you are done.
Customizing the Mailer Object
If configuring the mailer via the factories.yml
is not enough, you can listen to the ~mailer.configure
~ event, and further customize the mailer.
You can connect to this event in your ProjectConfiguration
class like shown below:
class ProjectConfiguration extends sfProjectConfiguration { public function setup() { // ... $this->dispatcher->connect( 'mailer.configure', array($this, 'configureMailer') ); } public function configureMailer(sfEvent $event) { $mailer = $event->getSubject(); // do something with the mailer } }
The following section illustrates a powerful usage of this technique.
Using ~Swift Mailer Plugins~
To use Swift Mailer plugins, listen to the mailer.configure
event (see the section above):
public function configureMailer(sfEvent $event) { $mailer = $event->getSubject(); $plugin = new Swift_Plugins_ThrottlerPlugin( 100, Swift_Plugins_ThrottlerPlugin::MESSAGES_PER_MINUTE ); $mailer->registerPlugin($plugin); }
Some plugins should be triggered when the emails are actually sent out (AntiFlood, BandwithMonitor, Throttler). They should be registered for the realtime transport only. Otherwise they would be triggered when email is queued rather than when the queue is flushed.
In order for those plugins to have the expected behavior, the code below should always be used to register the plugins when a spool is used. Note than this code is still valid when the mailer doesn't use a spool.
public function configureMailer(sfEvent $event) { $mailer = $event->getSubject(); $transport = $mailer->getRealtimeTransport(); $transport->registerPlugin(new Swift_Plugins_ThrottlerPlugin( 100, Swift_Plugins_ThrottlerPlugin::MESSAGES_PER_MINUTE )); $transport->registerPlugin(new Swift_Plugins_AntiFloodPlugin(30)); }
The "Plugins" section of the Swift Mailer official documentation describes all you need to know about the built-in plugins.
Customizing the Spool Behavior
The built-in implementation of the spools is very simple. Each spool retrieves all emails from the queue in a random order and sends them.
You can configure a spool to limit the time spent to send emails (in seconds), or to limit the number of messages to send:
$spool = $mailer->getSpool(); $spool->setMessageLimit(10); $spool->setTimeLimit(10);
In this section, you will learn how to implement a priority system for the queue. It will give you all the information needed to implement your own logic.
First, add a priority
column to the schema:
When sending an email, set the priority header (1 means highest):
$message = $this->getMailer() ->compose('[email protected]', '[email protected]', 'Subject', 'Body') ->setPriority(1) ; $this->getMailer()->send($message);
Then, override the default setMessage()
method to change the priority of the MailMessage
object itself:
// for Propel class MailMessage extends BaseMailMessage { public function setMessage($message) { $msg = unserialize($message); $this->setPriority($msg->getPriority()); return parent::setMessage($message); } } // for Doctrine class MailMessage extends BaseMailMessage { public function setMessage($message) { $msg = unserialize($message); $this->priority = $msg->getPriority(); return $this->_set('message', $message); } }
Notice that the message is serialized by the queue, so it has to be unserialized before getting the priority value. Now, create a method that orders the messages by priority:
// for Propel class MailMessagePeer extends BaseMailMessagePeer { static public function getSpooledMessages(Criteria $criteria) { $criteria->addAscendingOrderByColumn(self::PRIORITY); return self::doSelect($criteria); } // ... } // for Doctrine class MailMessageTable extends Doctrine_Table { public function getSpooledMessages() { return $this->createQuery('m') ->orderBy('m.priority') ; } // ... }
The last step is to define the retrieval method in the factories.yml
configuration to change the default way in which the messages are obtained from the queue:
---
spool_arguments: [ MailMessage, message, getSpooledMessages ]
That's all there is to it. Now, each time you run the project:send-emails
task, each email will be sent according to its priority.
Customizing the Spool with any Criteria
The previous example uses a standard message header, the priority. But if you want to use any criteria, or if you don't want to alter the sent message, you can also store the criteria as a custom header, and remove it before sending the email.
First, add a custom header to the message to be sent:
public function executeIndex() { $message = $this->getMailer() ->compose('[email protected]', '[email protected]', 'Subject', 'Body') ; $message->getHeaders()->addTextHeader('X-Queue-Criteria', 'foo'); $this->getMailer()->send($message); }Then, retrieve the value from this header when storing the message in the queue, and remove it immediately:
public function setMessage($message) { $msg = unserialize($message); $headers = $msg->getHeaders(); $criteria = $headers->get('X-Queue-Criteria')->getFieldBody(); $this->setCriteria($criteria); $headers->remove('X-Queue-Criteria'); return parent::_set('message', serialize($msg)); }
インデックス
Document Index
関連ページリスト
Related Pages
- Introduction
- Advanced Routing
- Enhance your Productivity
- Emails
- Custom Widgets and Validators
- Advanced Forms
- Extending the Web Debug Toolbar
- Advanced Doctrine Usage
- Taking Advantage of Doctrine Table Inheritance
- Symfony Internals
- Windows and symfony
- Developing for Facebook
- Leveraging the Power of the Command Line
- Playing with symfony's Config Cache
- Working with the symfony Community
- Appendix A - JavaScript code for sfWidgetFormGMapAddress
- About the Authors
- Appendix B - Custom Installer Example
- Appendix C - License
日本語ドキュメント
Japanese Documents
- 2011/01/18 Chapter 17 - Extending Symfony
- 2011/01/18 The generator.yml Configuration File
- 2011/01/18 Les tâches
- 2011/01/18 Emails
- 2010/11/26 blogチュートリアル(8) ビューの作成