Starting with POST [API Platform]
The API Platform setup we're using - aka the default provided via docker-compose.yaml
- runs a Postgres DB. In the two previous Symfony 4 API implementations we were using MySQL. As such, our cleanUpDatabase
function expects a MySQL DB. And that's not to mention all the credentials are incorrect.
I'm going to add a new method to the Behat project's FeatureContext
:
/**
* @BeforeScenario
*/
public function cleanUpPostgresDatabase()
{
$host = '0.0.0.0';
$db = 'api_platform';
$port = 5432;
$user = 'dbuser';
$pass = 'dbpassword';
$dsn = "pgsql:host=$host;port=$port;dbname=$db";
$opt = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
];
$pdo = new PDO($dsn, $user, $pass, $opt);
$pdo->exec('TRUNCATE TABLE album;');
$pdo->exec('ALTER SEQUENCE album_id_seq RESTART 1;');
}
Also, I will remove the @BeforeScenario
tag from the original cleanUpDatabase
method. By remove, I simply mean adding a space between @
and BeforeScenario
. That's enough to disable the MySQL variant from running. Ok - it's a manual step, but switching between projects isn't ever going to be super streamlined, unfortunately.
The credentials used here do not match up with those provided in docker-compose.yaml
, nor the api/.env
file. Please update accordingly:
# docker-compose.yaml
# ... other stuff
db:
image: postgres:9.6-alpine
environment:
- POSTGRES_DB=api_platform
- POSTGRES_USER=dbuser
- POSTGRES_PASSWORD=dbpassword
volumes:
- db-data:/var/lib/postgresql/data:rw
ports:
- "5432:5432"
And the api/.env
file:
DATABASE_URL=pgsql://dbuser:dbpassword@db/api_platform
One extra step that I needed to take is to restart the ID sequence back to 1, independently of truncating the album
table.
Without this step, the DB would truncate / remove all the albums, but the next time the test runs, the album ID sequence would start at some number other than 1. This really messes up the tests.
As I've changed the api/.env
file, I need to restart my stack. As covered in the previous video, if using the Makefile
approach this can be done with a make dev
. If not, then docker-compose down
, and then docker-compose up -d
.
If you've changed the DB credentials then be sure to remove the db-data
Docker volume, as shown in the video.
Starting with POST
Our Behat setup relies primarily on us being able to send in a POST
request. As a quick recap:
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 single Album
Given I request "/album/1" 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"
}
"""
Scenario: Can add a new Album
Given the request body is:
"""
{
"title": "Awesome new Album",
"track_count": 7,
"release_date": "2030-12-05T01:02:03+00:00"
}
"""
When I request "/album" using HTTP POST
Then the response code is 201
Because our Behat project needed to be independent of our individual API implementations, we need to be able to POST
data into our API in order to satisfy the Background
steps. This then sets up our database, allowing the other GET
, PUT
, DELETE
, (etc) tests to run properly.
With this in mind, I'm starting with the POST
test.
vendor/bin/behat features/album.feature --tags=t
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: # 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 |
And the "Content-Type" request header is "application/json" # Imbo\BehatApiExtension\Context\ApiContext::setRequestHeader()
@t
Scenario: Can add a new Album # features/album.feature:30
Given the request body is: # Imbo\BehatApiExtension\Context\ApiContext::setRequestBody()
"""
{
"title": "Awesome new Album",
"track_count": 7,
"release_date": "2030-12-05T01:02:03+00:00"
}
"""
When I request "/album" using HTTP POST # Imbo\BehatApiExtension\Context\ApiContext::requestPath()
Then the response code is 201 # Imbo\BehatApiExtension\Context\ApiContext::assertResponseCodeIs()
Expected response code 201, got 404. (Imbo\BehatApiExtension\Exception\AssertionFailedException)
--- Failed scenarios:
features/album.feature:30
1 scenario (1 failed)
5 steps (4 passed, 1 failed)
0m0.21s (10.63Mb)
It fails. Of course.
But that's fine. We're just starting out.
In order to add new Albums, we need to setup an Album
entity.
Somewhat unusually, the Greeting
entity that comes in the api/src/Entity
folder by default uses public
class properties. From the documentation this is stated as being for simplicity. I'm going with the more traditional private
class properties, which will mean the use of getters
and setters
.
USe whichever approach you prefer. Either way, keeping the $id
property as private
, and using a getter
would be a good shout.
The starting point for our Album
entity could be a copy / paste from our previous projects:
<?php
// api/src/Entity/Album.php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity()
* @ApiResource()
*/
class Album
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @Assert\NotBlank()
* @ORM\column(type="string")
*/
private $title;
/**
* @var \DateTime|null
* @ORM\column(type="datetime")
*/
private $releaseDate;
/**
* @Assert\GreaterThan(0)
* @ORM\column(type="integer")
*/
private $trackCount;
/**
* @return int
*/
public function getId(): ?int
{
return $this->id;
}
/**
* @return string|null
*/
public function getTitle(): ?string
{
return $this->title;
}
/**
* @param string $title
*
* @return Album
*/
public function setTitle($title): Album
{
$this->title = $title;
return $this;
}
/**
* @return \DateTime|null
*/
public function getReleaseDate(): ?\DateTime
{
return $this->releaseDate;
}
/**
* @param \DateTime $releaseDate
*
* @return Album
*/
public function setReleaseDate($releaseDate): Album
{
$this->releaseDate = $releaseDate;
return $this;
}
/**
* @return int|null
*/
public function getTrackCount(): ?int
{
return $this->trackCount;
}
/**
* @param int $trackCount
*
* @return Album
*/
public function setTrackCount($trackCount): Album
{
$this->trackCount = $trackCount;
return $this;
}
}
The only absolutely essential addition over what we had previously being the inclusion of the @ApiResource()
annotation on the class itself.
You don't need to copy over the AlbumRepository
, and this will still work just fine.
However, there is an alternative way to work with your data models in an API Platform project. We will cover this alternative approach in the next video where we generate an entity using the API Platform Schema Generator.
You can take either approach - feel free to skip ahead to the next video at this point, and then return back to this point when done.
Making Doctrine Aware Of Our Changes
As we have added a new entity, we do need to tell Doctrine to update our database's schema.
I will take this opportunity to delete the default Greeting
entity, also.
I'm going to force through this change, but in a real project I would strongly recommend using Doctrine Migrations for this process as the greeting
table itself will not be automatically removed. I'm going to have to do this manually via my Database GUI (DataGrip, by JetBrains).
docker-compose exec php php bin/console doctrine:schema:update --force
Updating database schema...
2 queries were executed
[OK] Database schema updated successfully!
As quickly as this we have 5 new routes for working with Albums:
docker-compose exec php php bin/console debug:router
---------------------------- -------- -------- ------ ---------------------------------
Name Method Scheme Host Path
---------------------------- -------- -------- ------ ---------------------------------
api_entrypoint ANY ANY ANY /{index}.{_format}
api_doc ANY ANY ANY /docs.{_format}
api_jsonld_context ANY ANY ANY /contexts/{shortName}.{_format}
api_albums_get_collection GET ANY ANY /albums.{_format}
api_albums_post_collection POST ANY ANY /albums.{_format}
api_albums_get_item GET ANY ANY /albums/{id}.{_format}
api_albums_delete_item DELETE ANY ANY /albums/{id}.{_format}
api_albums_put_item PUT ANY ANY /albums/{id}.{_format}
_twig_error_test ANY ANY ANY /_error/{code}.{_format}
---------------------------- -------- -------- ------ ---------------------------------
Unfortunately, our test still fails:
vendor/bin/behat features/album.feature --tags=t
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: # 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 |
And the "Content-Type" request header is "application/json" # Imbo\BehatApiExtension\Context\ApiContext::setRequestHeader()
@t
Scenario: Can add a new Album # features/album.feature:30
Given the request body is: # Imbo\BehatApiExtension\Context\ApiContext::setRequestBody()
"""
{
"title": "Awesome new Album",
"track_count": 7,
"release_date": "2030-12-05T01:02:03+00:00"
}
"""
When I request "/album" using HTTP POST # Imbo\BehatApiExtension\Context\ApiContext::requestPath()
Then the response code is 201 # Imbo\BehatApiExtension\Context\ApiContext::assertResponseCodeIs()
Expected response code 201, got 404. (Imbo\BehatApiExtension\Exception\AssertionFailedException)
--- Failed scenarios:
features/album.feature:30
1 scenario (1 failed)
5 steps (4 passed, 1 failed)
0m0.19s (10.62Mb)
Why?
Well, our routes are pluralised: /albums
not /album
.
In order to change this, we need to define our own Operations. We'll get on to this after we've looked at an alternative way to generate entities in the very next video.