Send in JSON data using POST [Raw Symfony 4]


In the the previous video we created a new Symfony 4 application, and we added a simple Healthcheck endpoint to ensure that Behat can talk to our API.

The next step is to ensure Behat can POST data into our API. This is important as this is how we will set up the Background / expected state of our application whilst in the test environment.

As per our Behat test, we are expecting to receive data via POST with the following shape:

  {
    "title": "Awesome new Album",
    "track_count": 7,
    "release_date": "2030-12-05T01:02:03+00:00"
  }

There's a bunch of things we now need to do to handle requests like this.

First, we need a new Controller. We'll use the Symfony Maker Bundle to generate a Controller class:

bin/console make:controller AlbumController

 created: src/Controller/AlbumController.php
 created: templates/album/index.html.twig

  Success! 

 Next: Open your new controller class and add some pages!

We don't need the templates/album/index.html.twig template, so please feel free to delete it.

POST Haste

Out of the box, Symfony doesn't play super nicely with JSON.

The FOSRESTBundle library is often used as it makes working with JSON data inside a Symfony application feel much more natural.

Likewise, the API Platform puts JSON (primarily JSON-LD) front and central.

We're going to cover both of the implementations shortly.

For now, we're going the manual route.

Inside our new AlbumController, I'm going to make the following changes:

<?php

// src/Controller/AlbumController.php

namespace App\Controller;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class AlbumController extends AbstractController
{
    /**
     * @Route("/album", name="post_album")
     */
    public function post()
    {
        return new JsonResponse(
            [
                'status' => 'ok',
            ],
            JsonResponse::HTTP_CREATED
        );
    }
}

Hopefully most of this looks familiar to you.

This is Symfony 4, so no need to use the Action suffix on our Controller method names - post, not postAction.

We're in the App namespace, not AppBundle.

I've set the route's name to be post_album, rather than just album as generated. We will have post_album, put_album, delete_album, and so on. We can't use album as it's not unique, and we can't just use get, put, etc, as these would only be unique whilst we have just one JSON API Controller.

Also note we extend AbstractController.

Everything else should look very similar to Symfony 2 or Symfony 3.

Always worth checking at this point is that our Route is available in Symfony's eyes:

bin/console debug:router
 -------------------------- -------- -------- ------ ----------------------------------- 
  Name                       Method   Scheme   Host   Path                               
 -------------------------- -------- -------- ------ ----------------------------------- 
  album                      ANY      ANY      ANY    /album                             
  healthcheck                ANY      ANY      ANY    /ping                              
  _twig_error_test           ANY      ANY      ANY    /_error/{code}.{_format}           
  _wdt                       ANY      ANY      ANY    /_wdt/{token}                      
  _profiler_home             ANY      ANY      ANY    /_profiler/                        
  _profiler_search           ANY      ANY      ANY    /_profiler/search                  
  _profiler_search_bar       ANY      ANY      ANY    /_profiler/search_bar              
  _profiler_phpinfo          ANY      ANY      ANY    /_profiler/phpinfo                 
  _profiler_search_results   ANY      ANY      ANY    /_profiler/{token}/search/results  
  _profiler_open_file        ANY      ANY      ANY    /_profiler/open                    
  _profiler                  ANY      ANY      ANY    /_profiler/{token}                 
  _profiler_router           ANY      ANY      ANY    /_profiler/{token}/router          
  _profiler_exception        ANY      ANY      ANY    /_profiler/{token}/exception       
  _profiler_exception_css    ANY      ANY      ANY    /_profiler/{token}/exception.css   
 -------------------------- -------- -------- ------ ----------------------------------- 

Sweet.

Handling a POST

Tip: I'd strongly recommend using a GUI client for working with an API. Postman is what I use.

With our basic setup, sending in a GET, POST, DELETE, or whatever will all do the same thing:

POST /album HTTP/1.1
Host: 127.0.0.1:8000
Content-Type: application/json
Cache-Control: no-cache

{ "title": "Awesome new Album", "track_count": 7, "release_date": "2030-12-05T01:02:03+00:00" }

And what do we get back?

{"status":"ok"}

Of course, it's nonsense. Anything we do returns this response.

We can immediately restrict this route down to just POST requests:

// src/Controller/AlbumController.php

