If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeThe 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.
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.
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.
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!
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)
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!
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
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!
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...
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.
I would not recommend removing the type-hint, that's how Symfony detects what to inject into your services.
Cheers!
Ok I added ApiProblemExceptionClass to service.yml
api_exception:
class: AppBundle\Api\ApiProblemException
public: false
And it seems to work
// 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
}
}
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