Exposing an API with API Platform
We have finished the implementation of the Guestbook website. To allow more usage of the data, what about exposing an API now? An API could be used by a mobile application to display all conferences, their comments, and maybe let attendees submit comments.
In this step, we are going to implement a read-only API.
Installing API Platform
Exposing an API by writing some code is possible, but if we want to use standards, we'd better use a solution that already takes care of the heavy lifting. A solution like API Platform:
1
$ symfony composer req api
Exposing an API for Conferences
A few attributes on the Conference class is all we need to configure the 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
--- a/src/Entity/Conference.php
+++ b/src/Entity/Conference.php
@@ -2,29 +2,45 @@
namespace App\Entity;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\GetCollection;
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\Attribute\Groups;
use Symfony\Component\String\Slugger\SluggerInterface;
#[ORM\Entity(repositoryClass: ConferenceRepository::class)]
#[UniqueEntity('slug')]
+#[ApiResource(
+ operations: [
+ new Get(normalizationContext: ['groups' => 'conference:item']),
+ new GetCollection(normalizationContext: ['groups' => 'conference:list'])
+ ],
+ order: ['year' => 'DESC', 'city' => 'ASC'],
+ paginationEnabled: false,
+)]
class Conference
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
+ #[Groups(['conference:list', 'conference:item'])]
private ?int $id = null;
#[ORM\Column(length: 255)]
+ #[Groups(['conference:list', 'conference:item'])]
private ?string $city = null;
#[ORM\Column(length: 4)]
+ #[Groups(['conference:list', 'conference:item'])]
private ?string $year = null;
#[ORM\Column]
+ #[Groups(['conference:list', 'conference:item'])]
private ?bool $isInternational = null;
/**
@@ -34,6 +50,7 @@ class Conference
private Collection $comments;
#[ORM\Column(length: 255, unique: true)]
+ #[Groups(['conference:list', 'conference:item'])]
private ?string $slug = null;
public function __construct()
The main ApiResource
attribute configures the API for conferences. It restricts possible operations to get
and configures various things: like which fields to display and how to order the conferences.
By default, the main entry point for the API is /api
thanks to configuration from config/routes/api_platform.yaml
that was added by the package's recipe.
A web interface allows you to interact with the API:
Use it to test the various possibilities:
Imagine the time it would take to implement all of this from scratch!
Exposing an API for Comments
Do the same for comments:
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/Comment.php
+++ b/src/Entity/Comment.php
@@ -2,41 +2,63 @@
namespace App\Entity;
+use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
+use ApiPlatform\Metadata\ApiFilter;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\GetCollection;
use App\Repository\CommentRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: CommentRepository::class)]
#[ORM\HasLifecycleCallbacks]
+#[ApiResource(
+ operations: [
+ new Get(normalizationContext: ['groups' => 'comment:item']),
+ new GetCollection(normalizationContext: ['groups' => 'comment:list'])
+ ],
+ order: ['createdAt' => 'DESC'],
+ paginationEnabled: false,
+)]
+#[ApiFilter(SearchFilter::class, properties: ['conference' => 'exact'])]
class Comment
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
+ #[Groups(['comment:list', 'comment:item'])]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Assert\NotBlank]
+ #[Groups(['comment:list', 'comment:item'])]
private ?string $author = null;
#[ORM\Column(type: Types::TEXT)]
#[Assert\NotBlank]
+ #[Groups(['comment:list', 'comment:item'])]
private ?string $text = null;
#[ORM\Column(length: 255)]
#[Assert\NotBlank]
#[Assert\Email]
+ #[Groups(['comment:list', 'comment:item'])]
private ?string $email = null;
#[ORM\Column]
+ #[Groups(['comment:list', 'comment:item'])]
private ?\DateTimeImmutable $createdAt = null;
#[ORM\ManyToOne(inversedBy: 'comments')]
#[ORM\JoinColumn(nullable: false)]
+ #[Groups(['comment:list', 'comment:item'])]
private ?Conference $conference = null;
#[ORM\Column(length: 255, nullable: true)]
+ #[Groups(['comment:list', 'comment:item'])]
private ?string $photoFilename = null;
#[ORM\Column(length: 255, options: ['default' => 'submitted'])]
The same kind of attributes are used to configure the class.
Restricting Comments exposed by the API
By default, API Platform exposes all entries from the database. But for comments, only the published ones should be part of the API.
When you need to restrict the items returned by the API, create a service that implements QueryCollectionExtensionInterface
to control the Doctrine query used for collections and/or QueryItemExtensionInterface
to control items:
The query extension class applies its logic only for the Comment
resource and modify the Doctrine query builder to only consider comments in the published
state.
Configuring CORS
By default, the same-origin security policy of modern HTTP clients make calling the API from another domain forbidden. The CORS bundle, installed as part of composer req api
, sends Cross-Origin Resource Sharing headers based on the CORS_ALLOW_ORIGIN
environment variable.
By default, its value, defined in .env
, allows HTTP requests from localhost
and 127.0.0.1
on any port. That's exactly what we need as for the next step as we will create an SPA that will have its own web server that will call the API.