API PlatformでカスタムDELETEアクションを作成するときの注意点

# はじめに
[^apiplatform]API Platformを使っていてカスタムDELETEアクションを作成した所、思わぬ挙動になったので、その時の備忘録です。

# やりたかったこと

`User`、`Book`、`FavoriteBook`の3つEntityがあります。`FavoriteBook`はユーザーのお気に入りの本を表す中間テーブルです。
リレーションはこんな感じです。
![ER (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/614264/29642137-df52-9710-c0cf-a0e66526acbd.png)

APIとして提供するのは`User`と`Book`のCRUD操作だけで、`FavoriteBook`のCRUD操作のエンドポイントは提供しません。代わりに、`/api/books/{bookId}/favorite`にPOSTとDLETEメソッドを設け、ここでログインしているユーザーに紐づく`FavoriteBook`を作成します。


# 実装

重要な所だけ載せます。

`Book`に設定されているアノテーションはこのようになってます。

```php:src/Entity/Book.php

<?php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use App\Controller\AddFavoriteAction;
use App\Controller\DeleteFavoriteAction;
use App\Repository\BookRepository;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ApiResource(
 *     itemOperations={
 *          "get",
 *          "delete",
 *          "put",
 *          "add_favorite"={
 *              "method"="POST",
 *              "path"="/books/{id}/favorite",
 *              "controller"=AddFavoriteAction::class,
 *              "denormalization_context"={"groups"={"book:favorite"}},
 *          },
 *          "delete_favorite"={
 *              "method"="DELETE",
 *              "path"="/books/{id}/favorite",
 *              "controller"=DeleteFavoriteAction::class,
 *          }
 *     },
 *     collectionOperations={
 *          "get",
 *          "post",
 *     }
 * )
 * @ORM\Entity(repositoryClass=BookRepository::class)
 */
class Book
{
```

`AddFavoriteAction`と`DeleteFavoriteAction`はそれぞれこのようになっています。

```php:src/Controller/AddFavoriteAction.php

<?php

namespace App\Controller;

use App\Entity\Book;
use App\Entity\FavoriteBook;
use App\Entity\User;
use App\Repository\FavoriteBookRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;

class AddFavoriteAction
{
    private $entityManager;

    private $tokenStorage;

    public function __construct(
        EntityManagerInterface $entityManager,
        TokenStorageInterface $tokenStorage
    ) {
        $this->entityManager = $entityManager;
        $this->tokenStorage = $tokenStorage;
    }

    public function __invoke(Book $data)
    {
        $user = $this->tokenStorage->getToken()->getUser();
        if (!$user || !$user instanceof User) {
            throw new AuthenticationException();
        }
        /** @var FavoriteBookRepository $favoriteBookRepository */
        $favoriteBookRepository = $this->entityManager->getRepository(FavoriteBook::class);
        $favorite = $favoriteBookRepository->findOrCreateByUserAndBook($user, $data);
        $this->entityManager->persist($favorite);
        $this->entityManager->flush();

        return $data;
    }
}
```

```php:src/Controller/DeleteFavoriteAction.php

<?php

namespace App\Controller;

use App\Entity\Book;
use App\Entity\FavoriteBook;
use App\Entity\User;
use App\Repository\FavoriteBookRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;

class DeleteFavoriteAction
{
    private $entityManager;

    private $tokenStorage;

    public function __construct(
        EntityManagerInterface $entityManager,
        TokenStorageInterface $tokenStorage
    ) {
        $this->entityManager = $entityManager;
        $this->tokenStorage = $tokenStorage;
    }

    public function __invoke(Book $data)
    {
        $user = $this->tokenStorage->getToken()->getUser();
        if (!$user || !$user instanceof User) {
            throw new AuthenticationException();
        }

        /** @var FavoriteBookRepository $favoriteBookRepository */
        $favoriteBookRepository = $this->entityManager->getRepository(FavoriteBook::class);
        $favorite = $favoriteBookRepository->findOneBy(['user' => $user, 'book' => $data]);
        if ($favorite) {
            $this->entityManager->remove($favorite);
            $this->entityManager->flush();
        }

        return $data;
    }
}
```

`/api/docs/`にアクセスすると、このような表示になっています。
![スクリーンショット 2020-09-14 21.48.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/614264/dc54d522-4617-5dab-9a14-dba73d7c3788.png)

実際に`Book`を登録してから、`/api/books/{id}.favorite`のPOSTメソッドを叩くと...OKお気に入り登録されている。
DELETEを叩くと...OKお気に入り解除されてr...あれ？`Book`も消えてない...??


# 原因

カスタムアクションで指定した`DeleteFavoriteAction`の`__invoke()`メソッドでobjectをreturnしてしまうと、どうやらそのobjectがremoveされてしまうようです。

なので、`__invoke()`メソッドのreturnを削除してしまえばOKでした。


# 余談
`__invoke()`メソッドの引数ですが、変数名ベースのインジェクション[^setter_injection]を行っているらしく、`$data`以外の名前をつけるとエラーになりました。
ここらへんドキュメントに書いてあるのかなあ...。


# 参考文献
[^apiplatform]: https://api-platform.com/
[^setter_injection]: https://symfony.com/doc/current/service_container/autowiring.html#dealing-with-multiple-implementations-of-the-same-type