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

Debugging and Cleanup

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

We're finally to the exciting conclusion, just a few more small cleanup items that we need to take care of.

Starting with debugging. Let's look inside ProgrammerController. What happens if we mess something up? Like some exception gets thrown inside of newAction. To find out let's run just testPOST. As you can see we get a really nice response, but it contains absolutely no details about what went wrong inside of there. That's fine for clients but for debugging it's going to be a nightmare.

If we are in debug mode and the status code is 500, I would love for Symfony's normal exception handling to take over so we can see that big beautiful stacktrace.

In ApiExceptionSubscriber we'll need to figure out if we're in debug mode. The way to do that is to pass a $debug flag through the __construct method and create a property for it.

I just hit a shortcut called alt+enter. Go to initialize fields, select debug and hit ok. That's just a litte shortcut for PhpStorm to set that flag for me. Before we use that, go into services.yml and pass that value in.

The way to figure out if we're in debug mode is to use %kernel.debug% as an argument.

And if we are in debug mode and the status code is 500 we don't want our exception subscriber to do anything. So let's move the status code line up a little bit further, making sure it's after the line where we get the exception. The logic is as simple as if ($statusCode == 500 && $this->debug) then just return. Symfony's normal exception handling will take over from here.

Let's rerun testPOST and it should fail, but I'm hoping I can get some extra details. We get the JSON response still because I changed the request format but we also get the full long stack trace. That is looking really nice, so let's just go ahead and remove the exception.

Tip

There is one thing missing from our listener: logging! In your application, you should inject the logger service and log that an exception occurred. This is important so that you are aware of errors on production. The "finish" download code contains this change.

Thanks to Sylvain for pointing this out in the comments!

type is a URL

Onto the second thing we need to clean up! Inside the spec, under type it says that type should be an absolute URI, and if we put it in our browser, it should take us to the documentation for that. Right now, our types are just strings. We'll fix this in a future episode when we talk properly about documentation, but I at least want to make us kind of follow this rule.

In ApiExceptionSubscriber, instead of calling $apiProblem->toArray(), directly in the JSON response, let's put $data here and create a new $data variable that's set to that. We want to prefix the type key with a URL, except in the case of about:blank - because that's already a URL.

So let's add our if statement, if ($data['type'] != 'about:blank') then, $data['type'] = 'http://localhost:8000/docs/errors#'.$data['type']; which is just a fake URL for now. But you can get the idea of how we'll eventually put a real URL here to a page where people can look up what those error types actually mean. So that'll be kinda nice.

This stuff may have broken some tests, so let's rerun all of them! Ah yep, and one of them did fail. invalid_body_format failed because we're looking for this exact string and now it's at the end of a URL.

In your test, change assertResponsePropertyEquals to assertResponsePropertyContains which saves me from hardcoding my host name in there:

... lines 1 - 5
class ProgrammerControllerTest extends ApiTestCase
{
... lines 8 - 147
public function testInvalidJson()
{
... lines 150 - 162
$this->asserter()->assertResponsePropertyContains($response, 'type', 'invalid_body_format');
}
... lines 165 - 175
}

Copy just that test to our terminal and run it:

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

Perfect, back to green!

Fixing Web Errors

Okay, last thing we need to clean up. This site does have a web interface to it and right now, if I just invent a URL, on the web interface I get a JSON response. This makes sense because the subscriber has completely taken over the error handling for our site, even though, in realitym we only want this to handle errors for our API.

There are a couple of different ways to do this, but at least in our API, everything is under the URL /api. So fixing this is as simple as making our subscriber only do its magic when our URL starts with this. Let's do that!

First get the $request by saying $event->getRequest(). Then let's get our if statement in there. if (strpos()) and we'll look in the haystack which is $request->getPathInfo(), this is the full URL. For the needle use /api and if all of this !== 0, in other words, if the URL doesn't start exactly with /api, then let's just return:

... lines 1 - 12
class ApiExceptionSubscriber implements EventSubscriberInterface
{
... lines 15 - 21
public function onKernelException(GetResponseForExceptionEvent $event)
{
// only reply to /api URLs
if (strpos($event->getRequest()->getPathInfo(), '/api') !== 0) {
return;
}
... lines 28 - 70
}
... lines 72 - 78
}

Head back to the browser and refresh the page. Web interface errors restored!

Let's run the entire test suite to make sure we're done:

./bin/phpunit -c app

Look at that, this is a setup to be proud of.

In the next episode we're going to get back to work with pagination, filtering, and other tough but important things with API's.

