Symfony Advent Calendar 2022 18 日目の記事です ✨

はじめに

つい最近 Symfony を使ってメール送信機能を実装する機会がありました。
その時にスラスラ実装できなくてしんどい思いをしたのでつまりポイント等をまとめてました。

この記事の目的と対象者

この記事の目的と対象者は、なにかしらのフレームワークでメール送信機能を非同期で実装した事がある人が Symfony のドキュメント等を熟読せずにサクっとメール送信機能を実装できるようになる事が目的です!

今回のゴール

問い合わせフォームを作り、そこから送信された内容を外部ファイルに切り出したメールテンプレートに埋め込めこみ、更にファイルを添付してメッセンジャーのキューへ登録する所までをゴールとします。
サンプルソース

動作環境

windows 11
wsl2(Ubuntu 20.04)
php 8.1.12
Symfony 6.2

単純なテキストメールをメッセンジャーのキューに登録してみる。

Symfony を使ってメール周りの処理を実装した事がないので公式ドキュメントを頼りに単純なテキストメールをメッセンジャーのキューに登録していきます。 https://symfony.com/doc/current/mailer.html

まず symfony_mailer の インストールを行います。

$ composer require symfony/mailer

この記事では実際にメール送信が行われる所まではしないので MAILER_DSN の設定行いません。

MAILER_DSN=null://null
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
use Symfony\Component\Routing\Annotation\Route;

class MailerController extends AbstractController
{
  #[Route('/email')]
  public function sendEmail(MailerInterface $mailer): Response
  {
    $email = (new Email())
      ->from('hello@example.com')
      ->to('you@example.com')
      ->subject('Symfonyからのテストメールです!')
      ->text('メール内容はないよう');

    $mailer->send($email);

    return $this->render('index.html.twig');
  }
}

この状態で「/email」にアクセスしてみると・・・

An exception occurred while executing a query: SQLSTATE[42S02]: Base table or view not found: 1146 Table ‘symfony_mailer.messenger_messages’ doesn’t exist

1

messenger_messages テーブルがないと怒られました。

migrations ディレクトリ内に「messenger_messages」のテーブル定義は存在しませんしデータベース内にも存在しません。

$ ls -la migrations
total 8
drwxr-xr-x  2 ubuntu ubuntu 4096 Dec 11 10:52 .
drwxr-xr-x 12 ubuntu ubuntu 4096 Dec 11 10:53 ..
-rw-r--r--  1 ubuntu ubuntu    0 Dec 10 20:48 .gitignore
MariaDB [symfony_mailer]> show tables;
Empty set (0.000 sec)

なにこれどうすればいいの・・・?

調べてみるとメッセンジャーへのトランスポートを通じて何かしらの処理を行うと自動でテーブルが作られるらしいです。
https://github.com/symfony/symfony/issues/46609

とりあえずメッセンジャーのインストールを行います。

$ composer require symfony/messenger

メッセンジャーを起動してみます。

$ php bin/console messenger:consume async

「An exception occurred while executing a query: SQLSTATE[42S02]: Base table or view not found: 1146 Table ‘symfony_mailer.messenger_messages’ doesn’t exist」

2

また似たような内容で怒られました。

調べてみるとデフォルトの状態だとメッセンジャーで使用するテーブルが自動生成されないらしいです。
自動生成するには.env の auto_setup の値を 0 から 1 に変えなくてはならないとの事。

MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=1

再度メッセンジャーを起動してみます。

$ php bin/console messenger:consume async
[OK] Consuming messages from transport "async".
// The worker will automatically exit once it has received a stop signal via the messenger:stop-workers command.
// Quit the worker with CONTROL-C.
// Re-run the command with a -vv option to see logs about consumed messages.

メッセンジャーが起動しました!

messenger_messages テーブルも作られていますね。
まだ「/email」にアクセスしていないのでメッセンジャーのトランスポートを通じて処理は行っていないような気がしますがヨシとします。

MariaDB [symfony_mailer]> show tables;
+--------------------------+
| Tables_in_symfony_mailer |
+--------------------------+
| messenger_messages       |
+--------------------------+
1 row in set (0.000 sec)

再度「/email」にアクセスしてみます。
Symfony Profiler で確認するとメッセンジャーのトランスポートを通じてキューにメールが登録されていますね!

3

今回はやりませんが、この状態で.env の MAILER_DSN を指定すれば正しくメールが送信されるはずです。

問い合わせフォームを作成する

お次は問い合わせフォームを作っていきます。

フォームと Symfony の連携周りの話は mako10 さんの記事が参考になるので省略させていただきます。
https://qiita.com/mako5656/items/85b18f8e8fb8cb622f2b
https://qiita.com/mako5656/items/0b6c28901cf0f7edeeaa

<?php

namespace App\Controller;

use App\Form\Type\ContactType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use App\Form\Model\ContactModel;
use Symfony\Component\Routing\Annotation\Route;

