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.

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