We have seen how to write websites that render HTML. How different is this from writing APIs?
Place this controller in src/Controller/ApiMovieController.php
And compare it side by side with our MovieController
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 85 86 87 88 89 90 91 92
<?php namespace App\Controller; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\Routing\Annotation\Route; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; use App\Form\SaleType; use App\Form\EnquiryType; use App\Entity\Movie; use App\Entity\Sale; use App\Entity\Enquiry; use App\Entity\Ticket; use App\Event\SaleEvent; use App\Logger\SaleLogger; use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\HttpFoundation\JsonResponse; class ApiMovieController extends Controller { /** * @Route("/api/movies", name="get_movies") * @Method({"GET"}) */ public function index(SerializerInterface $serializer) { $movies = $this->getDoctrine() ->getRepository(Movie::class) ->findAll(); return new JsonResponse($serializer->serialize($movies, 'json', ['groups' => ['movie']]), 200, [], true); } /** * @Route("/api/movies/{movieId}", name="get_movie") * @Method({"GET"}) */ public function movie(SerializerInterface $serializer, int $movieId) { $em = $this->getDoctrine()->getManager(); $movie = $em->getRepository(Movie::class)->findOneWithActors($movieId); return new JsonResponse($serializer->serialize($movie, 'json', ['groups' => ['movie']]), 200, [], true); } /** * @Route("/api/movies/{movieId}/sale", name="post_booking") * @Method({"POST"}) */ public function newSale(SaleLogger $saleLogger, Request $request, $movieId) { $sale = new Sale(); $movie = $this->getDoctrine() ->getRepository(Movie::class) ->find($movieId); $form = $this->createForm(SaleType::class, $sale, ['movie' => $movie, 'csrf_protection' => false]); $data = json_decode($request->getContent(), true); $form->submit($data); if ($form->isSubmitted() && $form->isValid()) { $em = $this->getDoctrine()->getManager(); $em->persist($sale); $numTickets = $data['numTickets']; $this->get('event_dispatcher') ->dispatch( SaleEvent::NAME, new SaleEvent($sale, $movie, $numTickets) ); $em->flush(); $saleLogger->log($sale); // Location should be a GET request to the sale return new Response(null, 201, [ 'Location' => $this->generateUrl( 'get_movie', ['movieId' => $movie->getId()] ), ]); } // Here we should serialize the errors of the form return new Response(null, 406); } }
There are not many changes. Basically:
Other parts of our code (Event Listeners, Services... remain untouched).
This is another advantage of having lean Controllers.
A big part of writing great APIs is dealing with serialization.
In this case, for instance, we are saying that we are serializing properties that are in the group "movie". We can define these groups in our Entities.
Serializer for the win.
Install it with
1 2 3
composer req serializer
Let's see the definition of groups in src/Entity/Movie.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 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
use Symfony\Component\Serializer\Annotation as Serializer; //... /** * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") * @Serializer\Groups({"movie", "actor"}) */ private $id; /** * @ORM\Column(type="string") * @Serializer\Groups({"movie"}) */ private $name; /** * @ORM\Column(type="string", length=100) * @Serializer\Groups({"movie"}) */ private $director; /** * @ORM\Column(type="smallint") * @Serializer\Groups({"movie"}) */ private $year; /** * @ORM\Column(type="string") * @Serializer\Groups({"movie"}) */ private $picture; /** * @ORM\ManyToMany(targetEntity="Actor", inversedBy="movies") * @Serializer\Groups({"movie"}) */ private $actors; /** * @ORM\OneToMany(targetEntity="Ticket", mappedBy="movie") */ private $tickets;
And now in src/Entity/Actor.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 25 26 27 28 29
use Symfony\Component\Serializer\Annotation as Serializer; //... /** * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") * @Serializer\Groups({"movie", "actor"}) */ private $id; /** * @ORM\Column(type="string") * @Serializer\Groups({"actor"}) */ private $name; /** * @ORM\Column(type="string") * @Serializer\Groups({"actor"}) */ private $picture; /** * @ORM\ManyToMany(targetEntity="Movie", mappedBy="actors") * @Serializer\Groups({"actor"}) */ private $movies;
Let's try our API:
1 2 3
curl -X GET http://localhost:8000/api/movies
1 2 3
curl -X GET http://localhost:8000/api/movies/1
1 2 3 4 5 6
curl -X POST http://localhost:8000/api/movies/1/sale -d '{ "numTickets": 4, "fullName": "Barack Obama" }'
We could continue seeing examples of how to use the serializer. But let's see instead a powerful tool for building APIs.
Appi Platform allows us to create very complete APIs with lots of useful tools requiring minimal configuration.
To install it:
composer req api
As we have more routes, let's configure the routes of Api Platform so they are prefixed by "/apiplat":
Edit config/routes/api_platform.yaml
:
1 2 3 4 5
api_platform: resource: . type: api_platform prefix: '/apiplat'
We need to tell Api Platform which Entities are going to be used as resources of our API. Edit src/Entity/Movie.php
:
1 2 3 4 5 6 7 8 9 10
use ApiPlatform\Core\Annotation\ApiResource; /** * @ORM\Entity(repositoryClass="App\Repository\MovieRepository") * @ORM\Table(name="movie") * @ApiResource(attributes={"normalization_context"={"groups"={"movie"}}}) */ class Movie
And the same in src/Entity/Actor.php
:
1 2 3 4 5 6 7 8 9 10
use ApiPlatform\Core\Annotation\ApiResource; /** * @ORM\Entity(repositoryClass="App\Repository\MovieRepository") * @ORM\Table(name="actor") * @ApiResource(attributes={"normalization_context"={"groups"={"actor"}}}) */ class Actor
And that is it! Visit http://localhost:8000/apiplat.
Play around a bit with API Platform, testing the different endpoints that have been automatically created for you.
Api Platform has created a fully functional Level 3 Restful API from the mapping of our entities, with documentation.
Note however that real projects are much more than a CRUD, so you will still have to implement your own custom Controllers, Events, Services and so on, using the concepts from previous sections.