Magento 2 Webhook Notifications

web

Webhooks are used to indicate when an important event has occurred, usually by sending a notification in the form of a message to a specified webhook URL or endpoint. This can be really useful for certain events, like when a customer makes an order or leaves a comment.

It can also be a really handy way for developers to get instantly notified when an exception occurs, providing them with the information they need to quickly locate and fix the issue.

In this article, I will show you how to create a module that allows you to configure your very own webhooks for Magento 2, utilizing the Incoming Webhooks feature from Slack.

The Module

Let’s get started by creating a module in the /app/code directory of our Magento 2 module. In this example, I am going to use Inchoo as the module vendor and I am going to name the module Webhooks. You can name them as you wish, cause that’s your business.

We are going to need the following directories and files:

  • registration.php
  • /etc/module.xml
  • /etc/adminhtml/system.xml
  • /Model/Helper/Data.php
  • /Model/Webhook.php

registration.php

Register the module in the Magento system. Copy the following code to the registration.php file:

<?php
 
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Inchoo_Webhooks',
    __DIR__
);

module.xml

Declare the module name and existence. Copy the following code to the /etc/module.xml file:

<?xml version="1.0"?>
 
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Inchoo_Webhooks" setup_version="1.0.0">
    </module>
</config>

The Configuration

After setting up the module, we need to create the admin configuration for our webhooks.

