Healthcheck [FOSRESTBundle]


We have our Symfony 4 project up and running, and we've added all the dependencies needed for FOSRESTBundle to function on a basic level. As it stands, our functionality is still limited, and we'll need to add in various extra code and configuration to make things behave as we require.

If you haven't already done so, start your web server. I'm using the Symfony web server, but you can use the built in PHP server if you'd prefer:

bin/console server:start

 [OK] Server listening on http://127.0.0.1:8000

To begin with, we're going to run our Behat test suite, specifically the healthcheck.feature:

vendor/bin/behat features/healthcheck.feature

Feature: To ensure the API is responding in a simple manner

  In order to offer a working product
  As a conscientious software developer
  I need to ensure my JSON API is functioning

  Scenario: Basic healthcheck              # features/healthcheck.feature:8
    Given I request "/ping" using HTTP GET # Imbo\BehatApiExtension\Context\ApiContext::requestPath()
    Then the response code is 200          # Imbo\BehatApiExtension\Context\ApiContext::assertResponseCodeIs()
      Expected response code 200, got 404. (Imbo\BehatApiExtension\Exception\AssertionFailedException)
    And the response body is:              # Imbo\BehatApiExtension\Context\ApiContext::assertResponseBodyIs()
      """
      "pong"
      """

--- Failed scenarios:

    features/healthcheck.feature:8

1 scenario (1 failed)
3 steps (1 passed, 1 failed, 1 skipped)
0m0.26s (9.97Mb)

This is a good start.

Our server is being hit, but the /ping endpoint is not yet a real thing. We need to implement that.

We can cheat entirely here and copy paste from the Symfony 4 standalone JSON API project:

<?php

namespace App\Controller;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class HealthcheckController extends Controller
{
    /**
     * @Route("/ping", name="healthcheck")
     */
    public function index()
    {
        return new JsonResponse('pong');
    }
}

Check the router:

bin/console debug:router
 -------------------------- -------- -------- ------ ----------------------------------- 
  Name                       Method   Scheme   Host   Path                               
 -------------------------- -------- -------- ------ ----------------------------------- 
  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   
 -------------------------- -------- -------- ------ ----------------------------------- 

Yup, it's there, and:

vendor/bin/behat features/healthcheck.feature

Feature: To ensure the API is responding in a simple manner

  In order to offer a working product
  As a conscientious software developer
  I need to ensure my JSON API is functioning

  Scenario: Basic healthcheck              # features/healthcheck.feature:8
    Given I request "/ping" using HTTP GET # Imbo\BehatApiExtension\Context\ApiContext::requestPath()
    Then the response code is 200          # Imbo\BehatApiExtension\Context\ApiContext::assertResponseCodeIs()
    And the response body is:              # Imbo\BehatApiExtension\Context\ApiContext::assertResponseBodyIs()
      """
      "pong"
      """

1 scenario (1 passed)
3 steps (3 passed)
0m0.48s (9.59Mb)

Are we done?

Yes, and no.

This works. That's good enough.

But it's not using FOSRESTBundle.

Let's change the implementation a little. Please note, you really do not need to do this. We've just seen that this works, and passes.

FOSRESTBundle Changes

<?php

namespace App\Controller;

+use FOS\RestBundle\Controller\FOSRestController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
-use Symfony\Bundle\FrameworkBundle\Controller\Controller;

-class HealthcheckController extends Controller
+class HealthcheckController extends FOSRestController
{
    /**
     * @Route("/ping", name="healthcheck")
     */
    public function get()
    {
        return new JsonResponse('pong');
    }
}

Firstly, I need to highlight that whilst Symfony 4 controllers allow route naming without the Action suffix (get vs getAction), we must (at the time of recording) still use the Action suffix in FOSRESTBundle controllers:

bin/console debug:router

In HealthcheckController.php line 9:

  Warning: Declaration of
    App\Controller\HealthcheckController::get()
    should be compatible with
    Symfony\Bundle\FrameworkBundle\Controller\Controller::get(string $id)

Therefore:

<?php

