jarjobs/swiftmailerdatabasebundle
A Symfony bundle that stores SwiftMailer messages in a database via Doctrine, making it easy to queue, inspect, and manage outgoing email without filesystem or memory spooling.
Before you install, confirm the package is still reachable. A zero-star, zero-dependent Symfony bundle from this era is almost certainly not on Packagist anymore, and the repo may be archived. Try:
composer require jarjobs/swiftmailerdatabasebundle
If that fails, add a VCS repository entry and pin a commit:
{
"repositories": [
{ "type": "vcs", "url": "https://github.com/JarJobs/SwiftMailerDatabaseBundle" }
],
"require": {
"jarjobs/swiftmailerdatabasebundle": "dev-master"
}
}
First three places to look in the repo, in order:
Resources/config/services.yml (or services.xml) — this is where the spool service id is declared. You need that id; it is the only thing that ties the bundle to your app config. It will be something like jarjobs.swiftmailer.spool.db or swiftmailer.spool.database.Entity/ or Resources/config/doctrine/ directory, or a .sql file — this tells you the table schema and column types (critical: TEXT vs LONGBLOB for the serialized message body).DependencyInjection/Configuration.php and the *Extension.php — for the config key namespace (jarjobs_swift_mailer_database: or similar) and whether the connection name is configurable.Register the bundle (Symfony 2/3 style, which is what this targets):
// app/AppKernel.php
$bundles = [
// ...
new JarJobs\SwiftMailerDatabaseBundle\SwiftMailerDatabaseBundle(),
];
Wire it as the spool. The bundle does not change how you send mail — it only swaps the spool implementation. In config.yml:
swiftmailer:
spool:
type: service
id: <the-spool-service-id-from-services.yml>
Create the table, either from the bundle's SQL file or by running doctrine:schema:update --dump-sql if the bundle ships a mapped entity. Read the SQL before you execute it — do not trust a 10-year-old migration file against a modern MySQL.
First use case: a registration/password-reset email that must not block the HTTP request when SMTP is slow, and must not be lost if the SMTP server is down for ten minutes. Send as normal:
$mailer->send($message); // writes to DB, returns immediately
Then flush on a cron:
*/2 * * * * php bin/console swiftmailer:spool:send --env=prod
The built-in swiftmailer:spool:send command is spool-agnostic — it operates on whatever swiftmailer.spool points at, which is why the type: service indirection above is the whole integration.
Environment-specific spool. Database spooling is a production concern. Keep dev and test in memory so you don't accumulate junk rows and don't need SMTP:
# config_dev.yml
swiftmailer:
spool: { type: memory }
# config_test.yml — inspect instead of send
swiftmailer:
spool: { type: memory }
Per-send bypass for time-sensitive mail. Password resets and 2FA codes should not sit in a queue for two minutes. Use a second, non-spooled mailer for those, or flush inline after sending:
$mailer->send($resetMessage);
$spool = $container->get('swiftmailer.spool');
$spool->flushQueue($transport);
Wrapping that in a try/catch is wise — if the transport throws, you have now coupled the request to SMTP availability, which is the thing you were avoiding. Prefer: spool everything, flush on cron, and accept the delay.
Bulk/batch sends. For newsletters, spool is the right tool: enqueue thousands of messages inside a transaction-free loop, then let the cron worker drain them with limits:
php bin/console swiftmailer:spool:send --message-limit=200 --time-limit=60
The limits are what keep a cron from overlapping itself. Set --time-limit below your cron interval.
Monitoring queue depth. You own the table, so a health check is one query. Add a repository method or raw DQL:
$pending = $em->createQuery(
'SELECT COUNT(m.id) FROM YourBundle:SpoolMessage m WHERE m.isSent = false'
)->getSingleScalarResult();
Alert when $pending grows monotonically across a few checks — that's the signal that SMTP auth broke or the cron died.
Retry semantics. Decide up front what "failed" means. The stock Swift_Spool contract has no retry concept: flushQueue() only removes messages that were sent successfully, so failures stay queued and are retried on the next run forever. If you need backoff or a dead-letter state, add attempts and last_attempted_at columns and either:
flushQueue(), then override the service id in your app config, orCross-application mail. The main reason to pick DB spool over the built-in file spool is a shared store. A worker on box B can drain mail enqueued by web node A without a shared filesystem. If you're single-node, the built-in type: file spool is simpler and has no schema to maintain — use this bundle only when you actually need the shared queue.
Composing with business transactions. If you enqueue inside a Doctrine transaction that later rolls back, the mail row rolls back with it (good). But if the mail is flushed by cron before the transaction commits, you've sent mail for work that never happened. In practice cron lag makes this rare; if you need a guarantee, use the transactional outbox pattern explicitly: write your own outbox row in the same transaction, and have a worker convert outbox rows into spool rows after commit.
This is a dead-end dependency, and its foundation is EOL too. SwiftMailer was abandoned in November 2021 in favour of symfony/mailer, which ships a Doctrine transport (doctrine://default) that does exactly what this bundle does — table-backed queue plus messenger:consume. If you are starting greenfield, skip this package entirely. If you are inheriting an app already on it, budget for the migration rather than extending it. Any custom code you write against this bundle's service ids and schema is code you throw away later.
The service id is not guessable. swiftmailer:spool:send resolves the swiftmailer.spool alias, which by default points at the file or memory spool. If you configure type: service with the wrong id, Symfony will fail at container compile time with a clear message — but if you configure type: file by accident, the bundle silently does nothing and mail goes to disk. Verify by checking var/cache/prod/appProdProjectContainer.php for the alias, or run:
php bin/console debug:container swiftmailer.spool
Column types are the most common failure. Serialized Swift_Message objects are large, and base64-encoded attachments inflate by ~33%. A TEXT column caps at 64KB and will truncate or throw on realistic attachments. Insist on LONGBLOB/LONGTEXT, and verify the actual DDL rather than assuming the bundle's mapping is sane.
Attachments in the database are usually a mistake. Every invoice PDF now lives in the mail table, gets copied on every backup, and slows flushQueue() as rows are hydrated. Store attachments on object storage and reference them, or at minimum prune aggressively.
Serialization is class-sensitive. The spool stores PHP-serialized Swift_Message objects. Renaming or moving classes, changing SwiftMailer major versions, or deploying code that alters serialized properties can make old queued rows unserializable — flushQueue() will throw on the whole batch, not just the one bad row. After any SwiftMailer upgrade, drain the queue to zero before deploying, and add a try/catch around per-message hydration in your own drainer if the queue can't be emptied.
Two crons will double-send. There is no row-level lock in the typical implementation of this pattern. flushQueue() reads and deletes; concurrent invocations can both read the same unsent row. Use a single cron, or guard with flock:
flock -n /var/lock/spool.lock php bin/console swiftmailer:spool:send
Failed messages accumulate silently. Check the row count after the first week in production. A queue that only ever grows usually means bad SMTP credentials, a port blocked by the host, or a TLS mismatch — not a spool bug. The spool hides transport failures from the request that triggered them, which is the point, but it also hides them from you. Log flushQueue() exceptions in the cron wrapper and alert on them.
Prune the table. Unless the spool deletes sent rows (check flushQueue() — most delete rather than flag), you'll need a nightly cleanup:
DELETE FROM swiftmailer_spool WHERE is_sent = 1 AND created_at < DATE_SUB(NOW(), INTERVAL 7 DAY) LIMIT 10000;
Batch the delete; an unbounded DELETE on a large table will lock it.
Doctrine connection selection. If the bundle exposes a connection name in config, set it explicitly to the connection that persists across your deploy boundary — not a read replica, and not a connection that's recreated on every request in some ephemeral setup.
Debugging the queue directly. Don't debug through the mailer. Query the table:
$rows = $conn->fetchAllAssociative(
'SELECT id, created_at, LENGTH(message) AS bytes FROM swiftmailer_spool ORDER BY id DESC LIMIT 20'
);
A row with a plausible bytes value and a created_at from two hours ago localises the problem to the drain job, not the enqueue path.
Extension point. If the bundle's spool class is not final, subclass and override flushQueue() for retry/dead-letter logic, then redefine the service id in your app's services.yml. This is the cleanest hook — a CompilerPass or a decorator both work, but a plain service override is fewer moving parts for a bundle this old.
Symfony 4+ note. The Bundle class registration moves to config/bundles.php, and Resources/config/ paths are not auto-discovered the same way. Older bundles sometimes need an explicit $loader->load('services.yml') path fix or a getContainerExtension override. If the container fails to compile after registration, that's the first place to look.
How can I help you explore Laravel packages today?