Copy the following code to the /etc/adminhtml/system.xml file:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
    <system>
        <section id="system">
            <group id="inchoo_webhooks" sortOrder="999" translate="label comment" showInDefault="1" showInWebsite="0" showInStore="0">
                <label>Webhook Notifications</label>
                <comment>
                    <![CDATA[To create Slack Incoming webhooks, visit <a href="https://api.slack.com/incoming-webhooks">https://api.slack.com/incoming-webhooks</a>]]>
                </comment>
 
                <field id="enable_webhooks" translate="label comment" type="select" sortOrder="5" showInDefault="1" showInWebsite="0" showInStore="0">
                    <label>Enabled</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
 
                <field id="incoming_webhook_url" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0">
                    <label>Incoming Webhook URL</label>
                    <depends>
                        <field id="enable_webhooks">1</field>
                    </depends>
                </field>
 
                <field id="webhook_store_label" translate="label comment" type="text" sortOrder="15" showInDefault="1" showInWebsite="0" showInStore="0">
                    <label>Store Label</label>
                    <depends>
                        <field id="enable_webhooks">1</field>
                    </depends>
                </field>
 
                <field id="webhook_message_prefix" translate="label comment" type="text" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0">
                    <label>Message Prefix</label>
                    <depends>
                        <field id="enable_webhooks">1</field>
                    </depends>
                </field>
 
                <field id="enable_stack_trace" translate="label comment" type="select" sortOrder="25" showInDefault="1" showInWebsite="0" showInStore="0">
                    <label>Stack Trace</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                    <depends>
                        <field id="enable_webhooks">1</field>
                    </depends>
                </field>
            </group>
        </section>
    </system>
</config>

What this does is it adds webhook configuration fields to the Magento 2 admin interface. These fields can be found under Stores -> Configuration -> Advanced -> Webhook Notifications and they should look like this:

  • Enabled: toggles the webhook functionality
  • Incoming Webhook URL: a webhook URL provided by a configured Slack app
  • Store Label: the label that will be shown in the webhook message, it will contain the store URL
  • Message Prefix: text that will be placed before the webhook message, Slack emoji codes can be used here
  • Stack Trace: if enabled, webhook messages will show a stack trace after the provided message

To create your own Incoming Webhook URL, visit https://api.slack.com/messaging/webhooks and follow the steps there. Feel free to use any other service.

The Helper

The data entered in the configuration fields gets saved to the core_config_data table, and we need a way to retrieve that information. Copy the following code to the /Helper/Data.php file:

<?php
 
declare(strict_types=1);
 
namespace Inchoo\Webhooks\Helper;
 
use Magento\Framework\App\Helper\AbstractHelper;
use Magento\Framework\App\Helper\Context;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Store\Model\StoreManagerInterface;
 
/**
 * Class Data
 * @package Inchoo\Webhooks\Helper
 */
class Data extends AbstractHelper
{
    /**
     * @var StoreManagerInterface
     */
    private $storeManager;
 
    /**
     * Base path to Inchoo Webhooks configuration values
     */
    private const XML_PATH_WEBHOOKS = 'system/inchoo_webhooks';
    private const XML_WEBHOOKS_ENABLED = self::XML_PATH_WEBHOOKS . '/enable_webhooks';
    private const XML_WEBHOOKS_STACK_TRACE_ENABLED = self::XML_PATH_WEBHOOKS . '/enable_stack_trace';
    private const XML_WEBHOOKS_INCOMING_WEBHOOK_URL = self::XML_PATH_WEBHOOKS . '/incoming_webhook_url';
    private const XML_WEBHOOKS_MESSAGE_PREFIX = self::XML_PATH_WEBHOOKS . '/webhook_message_prefix';
    private const XML_WEBHOOKS_STORE_LABEL = self::XML_PATH_WEBHOOKS . '/webhook_store_label';
 
    /**
     * Data constructor.
     *
     * @param Context $context
     * @param ScopeConfigInterface $scopeConfig
     * @param StoreManagerInterface $storeManager
     */
    public function __construct(
        Context $context,
        ScopeConfigInterface $scopeConfig,
        StoreManagerInterface $storeManager
    ) {
        $this->scopeConfig = $scopeConfig;
        $this->storeManager = $storeManager;
        parent::__construct($context);
    }
 
    /**
     * @return bool
     */
    public function isEnabled(): bool
    {
        return $this->scopeConfig->isSetFlag(self::XML_WEBHOOKS_ENABLED);
    }
 
    /**
     * @return bool
     */
    public function isStackTraceEnabled(): bool
    {
        return $this->scopeConfig->isSetFlag(self::XML_WEBHOOKS_STACK_TRACE_ENABLED);
    }
 
    /**
     * @return mixed
     */
    public function getIncomingWebhookURL()
    {
        return $this->scopeConfig->getValue(self::XML_WEBHOOKS_INCOMING_WEBHOOK_URL);
    }
 
    /**
     * @return mixed
     */
    public function getWebhookMessagePrefix()
    {
        return $this->scopeConfig->getValue(self::XML_WEBHOOKS_MESSAGE_PREFIX);
    }
 
    /**
     * @return mixed
     */
    public function getStoreLabel()
    {
        return $this->scopeConfig->getValue(self::XML_WEBHOOKS_STORE_LABEL);
    }
 
    /**
     * @return string
     * @throws NoSuchEntityException
     */
    public function getStoreName(): string
    {
        return $this->storeManager->getStore()->getName();
    }
 
    /**
     * @return mixed
     * @throws NoSuchEntityException
     */
    public function getStoreUrl()
    {
        return $this->storeManager->getStore()->getBaseUrl();
    }
}

This class contains the paths to our saved values in the core_config_data

table and a bunch of public methods that we are going to use in our Webhook class to retrieve that data.

The Webhook

Now that our configuration is set up and we have our helper methods, we have to define the Webhook class which is going to hold the logic that processes all of the data and sends a message to a configured webhook URL.

Copy the following code to the /Model/Webhook.php file:

<?php
 
declare(strict_types=1);
 
namespace Inchoo\Webhooks\Model;
 
use Inchoo\Webhooks\Helper\Data;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\HTTP\Adapter\CurlFactory;
use Psr\Log\LoggerInterface;
 
/**
 * Class Webhook
 * @package Inchoo\Webhooks\Model
 */
class Webhook
{
    /**
     * @var Data
     */
    private $webHookHelper;
 
    /**
     * @var CurlFactory
     */
    private $curlFactory;
 
    /**
     * @var LoggerInterface
     */
    private $logger;
 
    /**
     * Webhook constructor.
     * @param Data $webHookHelper
     * @param CurlFactory $curlFactory
     * @param LoggerInterface $logger
     */
    public function __construct(
        Data $webHookHelper,
        CurlFactory $curlFactory,
        LoggerInterface $logger
    ) {
        $this->webHookHelper = $webHookHelper;
        $this->curlFactory = $curlFactory;
        $this->logger = $logger;
    }
 
    /**
     * @param string $message
     * @return void
     * @throws NoSuchEntityException
     */
    public function sendWebhook(string $message): void
    {
        if (!$this->webHookHelper->isEnabled()) {
            return;
        }
 
        $url = $this->webHookHelper->getIncomingWebhookURL();
        $messagePrefix = $this->webHookHelper->getWebhookMessagePrefix();
 
        $storeName = $this->webHookHelper->getStoreName();
        $storeUrl = $this->webHookHelper->getStoreUrl();
        $storeLabel = $this->webHookHelper->getStoreLabel();
 
        $message = $messagePrefix ? $messagePrefix . ' ' . $message : $message;
 
        $text = '<' . $storeUrl . '|' . $storeLabel . '> | ' . $storeName . ': ' . $message;
        $text = $this->webHookHelper->isStackTraceEnabled() ? $text . ' ' . $this->stackTrace() : $text;
 
        $header = ['Content-type: application/json'];
        $body = json_encode(["text" =>  $text]);
 
        $client = $this->curlFactory->create();
        $client->addOption(CURLOPT_CONNECTTIMEOUT, 2);
        $client->addOption(CURLOPT_TIMEOUT, 3);
        $client->write(\Zend_Http_Client::POST, $url, '1.1', $header, $body);
 
        $response = $client->read();
        $responseCode = \Zend_Http_Response::extractCode($response);
 
        if ($responseCode !== 200) {
            $this->logger->log(100, 'Failed to send a message to the following Webhook: ' . $url);
        }
 
        $client->close();
    }
 
    /**
     * @return string
     */
    private function stackTrace(): string
    {
        $stackTrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
        $trace = [];
        foreach ($stackTrace as $row => $data) {
            if (!array_key_exists('file', $data) || !array_key_exists('line', $data)) {
                $trace[] = "# <unknown>";
            } else {
                $trace[] = "#{$row} {$data['file']}:{$data['line']} -> {$data['function']}()";
            }
        }
 
        return '```' .  implode("\n", $trace) . '```';
    }
}

Let’s go through the code.

The Webhook class contains two methods, the public sendWebhook() method, and the private stackTrace() method.

The sendWebhook() method is where most of the magic happens. Only one argument can be passed to this method, and that is the message that we want to send through our webhook, in our case, to a Slack channel. For example, we can call this method in a catch block and the argument passed to it can be the caught exception message and status code.

In the method, we first check if webhooks are enabled in the admin configuration. If true, the method continues and retrieves the Incoming Webhook URL, message prefix, store name, store URL, and store label by using the methods defined in our Data.php helper.

After that, the message gets formatted to include the message prefix, store URL, store label, and store name. If Stack Trace is enabled in the configuration, it will be processed by the stackTrace() method and added to the final text.

The request header gets set as Content-type: application/json and the final text gets json encoded and set in the request body.

Since we are executing the webhook call synchronously, we create a Curl instance and set the connection attempt timeout to 2 seconds and execution time to 3 seconds as we don’t want the request to execute for too long. We then send the request through the webhook URL and log any failed attempts to send the message.

The End

To finish things up, invoke the bin/magento setup:upgrade command and configure the webhook through the admin interface. You can now inject the created Webhook class in any other custom code and call its sendWebhook() method.

A webhook notification with Stack Trace enabled should look something like this:

If you have any questions or issues, please don’t hesitate to leave a comment below.

You made it all the way down here so you must have enjoyed this post! You may also like:

How to programmatically create customers in Magento 2.3.x Deni Pesic
Deni Pesic, | 2

How to programmatically create customers in Magento 2.3.x

Development environment for Magento 2 using Docker Tomas Novoselic
, | 14

Development environment for Magento 2 using Docker

Create configuration for your Magento extension Mladen Lotar
Mladen Lotar, | 29

Create configuration for your Magento extension

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <blockquote cite=""> <code> <del datetime=""> <em> <s> <strike> <strong>. You may use following syntax for source code: <pre><code>$current = "Inchoo";</code></pre>.

Tell us about your project

Drop us a line. We'd love to know more about your project.