![]() |
|
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 Strategy single_address Strategy spool Strategy none Strategy 
|
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License. |
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.
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.
From an action, retrieving the mailer instance is made simple with the
getMailer() shortcut method:
$mailer = $this->getMailer();
Sending an email is then as simple as using the sfMailer::composeAndSend()
method:
$this->getMailer()->composeAndSend( 'from@example.com', 'fabien@example.com', 'Subject', 'Body' );
The composeAndSend() method takes four arguments:
from);to);Whenever a method takes an email address as a parameter, you can pass a string or an array:
$address = 'fabien@example.com'; $address = array('fabien@example.com' => '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( 'foo@example.com', 'bar@example.com', ); $this->getMailer()->composeAndSend('from@example.com', $to, 'Subject', 'Body'); $to = array( 'foo@example.com' => 'Mr Foo', 'bar@example.com' => 'Miss Bar', ); $this->getMailer()->composeAndSend('from@example.com', $to, 'Subject', 'Body');
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 as shown below:
// create a message object $message = $this->getMailer() ->compose('from@example.com', 'fabien@example.com', 'Subject', 'Body') ->attach(Swift_Attachment::fromPath('/path/to/a/file.zip')) ; // send the message $this->getMailer()->send($message);
You can also create a message object directly for even more flexibility:
$message = Swift_Message::newInstance() ->setFrom('from@example.com') ->setTo('to@example.com') ->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.
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));
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
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.realtime StrategyThe 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).
single_address StrategyWith 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, andbccrecipients, they are available as values of the following headers:X-Swift-To,X-Swift-Cc, andX-Swift-Bccrespectively.
Email messages are sent via the same email transport as the one used for the
realtime strategy.
spool StrategyWith 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:
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:
# Schema configuration in schema.yml
MailMessage:
actAs: { Timestampable: ~ }
columns:
message: { type: blob, notnull: true }
# 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:
# Schema configuration in schema.yml
mail_message:
message: { type: blob, required: true }
created_at: ~
# 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-emailstask takes anapplicationandenvoptions.
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-emailstask 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
realtimestrategy. 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.
none StrategyThis 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).
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: Uses sendmail to send messages.
Swift_MailTransport: Uses the native PHP mail() 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 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);
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.
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('foo@example.com')-> 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() ;
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('app@example.com' => '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('app@example.com' => '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'); } }
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.
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.
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.
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:
# for Propel
mail_message:
message: { type: blob, required: true }
created_at: ~
priority: { type: integer, default: 3 }
# for Doctrine
MailMessage:
actAs: { Timestampable: ~ }
columns:
message: { type: blob, notnull: true }
priority: { type: integer }
When sending an email, set the priority header (1 means highest):
$message = $this->getMailer() ->compose('john@doe.com', 'foo@example.com', '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('john@doe.com', 'foo@example.com', '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)); }
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.