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.

ApiProblemException and Exception Handling

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

ApiProblemException and Exception Handling

In order to be able to throw an exception that results in a JSON response, we need to hit the gym and first create a new class called ApiProblemException. Make it extend that special HttpException class:

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

use Symfony\Component\HttpKernel\Exception\HttpException;

class ApiProblemException extends HttpException
{
}

The purpose of this class is to act like a normal exception, but also to hold the ApiProblem inside of it. To do this, add an $apiProblem property and override the __construct method so that an ApiProblem object is the first argument:

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

use Symfony\Component\HttpKernel\Exception\HttpException;

class ApiProblemException extends HttpException
{
    private $apiProblem;

    public function __construct(ApiProblem $apiProblem, \Exception $previous = null, array $headers = array(), $code = 0)
    {
        $this->apiProblem = $apiProblem;

        parent::__construct(
            $apiProblem->getStatusCode(),
            $apiProblem->getTitle(),
            $previous,
            $headers,
            $code
        );
    }
}

The exception still needs a message and I’m calling getTitle() on the ApiProblem object to get it. Open up the ApiProblem class and add this getTitle() function so we can access it:

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

class ApiProblem
{
    // ...

    public function getTitle()
    {
        return $this->title;
    }
}

Finally, go back to ApiProblemException and add a getApiProblem getter function. Hang tight, we’ll use this in a few minutes:

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

use Symfony\Component\HttpKernel\Exception\HttpException;

class ApiProblemException extends HttpException
{
    // ...

    public function getApiProblem()
    {
        return $this->apiProblem;
    }
}

Back in the controller, throw a new ApiProblemException and pass the ApiProblem object into it:

// src/KnpU/CodeBattle/Controller/Api/ProgrammerController.php
// ...
use KnpU\CodeBattle\Api\ApiProblemException;
// ...

private function handleRequest(Request $request, Programmer $programmer)
{
    // ...

    if ($data === null) {
        $problem = new ApiProblem(
            400,
            ApiProblem::TYPE_INVALID_REQUEST_BODY_FORMAT
        );

        throw new ApiProblemException($problem);
    }

    // ...
}

Exception Listener

If we run the tests now, they still fail. But notice that the status code is still 400. Our new exception class extends HttpException, so we really have the same behavior as before.

When an exception is thrown anywhere in our app, Silex catches it and gives us an opportunity to process it. In fact, this is true in just about every framework. So if you’re not using Silex, just find out how to extend the exception handling in your framework and repeat what we’re doing here.

Open up the Application.php class in the src/KnpU/CodeBattle/ directory. This is the heart of my application, but you don’t need to worry about it too much. At the bottom of the class, I’ve created a configureListeners function. By calling $this->error, we can pass it an anonymous function that will be called whenever there is an exception anywhere in our app. Add a debug statement so we can test it:

// src/KnpU/CodeBattle/Application.php
// ...

private function configureListeners()
{
    $this->error(function() {
        die('hallo!');
    });
}

To try it out, just open up the app in your browser and go to any 404 page, since a 404 is a type of exception:

http://localhost:8000/foo/bar

Awesome! We see the die code.

Filling in the Exception Listener

When Silex calls the function, it passes it 2 arguments: the exception that was thrown and the status code we should use:

// src/KnpU/CodeBattle/Application.php
// ...

private function configureListeners()
{
    $this->error(function(\Exception $e, $statusCode) {
        die('hallo!');
    });
}

Tip

Silex passes a $statusCode argument, which is equal to the status code of the HttpException object that was thrown. If some other type of exception was thrown, it will equal 500.

Here’s the cool part: if the exception is an ApiProblemException, then we can get the embedded ApiProblem object and use it to create the proper JsonResponse.

Let’s first check for this - if it’s not an ApiProblemException, we won’t do any special processing. And if it is, we’ll create the JsonResponse just like we might normally do in a controller:

// src/KnpU/CodeBattle/Application.php
// ...

private function configureListeners()
{
    $this->error(function(\Exception $e, $statusCode) {
        // only do something special if we have an ApiProblemException!
        if (!$e instanceof ApiProblemException) {
            return;
        }

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

        return $response;
    });
}

That’s it! If we throw an ApiProblemException, this function will transform it into the JsonResponse we want. Don’t believe me? Try running the tests now:

