You too can become a hero and support this Blog at GitHub Sponsors or Patreon.

More about support

During the development of TYPO3 v12.1 the core got a new treat: webhook support.

To be a little bit more precise: the core now provides a basic structure to support creating custom endpoints for webhooks to call (Patch, Feature RST). This allows for an additional way to interact with a TYPO3 system suitable for webhooks or any use cases where a CLI interaction (read the post about TYPO3 and CLI at this blog) is not possible or not sufficient. The new functionality lives in a system extension called "reactions".

In this post we will look at webhooks in general, the implementation in the TYPO3 core (EXT:reactions) and last but not least how to add our own reactions.

Webhooks

Webhooks are a commonly used way for applications to signal that a certain event has occured. For example, GitHub offers lots of webhooks for all kind of events. When a developer pushes code to a repository there is a webhook. When a branch is created there is a webhook. At any of those (and many more) events you could register an endpoint URL to receive a request with the data associated with that event as a payload. People do this for example at packagist.org to automatically update a package once they push or add a tag to the connected git repository.

A webhook sends a POST request to the provided endpoint URL and often contains a payload in form of JSON encoded data. It also sends some sort of authentication or checksum header to let the receiving system know that this is a legit webhook call.

So basically an application with a webhook is saying: "At this point, I happily send you data about what just happened if you provide me with an URL. I'll also add your secret on top, so you know it's me".

Top

EXT:reactions - Implementation in TYPO3

While nothing was stopping us from implementing such an endpoint on our own prior to TYPO3 v12.1, the whole process became much more convenient with the introduction of the new system extension: EXT:reactions. A new backend module is added with this extension where we can add ReactionInstruction records. Those records are stored in the new database table sys_reaction.

The module will list all created ReactionInstruction records and allows us to add new ones. These records configure and create endpoints for any one of the available reaction types. They feature an identifier which will be part of the endpoint URL. They will also provide a secret that has to be sent as a HTTP request header (x-api-key) to the endpoint and is only valid for one endpoint. If the secret is wrong or missing, the request is not dispatched to the connected reaction class.

The secret is generated with the new TCA field control passwordGenerator (Feature RST) that will only reveal the resulting string once after generating it and then never again. In the database, the secret is encrypted. We have to make sure to store the secret when creating it. Otherwise, we have to create a new one.

The response will also include a special HTTP header: x-typo3-reaction-success will be sent if the request has been processed successfully.

As a bonus, all registered reaction types are listed in the configuration module (PatchFeature RST).

Top

Included Reaction: Create Records

TYPO3 12.1 comes with one reaction already implemented and ready to use: Creating records.

From a list of possible tables (page, content, file collection, internal note or category) we can choose what record should be created, where to put it (storage PID), which backend user to simulate while creating it and whether we want to map payload data to any fields. The placeholder syntax for mapping is ${property.subproperty}. If the JSON body of the request contains a matching data structure, the placeholder is replaced with the content from the JSON data.

Here is how this looks for creating a page record with title and description taken from the payload:


With the above configuration a call to the endpoint with the correct secret as a header will create a new page record.

The payload could look like this:

{
	"page": {
		"title": "Made with ❤️ by a webhook",
		"description": "My first reaction was to create this page Page"
	}
}

A POST request with the above body and the matching secret header will trigger the reaction and a new page record will be created each time the endpoint is called.

Creating pages might not be the most useful thing triggered by requests from 3rd party applications. Therefore we can (since this Patch) register our own tables for the reaction by simply adding to the TCA:

\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTcaSelectItem(
	'sys_reaction',
	'my_table',
	[
		'LLL:EXT:my_ext/Resources/Private/Language/locallang.xlf:my_table.title',
		'my_table',
		'my_ext-table_icon',
	]
);

All fields of my_table that in the TCA are configured with type input, text, or textarea will be listed for data mapping in the ReactionInstruction automatically.

If you are interested in how this works, have a look at the class \TYPO3\CMS\Reactions\Form\Element\FieldMapElement that is registered for the fields field in the TCA of the table sys_reaction. In combination with the new TCA dbType json (Patch, Patch, Feature RST) this is a clean solution for a field with dynamic content depending on the selected table.

We now have seen the built in reaction that comes with the core.

Let's move on to a custom implementation.

Top

Custom Reaction Example: Clear Caches by Webhook

The following example reflects the state as of TYPO3 12.1 and will be updated if things change. It is very likely that we see some changes to the API with the release of TYPO3 v12 LTS.

To have a somewhat useful example we imagine the following scenario:

Our website displays product information that are being received through an API. We cache the output with the ordinary TYPO3 page cache, tagging each cache entry with the ID of each product that is rendered. We want to invalidate the cache as soon as product information change at the API side. Luckily the 3rd party API offers a webhook that is fired whenever changes to products are published. We want to register to this webhook and clear all cache tags associated with the changed products.

To get our example reaction up and running we need to have a few things configured. We also need a class that will receive the incoming data. This can be any PHP class as long as it implements the \TYPO3\CMS\Reactions\Reaction\ReactionInterface. This interface will kindly ask you to implement the following methods:

  • getType(): string - This is an identifier of the reaction and should therefore be a unique string. Use your extension key or vendor name to make sure there won't be any conflicts
  • getDescription(): string - This is a human readable (short) description of the reaction to be displayed in dropdowns. Note: this can be any string as well as a label string readable by the f:translate ViewHelper (e.g. LLL:EXT:my_ext/Path/To/locallang.xlf:label-key).
  • getIconIdentifier(): string - Each reaction has its own icon in the backend module. This defines the icon.
  • react(ServerRequestInterface $request, array $payload, ReactionInstruction $reaction): ResponseInterface - The main method and entry point for incoming data. Do whatever you want in here and return a response object - most likely a JSONResponse.

