Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This course is archived!
This tutorial uses a deprecated micro-framework called Silex. The fundamentals of REST are still ? valid, but the code we use can't be used in a real application.

Enforcing Consistency with ApiProblem

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

Enforcing Consistency with ApiProblem

We’ll be returning a lot of application/problem+json responses, like for validation errors, 404 pages and really any error response.

And of course, I want us to always be consistent. To make this really easy, why not create a new ApiProblem class that holds all the fields we need?

Start by creating a new Api directory and class called ApiProblem:

// src/KnpU/CodeBattle/Api/ApiProblem.php
namespace KnpU\CodeBattle\Api;

class ApiProblem
{
}

Note

The Apigility project has a similar class, which I liked and stole the basic idea :).

By looking at the spec, I’ve decided that I want my problem responses to always have status, type and title fields, so I’ll create these three properties and a __construct function that requires them. I’ll also create a getStatusCode function, which we’ll use in a moment:

// src/KnpU/CodeBattle/Api/ApiProblem.php
namespace KnpU\CodeBattle\Api;

class ApiProblem
{
    private $statusCode;

    private $type;

    private $title;

    public function __construct($statusCode, $type, $title)
    {
        $this->statusCode = $statusCode;
        $this->type = $type;
        $this->title = $title;
    }

    public function getStatusCode()
    {
        return $this->statusCode;
    }
}

Finally, since I’ll need the ability to add additional fields, let’s create an $extraData array property and a set function that can be used to populate it. We can use this to set the errors key when we’re creating a validation error response:

// src/KnpU/CodeBattle/Api/ApiProblem.php
namespace KnpU\CodeBattle\Api;

class ApiProblem
{
    // ...

    private $extraData = array();

    // ...

    public function set($name, $value)
    {
        $this->extraData[$name] = $value;
    }
}

Back in the controller, instead of creating an array, we can now create a new ApiProblem object and set the data on it. This helps us enforce the structure and avoid typos:

// src/KnpU/CodeBattle/Controller/Api/ProgrammerController.php
// ...

private function handleValidationResponse(array $errors)
{
    $apiProblem = new ApiProblem(
        400,
        'validation_error',
        'There was a validation error'
    );
    $apiProblem->set('errors', $errors);

    // ...
}

Now, if we could turn the ApiProblem into an array, then we could just pass it to the new JsonResponse and be done. To do that, add a new toArray function to ApiProblem. We need to include the type, title and status properties as well as any extra things we set on extraData:

// src/KnpU/CodeBattle/Api/ApiProblem.php
namespace KnpU\CodeBattle\Api;

class ApiProblem
{
    // ...

    public function toArray()
    {
        return array_merge(
            $this->extraData,
            [
                'status' => $this->statusCode,
                'type' => $this->type,
                'title' => $this->title,
            ]
        );
    }
}

Cool! Use it and the getStatusCode function to create the JsonResponse:

// src/KnpU/CodeBattle/Controller/Api/ProgrammerController.php
// ...

private function handleValidationResponse(array $errors)
{
    // ...
    $apiProblem->set('errors', $errors);

    $response = new JsonResponse(
        $apiProblem->toArray(),
        $apiProblem->getStatusCode()
    );
    $response->headers->set('Content-Type', 'application/problem+json');

    return $response;
}

Ok! This step made no difference to our API externally, but gave us a solid class to use for errors. This will make our code more consistent and easy to read, especially since we’ll probably need to create problem responses in many places.

To try it out, just re-run the tests:

Now, just like each resource, our error responses have a PHP class that helps to model them. Very nice!

Constants: More Consistency

The type field is the unique identifier of an error, and we’re supposed to have documentation for each type. So it’s really important to keep track of these and never misspell them.

That sounds like a perfect use-case for constants! Add a constant on ApiProblem for the validation_error key:

// src/KnpU/CodeBattle/Api/ApiProblem.php
// ...

class ApiProblem
{
    const TYPE_VALIDATION_ERROR = 'validation_error';

    // ...
}

Now, just reference the constant when instantiating ApiProblem:

// src/KnpU/CodeBattle/Controller/Api/ProgrammerController.php
// ...