class AlbumController extends AbstractController
{
    /**
-    * @Route("/album", name="post_album")
+    * @Route("/album", name="post_album", methods={"POST"})
     */

And that's us good:

bin/console debug:router
 -------------------------- -------- -------- ------ ----------------------------------- 
  Name                       Method   Scheme   Host   Path                               
 -------------------------- -------- -------- ------ ----------------------------------- 
  post_album                 POST     ANY      ANY    /album                             

Now any other verb - GET, PATCH, DELETE, etc, will get a 405 "Method not allowed" error. Progress.

Next we need to get that raw JSON that has been sent in, and convert it into a format that PHP can work with (hint: an array).

This is where the afore mentioned FOSRESTBundle, and API Platform come into their own. We're going to have to do this step ourselves.

<?php

namespace App\Controller;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class AlbumController extends AbstractController
{
    /**
     * @Route("/album", name="post_album", methods={"POST"})
     */
    public function post(
        Request $request
    )
    {
        $data = json_decode(
            $request->getContent(),
            true
        );

        exit(\Doctrine\Common\Util\Debug::dump($data));

        return new JsonResponse(
            [
                'status' => 'ok',
            ],
            JsonResponse::HTTP_CREATED
        );
    }
}

We start by injecting the Request. This was a very common practice in Symfony 2, and Symfony 3. Nothing new here.

From the request we need need access to the raw request body content.

If at all unsure, our request body is:

  {
    "title": "Awesome new Album",
    "track_count": 7,
    "release_date": "2030-12-05T01:02:03+00:00"
  }

The outcome of $request->getContent() will be a string. The JSON above in a string format.

Calling PHP's native json_decode function, passing in this string, and the second argument of true will return us an associative array.

We can see this for ourselves now by sending in a request and hitting the exit / dump statement:

array(3) {
  ["title"]=>
  string(17) "Awesome new Album"
  ["track_count"]=>
  int(7)
  ["release_date"]=>
  string(25) "2030-12-05T01:02:03+00:00"
}

At this point we have a working controller class that has a post method that can handle our incoming POST requests.

By restricting down to just the POST method we can guarantee we won't use this code in any other situation - GET, PUT, PATCH, or DELETE,

We can send in raw JSON data, just like we will receive from our front end clients (think: JavaScript), and turn this into a format PHP can happily work with.

There are still a bunch of issues to address.

We need a way to tidy up this submission, and get this data into our database.

For this we will use Symfony's Form, and Doctrine.

Let's get on to that in the very next video.

Episodes

# Title Duration
1 What will our JSON API actually do? 08:46
2 What needs to be in our Database for our Tests to work? 12:32
3 Cleaning up after each Test Run 02:40
4 Docker makes for Easy Databases 09:01
5 Healthcheck [Raw Symfony 4] 07:53
6 Send in JSON data using POST [Raw Symfony 4] 05:33
7 Keep your data nice and tidy using Symfony's Form [Raw Symfony 4] 10:48
8 Validating incoming JSON [Raw Symfony 4] 08:26
9 Nicer error messages [Raw Symfony 4] 06:23
10 GET'ting data from our Symfony 4 API [Raw Symfony 4] 08:11
11 GET'ting a collection of Albums [Raw Symfony 4] 01:50
12 Update existing Albums with PUT [Raw Symfony 4] 05:00
13 Upsetting Purists with PATCH [Raw Symfony 4] 02:39
14 Hitting DELETE [Raw Symfony 4] 02:11
15 How to open your API to the outside world with CORS [Raw Symfony 4] 07:48
16 Getting Setup with Symfony 4 and FOSRESTBundle [FOSRESTBundle] 09:11
17 Healthcheck [FOSRESTBundle] 06:14
18 Handling POST requests [FOSRESTBundle] 08:31
19 Saving POST data to the database [FOSRESTBundle] 09:44
20 Work with XML, or JSON, or Both [FOSRESTBundle] 04:31
21 Going far, then Too Far with the ViewResponseListener [FOSRESTBundle] 03:19
22 GET'ting data from your Symfony 4 API [FOSRESTBundle] 05:58
23 GET'ting a Collection of data from your Symfony 4 API [FOSRESTBundle] 01:27
24 Updating with PUT [FOSRESTBundle] 02:58
25 Partially Updating with PATCH [FOSRESTBundle] 02:15
26 DELETE'ing Albums [FOSRESTBundle] 01:27
27 Handling Errors [FOSRESTBundle] 08:58
28 Introducing the API Platform [API Platform] 08:19
29 The Entry Point [API Platform] 04:30
30 The Context [API Platform] 05:52
31 Healthcheck - Custom Endpoint [API Platform] 05:17
32 Starting with POST [API Platform] 07:08
33 Creating Entities with the Schema Generator [API Platform] 07:38
34 Defining A Custom POST Route [API Platform] 07:31
35 Finishing POST [API Platform] 06:29
36 GET'ting One Resource [API Platform] 02:50
37 GET'ting Multiple Resources [API Platform] 02:59
38 PUT to Update Existing Data [API Platform] 02:19
39 DELETE to Remove Data [API Platform] 01:15
40 No One Likes Errors [API Platform] 03:28