Mailer¶
Note
Not part of core. Install it separately:
composer require kinetis/mailer
Mail sending via Symfony\Component\Mailer. A single DSN selects the
transport — SMTP, or any of Symfony’s own API-based bridges (Sendgrid,
Mailgun, Postmark, SES, …).
use Kinetis\Mailer\MailerFactory;
use Symfony\Component\Mime\Email;
$mailer = MailerFactory::fromConfig($config);
$email = (new Email())
->from('noreply@example.com')
->to('user@example.com')
->subject('Welcome!')
->text('Thanks for signing up.');
$mailer->send($email);
Configuring¶
MAILER_DSN=smtp://user:pass@smtp.example.com:587
Or, using an API-based transport instead — the scheme selects it:
MAILER_DSN=sendgrid+api://KEY@default
Whichever scheme you use, composer require the matching Symfony bridge
package too (symfony/sendgrid-mailer, symfony/mailgun-mailer,
symfony/postmark-mailer, symfony/amazon-mailer, …) — kinetis/mailer
has no dispatch logic of its own here; Symfony\Component\Mailer\Transport
discovers whichever bridge classes are actually installed.
Important
Only the API-based transports are non-blocking. SMTP opens a raw socket
directly (stream_socket_client()), with no Fiber-yield point at all —
sending over SMTP blocks the worker for as long as the send takes, the
same way any other genuinely blocking call would. See
Queueing mail below for the practical fix, regardless
of which transport you choose.
Named connections¶
$transactional = MailerFactory::fromConfig($config, 'transactional');
MAILER_TRANSACTIONAL_DSN=sendgrid+api://KEY@default
Same convention as everywhere else in Kinetis (see Configuration):
'default' reads the plain MAILER_DSN above, and any other name reads
MAILER_{NAME}_DSN instead.
Queueing mail¶
Sending mail from inside a request means the client waits for it — and if
you’re on SMTP, that wait is a genuinely blocking one. The fix isn’t
anything kinetis/mailer needs to provide: a queue job
that constructor-injects MailerInterface already gets this for free,
with no extra code in either package.
use Kinetis\Queue\Job;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
final readonly class SendWelcomeEmailJob implements Job
{
public function __construct(
public string $toEmail,
) {}
public function handle(MailerInterface $mailer): void
{
$email = (new Email())
->from('noreply@example.com')
->to($this->toEmail)
->subject('Welcome!')
->text('Thanks for signing up.');
$mailer->send($email);
}
}
$queue->push(new SendWelcomeEmailJob($user->email));
Register MailerInterface once, in bootstrap.php, and any job’s
handle() method can depend on it exactly like PingRepository or any
other service:
use Kinetis\Mailer\MailerFactory;
use Symfony\Component\Mailer\MailerInterface;
return static function (AppScope $app, Config $config): void {
$app->instance(MailerInterface::class, MailerFactory::fromConfig($config));
};
Now a slow SMTP send only occupies one queue worker’s own Fiber for one job, instead of the worker handling that HTTP request — the same reasoning behind queueing any other slow, non-critical-path work.
See also¶
Appendix: Revolt HTTP Client — the non-blocking HTTP client every API-based transport actually runs through.
Queue — job queues, retries, and named connections in full.
Configuration — the named-connection convention used above.