namespace App\Controller;

use FOS\RestBundle\Controller\FOSRestController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;

class HealthcheckController extends FOSRestController
{
    /**
     * @Route("/ping", name="healthcheck")
     */
-   public function get()
+   public function getAction()
    {
        return new JsonResponse('pong');
    }
}

Again, at this point we have a passing test:

vendor/bin/behat features/healthcheck.feature

Feature: To ensure the API is responding in a simple manner

  In order to offer a working product
  As a conscientious software developer
  I need to ensure my JSON API is functioning

  Scenario: Basic healthcheck              # features/healthcheck.feature:8
    Given I request "/ping" using HTTP GET # Imbo\BehatApiExtension\Context\ApiContext::requestPath()
    Then the response code is 200          # Imbo\BehatApiExtension\Context\ApiContext::assertResponseCodeIs()
    And the response body is:              # Imbo\BehatApiExtension\Context\ApiContext::assertResponseBodyIs()
      """
      "pong"
      """

1 scenario (1 passed)
3 steps (3 passed)
0m0.07s (9.59Mb)

We still haven't locked down our route though:

bin/console debug:router
 -------------------------- -------- -------- ------ ----------------------------------- 
  Name                       Method   Scheme   Host   Path                               
 -------------------------- -------- -------- ------ ----------------------------------- 
  healthcheck                ANY      ANY      ANY    /ping     

We could use the methods option of the Routes annotation:

     * @Route(
     *     "/ping",
     *     name="healthcheck",
     *     methods={"GET"}
     * )

But FOSRESTBundle provides a more specific annotation for this purpose:

<?php

namespace App\Controller;

use FOS\RestBundle\Controller\FOSRestController;
use Symfony\Component\HttpFoundation\JsonResponse;
-use Symfony\Component\Routing\Annotation\Route;
+use FOS\RestBundle\Controller\Annotations;

class HealthcheckController extends FOSRestController
{
-   /**
-    * @Route("/ping", name="healthcheck")
-    */
+   /**
+    * @Annotations\Get(
+    *     path="/ping"
+    * )
+    */
    public function getAction()
    {
        return new JsonResponse('pong');
    }
}

And a quick check of the router:

bin/console debug:router
 -------------------------- -------- -------- ------ ----------------------------------- 
  Name                       Method   Scheme   Host   Path                               
 -------------------------- -------- -------- ------ ----------------------------------- 
  app_healthcheck_get        GET      ANY      ANY    /ping  

Ok, we are now locked down to just a GET request, and we've overridden the generated path with our own /ping endpoint.

We could also add in a name option to change app_healthcheck_get to healthcheck, but I don't see any value in doing that.

And just to confirm:

vendor/bin/behat features/healthcheck.feature

Feature: To ensure the API is responding in a simple manner

  In order to offer a working product
  As a conscientious software developer
  I need to ensure my JSON API is functioning

  Scenario: Basic healthcheck              # features/healthcheck.feature:8
    Given I request "/ping" using HTTP GET # Imbo\BehatApiExtension\Context\ApiContext::requestPath()
    Then the response code is 200          # Imbo\BehatApiExtension\Context\ApiContext::assertResponseCodeIs()
    And the response body is:              # Imbo\BehatApiExtension\Context\ApiContext::assertResponseBodyIs()
      """
      "pong"
      """

1 scenario (1 passed)
3 steps (3 passed)
0m0.06s (9.59Mb)

In this example we've made use of Manual Route Definitions.

If we follow a set of naming conventions, FOSRESTBundle can take care of generating routes for us automatically. Depending on the complexity of our entities / resources, this will either be quite straightforward, or a little more complex.

Personally I like the flexibility of the manual route definitions.

Because the FOSRESTBundle routing annotations are extended / extends from Sensio\Bundle\FrameworkExtraBundle\Configuration\Route, we needed to composer require annotations in the previous video. If you stick to the FOSRESTBundle automated routes, you can skip this dependency.

Ok, that's our basic healthcheck out of the way. Onwards to the creation / POST of our Albums.

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