private function handleValidationResponse(array $errors)
{
    $apiProblem = new ApiProblem(
        400,
        ApiProblem::TYPE_VALIDATION_ERROR,
        'There was a validation error'
    );

    // ...
}

Awesomely enough that’s one less spot for me to screw up.

Mapping title to type

But we can go further. According to the spec, the title field is the description of a given type. In other words, we should have the exact same title everywhere that we use the validation_error type.

To force this consistency, create an array map on ApiProblem from type to its human-description:

// src/KnpU/CodeBattle/Api/ApiProblem.php
// ...

class ApiProblem
{
    const TYPE_VALIDATION_ERROR = 'validation_error';

    static private $titles = array(
        self::TYPE_VALIDATION_ERROR => 'There was a validation error'
    );

    // ...
}

Note

You can also choose to translate the title. If you need this, you’ll need to run the key through your translator before returning it.

And instead of passing the $title as the third argument to the constructor, we can just look it up by the $type. And like the good programmers we are, we’ll throw a huge, ugly and descriptive exception if we don’t find a title:

// src/KnpU/CodeBattle/Api/ApiProblem.php
// ...

class ApiProblem
{
    // ...

    public function __construct($statusCode, $type)
    {
        $this->statusCode = $statusCode;
        $this->type = $type;

        if (!isset(self::$titles[$type])) {
            throw new \InvalidArgumentException('No title for type '.$type);
        }

        $this->title = self::$titles[$type];
    }
}

Back in the controller, we can now safely remove the last argument when constructing the ApiProblem object:

// src/KnpU/CodeBattle/Controller/Api/ProgrammerController.php
// ...

private function handleValidationResponse(array $errors)
{
    $apiProblem = new ApiProblem(
        400,
        ApiProblem::TYPE_VALIDATION_ERROR
    );

    // ...
}

Bam! We have an ApiProblem class to keep things consistent, a constant for the one problem type we have so far, and a title that’s automatically chosen from the type.

Leave a comment!

5
Login or Register to join the conversation

Hello! Maybe more of an OOP question but, why did you set the errors separately rather than through the constructor, if they're all available at the same time?

Reply

Hey BL,

Well, it's not too much important, you really can pass those errors in the constructor. I think the reason behind of it is that doing it as a setter gives you a bit more flexibility. Except errors, you also may pass some other extra data if there're any, so this was just a "universal" way of working with extra data. I don't remember for sure, but probably later in this course or next courses it will be more explicit why this is done this way. But once again, you can do whatever you want, if it's more convenient for you to pass those errors in the constructor - why not? Especially if you *always* going to pass some errors :)

Cheers!

Reply

We also could to implement `JsonSerializable` interface by `ApiProblem` and then we could not call `toArray` method manually. All that we will be need is to just pass `$apiProblem` object to the `JsonResponse`

Reply

Yea, great idea! I haven't tried it, but you could in theory get even fancier by using some annotations to try to run the ApiProblem through the Json serializer. Might be overkill, but a cool thought :).

Reply

Sounds great! :) So far I have enough to use `JsonSerializable` interface, but now I want to deep into the `JMSSerializer` to discover more features with it, thanks.

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses a deprecated micro-framework called Silex. The fundamentals of REST are still ? valid, but the code we use can't be used in a real application.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "silex/silex": "~1.0", // v1.3.2
        "symfony/twig-bridge": "~2.1", // v2.7.3
        "symfony/security": "~2.4", // v2.7.3
        "doctrine/dbal": "^2.5.4", // v2.5.4
        "monolog/monolog": "~1.7.0", // 1.7.0
        "symfony/validator": "~2.4", // v2.7.3
        "symfony/expression-language": "~2.4" // v2.7.3
    },
    "require-dev": {
        "behat/mink": "~1.5", // v1.5.0
        "behat/mink-goutte-driver": "~1.0.9", // v1.0.9
        "behat/mink-selenium2-driver": "~1.1.1", // v1.1.1
        "behat/behat": "~2.5", // v2.5.5
        "behat/mink-extension": "~1.2.0", // v1.2.0
        "phpunit/phpunit": "~5.7.0", // 5.7.27
        "guzzle/guzzle": "~3.7" // v3.9.3
    }
}
userVoice