Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This course is archived!
This tutorial uses an older version of Symfony of the stripe-php SDK. The majority of the concepts are still valid, though there *are* differences. We've done our best to add notes & comments that describe these changes.

Webhook Endpoint Setup

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

Let's get right to work on our webhook endpoint. In the src/AppBundle/Controller directory, create a new WebhookController class. Make it extend a BaseController class I created - that just has a few small shortcuts:

... lines 1 - 2
namespace AppBundle\Controller;
... lines 4 - 8
class WebhookController extends BaseController
{
... lines 11 - 17
}

Now, create the endpoint with public function stripeWebhookAction(). Give an @Route annotation set to /webhooks/stripe and a similar name. Make sure you have the Route use statement:

... lines 1 - 4
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Request;
... lines 7 - 8
class WebhookController extends BaseController
{
/**
* @Route("/webhooks/stripe", name="webhook_stripe")
*/
public function stripeWebhookAction(Request $request)
{
... line 16
}
}

Start simple: return a new Response() from the HttpFoundation component:

... lines 1 - 6
use Symfony\Component\HttpFoundation\Response;
... line 8
class WebhookController extends BaseController
{
... lines 11 - 13
public function stripeWebhookAction(Request $request)
{
return new Response('baaaaaa');
}
}

That's just enough to try it out: find your browser and go to /webhooks/stripe. It's alive!

Decoding the Event

Thanks to RequestBin, we know more or less what the JSON body will look like. The most important thing is this event id. Let's decode the JSON and grab this.

To do that, add $data = json_decode(), but pause there. We need to pass this the body of the Request. In Symfony, we get this by adding a Request argument - don't forget the use statement! Then, use $request->getContent(). Also, pass true as the second argument so that json_decode returns an associative array:

... lines 1 - 5
use Symfony\Component\HttpFoundation\Request;
... lines 7 - 8
class WebhookController extends BaseController
{
... lines 11 - 13
public function stripeWebhookAction(Request $request)
{
$data = json_decode($request->getContent(), true);
... lines 17 - 34
}
}

Next, it shouldn't happen, but just in case, if $data is null, that means Stripe sent us invalid JSON. Shame on you Stripe! Throw an exception in this case... and make sure you spell Exception correctly!

... lines 1 - 5
use Symfony\Component\HttpFoundation\Request;
... lines 7 - 8
class WebhookController extends BaseController
{
... lines 11 - 13
public function stripeWebhookAction(Request $request)
{
$data = json_decode($request->getContent(), true);
if ($data === null) {
throw new \Exception('Bad JSON body from Stripe!');
}
... lines 20 - 34
}
}

Finally, get the $eventId from $data['id']:

... lines 1 - 5
use Symfony\Component\HttpFoundation\Request;
... lines 7 - 8
class WebhookController extends BaseController
{
... lines 11 - 13
public function stripeWebhookAction(Request $request)
{
$data = json_decode($request->getContent(), true);
if ($data === null) {
throw new \Exception('Bad JSON body from Stripe!');
}
$eventId = $data['id'];
... lines 22 - 34
}
}

We Found the Event! Now, Fetch the Event?!

Ok, let's refocus on the next steps. Ultimately, I want to read these fields in the event, find the Subscription in the database, and cancel it. But instead of reading the JSON body directly, we're going to use Stripe's API to fetch the Event object by using this $eventId.

Wait, but won't that just return the exact same data we already have? Yes! We do this not because we need to, but for security. If we read the request JSON directly, it's possible that the request is coming from some external, mean-spirited person instead of from Stripe. By fetching a fresh event from Stripe, it guarantees the event data is legitimate.

Since we make all API requests through the StripeClient class, open it up and scroll to the bottom. Add a new public function called findEvent() with an $eventId argument. Inside, just return \Stripe\Event::retrieve() and pass it $eventId:

... lines 1 - 8
class StripeClient
{
... lines 11 - 117
/**
* @param $eventId
* @return \Stripe\Event
*/
public function findEvent($eventId)
{
return \Stripe\Event::retrieve($eventId);
}
}

Back in the controller, add $stripeEvent = $this->get('stripe_client')->findEvent($eventId):

... lines 1 - 8
class WebhookController extends BaseController
{
... lines 11 - 13
public function stripeWebhookAction(Request $request)
{
... lines 16 - 20
$eventId = $data['id'];
$stripeEvent = $this->get('stripe_client')
->findEvent($eventId);
... lines 25 - 34
}
}

If this were an invalid event ID, Stripe would throw an exception.

With that, we're prepped to handle some event types.

Leave a comment!

0
Login or Register to join the conversation
Cat in space

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

This tutorial uses an older version of Symfony of the stripe-php SDK. The majority of the concepts are still valid, though there *are* differences. We've done our best to add notes & comments that describe these changes.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9, <7.4",
        "symfony/symfony": "3.1.*", // v3.1.10
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.8
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.6.2
        "symfony/monolog-bundle": "^2.8", // v2.12.1
        "symfony/polyfill-apcu": "^1.0", // v1.3.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.26
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "friendsofsymfony/user-bundle": "~2.0.1", // v2.0.1
        "stof/doctrine-extensions-bundle": "^1.2", // v1.2.2
        "stripe/stripe-php": "^3.15", // v3.23.0
        "doctrine/doctrine-migrations-bundle": "^1.1", // v1.2.1
        "phpunit/phpunit": "^5.5", // 5.7.20
        "composer/package-versions-deprecated": "^1.11" // 1.11.99
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.4
        "symfony/phpunit-bridge": "^3.0", // v3.3.0
        "hautelook/alice-bundle": "^1.3", // v1.4.1
        "doctrine/data-fixtures": "^1.2" // v1.2.2
    }
}
userVoice