SerializerとValidatorでPOSTを処理する

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


普段、フォームなどのPOST処理をするときは[Formコンポーネント](https://symfony.com/doc/current/forms.html)を使うと思います。
今回はSerializerとValidaotrを使って、POST処理をします。

APIで以下のようなJSONが送られてきた想定で、書籍の登録をします。

```json
{
    title: "ちょうぜつソフトウェア設計入門"
    price: 3080
    authorId: 1
}
```

なお、authorId:1で紐づく著者は田中ひさてるさんです。


## Entity側

いつも通りつくればOKです。

```php:Author.php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use App\Repository\AuthorRepository;

#[ORM\Entity(repositoryClass: AuthorRepository::class)]
#[ORM\Table(name: '`author`')]
class Author
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id;

    #[ORM\Column(length: 180, unique: true)]
    #[Assert\NotBlank]
    #[Assert\Email]
    private ?string $email = null;
    
    #[ORM\Column]
    #[Assert\NotBlank]
    private ?string $name;

    // 以下Getter, Setter
}
```

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

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use App\Repository\BookRepository;

#[ORM\Entity(repositoryClass: BookRepository::class)]
#[ORM\Table(name: '`book`')]
class Author
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id;

    #[ORM\Column]
    #[Assert\NotBlank]
    #[Assert\Length(max:200)]
    private ?string $title;

    #[ORM\Column]
    #[Assert\NotBlank]
    private ?int $price;

    #[ORM\ManyToOne(targetEntity: Author::class)]
    private Author $author;

    // 以下Getter, Setter
}

```

## Controller側

`Serializer`と`Validator`をDIして利用します。Serializerでリクエストをオブジェクトに置換し、Validatorを使ってデータ検証します。問題なければ登録です。

```php:BookController.php

namespace App\Controller;

use App\Entity\Book;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;

class BookController extends AbstractController
{
    #[Route('/book', name: 'app_book_create', methods: ['POST'])]
    public function create(
        SerializerInterface $serializer,
        ValidatorInterface $validator,
        EntityManagerInterface $entityManager,
        Request $request
    ): JsonResponse
    {

        // リクエストをBookエンティティに置換
        $book = $serializer->deserialize($request->getContent(), Book::class, 'json');

        // データ検証
        $errors = $validator->validate($book);
        if ($errors->count()) {
            // エラーだったらBadRequest
            throw new BadRequestHttpException((string) $errors);
        }

        // DB保存
        $entityManager->persist($book);
        $entityManager->flush();
        
        // IDを出力
        return $this->json([
            'id' => $book->getid(),
        ]);
    }
}
```

`Serializer::deserialize()`で置換するクラスとフォーマットを指定して、置換します。
`Validator::validate()`でエンティティを渡すとクラスのアトリビュートを解析して、データ検証を行います。
`EntityManager::persist()`, `EntityManager::flush()`は永続化対象としてマーキング＆DBと同期です。


ただ、このままだと**author_idからAuthorへ置換しないので**、著者が誰か保存されません。
`Serializer`にはIDからオブジェクトを取得する機能はないからです。

そう考えると`Form`コンポーネントはよくできています。この部分意識しなくて良いですからね。

## Denormalizerをつくる

`Serializer::deserialize()`でAuthorもオブジェクトにできるように`Denormalizer`を作ります。
`Denormalizer`について、くわしくはこちら。

https://speakerdeck.com/suzuki/symfony-serializer-deep-dive

この独自Denormalizerで、DBからデータを取得してオブジェクトにセットすることができます。

```php:BookDenormalizer.php

namespace App\Serializer\Normalizer;

use App\Entity\Book;
use App\Repository\AuthorRepository;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;

class BookDenormalizer implements DenormalizerInterface
{
    public function __construct(
        private readonly AuthorRepository $authorRepository,
        private readonly ObjectNormalizer $objectNormalizer,
    )
    {
    }

    // Denormalize
    public function denormalize(mixed $data, string $type, string $format = null, array $context = [])
    {
        $book = $this->objectNormalizer->denormalize($data, $type, $format, $context);
        assert($book instanceof Book);
        
        // 'authorId'がある場合は、DBからデータを取得してBookにセット
        if (array_key_exists('authorId', $data)) {
            $author = $this->authorRepository->find($data['author_id']);
            $book->setAuthor($author);
        }

        return $book;
    }

    // 対象のクラスを指定する
    public function supportsDenormalization(mixed $data, string $type, string $format = null): bool
    {
        return $type === Book::class;
    }
}
```

これにより、ただしく田中ひさてるさんの著書として登録できます。