Alright guys, see ya next time!

Leave a comment!

12
Login or Register to join the conversation
Mouad E. Avatar
Mouad E. Avatar Mouad E. | posted 3 years ago

Hi, in sf 4.4 i didn'd add api_exception_subscriber with it's argument and api.response_factory from rest4 and all working good, does they work by autowiring or am ruining how symfony services should work?

Reply

Hey Mouad,

Yes, it should work in 4.4. with thanks to the interface the subscriber implements. To be 100% sure it works - you can put a "die" statement somewhere in ApiExtensionSubscriber, e.g. in onKernelException and try to trigger that code. If you hit your "die" statement - then it 100% work :)

Cheers!

Reply

Thanks a lot! great one. I am anxiously waiting for the next episodes. Cheers!

Reply
Default user avatar
Default user avatar Sylvain Cyr | posted 5 years ago

Might only be me but I see an issue. In production, if you get an internal error 500, it will send a nice json response. But if you don't put a logger in there, yes the error is raised but never logged server side. So you're blind if there are issues in production

Reply

Actually, I think you're 100% correct about that. Exceptions are normally logged by Symfony, but this happens in the default ExceptionListener. By setting the response in *our* listener, we stop propagation and that other listener is never called (so the exception is never logged). You *should* log the exception inside of here :). I'll add a note about that!

Thanks for pointing this out!

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

Hi,
I run symfony 3 and I have a AutowiringFailedException syaing that $debug is missing type-hint or have a given a value explicitly
What can I do?

Reply

Yo julien moulis!

If you're going to use Symfony 3.3 with the new service configuration stuff, then you'll need to change a few things. The most important one is that you'll need to use class names as the service ids. Let me explain (and I believe this is the solution to your problem). The tutorial has this:


    api_exception_subscriber:
        class: AppBundle\EventListener\ApiExceptionSubscriber
        arguments: ['%kernel.debug%', '@api.response_factory', '@logger']
        tags:
            - { name: kernel.event_subscriber }

But if you're using the Symfony 3.3 config, then there is a section above this (the section with resource) which auto-registers all services in your src/ directory. When it does this, the service ids use the class name. So, if you keep the above code, you're actually registering a second service with the container. This second service is configured correctly. But the first service (the one that's auto-registered) is missing its first argument ($debug).

So, change to use the class name as the service id:


    AppBundle\EventListener\ApiExceptionSubscriber:
        arguments: ['%kernel.debug%', '@api.response_factory', '@logger']
        tags:
            - { name: kernel.event_subscriber }

When you do this, you're overriding the auto-registered service and configuring it further. Now, we have just one service, and we ARE passing in the missing first argument. But actually, you can do even less work. You only need this:


    AppBundle\EventListener\ApiExceptionSubscriber:
        arguments:
            $debug: '%kernel.debug%'

Thanks to autowiring, all the other arguments of the constructor will still be autowired automatically. And thanks to autoconfigure, you actually don't need to specify the tag - it's added automatically.

I don't know if you've gone through it yet, but check out our Symfony 3.3 tutorial (https://knpuniversity.com/screencast/symfony-3.3) if you're interested in this stuff. There is an optional paradigm shift happening in Symfony right now that will feel a bit disruptive. But, the end result is awesome: you're going to working faster and writing more of your code, instead of going back to configuration files constantly.

Cheers!

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

This no longer appear to work (Symfony 4)?

I have put:


   App\EventListener\ApiExceptionSubscriber:
        arguments:
            $debug: '%kernel.debug%'

Yet still get this error:


    Cannot autowire service "App\EventListener\ApiExceptionSubscriber": argument "$debug" of method "__construct()" must have a type-hint or be given a value explicitly.
Reply

Hey Doug Hayward

Sorry for the late response! Do you still have this problem?

BTW, it should work, maybe you just have to clear the cache. If that's not the case, lets us know :)

Cheers!

Reply
Default user avatar
Default user avatar Doug Hayward | MolloKhan | posted 5 years ago | edited

I cured the error by setting the value default value in the constructor, I'm sure this is an incorrect way of fixing it but it worked for the time being:


    public function __construct($debug = true, ResponseFactory $responseFactory)
    {
        $this->$debug = $debug;
        $this->responseFactory = $responseFactory;
    }

Yes I cleared my cache no it didn't fix it.

Reply

Wow, that's strange. I'm going to investigate why this is happening, but if in the meantime you find the answer, please, let us know :)

Have a nice day

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

Thank you so much!!!!!

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