步骤 26: 用 API Platform 暴露 API 接口

我们已经完成了留言本网站的开发。为了让网站数据获得更大范围的使用,现在来让它暴露出一套 API 接口怎么样?API 可以用于在移动应用中展示所有会议和关于它们的评论,或许也可以让参会者提交评论。

在本步骤中,我们会实现一套只读 API。

安装 API Platform

通过编写代码来暴露一套 API 是可行的,但如果我们想要利用标准,我们最好可以使用一个已为我们完成了大量繁重工作的方案。比如 API Platform 这个方案:

  1. $ symfony composer req api

为会议暴露一个 API

我们配置 API 所需要做的就是在 Conference 类里添加一些注解:

patch_file

  1. --- a/src/Entity/Conference.php
  2. +++ b/src/Entity/Conference.php
  3. @@ -2,16 +2,25 @@
  4. namespace App\Entity;
  5. +use ApiPlatform\Core\Annotation\ApiResource;
  6. use App\Repository\ConferenceRepository;
  7. use Doctrine\Common\Collections\ArrayCollection;
  8. use Doctrine\Common\Collections\Collection;
  9. use Doctrine\ORM\Mapping as ORM;
  10. use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
  11. +use Symfony\Component\Serializer\Annotation\Groups;
  12. use Symfony\Component\String\Slugger\SluggerInterface;
  13. /**
  14. * @ORM\Entity(repositoryClass=ConferenceRepository::class)
  15. * @UniqueEntity("slug")
  16. + *
  17. + * @ApiResource(
  18. + * collectionOperations={"get"={"normalization_context"={"groups"="conference:list"}}},
  19. + * itemOperations={"get"={"normalization_context"={"groups"="conference:item"}}},
  20. + * order={"year"="DESC", "city"="ASC"},
  21. + * paginationEnabled=false
  22. + * )
  23. */
  24. class Conference
  25. {
  26. @@ -20,21 +29,25 @@ class Conference
  27. * @ORM\GeneratedValue
  28. * @ORM\Column(type="integer")
  29. */
  30. + #[Groups(['conference:list', 'conference:item'])]
  31. private $id;
  32. /**
  33. * @ORM\Column(type="string", length=255)
  34. */
  35. + #[Groups(['conference:list', 'conference:item'])]
  36. private $city;
  37. /**
  38. * @ORM\Column(type="string", length=4)
  39. */
  40. + #[Groups(['conference:list', 'conference:item'])]
  41. private $year;
  42. /**
  43. * @ORM\Column(type="boolean")
  44. */
  45. + #[Groups(['conference:list', 'conference:item'])]
  46. private $isInternational;
  47. /**
  48. @@ -45,6 +58,7 @@ class Conference
  49. /**
  50. * @ORM\Column(type="string", length=255, unique=true)
  51. */
  52. + #[Groups(['conference:list', 'conference:item'])]
  53. private $slug;
  54. public function __construct()

@ApiResource 注解为会议配置了 API。它把允许的操作限制为 get,也配置了各种信息:比如展示哪些字段,以及如何为会议排序。

安装包的 recipe 会添加 config/routes/api_platform.yaml 文件,根据该文件中的配置,API 的默认主入口是 /api 路径。

有 web 界面可以让你与 API 进行交互:

步骤 26: 用 API Platform 暴露 API 接口 - 图1

用它来测试各种可能性:

步骤 26: 用 API Platform 暴露 API 接口 - 图2

想象一下从零开始开发所有这些所需要的时间吧!

为评论暴露一个 API

为评论做同样的修改:

patch_file

  1. --- a/src/Entity/Comment.php
  2. +++ b/src/Entity/Comment.php
  3. @@ -2,13 +2,26 @@
  4. namespace App\Entity;
  5. +use ApiPlatform\Core\Annotation\ApiFilter;
  6. +use ApiPlatform\Core\Annotation\ApiResource;
  7. +use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
  8. use App\Repository\CommentRepository;
  9. use Doctrine\ORM\Mapping as ORM;
  10. +use Symfony\Component\Serializer\Annotation\Groups;
  11. use Symfony\Component\Validator\Constraints as Assert;
  12. /**
  13. * @ORM\Entity(repositoryClass=CommentRepository::class)
  14. * @ORM\HasLifecycleCallbacks()
  15. + *
  16. + * @ApiResource(
  17. + * collectionOperations={"get"={"normalization_context"={"groups"="comment:list"}}},
  18. + * itemOperations={"get"={"normalization_context"={"groups"="comment:item"}}},
  19. + * order={"createdAt"="DESC"},
  20. + * paginationEnabled=false
  21. + * )
  22. + *
  23. + * @ApiFilter(SearchFilter::class, properties={"conference": "exact"})
  24. */
  25. class Comment
  26. {
  27. @@ -17,18 +30,21 @@ class Comment
  28. * @ORM\GeneratedValue
  29. * @ORM\Column(type="integer")
  30. */
  31. + #[Groups(['comment:list', 'comment:item'])]
  32. private $id;
  33. /**
  34. * @ORM\Column(type="string", length=255)
  35. */
  36. #[Assert\NotBlank]
  37. + #[Groups(['comment:list', 'comment:item'])]
  38. private $author;
  39. /**
  40. * @ORM\Column(type="text")
  41. */
  42. #[Assert\NotBlank]
  43. + #[Groups(['comment:list', 'comment:item'])]
  44. private $text;
  45. /**
  46. @@ -36,22 +52,26 @@ class Comment
  47. */
  48. #[Assert\NotBlank]
  49. #[Assert\Email]
  50. + #[Groups(['comment:list', 'comment:item'])]
  51. private $email;
  52. /**
  53. * @ORM\Column(type="datetime")
  54. */
  55. + #[Groups(['comment:list', 'comment:item'])]
  56. private $createdAt;
  57. /**
  58. * @ORM\ManyToOne(targetEntity=Conference::class, inversedBy="comments")
  59. * @ORM\JoinColumn(nullable=false)
  60. */
  61. + #[Groups(['comment:list', 'comment:item'])]
  62. private $conference;
  63. /**
  64. * @ORM\Column(type="string", length=255, nullable=true)
  65. */
  66. + #[Groups(['comment:list', 'comment:item'])]
  67. private $photoFilename;
  68. /**

用类似的注解来配置评论类。

限制 API 暴露的评论

默认情况下,API Platform 会暴露数据库里的所有记录。但对于评论,只有发布的那些才应该被 API 暴露。

当你需要限制 API 返回的记录时,创建一个实现了 QueryCollectionExtensionInterface 接口的服务来控制 Doctrine 查询,这个查询会用于集合和 QueryItemExtensionInterface 接口中,以此来控制其中的元素:

src/Api/FilterPublishedCommentQueryExtension.php

  1. namespace App\Api;
  2. use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
  3. use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
  4. use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
  5. use App\Entity\Comment;
  6. use Doctrine\ORM\QueryBuilder;
  7. class FilterPublishedCommentQueryExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
  8. {
  9. public function applyToCollection(QueryBuilder $qb, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
  10. {
  11. if (Comment::class === $resourceClass) {
  12. $qb->andWhere(sprintf("%s.state = 'published'", $qb->getRootAliases()[0]));
  13. }
  14. }
  15. public function applyToItem(QueryBuilder $qb, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = [])
  16. {
  17. if (Comment::class === $resourceClass) {
  18. $qb->andWhere(sprintf("%s.state = 'published'", $qb->getRootAliases()[0]));
  19. }
  20. }
  21. }

这个请求扩展类只针对 Comment 资源才会应用它的逻辑,它会修改 Doctrine 的请求构建器,让它只查询处于 published 状态的评论。

配置 CORS

默认情况下,现代 HTTP 客户端的同源安全策略会禁止调用另一个域名的 API。在执行 composer req api 时一起安装了 CORS bundle,它会根据 CORS_ALLOW_ORIGIN 环境变量发送跨域资源共享的 HTTP 头。

默认情况下,它的值定义在 .env 文件,它允许来自 localhost127.0.0.1 任意端口的 HTTP 请求。这正是我们在下一个步骤中需要的,到时我们会创建一个有自己服务器的单页应用,该服务器会调用此处的 API。

深入学习


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