Buy

The ApiProblem object knows everything about how the response should look, including the status code and the response body information. So, it'd be great to have an easy way to convert this into a Response.

But, I want to go further. Sometimes, having a Response isn't enough. Like in processForm(): since nothing uses its return value. So the only way to break the flow is by throwing an exception.

Here's the goal: create a special exception class, pass it the ApiProblem object, and then have some central layer convert that into our beautiful API problem JSON formatted response. So whenever something goes wrong, we'll just need to create the ApiProblem object and then throw this special exception. That'll be it, in any situation.

Create the ApiProblemException

In the Api directory, create a new class called ApiProblemException. Make this extend HttpException - because I like that ability to set the status code on this:

10 lines src/AppBundle/Api/ApiProblemException.php
... lines 1 - 2
namespace AppBundle\Api;
use Symfony\Component\HttpKernel\Exception\HttpException;
class ApiProblemException extends HttpException
{
}

Next, we need to be able to attach an ApiProblem object to this exception class, so that we have access to it later when we handle all of this. Let's pass this via the constructor. Use cmd+n - or go to the "Generate" menu at the top - and override the __construct method. Now, add ApiProblem $apiProblem as the first argument. Also create an $apiProblem property and set this there:

18 lines src/AppBundle/Api/ApiProblemException.php
... lines 1 - 6
class ApiProblemException extends HttpException
{
private $apiProblem;
public function __construct(ApiProblem $apiProblem, $statusCode, $message = null, \Exception $previous = null, array $headers = array(), $code = 0)
{
$this->apiProblem = $apiProblem;
parent::__construct($statusCode, $message, $previous, $headers, $code);
}
}

This won't do anything special yet: this is still just an HttpException that happens to have an ApiProblem attached to it.

Back in ProgrammerController, we can start using this. Throw a new ApiProblemException. Pass it $apiProblem as the first argument and 400 next:

193 lines src/AppBundle/Controller/Api/ProgrammerController.php
... lines 1 - 142
private function processForm(Request $request, FormInterface $form)
{
$data = json_decode($request->getContent(), true);
if ($data === null) {
$apiProblem = new ApiProblem(400, ApiProblem::TYPE_INVALID_REQUEST_BODY_FORMAT);
throw new ApiProblemException(
$apiProblem,
400
);
}
... lines 154 - 156
}
... lines 158 - 193

Run the test:

./bin/phpunit -c app --filter testInvalidJson

It still acts like before: with a 400 status code, and now an exception with no message.

Simplifying the ApiProblemException Constructor

Before we handle this, we can make one minor improvement. Remove the $statusCode and $message arguments because we can get those from the ApiProblem itself. Replace that with $statusCode = $apiProblem->getStatusCode(). And I just realized I messed up my first line - make sure you have $this->apiProblem = $apiProblem. Also add $message = $apiProblem->getTitle():

20 lines src/AppBundle/Api/ApiProblemException.php
... lines 1 - 6
class ApiProblemException extends HttpException
{
... lines 9 - 10
public function __construct(ApiProblem $apiProblem, \Exception $previous = null, array $headers = array(), $code = 0)
{
$this->apiProblem = $apiProblem;
$statusCode = $apiProblem->getStatusCode();
$message = $apiProblem->getTitle();
parent::__construct($statusCode, $message, $previous, $headers, $code);
}
}

Hey wait! ApiProblem doesn't have a getTitle() method yet. Ok, let's go add one. Use the Generate menu again, select "Getters" and choose title:

65 lines src/AppBundle/Api/ApiProblem.php
... lines 1 - 7
class ApiProblem
{
... lines 10 - 59
public function getTitle()
{
return $this->title;
}
}

In ProgrammerController, simplify this:

190 lines src/AppBundle/Controller/Api/ProgrammerController.php
... lines 1 - 142
private function processForm(Request $request, FormInterface $form)
{
$data = json_decode($request->getContent(), true);
if ($data === null) {
$apiProblem = new ApiProblem(400, ApiProblem::TYPE_INVALID_REQUEST_BODY_FORMAT);
throw new ApiProblemException($apiProblem);
}
... lines 151 - 153
}
... lines 155 - 190

It'll figure out the status code and message for us.

./bin/phpunit -c app --filter testInvalidJson

The exception class is perfect - we just need to add that central layer that'll convert this into the beautiful API Problem JSON response. Instead of this HTML stuff.

