گام 26: ارائه‌ی یک API با استفاده از API Platform

5.0 version
Maintained

ارائه‌ی یک API با استفاده از API Platform

ما پیاده‌سازی وب‌سایت Guestbook را تمام کرده‌ایم. برای اینکه اجازه دهیم از داده‌ها بیشتر استفاده شود، نظرتان در مورد ارائه‌ی یک API چیست؟ یک API می‌تواند توسط اپلیکیشن‌های موبایلی، برای نمایش تمام کنفرانس‌ها و کامنت‌هایشان مورد استفاده قرار بگیرد یا حتی به کاربران اجازه دهد تا کامنت ارسال کنند.

در این گام، می‌خواهیم یک API فقط‌خواندنی را پیاده‌سازی کنیم.

نصب API Platform

ارائه‌ی یک API با نوشتن مقداری کد امکان‌پذیر است. اما اگر می‌خواهیم از استانداردها استفاده کنیم، بهتر است از راهکاری بهره بگیریم که بخش سخت کار را انجام دهد. راهکاری مثل API Platform:

1
$ symfony composer req api

ارائه‌ی یک API برای کنفرانس‌ها

تعدادی حاشیه‌نویسی بر روی کلاس Conference، تمام چیزی است که برای پیکربندی API احتیاج داریم:

patch_file
 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
62
63
64
65
66
--- a/src/Entity/Conference.php
+++ b/src/Entity/Conference.php
@@ -2,15 +2,24 @@

 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
 {
@@ -18,21 +26,29 @@ 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;

@@ -43,6 +59,8 @@ class Conference

     /**
      * @ORM\Column(type="string", length=255, unique=true)
+     *
+     * @Groups({"conference:list", "conference:item"})
      */
     private $slug;

حاشیه‌نویسی اصلی @ApiResource، API را برای کنفرانس‌ها پیکربندی می‌کند. این حاشیه‌نویسی عملیات‌های ممکن را به get محدود می‌کند و چیزهای مختلفی را پیکربندی می‌کند: همچون اینکه چه فیلدهایی نمایش داده شود و ترتیب کنفرانس‌ها به چه شکل باشد.

به صورت پیشفرض و به لطف پیکربندی موجود در config/routes/api_platform.yaml که توسط recipe‌ مربوط به بسته اضافه شده است، مدخل اصلی برای API همان /api است.

رابط وب به شما اجازه می‌ده تا با API فعل‌وانفعال داشته باشید:

از آن استفاده کنید تا امکانات مختلف را امتحان کنید:

تصور کنید که اگر تمام این‌ها را از ابتدا پیاده‌سازی می‌کردید چقدر طول می‌کشید!

ارائه‌ی API برای کامنت‌ها

همین کار را برای کامنت‌ها بکنید:

patch_file
 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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
--- a/src/Entity/Comment.php
+++ b/src/Entity/Comment.php
@@ -2,12 +2,25 @@

 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
 {
@@ -15,18 +27,24 @@ 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;

@@ -34,22 +52,30 @@ class Comment
      * @ORM\Column(type="string", length=255)
      * @Assert\NotBlank
      * @Assert\Email
+     *
+     * @Groups({"comment:list", "comment:item"})
      */
     private $email;

     /**
      * @ORM\Column(type="datetime")
+     *
+     * @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;

از حاشیه‌نویسی‌های مشابه‌ای برای پیکربندی کلاس استفاده شده است.

محدودسازی کامنت‌هایی که توسط API ارائه گردیده

به صورت پیشفرض، API Platform تمام کامنت‌های درون پایگاه‌داده را ارائه می‌کند. اما برای کامنت‌ها، تنها باید آن‌هایی که منتشر‌شده هستند بخشی از API باشند.

زمانی که لازم دارید آیتم‌های بازگردانده‌شده توسط API را محدود کنید، سرویسی ایجاد کنید که یا رابط QueryCollectionExtensionInterface را که برای کنترل پرس‌وجو‌های Doctrine مربوط برای collectionها است، پیاده‌سازی کند یا اینکه رابط 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]));
        }
    }
}

این کلاسِ بسط پرس‌وجو (query extension)، منطقش را تنها به منبع Comment اعمال می‌کند و سازنده‌ی پرس‌وجوی Doctrine را تغییر می‌دهد تا تنها کامنت‌هایی با وضعیت published را در نظر بگیرد.

پیکربندی CORS

به صورت پیشفرض، در تمام کلاینت‌های مدرن HTTP، سیاست امنیتی same-origin، فراخوانی API از سایر دامنه‌ها را ممنوع می‌کند. باندل CORS، که به عنوان بخشی از composer req api نصب گردیده است، سربرگ Cross-Origin Resource Sharing را بر اساس متغیر محیط CORS_ALLOW_ORIGIN، ارسال می‌کند.

به صورت پیشفرض، مقدار آن که در .env تعریف شده است، درخواست‌های HTTP از localhost و 127.0.0.1 را بر روی هر درگاهی (port) اجازه می‌دهد. این دقیقاً همان چیزی است که ما در گام بعدی لازم داریم، چرا که می‌خواهیم یک SPA ایجاد کنیم که وب سرور خود را خواهد داشت که API را فراخوانی می‌کند.


  • « Previous گام 25: اطلاع‌رسانی با تمام قوا
  • Next » گام 27: ساخت یک SPA

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