PUT to Update Existing Data [API Platform]
At this point in our API Platform implementation we can GET
a single Album, we can GET
a collection of Albums, and we can POST
in new Album data to create new resources. It would be super useful if we could update existing Albums, too.
As a heads up, in the Symfony 4 JSON API, and Symfony 4 FOSRESTBundle implementations we implemented PUT
and PATCH
. The API Platform implementation will by-pass PATCH
. However, PATCH
is supported, and is automatically configured (if not customising item operations) when using raw JSON.
Anyway, let's implement PUT
.
First, let's recap our Behat test:
Feature: Provide a consistent standard JSON API endpoint
In order to build interchangeable front ends
As a JSON API developer
I need to allow Create, Read, Update, and Delete functionality
Background:
Given there are Albums with the following details:
| title | track_count | release_date |
| some fake album name | 12 | 2020-01-08T00:00:00+00:00 |
| another great album | 9 | 2019-01-07T23:22:21+00:00 |
| now that's what I call Album vol 2 | 23 | 2018-02-06T11:10:09+00:00 |
And the "Content-Type" request header is "application/json"
Scenario: Can update an existing Album - PUT
Given the request body is:
"""
{
"title": "Renamed an album",
"track_count": 9,
"release_date": "2019-01-07T23:22:21+00:00"
}
"""
When I request "/album/2" using HTTP PUT
Then the response code is 204
We already know that we don't have a defined route for PUT
:
docker-compose exec php php bin/console debug:router
---------------------------- -------- -------- ------ ---------------------------------
Name Method Scheme Host Path
---------------------------- -------- -------- ------ ---------------------------------
api_albums_get_collection GET ANY ANY /album.{_format}
api_albums_post_collection POST ANY ANY /album.{_format}
api_albums_get_item GET ANY ANY /album/{id}.{_format}
---------------------------- -------- -------- ------ ---------------------------------
We have manually defined our itemOperations
, and at present only have a GET
item operation defined. Adding in PUT
is easy enough:
<?php
// api/src/Entity/Album.php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ApiResource(
* collectionOperations = {
* "get"={
* "method"="GET",
* "path"="/album.{_format}",
* },
* "post"={
* "method"="POST",
* "path"="/album.{_format}",
* },
* },
* itemOperations={
* "get"={
* "method"="GET",
* "path"="/album/{id}.{_format}",
* },
+* "put"={
+* "path"="/album/{id}.{_format}",
+* },
* }
* )
* @ORM\Entity()
* @ApiResource(iri="http://schema.org/MusicAlbum")
*/
class Album
{
Let's quickly check the router:
docker-compose exec php php bin/console debug:router
---------------------------- -------- -------- ------ ---------------------------------
Name Method Scheme Host Path
---------------------------- -------- -------- ------ ---------------------------------
api_albums_get_collection GET ANY ANY /album.{_format}
api_albums_post_collection POST ANY ANY /album.{_format}
api_albums_get_item GET ANY ANY /album/{id}.{_format}
api_albums_put_item PUT ANY ANY /album/{id}.{_format}
---------------------------- -------- -------- ------ ---------------------------------
Sweet.
Now, let's try the test:
vendor/bin/behat features/album.feature --tags=t
Feature: Provide insight into how Symfony 4 behaves on the unhappy path
In order to eliminate bad Album data
As a JSON API developer
I need to ensure Album data meets expected criteria
Background: # features/album.feature:7
Given there are Albums with the following details: # FeatureContext::thereAreAlbumsWithTheFollowingDetails()
| title | track_count | release_date |
| some fake album name | 12 | 2020-01-08T00:00:00+00:00 |
| another great album | 9 | 2019-01-07T23:22:21+00:00 |
| now that's what I call Album vol 2 | 23 | 2018-02-06T11:10:09+00:00 |
@t
Scenario: Can update an existing Album - PUT # features/album.feature:43
Given the request body is: # Imbo\BehatApiExtension\Context\ApiContext::setRequestBody()
"""
{
"title": "Renamed an album",
"track_count": 9,
"release_date": "2019-01-07T23:22:21+00:00"
}
"""
When I request "/album/2" using HTTP PUT # Imbo\BehatApiExtension\Context\ApiContext::requestPath()
Then the response code is 204 # Imbo\BehatApiExtension\Context\ApiContext::assertResponseCodeIs()
Expected response code 204, got 200. (Imbo\BehatApiExtension\Exception\AssertionFailedException)
--- Failed scenarios:
features/album.feature:43
1 scenario (1 failed)
4 steps (3 passed, 1 failed)
0m0.16s (9.75Mb)
Arghh!!
It almost passes.
It fails because we expected a 204
, but instead we get a 200
status code.
Such is the problem with HTTP API implementations. Both 200
and 204
are legitimate responses from a PUT
request.
In the Symfony 4 JSON API, and Symfony 4 FOSRESTBundle implementations, my preference is to return a 204
. This code means "No Content". And as covered, what's the point of returning any content when the person sending in the request not only knows all the existing and new data, they also know the exact ID in order to send the request in in the first place. What else can we tell them that they don't already know?
The flip side to this is to simply return the full resource. Which is what API Platform choose to do.
There's no right or wrong. It's personal preference.
But this throws a spanner in the Behat test works.
Yet again, do we update the tests and change the previous implementations. Or do we write a separate test for JSON-LD?
I don't have a definitive answer. I'm just going to write another test :(
vendor/bin/behat features/album_jsonld.feature --tags=t
Feature: Offer a fully featured Hypermedia API
In order to offer a Hypermedia API
As an API provider
I need to ensure that JSON-LD works as expected
Background: # features/album_jsonld.feature:7
Given there are Albums with the following details: # FeatureContext::thereAreAlbumsWithTheFollowingDetails()
| title | track_count | release_date |
| some fake album name | 12 | 2020-01-08T00:00:00+00:00 |
| another great album | 9 | 2019-01-07T23:22:21+00:00 |
| now that's what I call Album vol 2 | 23 | 2018-02-06T11:10:09+00:00 |
And the "Content-Type" request header is "application/json" # Imbo\BehatApiExtension\Context\ApiContext::setRequestHeader()
@api_platform @t
Scenario: Can update an existing Album - PUT # features/album_jsonld.feature:57
Given the request body is: # Imbo\BehatApiExtension\Context\ApiContext::setRequestBody()
"""
{
"title": "Renamed an album",
"track_count": 9,
"release_date": "2019-01-07T23:22:21+00:00"
}
"""
When I request "/album/2" using HTTP PUT # Imbo\BehatApiExtension\Context\ApiContext::requestPath()
Then the response code is 200 # Imbo\BehatApiExtension\Context\ApiContext::assertResponseCodeIs()
1 scenario (1 passed)
5 steps (5 passed)
0m0.14s (9.73Mb)
It's a passing test. Hoorah. On to DELETE
.