ApiProblemException for Validation

This is really powerful. If we need to return a “problem” anywhere in our API, we only need to create an ApiProblem object and throw an ApiProblemException.

Let’s take advantage of this for our validation errors. Find handleValidationResponse and throw a new ApiProblemException instead of creating and returning a JsonResponse object. And to keep things clear, let’s also rename this function to throwApiProblemValidationException:

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

private function throwApiProblemValidationException(array $errors)
{
    $apiProblem = new ApiProblem(
        400,
        ApiProblem::TYPE_VALIDATION_ERROR
    );
    $apiProblem->set('errors', $errors);

    throw new ApiProblemException($apiProblem);
}

Now, update newAction and updateAction to use the new function name. We can also remove the return statements from each: we don’t need that anymore:

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

// newAction() and updateAction()
if ($errors = $this->validate($programmer)) {
    $this->throwApiProblemValidationException($errors);
}

And when we run the tests, all green! Piece by piece, we’re making our code more consistent so that we can guarantee that our API is consistent.

Leave a comment!

11
Login or Register to join the conversation
Default user avatar
Default user avatar Neandher Carlos | posted 5 years ago | edited

Hi.

I think it's missing a status code in the new instance of the class ApiProblem in throwApiProblemValidationException function:


$apiProblem = new ApiProblem(
    400, // <= <= <= is missing in throwApiProblemValidationException
    ApiProblem::TYPE_VALIDATION_ERROR
);

Now yes: all green!

:)

Reply

I'm watching again this wonderful tutorial, while trying to implement the same functions in Symfony: which is the equivalent of $this->error? I thought it was an event listener for "event.exception", but the event does not contain the status code... Any suggestion?

Reply

Hi Pietrino!

You're basically right that $this->error() is equivalent to registering a listener on the "kernel.exception" even (you said, "event.exception", but I know what you meant!). But in Symfony, you are passed a "GetResponseForExceptionEvent" event object to your method. The following code will get you what you need:


public function onKernelException(GetResponseForExceptionEvent $e)
{
   $exception = $e->getException();
   $code = $exception instanceof HttpExceptionInterface ? $exception->getStatusCode() : 500;
  // now go crazy! Create the response!

    $e->getResponse($response);
}

Silex basically does this to get the status code for you and then calls your callback that you passed to error().

Cheers!

1 Reply

Thanks, it worked like a charm! :)

Reply
Default user avatar

What was the reasoning behind creating two separate classes: ApiProblem and ApiProblemException? We could combine them and I don't think that would break principles like SRP.

Reply

I can't immediately think of a problem with this. If you try it out, let me know if you run into anything ugly :). The reason was just a natural evolution: I wanted to model the ApiProblem first... and then later, I was worrying and handling exceptions. That's why I think you might be right that combining these won't be an issue.

Cheers!

1 Reply
Default user avatar
Default user avatar Johan | weaverryan | posted 5 years ago | edited

What I did now is the following: I created a directory called 'Exception' in my AppBundle and created an interface called 'Exception'. This interface has one method: buildResponse. Then I created the ApiProblemException (https://gist.github.com/thejager/e2b26505aa4d2bb0b266887541f2351b ) and made it implement that interface. It basically has the ApiProblem build into it and the buildResponse method, well..., builds the response ;).

Then my ExceptionListener (I'm using Symfony 3) looks like this:



    public function onKernelException(GetResponseForExceptionEvent $event)
    {
        $exception = $event->getException();

        if($exception instanceof \AppBundle\Exception\Exception) {
            $event->setResponse($exception->buildResponse());
        }
    }

Now if I need a new exception type, for some reason, with some crazy custom Response I can just create a new exception class and make it implement the Exception interface. No need to mess with the ExceptionListener. I think it's cool but only time will tell if it actually works well ;)

1 Reply

Don't worry - rescued the message :). I have no idea why Disqus occasionally marks things as spam!

And yep, I like this approach - the approach if creating an interface for common exceptions (this is especially cool in a library - where that library can have every exception it throws implement some base interface). So, I like it!

Cheers!

1 Reply
Default user avatar

Cool, thanks. I will do that :)

Reply
Default user avatar

Hey I don't know if you can have anything control over it but my last comment was "Detected as spam". :(

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