Leave a comment!

  • 2018-04-25 Diego Aguiar

    I would not recommend removing the type-hint, that's how Symfony detects what to inject into your services.

    Cheers!

  • 2018-04-25 Chris

    Hi,

    +1 for this explanation and solution.

    This happened to me, as well. Since I do not have sufficient time to watch your new S4 courses, I'm stucked for the time being at Symfony 3.3 :).

    I also received the following error message.

    When I write


    public function __construct($apiProblem, \Exception $previous = null, array $headers = array(), $code = 0) {
    .....

    instead of


    public function __construct(ApiProblem $apiProblem, \Exception $previous = null, array $headers = array(), $code = 0) {
    .....

    by missing out the ApiProblem typehint in the constructor of ApiProblemException.php, the error is gone :-)

    But I suppose your solution is a bit cleaner.

  • 2017-11-30 julien moulis

    Ok it’s clear. What’s terrible, it’s that I followed the 3.3 services tutorial... Well I guess I got to back on it...

  • 2017-11-21 weaverryan

    Hey julien moulis!

    Well dang :). But, you piqued my curiosity for sure! And I AM able to replicate this :). So, let's talk about a few things:

    A) First, the fix: any workaround that doesn't feel like a hack is fine :). This will make sense when I explain more :). Technically the best fix would be to add one more excludes to your service auto-registration:


    # ...
    - exclude: '../../src/AppBundle/{Entity,Repository,Tests}'
    + exclude: '../../src/AppBundle/{Entity,Repository,Tests,Api/ApiProblemException.php}'

    B) So... what the heck is going on? In Symfony 3.3 and lower, there is a bunch of extra magic in autowiring. And actually, it's the \Exception argument in ApiProblemException (+ some other bad luck) which causes the crazy error. In Symfony 4, this extra magic is *completely* removed. In other words, this is kind of a Symfony bug, and it's gone in Symfony 4. In Symfony 3.4, you can opt into the proper, 4.0 behavior by adding this flag: https://github.com/symfony/...

    If you want to nerd-out further, here's the full explanation:

    1) All your classes in src/ are loaded as services. Some don't need to be services (like ApiProblemExeption), but that's ok - as long as you never try to use them as services, they're discarded.

    2) But, Symfony still tries to figure out how to autowiring ApiProblemException. When it sees the \Exception type-hint, in Symfony 3.3 (this is the bad behavior), it looks through ALL the services on the container to see if any extend \Exception. And guess what!? Exactly one does: ApiProblemException itself! So, it trie to autowire itself, into itself :).

    I hope that helps!

  • 2017-11-21 julien moulis

    Hello @weaverryan,
    Well... I guess it's probably because I mixed up different version of the code. Because, nos it works... Without doin' anything except going further in the tutos... Sorry
    Thanks

  • 2017-11-20 weaverryan

    Yo julien moulis!

    Ok, I have a theory :). Your ApiProblemException constructor should look like this:


    public function __construct(ApiProblem $apiProblem, $statusCode, ...)

    Is it possible that your's looks like this by mistake?


    public function __construct(ApiProblemException $apiProblem, $statusCode, ...)

    Let me know! If my theory is wrong, then I want to look further :).

    Cheers!

  • 2017-11-17 julien moulis

    Ok I added ApiProblemExceptionClass to service.yml


    api_exception:
    class: AppBundle\Api\ApiProblemException
    public: false


    And it seems to work

  • 2017-11-17 julien moulis

    Hey Diego Aguiar Thanks for reply.
    I'm using Symfony 3.3.13.
    Actually if you add $statusCode as attributes in the ApiProblemException constructor, there is no circular error.

    Here is my service.yml (it's the standard one), nothing fancy.


    parameters:
    #parameter_name: value

    services:
    _defaults:
    autowire: true
    autoconfigure: true
    public: false
    AppBundle\:
    resource: '../../src/AppBundle/*'
    exclude: '../../src/AppBundle/{Entity,Repository,Tests}'

    AppBundle\Controller\:
    resource: '../../src/AppBundle/Controller'
    public: true
    tags: ['controller.service_arguments']

    Here is the full error:
    (1/1) ServiceCircularReferenceException
    Circular reference detected for service "AppBundle\Api\ApiProblemException", path: "AppBundle\Api\ApiProblemException -> AppBundle\Api\ApiProblemException".
    in CheckCircularReferencesPass.php (line 67)
    at CheckCircularReferencesPass->checkOutEdges(array(object(ServiceReferenceGraphEdge), object(ServiceReferenceGraphEdge)))
    in CheckCircularReferencesPass.php (line 43)
    at CheckCircularReferencesPass->process(object(ContainerBuilder))
    in Compiler.php (line 141)
    at Compiler->compile(object(ContainerBuilder))
    in ContainerBuilder.php (line 731)
    at ContainerBuilder->compile()
    in Kernel.php (line 573)
    at Kernel->initializeContainer()
    in Kernel.php (line 117)
    at Kernel->boot()
    in Kernel.php (line 166)
    at Kernel->handle(object(Request))
    in app_dev.php (line 29)
    at require('/var/www/html/webagence/web/app_dev.php')
    in router.php (line 42)

  • 2017-11-17 Diego Aguiar

    Hey julien moulis

    This is strange, I believe this error happens when a service (lets call it A) requires service B, but service B requires service A aswell.
    Can you tell me which version of Symfony are you using?
    Try clearing the cache or running "composer update", if that doesn't fix it, show me the full error message and if possible your services.yml

    Cheers!

  • 2017-11-17 julien moulis

    Hi everyone,
    At the end of the video I have this error on my code
    (1/1) ServiceCircularReferenceException
    Circular reference detected for service "AppBundle\Api\ApiProblemException", path: "AppBundle\Api\ApiProblemException -> AppBundle\Api\ApiProblemException"..

    I tried few things, it seems it has something to do with the $statusCode from the ApiProblemException.

    Any idea?

    Thanks,
    Julien