API Platformを使ってAPIを公開する

API Platformを使ってAPIを公開する

ゲストブックのWebサイト実装が完了しました。より多くデータを利用してもらうために、APIを公開するのはどうでしょうか?APIをモバイルアプリケーションで利用して、カンファレンスやコメントを表示したり、参加者がコメントを残すことができるようにすることが可能です。

このステップでは、読み取り専用のAPIを実装します。

API Platform をインストールする

いくつかのコードを実装することでAPIを公開することはできますが、スタンダードなやり方を求める場合は、API Platformのようなすでに面倒な実装を行っているソリューションを使用した方が良いでしょう。

1
$ symfony composer req api

カンファレンス用のAPIを公開する

Conferenceクラスにいくつかのアトリビュートを設定するだけで、APIの設定を行うことができます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
--- a/src/Entity/Conference.php
+++ b/src/Entity/Conference.php
@@ -2,35 +2,48 @@

 namespace App\Entity;

+use ApiPlatform\Core\Annotation\ApiResource;
 use App\Repository\ConferenceRepository;
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\Common\Collections\Collection;
 use Doctrine\ORM\Mapping as ORM;
 use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
+use Symfony\Component\Serializer\Annotation\Groups;
 use Symfony\Component\String\Slugger\SluggerInterface;

 #[ORM\Entity(repositoryClass: ConferenceRepository::class)]
 #[UniqueEntity('slug')]
+#[ApiResource(
+    collectionOperations: ['get' => ['normalization_context' => ['groups' => 'conference:list']]],
+    itemOperations: ['get' => ['normalization_context' => ['groups' => 'conference:item']]],
+    order: ['year' => 'DESC', 'city' => 'ASC'],
+    paginationEnabled: false,
+)]
 class Conference
 {
     #[ORM\Id]
     #[ORM\GeneratedValue]
     #[ORM\Column(type: 'integer')]
+    #[Groups(['conference:list', 'conference:item'])]
     private $id;

     #[ORM\Column(type: 'string', length: 255)]
+    #[Groups(['conference:list', 'conference:item'])]
     private $city;

     #[ORM\Column(type: 'string', length: 4)]
+    #[Groups(['conference:list', 'conference:item'])]
     private $year;

     #[ORM\Column(type: 'boolean')]
+    #[Groups(['conference:list', 'conference:item'])]
     private $isInternational;

     #[ORM\OneToMany(mappedBy: 'conference', targetEntity: Comment::class, orphanRemoval: true)]
     private $comments;

     #[ORM\Column(type: 'string', length: 255, unique: true)]
+    #[Groups(['conference:list', 'conference:item'])]
     private $slug;

     public function __construct()

メインの ApiResource アトリビュートは、カンファレンス用のAPIを構成します。 可能な操作を get に制限して、表示するフィールドや表示順など様々な設定をしています。

デフォルトでは、APIのメインエントリーポイントは /api です。これは、API Platformのパッケージによって追加された config/routes/api_platform.yaml によって構成されています。

Webインターフェースを使用することで、APIと対話することができます。

/api

これを使用して様々な可能性を検証します。

/api

ここまでの全てをゼロから実装するためにかかる時間を想像してください!

コメントAPIの公開

コメントにも同様の設定を行います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
--- a/src/Entity/Comment.php
+++ b/src/Entity/Comment.php
@@ -2,40 +2,58 @@

 namespace App\Entity;

+use ApiPlatform\Core\Annotation\ApiFilter;
+use ApiPlatform\Core\Annotation\ApiResource;
+use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
 use App\Repository\CommentRepository;
 use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Serializer\Annotation\Groups;
 use Symfony\Component\Validator\Constraints as Assert;

 #[ORM\Entity(repositoryClass: CommentRepository::class)]
 #[ORM\HasLifecycleCallbacks]
+#[ApiResource(
+    collectionOperations: ['get' => ['normalization_context' => ['groups' => 'comment:list']]],
+    itemOperations: ['get' => ['normalization_context' => ['groups' => 'comment:item']]],
+    order: ['createdAt' => 'DESC'],
+    paginationEnabled: false,
+)]
+#[ApiFilter(SearchFilter::class, properties: ['conference' => 'exact'])]
 class Comment
 {
     #[ORM\Id]
     #[ORM\GeneratedValue]
     #[ORM\Column(type: 'integer')]
+    #[Groups(['comment:list', 'comment:item'])]
     private $id;

     #[ORM\Column(type: 'string', length: 255)]
     #[Assert\NotBlank]
+    #[Groups(['comment:list', 'comment:item'])]
     private $author;

     #[ORM\Column(type: 'text')]
     #[Assert\NotBlank]
+    #[Groups(['comment:list', 'comment:item'])]
     private $text;

     #[ORM\Column(type: 'string', length: 255)]
     #[Assert\NotBlank]
     #[Assert\Email]
+    #[Groups(['comment:list', 'comment:item'])]
     private $email;

     #[ORM\Column(type: 'datetime_immutable')]
+    #[Groups(['comment:list', 'comment:item'])]
     private $createdAt;

     #[ORM\ManyToOne(targetEntity: Conference::class, inversedBy: 'comments')]
     #[ORM\JoinColumn(nullable: false)]
+    #[Groups(['comment:list', 'comment:item'])]
     private $conference;

     #[ORM\Column(type: 'string', length: 255, nullable: true)]
+    #[Groups(['comment:list', 'comment:item'])]
     private $photoFilename;

     #[ORM\Column(type: 'string', length: 255, options: ["default" => "submitted"])]

カンファレンスクラスで設定したのと同じ種類のアトリビュートをクラスに設定します。

APIによって公開されるコメントを制限する

デフォルトでは、API Platformはデータベースから全てのデータを出力します。ただし、コメントについては、公開が許可されたもののみが出力される必要があります。

APIによって返されるアイテムを制限する必要がある場合、コレクションの Doctrine クエリーを制御する QueryCollectionExtensionInterface や、アイテムを制御する QueryItemExtensionInterface を実装したサービスを作成します。

src/Api/FilterPublishedCommentQueryExtension.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
namespace App\Api;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use App\Entity\Comment;
use Doctrine\ORM\QueryBuilder;

class FilterPublishedCommentQueryExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{
    public function applyToCollection(QueryBuilder $qb, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
    {
        if (Comment::class === $resourceClass) {
            $qb->andWhere(sprintf("%s.state = 'published'", $qb->getRootAliases()[0]));
        }
    }

    public function applyToItem(QueryBuilder $qb, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = [])
    {
        if (Comment::class === $resourceClass) {
            $qb->andWhere(sprintf("%s.state = 'published'", $qb->getRootAliases()[0]));
        }
    }
}

このクエリ拡張クラスは Comment のリソースに対してのみロジックを適用し、Doctrineクエリービルダーを変更して、 published 状態のコメントのみ扱うようにします。

CORS を設定する

デフォルトでは、最新HTTPクライアントの同一生成元ポリシーにより、別ドメインからのAPI呼び出しは禁止されています。 composer req api によりインストールされるCORSバンドルは、環境変数 CORS_ALLOW_ORIGIN をもとに、オリジン間リソース共有ヘッダーを送信します。

デフォルトでは、 .env で定義されている localhost127.0.0.1 の任意ポートからのHTTPリクエストが許可されています。APIを呼び出す独自のWebサーバを作成するためには、次のステップが必要です。

This work, including the code samples, is licensed under a Creative Commons BY-NC-SA 4.0 license.