GET'ting Multiple Resources [API Platform]
We are now well on our way with our customised API Platform implementation. We are following along with the same Behat test suite that we used for our standalone Symfony 4 JSON API, and our Symfony 4 with FOSRESTBundle implementations. So far, things are looking pretty good - and with quite a lot less effort on our part.
Previously we covered the GET
operation for a single Album - e.g. /album/3
.
Now we want to get back a list of all Album resources in our system. This would be a Collection Operation.
We've already defined this route. We had to. In API Platform, both GET
operations are mandatory, and we had to define a custom collectionOperation
option in order to get our POST
test to pass.
Here are the annotations so far:
<?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;
/**
* @ORM\Entity()
* @ApiResource(
* collectionOperations = {
* "get"={
* "method"="GET",
* "path"="/album.{_format}",
* },
* "post"={
* "method"="POST",
* "path"="/album.{_format}",
* },
* },
* itemOperations={
* "get"={
* "method"="GET",
* "path"="/album/{id}.{_format}",
* },
* }
* )
* @ApiResource(iri="http://schema.org/MusicAlbum")
*/
class Album
{
And again we already have a predefined Behat test for this:
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 get a collection of Albums
Given I request "/album" using HTTP GET
Then the response code is 200
And the response body contains JSON:
"""
[
{
"id": 1,
"title": "some fake album name",
"track_count": 12,
"release_date": "2020-01-08T00:00:00+00:00"
},
{
"id": 2,
"title": "another great album",
"track_count": 9,
"release_date": "2019-01-07T23:22:21+00:00"
},
{
"id": 3,
"title": "now that's what I call Album vol 2",
"track_count": 23,
"release_date": "2018-02-06T11:10:09+00:00"
}
]
"""
However, at this point we do hit on a problem with our generic Behat test setup.
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 |
@symfony_4_edge_case @t
Scenario: Can get a collection of Albums # features/album.feature:15
Given I request "/album" using HTTP GET # Imbo\BehatApiExtension\Context\ApiContext::requestPath()
Then the response code is 200 # Imbo\BehatApiExtension\Context\ApiContext::assertResponseCodeIs()
And the response body contains JSON: # Imbo\BehatApiExtension\Context\ApiContext::assertResponseBodyContainsJson()
"""
[
{
"id": 1,
"title": "some fake album name",
"track_count": 12,
"release_date": "2020-01-08T00:00:00+00:00"
},
{
"id": 2,
"title": "another great album",
"track_count": 9,
"release_date": "2019-01-07T23:22:21+00:00"
},
{
"id": 3,
"title": "now that's what I call Album vol 2",
"track_count": 23,
"release_date": "2018-02-06T11:10:09+00:00"
}
]
"""
The needle is a list, while the haystack is not.
================================================================================
= Needle =======================================================================
================================================================================
[
{
"id": 1,
"title": "some fake album name",
"track_count": 12,
"release_date": "2020-01-08T00:00:00+00:00"
},
{
"id": 2,
"title": "another great album",
"track_count": 9,
"release_date": "2019-01-07T23:22:21+00:00"
},
{
"id": 3,
"title": "now that's what I call Album vol 2",
"track_count": 23,
"release_date": "2018-02-06T11:10:09+00:00"
}
]
================================================================================
= Haystack =====================================================================
================================================================================
{
"@context": "\/contexts\/Album",
"@id": "\/album",
"@type": "hydra:Collection",
"hydra:member": [
{
"@id": "\/album\/1",
"@type": "Album",
"id": 1,
"title": "some fake album name",
"release_date": "2020-01-08T00:00:00+00:00",
"track_count": 12
},
{
"@id": "\/album\/2",
"@type": "Album",
"id": 2,
"title": "another great album",
"release_date": "2019-01-07T23:22:21+00:00",
"track_count": 9
},
{
"@id": "\/album\/3",
"@type": "Album",
"id": 3,
"title": "now that's what I call Album vol 2",
"release_date": "2018-02-06T11:10:09+00:00",
"track_count": 23
}
],
"hydra:totalItems": 3
}
(Imbo\BehatApiExtension\Exception\ArrayContainsComparatorException)
--- Failed scenarios:
features/album.feature:15
1 scenario (1 failed)
4 steps (3 passed, 1 failed)
0m0.14s (9.75Mb)
Yeah, so basically the entire thing is wrong :(
Why?
Well, if it's not immediately obvious from the test output (which is admittedly hard to read without nice console colouration), let's send in a request using Postman:
{
"@context": "/contexts/Album",
"@id": "/album",
"@type": "hydra:Collection",
"hydra:member": [
{
"@id": "/album/1",
"@type": "Album",
"id": 1,
"title": "some fake album name",
"release_date": "2020-01-08T00:00:00+00:00",
"track_count": 12
},
{
"@id": "/album/2",
"@type": "Album",
"id": 2,
"title": "another great album",
"release_date": "2019-01-07T23:22:21+00:00",
"track_count": 9
},
{
"@id": "/album/3",
"@type": "Album",
"id": 3,
"title": "now that's what I call Album vol 2",
"release_date": "2018-02-06T11:10:09+00:00",
"track_count": 23
}
],
"hydra:totalItems": 3
}
Ahh. So everything is wrapped in Hydra 'stuff'.
Now, interestingly we can get a pass. If we cheat.
Just change the requested endpoint from:
https://localhost:8443/album
to:
https://localhost:8443/album.json
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 |
@symfony_4_edge_case @t
Scenario: Can get a collection of Albums # features/album.feature:15
Given I request "/album.json" using HTTP GET # Imbo\BehatApiExtension\Context\ApiContext::requestPath()
Then the response code is 200 # Imbo\BehatApiExtension\Context\ApiContext::assertResponseCodeIs()
And the response body contains JSON: # Imbo\BehatApiExtension\Context\ApiContext::assertResponseBodyContainsJson()
"""
[
{
"id": 1,
"title": "some fake album name",
"track_count": 12,
"release_date": "2020-01-08T00:00:00+00:00"
},
{
"id": 2,
"title": "another great album",
"track_count": 9,
"release_date": "2019-01-07T23:22:21+00:00"
},
{
"id": 3,
"title": "now that's what I call Album vol 2",
"track_count": 23,
"release_date": "2018-02-06T11:10:09+00:00"
}
]
"""
1 scenario (1 passed)
4 steps (4 passed)
0m0.15s (9.75Mb)
Hey, would ya' look at that! A passing test. Easy peasy.
Well... we did have to change our test, which will have now broken this test for our other implementations.
And raw JSON isn't officially supported by API Platform, so there's that.
But we kinda know it roughly works. So that's something.
A Custom Test
Other than updating the previous implementations to use JSON-LD, I don't see how we can keep one test here. Alas, I'm going to suggest we diverge and implement a custom test. It's not ideal, but there seems to be no other alternative.
I'm going to create a new file in the Behat project:
touch features/album_jsonld.feature
And to this file:
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:
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"
@api_platform
Scenario: Can get a collection of Albums
Given I request "/album" using HTTP GET
Then the response code is 200
And the response body contains JSON:
"""
{
"@context": "/contexts/Album",
"@id": "/album",
"@type": "hydra:Collection",
"hydra:member": [
{
"@id": "/album/1",
"@type": "Album",
"id": 1,
"title": "some fake album name",
"release_date": "2020-01-08T00:00:00+00:00",
"track_count": 12
},
{
"@id": "/album/2",
"@type": "Album",
"id": 2,
"title": "another great album",
"release_date": "2019-01-07T23:22:21+00:00",
"track_count": 9
},
{
"@id": "/album/3",
"@type": "Album",
"id": 3,
"title": "now that's what I call Album vol 2",
"release_date": "2018-02-06T11:10:09+00:00",
"track_count": 23
}
],
"hydra:totalItems": 3
}
"""
Which when run:
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 get a collection of Albums # features/album_jsonld.feature:16
Given I request "/album" using HTTP GET # Imbo\BehatApiExtension\Context\ApiContext::requestPath()
Then the response code is 200 # Imbo\BehatApiExtension\Context\ApiContext::assertResponseCodeIs()
And the response body contains JSON: # Imbo\BehatApiExtension\Context\ApiContext::assertResponseBodyContainsJson()
"""
{
"@context": "/contexts/Album",
"@id": "/album",
"@type": "hydra:Collection",
"hydra:member": [
{
"@id": "/album/1",
"@type": "Album",
"id": 1,
"title": "some fake album name",
"release_date": "2020-01-08T00:00:00+00:00",
"track_count": 12
},
{
"@id": "/album/2",
"@type": "Album",
"id": 2,
"title": "another great album",
"release_date": "2019-01-07T23:22:21+00:00",
"track_count": 9
},
{
"@id": "/album/3",
"@type": "Album",
"id": 3,
"title": "now that's what I call Album vol 2",
"release_date": "2018-02-06T11:10:09+00:00",
"track_count": 23
}
],
"hydra:totalItems": 3
}
"""
1 scenario (1 passed)
5 steps (5 passed)
0m0.15s (9.73Mb)
Ok, so a passing test.
Did we cheat? It sort of feels like we did.
I'm open to an alternative test process where we keep everything in one file. I really don't see a way to do this at the moment though.
I've intentionally called the file album_jsonld.feature
, rather than album_apiplatform.feature
, or similar. We're testing JSON-LD generally. Our other implementations could be adapter to support this format.
Anyway, that's both GET
one and GET
many, and POST
covered. Let's move on to updating via PUT
.