Symfony3中Swift_Mailer发送事件监听器配置及附件删除问题
如何为SwiftMailer发送事件添加EventListener并安全删除附件文件?
你遇到的问题很典型:在邮件发送完成后删除临时附件,但当前的sendPerformed事件触发时机太早,导致SwiftMailer还在读取文件时就被删除,抛出Swift_IoException异常。下面我来帮你分析原因并给出两种可靠的解决方案:
问题根源
SwiftMailer默认以流方式读取附件文件,sendPerformed事件是在邮件的“发送动作”触发后立即执行,但此时传输层(比如SMTP连接)可能还在发送附件的数据流,文件资源并未完全释放。这时候删除文件,会导致后续读取操作失败。
解决方案1:将附件加载到内存(适合小文件)
如果你的附件文件不大,可以直接把文件内容读取到内存中创建附件,这样文件可以在创建附件后立即删除,完全不需要依赖事件监听器:
// 读取临时文件内容 $fileContent = file_get_contents($path); // 创建内存中的附件(替代从文件路径创建) $attachment = new \Swift_Attachment( $fileContent, basename($path), // 附件显示的文件名 'text/plain' // 文件的MIME类型,根据你的实际情况调整 ); // 组装邮件 $message = (new \Swift_Message('VAT checking result !')) ->setFrom('vah@gmail.com') ->setTo($vat->getEmail()) ->setBody('Hello, ...', 'text/plain') ->attach($attachment); // 现在可以安全删除临时文件了 unlink($path); // 发送邮件 $mailer = $this->container->get('mailer'); $mailer->send($message);
这种方式简单直接,不需要额外的监听器,适合小体积的附件。
解决方案2:监听Transport停止事件(适合大文件)
如果附件是大文件,加载到内存会占用过多资源,这时候可以修改监听器,监听transportStopped事件——这个事件会在整个邮件传输完全结束、所有文件流和连接都释放后触发,此时删除文件绝对安全。
步骤1:修改监听器类
namespace AppBundle\Listener; use Swift_Events_TransportListener; use Swift_Events_TransportEvent; class SendEmailListener implements Swift_Events_TransportListener { private $pathToFile; public function setPathToFile($path) { $this->pathToFile = $path; } // 以下两个方法是接口要求的空实现 public function beforeTransportStarted(Swift_Events_TransportEvent $evt) {} public function transportStarted(Swift_Events_TransportEvent $evt) {} // 关键:在传输停止后删除文件 public function transportStopped(Swift_Events_TransportEvent $evt) { // 先检查文件存在,避免报错 if ($this->pathToFile && file_exists($this->pathToFile)) { unlink($this->pathToFile); // 清空路径,防止重复删除 $this->pathToFile = null; } } }
步骤2:保持原有的监听器注册配置
你的服务配置不需要修改,依然是:
app.service.send_email_listener: class: AppBundle\Listener\SendEmailListener public: true tags: - { name: swiftmailer.plugin }
步骤3:发送邮件的代码不变
你原来的发送逻辑可以保留,监听器会自动在传输结束后删除文件:
file_put_contents($path, implode(";\n", $result)); $message = (new \Swift_Message('VAT checking result !')) ->setFrom('vah@gmail.com') ->setTo($vat->getEmail()) ->setBody('Hello, ...', 'text/plain') ->attach(\Swift_Attachment::fromPath($path)); $mailer = $this->container->get('mailer'); $listener = $this->container->get('app.service.send_email_listener'); $listener->setPathToFile($path); $mailer->registerPlugin($listener); $mailer->send($message);
额外注意事项
- 如果使用了SwiftMailer的邮件队列(Spool),邮件不会立即发送,这时候需要等到队列被消费时再删除文件,你可以结合队列的事件或者在消费逻辑中处理删除。
- 每次发送邮件后,建议重置监听器的
pathToFile属性,避免多次发送时文件路径被覆盖。
内容的提问来源于stack exchange,提问作者Andrew Vakhniuk
相关产品推荐
相关产品推荐

