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.