Schritt 26: Eine API mit API Platform bereitstellen

5.0 version
Maintained

Eine API mit API Platform bereitstellen

Wir haben die Implementierung der Gästebuch-Website abgeschlossen. Wie wäre es, wenn Du jetzt eine API veröffentlichen würdest, um eine bessere Nutzung der Daten zu ermöglichen? Eine API könnte von einer mobilen Anwendung verwendet werden, um alle Konferenzen und deren Kommentare anzuzeigen und die Teilnehmer*innen eventuell Kommentare abgeben zu lassen.

In diesem Schritt werden wir eine schreibgeschützte API implementieren.

API Platform installieren

Eine API durch Schreiben von Code anzulegen ist möglich, aber wenn wir Standards verwenden wollen, sollten wir besser eine Lösung verwenden, die sich bereits um einen Großteil der Aufgaben kümmert. Eine Lösung wie API Platform:

1
$ symfony composer req api

Eine API für Konferenzen bereitstellen

Ein paar Annotations in der Konferenzklasse reichen aus, um die API zu konfigurieren:

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;

Die Haupt-Annotation @ApiResource konfiguriert die API für Konferenzen. Sie beschränkt die möglichen Operationen auf get und konfiguriert verschiedene Dinge: z. B. welche Felder angezeigt werden und wie die Konferenzen sortiert werden sollen.

Der Haupteinstiegspunkt für die API ist standardmässig /api. Das ist so dank der Konfiguration in config/routes/api_platform.yaml, die durch das Recipe des Pakets hinzugefügt wurde.

Ein Web-Interface ermöglicht die Interaktion mit der API:

Benutze es, um die verschiedenen Möglichkeiten zu testen:

Stell Dir vor, wie lange es dauern würde, all dies von Grund auf neu zu implementieren!

Eine API für Kommentare bereitstellen

Mach das Gleiche für Kommentare:

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;

Die gleiche Art von Annotations werden verwendet, um die Klasse zu konfigurieren.

Einschränkung der Kommentare, die über die API zugänglich sind

Standardmäßig stellt die API Platform alle Einträge aus der Datenbank zur Verfügung. Aber für Kommentare sollten nur die veröffentlichten Teil der API sein.

Wenn Du die von der API zurückgegebenen Elemente einschränken musst, erstelle einen Service, der QueryCollectionExtensionInterface implementiert, um die Doctrine-Abfragen für Collections zu steuern, und/oder einen Service, der QueryItemExtensionInterface implementiert für die Steuerung von einzelnen Items (Elementen):

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]));
        }
    }
}

Die Query-Extension-Klasse wendet ihre Logik nur auf die Comment Ressource an und ändert den Doctrine Query Builder so, dass er nur Kommentare im published-Zustand berücksichtigt.

CORS konfigurieren

Standardmäßig ist der Aufruf der API von einer anderen Domain aus aufgrund der Same-Origin Sicherheitsrichtlinie moderner HTTP-Clients verboten. Das CORS-Bundle, das als Teil von composer req api installiert wurde, sendet Cross-Origin-Resource-Sharing-Header basierend auf der Environment-Variable CORS_ALLOW_ORIGIN.

Standardmäßig erlaubt sind HTTP-Anfragen von localhost und 127.0.0.1 auf jedem Port (in .env definiert). Das ist genau das, was wir für den nächsten Schritt benötigen, denn wir werden eine SPA (Single-Page Web Application) erstellen, welche über einen eigenen Webserver verfügt, der die API aufruft.


  • « Previous Schritt 25: Benachrichtigungen auf allen Kanälen
  • Next » Schritt 27: Aufbau einer SPA

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