If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
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!
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 | |
} | |
} |
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.
"Houston: no signs of life"
Start the conversation!
// 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
}
}