Symfony Advent Calendar 2021の記事です。
個人的にSymfonyの最推しポイントだと考えている、データのDB永続化について語ります。
システムにおいて、DBなどへのデータ永続化は非常に重要な部分と考えています。プログラムが壊れている(=不具合がある)よりもDBなどのデータが壊れている方がシステムに与える影響が大きく、また間違った答えを導き出す原因となるため、システムからのデータ永続化については細心の注意が必要です。その中でもDBへの永続化はどのフレームワークも簡単にできることもあり、さらに注意が必要な箇所だと言えるでしょう。
ここでは推しを語る上で重要な要素、EntityManager
、Entity
、オートワイヤリング
について紹介します。
永続化を司る。EntityManager
EntityManagerとは、Symfonyにおけるデータベースへの永続化を司るマネージャークラスで、正確にはDoctrineのクラスです。
SymfonyはActive Recordとはちがい、Entityクラスが永続化メソッドを持ちません。イメージとしてはこんな感じです。
Active Recordでは、それぞれのEntityオブジェクトが永続化処理を持っており、独立して動くことができます。
一方Symfonyは、Entityオブジェクトは何も持っておらず、EntityManagerの管理下に置くことにより、同期のタイミングで差分SQLを発行し、DBへの永続化を行います。
コードにすると以下のようになります。
$item = new Item();
$item->setName('商品1');
$entityManager = EntityManager::create($someParam);
$entityManager->persist($item); // DB永続化するオブジェクトとして管理下に置く
$entityManager->flush(); // DBと同期
LaravelなどのActive Record型と比べると若干めんどくさい書き方ではあります。$item->save()
したくなる気持ちはわかります。
ぱっと見、ここに推しがある感じがしませんが、非常に重要です。
なお、EntityManager::create()
でオブジェクトを作っていますが、普段はこんなことしません。
プレーン。Entity
SymfonyにおけるEntityも推しを語る上で重要です。永続化の処理を持たないので以下のようなシンプルなクラスになります。
#[ORM\Entity(repositoryClass: ItemRepository::class)]
class Item
{
// 一部省略
#[ORM\Column(type: 'string', length: 255)]
private ?string $name;
public function setName(?string $name): self
{
$this->name = $name;
return $this;
}
public function getName(): ?string
{
return $this->name;
}
}
このEntityのポイントをいくつか挙げます。
何にも依存していない
クラスにextends
がついていないところから分かる通り、DBの値をマッピングすることにフォーカスされており、このEntityからはどう足掻いても永続化することはできませんし、他のクラスなどにも束縛されることはなく、できることは限られますが非常にプレーンなクラスになります。
テーブルのフィールドに対応するプロパティがある。
テーブルのフィールドに対応するprivateなプロパティを定義します。どのテーブルのどのフィールドかは、AttributeもしくはAnnotationを利用して設定します。プロパティ定義は若干めんどくさくはありますが、どのようなプロパティがあるのかは一目瞭然になります。
Getter, Setterがある
privateなプロパティであるため、$item->name
のように直接プロパティにアクセスすることができません。そのため参照するにはGetterメソッドを、変更するにはSetterメソッドを用意する必要があります。言い換えると、そのシステムで値更新がないプロパティであればSetterを用意する必要はありません。
必要なオブジェクトを自動注入。オートワイヤリング
Symfonyにはオートワイヤリングという機能があります。これは、Entityと一部ファイルを除くすべてのクラスのコンストラクターとSetter、Controllerクラスはこれに加えてルーティングに対するアクションメソッドの引数に指定された、クラス・インターフェースのオブジェクトを自動的に注入する機能です。(設定により除外ファイルを変更できます)
例えば以下のようなContollerクラスがあった場合、
class ItemController extends AbstractController
{
private LoggerInterface $logger;
public function __construct(private ItemRepository $itemRepository)
{
}
#[Required]
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
#[Route('/item', name: 'item')]
public function index(): Response
{
$items = $this->itemRepository->findAll();
return $this->render('item/index.html.twig', [
'items' => $items,
]);
}
#[Route('/item/add', name: 'item_add')]
public function add(EntityManagerInterface $entityManager): Response
{
$item = new Item();
$item->setName('商品名');
$entityManager->persist($item);
$entityManager->flush();
$this->logger->info('商品登録したよ');
return $this->redirectToRoute('item');
}
}
コンストラクターの引数であるレポジトリクラスItemRepository
がインスタンス生成時に注入され、#[Required]
がついているSetterにはログ出力用インターフェースLoggerInterface
の実装クラスが注入される上にインスタンス生成時に自動で実行され、ルーティングに対するアクションメソッドの引数にあるDB永続化用インターフェースEntityManagerInterface
の実装クラスが、アクション実行時に自動注入されます。
Symfonyはこのように必要な機能を持ったクラスを、コンストラクターやSetter、アクションメソッドに引数として定義することで、システムを構築に必要な処理を追加していくことができます。
上記の例では、商品追加のアクションメソッドであるaddにEntityManagerInterface
があるから、DBへの永続化処理ができるわけです。
最推しポイント。EntityManagerが刺さっているところしかDB永続化できない!
- EntityManagerがDB永続化を司る
- Entityには余計な機能がない
- 必要な箇所にオートワイヤリングで機能を追加する
この3つのポイントが揃うことで最推しのポイントとなります。それはEntityManagerが引数として定義されているところしかDB永続化できないです。
最初に述べた通り、DB永続化には細心の注意を払う必要がありますが、そのためには『いつ、どこで永続化されているか』を把握する必要があります。Symfonyでは上記の3つのポイントのおかげで、EntityManagerが刺さっているクラス・アクションにのみ永続化処理があるので、そこに注目しておけばよくなります。
どこか奥深くで永続化されていたとしても、そこには必ずEntityManager
が刺さっています。つまり、プロジェクト内をEntityManager
で検索すれば、永続化処理を抜き出せるわけです。べんりー。
なお。。
この効果は『永続化する時はEntityManagerをメソッドに刺すんだ!』という意志が必要で、
昔懐かしい書き方である$this->getContainer()->get('doctrine.orm.entity_manager')
ってされたり、EntityManager
ではなくManagerRegistry
を使われたりするとその限りではない模様。