class ContactController extends AbstractController
{
  #[Route('/contact', methods: ['GET'])]
  public function getContact(): Response
  {
    $contact = new ContactModel();
    $contact->setContactContent('画面がバグってます');

    $form = $this->createForm(ContactType::class, $contact);

    return $this->render('contact.html.twig', [
      'form' => $form,
    ]);
  }
<?php

namespace App\Form\Model;

class ContactModel
{
  /**
   * 問い合わせ内容(テキスト)
   */
  protected $contactContent;
  /**
   * 問い合わせ用の画像(ファイル)
   * @var UploadedFile[]
   */
  private array $image = [];

  public function getContactContent(): string
  {
    return $this->contactContent;
  }

  public function setContactContent(string $contactContent): void
  {
    $this->contactContent = $contactContent;
  }

  /**
  * @return UploadedFile[]
  */
  public function getImage(): array
  {
    return $this->image;
  }

  /**
  * @param UploadedFile[] $image
  */
  public function setImage(array $image): self
  {
    $this->image = $image;
    return $this;
  }
}
<?php

namespace App\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;

use App\Form\Model\ContactModel;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ContactType extends AbstractType
{
  public function buildForm(FormBuilderInterface $builder, array $options): void
  {
    $builder
      ->add('contactContent', TextType::class)
      ->add('image', FileType::class, [
        'required' => false,
        'multiple' => true,
      ])
      ->add('save', SubmitType::class);
  }

  public function configureOptions(OptionsResolver $resolver): void
  {
    $resolver->setDefaults([
      'data_class' => ContactModel::class,
    ]);
  }
}

この状態で「/contact」にアクセスするとフォームが表示されます。

4

メールテンプレートを使ってメールを作成してメッセンジャーのキューに登録してみる。

公式ドキュメントを頼りに処理を実装します。
https://symfony.com/doc/current/mailer.html#text-content

<?php
namespace App\Controller;

use App\Form\Type\ContactType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use App\Form\Model\ContactModel;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mailer\MailerInterface;

class ContactController extends AbstractController
{
  #[Route('/postContact', methods: ['POST'])]
  public function newPost(Request $request, MailerInterface $mailer): Response
  {
    $contact = new ContactModel();
    $form = $this->createForm(ContactType::class, $contact);
    $form->handleRequest($request);
    if ($form->isSubmitted() && $form->isValid()) {
      $contact = $form->getData();
      $email = (new TemplatedEmail())
        ->from('hello@example.com')
        ->to('you@example.com')
        ->subject('symfony mail test title')
        ->textTemplate('emails/email.html.twig')
        ->context([
          'contact' => $contact,
      ]);
      // ファイルを添付
      foreach ($contact->getImage() as $attachmentFile) {
        $email->attachFromPath(
          $attachmentFile->getRealPath(),
          $attachmentFile->getClientOriginalName(),
        );
      }
      $mailer->send($email);
    }

    return $this->render('index.html.twig', [
      'form' => $form,
    ]);
  }
}

以下はメールテンプレートの twig です。

問い合わせを受け付けました。

■お問い合わせ内容
{{ contact.contactContent }}

この状態で「/postContact」に post リクエストを送ると・・

Serialization of ‘Symfony\Component\HttpFoundation\File\UploadedFile’ is not allowed

5

UploadedFile がシリアライズできずにエラーが発生しました。

UploadedFile のシリアライズは行ってはダメらしいですね。
https://github.com/symfony/symfony/issues/7238

UploadedFile とはなんぞや?

調べるとフォームからアップロードされたファイルの情報がまとまっている物らしいです。
https://runebook.dev/ja/docs/symfony/symfony/component/httpfoundation/file/uploadedfile

UploadedFile がシリアライズされている場所

調べるとメールテンプレの twig に必要な情報を渡している所でシリアライズされるらしいです。
公式ドキュメントをよく見るとシリアライズできる物のみ渡せと書いてありますね。

6

$email = (new TemplatedEmail())
        ->from('hello@example.com')
        ->to('you@example.com')
        ->subject('Symfony mail test title')
        ->textTemplate('emails/email.html.twig')
        // contextにはシリアライズ可能な物しか渡してはいけない。
        ->context([
          'contact' => $contact,
      ]);

公式ドキュメント通りにファイルをシリアライズの対象から外してみる。

<?php

namespace App\Form\Model;

class ContactModel
{
  public function __serialize(): array
  {
    return [
      'contactContent' => $this->contactContent,
    ];
  }

  public function __unserialize(array $data): void
  {
    $this->contactContent = $data['contactContent'];
  }

  protected $contactContent;
  /**
   * @var UploadedFile[]
   */
  private array $image = [];

  public function getContactContent(): string
  {
    return $this->contactContent;
  }

  public function setContactContent(string $contactContent): void
  {
    $this->contactContent = $contactContent;
  }

  /**
  * @return UploadedFile[]
  */
  public function getImage(): array
  {
    return $this->image;
  }

  /**
  * @param UploadedFile[] $image
  */
  public function setImage(array $image): self
  {
    $this->image = $image;
    return $this;
  }
}

この状態で「/contact」に post リクエストを送ると・・・・
正常に実行できました!

Symfony Profiler を確認するとキューにテンプレ通りの内容が送信されていておりファイルも添付されています!

7

最後に

ここまで長くなりましたが、最後まで読んでいただきありがとうございました。
どこかの誰かのお役に立てれば幸いです!