SymfonyでDBを扱うセット、"ORM Pack"

[Symfony Component Advent Calendar 2022](https://qiita.com/advent-calendar/2022/symfony-component)の8日目の記事です。

## 最初に
[Symfony](https://symfony.com)はPHPのフレームワークのひとつです。しかし、公式サイトの説明文には

> Symfony is a set of PHP Components, a Web Application framework, a Philosophy, and a Community — all working together in harmony.
  （SymfonyはPHPコンポーネントのセットで、Webアプリケーションフレームワークで、哲学、そしてコミュニティです。それらがハーモニーを奏でながら動作しています。）

と書かれている通り、PHPコンポーネントのセットで、たくさんのコンポーネントを提供しており、それらを組み合わせてひとつのフレームワークとして動作しています。Symfonyのコンポーネントは、Symfony上だけで動作するのではなく、他のPHPフレームワークやアプリケーションでも動作している強力なものが揃っています。

今回はそれらの中から、役立ちそうなもの・お薦めしたいものを紹介していきたいと思います。

※記事内ではautoloadのインポートは省略します。


## SymfonyでDBを扱うセット、"ORM Pack"

[ORM Pack](https://symfony.com/doc/current/doctrine.html)は、SymfonyでDBを扱うためのライブラリをセットにしたコンポーネントです。ほぼSymfony専用コンポーネントです。

個人的にSymfonyの推しポイントのひとつです。去年のアドベントカレンダーでも取り上げました。

https://qiita.com/ippey_s/items/be50ff0294837b8f8b1f

### インストール

```shell
composer require symfony/orm-pack
```


### Doctrine

このORM Packには[Doctrine](https://www.doctrine-project.org/)というライブラリが含まれており、これを利用してDBを扱います。
このDoctrineはData Mapper型のORMで、以下の特徴があります。

- EntityがPOPO (Plain Old PHP Object、何にも依存しない純粋なオブジェクト)である。
- DB参照はRepository、DBへの永続化はEntityManagerが行う
- RepositoryやEntityManagerはオートワイヤリングにより、Entity/DependencyInjection以外であれば、どこでも注入可能

詳しくはこちら

<script async class="speakerdeck-embed" data-slide="25" data-id="4c88a4abe67641a6b8d745d74f59785d?slide=25" data-ratio="1.77777777777778" src="//speakerdeck.com/assets/embed.js"></script>

LaravelのEloquentとの違いに関しては@77webさんのこちらのアドベントカレンダーをご覧ください。

https://qiita.com/advent-calendar/2022/php-doctrine-orm-vs-eloquent

### Entity

Entityは上記の通りPOPOですが、DBのテーブルやフィールドとの紐付けはAttributesで行います。


```php:Item.php
namespace App\Entity;

use App\Repository\ItemRepository;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: ItemRepository::class)]
class Item
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private ?string $name = null;

    #[ORM\Column]
    private ?int $price = null;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getName(): ?string
    {
        return $this->name;
    }

    public function setName(string $name): self
    {
        $this->name = $name;

        return $this;
    }

    public function getPrice(): ?int
    {
        return $this->price;
    }

    public function setPrice(int $price): self
    {
        $this->price = $price;

        return $this;
    }
```

`#[ORM\Entity(repositoryClass: ItemRepository::class)]`で紐づくRepositoryのクラス名を指定します。

`#[ORM\Column]`で紐づくテーブルのフィールドを指定します。指定がない場合は、プロパティ名をスネークケースしたフィールド名を自動的に紐づけます。
`#[ORM\Id]`はPKを表し、`#[ORM\GeneratedValue]`はオートインクリメント（正確には自動生成）を表します。


### Repository

Repositoryは`ServiceEntityRepositoy`の子クラスとなります。


```php:ItemRepository.php
namespace App\Repository;

use App\Entity\Item;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

/**
 * @extends ServiceEntityRepository<Item>
 *
 * @method Item|null find($id, $lockMode = null, $lockVersion = null)
 * @method Item|null findOneBy(array $criteria, array $orderBy = null)
 * @method Item[]    findAll()
 * @method Item[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
 */
class ItemRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Item::class);
    }
}
```

`parent::__construct()`で紐づくEntityクラスを指定します。上記のコメントの通り、データを取得するメソッドがいくつか用意されています。

### データの取得

上記の通り、既存の取得メソッドを使えば簡単にデータを取得できますが、複雑な条件の場合はQueryBuilderを利用します。

```php:ItemRepository.php

    public function findWithComplexCondition(): array
    {
        $queryBuilder = $this->createQueryBuilder('i');
        $queryBuilder
            ->andWhere('i.name = :name')
            ->setParameter('name', '商品名')
            ->orderBy('i.id', 'ASC')
            ->setMaxResults(10)
            ->getQuery()
            ->getResult()
        ;            
    }
```

`createQueryBuilder()`では、引数にテーブルのエイリアス名を指定します。`andWhere()`, `orWhere()`などの条件で`:{パラメータ名}`を指定し、`setParameter('パラメータ名', '値')`を呼ぶことで、値をバインドすることができます。

### データの保存、削除

データの保存は`EntityManager`を使います。

```php:ItemController.php
    
    public function create(Request $request, EntityManagerInterface $entityManager): Response
    {
        $item = new Item();
        $item
            ->setName($request->request->get('name'))
            ->setPrice($request->request->get('price'))
        ;
        
        $entityManager->persist($item); // DBで扱う（永続化）データとして登録
        $entityManager->flush(); // 永続化、DBと同期

        ...
    }

    public function delete(int $id, ItemRepository $itemRepository, EntityManagerInterface $entityManager): Response
    {
        $item = $itemRepository->find($id); // 該当データの取得
        
        $entityManager->remove($item); // DBで扱うデータから削除（ = DBから削除）
        $entityManager->flush(); // DBと同期

    }
```

オートワイヤリングにより、Controllerのメソッドに`EntityManagerInterface`を引数指定すれば、`EntityManager`オブジェクトが手に入ります。あとは、このオブジェクトで`persist()`, `flush()`することで、データを保存できます。

データの削除も`EntityManager`で行います。`remove()`で削除したいオブジェクトを指定することで、DB上から削除できます。

内部ではトランザクションを貼っていますが、`flush()`することで自動的にコミットします。明治的にトランザクションを貼ることも可能です。

```php

$entityManager->transactional(function ($entityManager) {
    $item = new Item();
    ...
    $entityManager->persist($item);
});
```

ネストも可能です。

## まとめ

今回は`ORM Pack`を紹介しました。SymfonyはActiveRecord型のORMと比べると、ちょっと特殊に感じるかもしれません。ただ、データの取得や保存がEntityの責務から離れていることで、テストが非常にしやすくなっています。ぜひいちど試してもらえればと思います。