Bitbucket Webhooks in Pimcore
In this guide, I will show you how easy it is to implement Bitbucket Webhooks in Pimcore. As for the usefulness of this integration, I will leave that up to your own personal creativity and inventiveness. While these concepts are written specifically with Symfony and Pimcore in mind, it should be easy enough to adapt to other application stacks as well. Let’s dive in!
Building the Webhook Controller
namespace App\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Pimcore\Controller\FrontendController;
class WebhookController extends FrontendController {
}
We begin by creating a controller class in our application named WebhookController in the App\Controller namespace by extending Pimcore’s FrontendController class. Be sure to also include the 2 Symfony components Request, and Route.
Add a Webhook Route to Pimcore
/**
* @Route("/webhook", name="Webhook")
*/
public function WebhookAction( Request $request ) {
// Check for a recognized network...
if( !self::GetNetworkValid( $request ) ) {
return $this->json( [
'error' => 'Request made from an unrecognized network.',
] );
}
// Process the webhook event...
if( self::WebhookPush( $request ) ) {
return $this->json( [
'status' => 'ok',
] );
}
return $this->json( [
'error' => 'Could not process event.',
] );
}
}
We begin by utilizing Symfony’s Route annotation syntax, assigning the WebhookAction to the /webhook URL. I typically use <name>Action when building my routes, but this is not a hard-and-fast requirement. Inside this Action method, we make two calls to static methods inside our WebhookController class. These methods are stateless which allows them to be called statically (self::) rather than by context ($this->). This function returns one of three possible states, a failure state, a success state, and an undefined state, which will help Bitbucket logging inside its own dashboard to help give you better clarity on what problems occur if they do.
Securing Your App with Atlassian’s IP Allow List
use Symfony\Component\HttpFoundation\IpUtils;
...
private static function GetNetworkValid( Request $request ) {
// Retrieve the remote IP making the request...
$client_ip = $request->getClientIp();
// Retrieve networks from the network discovery URL...
$json = json_decode( file_get_contents( 'https://ip-ranges.atlassian.com/' ), true );
if( !$json || empty( $json[ 'items' ] ) ) return false;
// Compile a list cidr ranges...
$cidrs = [];
foreach( $json[ 'items' ] as $item ) {
$cidrs[] = $item[ 'cidr' ];
}
return IpUtils::checkIp( $client_ip, $cidrs );
}
...
Lets begin creating this method by adding another Symfony component to our controller, this time IpUtils. In the method’s signature, we ask for a Request reference when calls to it are made. This method’s responsibility is to extract the IP from the remote resource making the request, and then compare it against a live list of official IP ranges supplied by Atlassian, using the IpUtils library. While this isn’t necessary to get the application to work, this does tighten the security to only trusted IP address can make requests to the application.
Building a Push Logger
use Pimcore\Model\DataObject\WebhookPush;
...
private static function WebhookPush( Request $request ) {
// Decode the JSON payload...
if( $json = json_decode( $request->getContent(), true ) ) {
// Create a new ORM object...
$item = new WebhookPush();
// Set headers...
$item->setXEventKey( $request->headers->get( 'X-Event-Key' ) );
$item->setXHookUUID( $request->headers->get( 'X-Hook-UUID' ) );
$item->setXRequestUUID( $request->headers->get( 'X-Request-UUID' ) );
$item->setXAttemptNumber( $request->headers->get( 'X-Attempt-Number' ) );
// Set fields...
$item->setActor( json_encode( $json[ 'actor' ] ) );
$item->setRepository( json_encode( $json[ 'repository' ] ) );
$item->setPush( json_encode( $json[ 'push' ] ) );
// Store it!
$item->setParentId( 1 );
$item->save();
return true;
}
return false;
}
...
The WebhookPush static method will take the data sent by the request and store it inside a Pimcore DataObject. Without getting too far into how Pimcore implements it’s ORM, consider that we create a DataObject class named WebhookPush that is designed to store the information sent by Bitbucket’s webhook assertions. We extract this from the request headers and the JSON payload itself and then save it into Pimcore’s ORM via our WebhookPush DataObject class. From this point you can work with any of the pushed data as you see fit, whether it’s just logging it away or using it for some new tool you want to build.
Official Webhook Documentation:
https://support.atlassian.com/bitbucket-cloud/docs/manage-webhooks/
Event Types:
https://support.atlassian.com/bitbucket-cloud/docs/event-payloads/
I hope you’ve enjoyed following along this guide on how to integrate Bitbucket Webhooks in Pimcore. Happy coding!