Our example should be a simple reaction that flushes caches, resp. cache tags. An implementation of the above mentioned interface could look like this:

(please do not copy this code to production)

class ClearCacheReaction implements ReactionInterface
{
    public function __construct(
        private readonly ResponseFactoryInterface $responseFactory,
        private readonly StreamFactoryInterface $streamFactory,
        private readonly FrontendInterface $cache
    )
    {
    }

    public static function getType(): string
    {
        return 'flexible-cache-clearance_reaction';
    }

    public static function getDescription(): string
    {
        return 'LLL:EXT:dummy_plugin/Resources/Private/Language/locallang.xlf:clear-cache-reaction-description';
    }

    public static function getIconIdentifier(): string
    {
        return 'actions-system-cache-clear';
    }

    public function react(
        ServerRequestInterface $request,
        array $payload,
        ReactionInstruction $reaction
    ): ResponseInterface {
        if (is_array($payload['changedProducts'] ?? false)) {
            $tags = [];
            foreach($payload['changedProducts'] as $productId) {
                $tag = 'product_' . $productId;
                if (!$this->cache->isValidTag($tag)) {
					continue;
                }
				$tags[] = $tag;
            }
            $this->cache->flushByTags($tags);
        }
        return $this->createJsonResponse(['status' => 'Caches have been flushed']);
    }

    // protected function createJsonResponse(array $data) {...}
}

Let's have a closer look at the three arguments of the react() methods:

  • ServerRequestInterface $request - This is the incoming POST request as PSR-7 ServerRequest object.
  • array $payload - The body of the incoming POST request but already with a json_decode() applied, so that we receive a PHP array representation of the incoming JSON data.
  • ReactionInstruction $reaction - This is an object representation of the above mentioned record (the one we can create in the new reaction module). Hint: We can access our own added fields by casting the object to an array.

With this we have all the incoming data available, and we know what endpoint was called. We now do our thing (flushing cache tags in our example) and return a PSR-7 response object. Our code will only be called if the correct secret was sent with the data. We do not have to check this ourselves.

There are, however, two more things to consider before we could create a ReactionInstruction record in the backend module for our custom reaction type.

Top

Auto-Registering a Reaction type by implementing the Interface

Good news, the implementation of the ReactionInterface is enough to automagically register our class as an available reaction type.

Under the hood our class will be tagged with reactions.reaction in the service container. We could also add a few lines to our DI container configuration at Configuration/Services.yaml (read more about the basics of Dependency Injection in TYPO3 in this post) manually:

services:
  [...]
  Vendor\MyExt\Reaction\ClearCacheReaction:
    tags: [ 'reactions.reaction' ]
    public: true

With this our custom reaction type should appear in the reaction backend module.

Unfortunately, if we want to create a ReactionInstruction record for our reaction type we won't find it in the type select of the record. The reason for this is that we first have to provide the TCA configuration for the ReactionInstruction record with our custom type.

Top

Providing the TCA

Let's head over to Configuration/TCA/Overrides/sys_reaction.php.

We need to add our reaction type to the type select of the record, and we need to define what fields should be editable once our type is selected.

Let's do that:

// Add the custom type to the type select
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTcaSelectItem(
    'sys_reaction',
    'reaction_type',
    [
        \Vendor\MyExt\Reaction\ClearCacheReaction::getDescription(),
        \Vendor\MyExt\Reaction\ClearCacheReaction::getType(),
        \Vendor\MyExt\Reaction\ClearCacheReaction::getIconIdentifier(),
    ]
);

// Type icon
$GLOBALS['TCA']['sys_reaction']['ctrl']['typeicon_classes'][\Vendor\MyExt\Reaction\ClearCacheReaction::getType()] = \Vendor\MyExt\Reaction\ClearCacheReaction::getIconIdentifier();

// What fields to display
$GLOBALS['TCA']['sys_reaction']['types'][\Vendor\MyExt\Reaction\ClearCacheReaction::getType()] = [
    'showitem' => '
        --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,
        --palette--;;config,
        --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,
        --palette--;;access'
];

Now, we can create a ReactionInstruction for our own reaction type. We give it a title, a description and roll a secret. The identifier is generated automatically for us.

It looks like this:

With that we have our endpoint URL: domain.tld/typo3/reaction/1b15280a-ca65-4a78-9c8f-1412d39cd8c5. We find this URL in the overview of the reactions backend module alongside an example CURL command to call our reaction. We could now send a POST request to this URL including the secret as a HTTP header. The body of our request could look something like this:

{
	"changedProducts": [357,5478,8104]
}

This will flush three cache tags. We successfully connected to an (imaginary) webhook of a 3rd party system (API) to react to it in our TYPO3 system (by clearing some caches).

Awesome.

Have fun coming up with useful reaction implementations and don't forget to share them with the community. Helping the core team polishing this feature until the LTS release (and beyond, of course) would also be much appreciated.

Thanks for reading and thanks supporting this blog.

Top

Further Information