Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

ApiProblemException

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 $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

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:

... 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:

... 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:

... 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():

... 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:

... lines 1 - 7
class ApiProblem
{
... lines 10 - 59
public function getTitle()
{
return $this->title;
}
}

In ProgrammerController, simplify this:

... 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!

10
Login or Register to join the conversation
Default user avatar
Default user avatar julien moulis | posted 5 years ago

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

Reply

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!

Reply
Default user avatar
Default user avatar julien moulis | MolloKhan | posted 5 years ago | edited

Hey MolloKhan 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)

Reply

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!

Reply
Default user avatar
Default user avatar julien moulis | weaverryan | posted 5 years ago

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

Reply

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/symfony-standard/blob/3.4/app/AppKernel.php#L54

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!

Reply
Default user avatar
Default user avatar julien moulis | weaverryan | posted 5 years ago

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...

Reply
Default user avatar
Default user avatar Chris | weaverryan | posted 5 years ago | edited

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.

Reply

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

Cheers!

Reply
Default user avatar
Default user avatar julien moulis | MolloKhan | posted 5 years ago | edited

Ok I added ApiProblemExceptionClass to service.yml


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

And it seems to work

Reply
Cat in space

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

This tutorial uses an older version of Symfony. The concepts of REST and errors are still valid, but I recommend using API Platform in new Symfony apps.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.3.3",
        "symfony/symfony": "2.6.*", // v2.6.11
        "doctrine/orm": "~2.2,>=2.2.3,<2.5", // v2.4.7
        "doctrine/dbal": "<2.5", // v2.4.4
        "doctrine/doctrine-bundle": "~1.2", // v1.4.0
        "twig/extensions": "~1.0", // v1.2.0
        "symfony/assetic-bundle": "~2.3", // v2.6.1
        "symfony/swiftmailer-bundle": "~2.3", // v2.3.8
        "symfony/monolog-bundle": "~2.4", // v2.7.1
        "sensio/distribution-bundle": "~3.0,>=3.0.12", // v3.0.21
        "sensio/framework-extra-bundle": "~3.0,>=3.0.2", // v3.0.7
        "incenteev/composer-parameter-handler": "~2.0", // v2.1.0
        "hautelook/alice-bundle": "0.2.*", // 0.2
        "jms/serializer-bundle": "0.13.*" // 0.13.0
    },
    "require-dev": {
        "sensio/generator-bundle": "~2.3", // v2.5.3
        "behat/behat": "~3.0", // v3.0.15
        "behat/mink-extension": "~2.0.1", // v2.0.1
        "behat/mink-goutte-driver": "~1.1.0", // v1.1.0
        "behat/mink-selenium2-driver": "~1.2.0", // v1.2.0
        "phpunit/phpunit": "~4.6.0" // 4.6.4
    }
}
userVoice