Skip to main content

Webhooks and events

Webhooks send data about activity in XenForo to external services. XenForo includes webhook support for posts, profile posts, profile post comments, reports, threads, users, and user upgrades.

If your add-on has its own content type, you can add webhook support to it. This page builds a Note content type: table and entity, content type registration, events, and a custom event.

Before you start​

  • Webhooks are queued, not sent immediately. Queueing a webhook adds one XF\Job\WebhookSend job for each active webhook that includes the content type; the job queue sends the request later.
  • queueWebhook() returns false without queueing anything if no active webhook includes the content type. This is expected when webhooks are not configured.
  • The request body contains content_type, event, content_id, and data. The data value is rendered from the entity or array that you pass to queueWebhook().

Standard events​

Every content type supports insert, update, and delete.

The XF:Webhook entity behavior queues these events when you save or delete an entity; direct database queries do not trigger them. Queue other events in your code and register them with getWebhookEvents(), so administrators can select them.

Create the table​

Add an install step to Setup.php:

Setup.php
<?php

namespace Vendor\Addon;

use XF\AddOn\AbstractSetup;
use XF\AddOn\StepRunnerInstallTrait;
use XF\AddOn\StepRunnerUninstallTrait;
use XF\Db\Schema\Create;

class Setup extends AbstractSetup
{
use StepRunnerInstallTrait;
use StepRunnerUninstallTrait;

public function installStep1(): void
{
$this->schemaManager()->createTable('xf_note', function (Create $table)
{
$table->addColumn('note_id', 'int')->autoIncrement();
$table->addColumn('title', 'varchar', 100);
$table->addColumn('content', 'text');
$table->addColumn('user_id', 'int')->nullable();
$table->addColumn('created_date', 'int')->setDefault(0);

$table->addPrimaryKey('note_id');
});
}

public function uninstallStep1(): void
{
$this->schemaManager()->dropTable('xf_note');
}
}

This creates an xf_note table with these columns:

ColumnTypeDescription
note_idint, auto-incrementThe primary key.
titlevarchar(100)The title of the note.
contenttextThe body of the note.
user_idint, nullableThe user who created the note.
created_dateintWhen the note was created, as a Unix timestamp.

Create the entity​

Create the entity with xf-make:entity (development mode required):

Terminal
php cmd.php xf-make:entity Vendor/Addon:Note

The finished entity looks like this:

Entity/Note.php
<?php

namespace Vendor\Addon\Entity;

use XF\Entity\User;
use XF\Mvc\Entity\Entity;
use XF\Mvc\Entity\Structure;

/**
* COLUMNS
* @property int $note_id
* @property string $title
* @property string $content
* @property int|null $user_id
* @property int $created_date
*
* RELATIONS
* @property-read User|null $User
*/
class Note extends Entity
{
public static function getStructure(Structure $structure): Structure
{
$structure->table = 'xf_note';
$structure->shortName = 'Vendor\Addon:Note';
$structure->primaryKey = 'note_id';
$structure->columns = [
'note_id' => ['type' => self::UINT, 'autoIncrement' => true, 'nullable' => true],
'title' => ['type' => self::STR, 'maxLength' => 100, 'required' => true],
'content' => ['type' => self::STR, 'default' => ''],
'user_id' => ['type' => self::UINT, 'nullable' => true],
'created_date' => ['type' => self::UINT, 'default' => \XF::$time],
];

$structure->relations = [
'User' => [
'entity' => 'XF:User',
'type' => self::TO_ONE,
'conditions' => 'user_id',
'primary' => true,
],
];

return $structure;
}
}

user_id is nullable, so the User relation can be null when a note has no user.

Register the content type​

Register vendor_addon_notes in the admin control panel under Development → Content types, and set Add-on to Vendor/Addon. Add these fields:

FieldValueDescription
entityVendor\Addon:NoteThe short class name of the entity.
webhook_handler_classVendor\Addon\Webhook\Event\NoteHandlerThe handler that lists the available events.
phrase_pluralvendor_addon_notesThe phrase title used for the content type. Add it with xf-make:phrase.

In development mode, the control panel writes these fields to _output/content_type_fields/. You can also create them with xf-make:content-type-field, for example:

Terminal
php cmd.php xf-make:content-type-field vendor_addon_notes entity 'Vendor\Addon:Note' --addon=Vendor/Addon

Repeat for webhook_handler_class and phrase_plural.

Create the webhook handler​

Create Webhook/Event/NoteHandler.php relative to your add-on root:

Webhook/Event/NoteHandler.php
<?php

namespace Vendor\Addon\Webhook\Event;

use XF\Webhook\Event\AbstractHandler;

class NoteHandler extends AbstractHandler
{
public static function getWebhookEvents(): array
{
return ['report'];
}

public function getEventHint(string $event): string
{
switch ($event)
{
case 'insert':
return 'When a new note is created';
case 'update':
return 'When an existing note is updated';
case 'delete':
return 'When a note is deleted';
case 'report':
return 'When a note activity report is generated';
default:
return '';
}
}
}

Do not override getEvents(). AbstractHandler::getEvents() returns insert, update, and delete, then merges the result of getWebhookEvents() from the content type handlers. The static method here registers the custom report event.

getEventHint() returns the description that the admin control panel shows for each event.

Enable webhooks on the entity​

Add these lines after the relations, before the existing return $structure;:

$structure->behaviors['XF:Webhook'] = [];
$structure->contentType = 'vendor_addon_notes';
  • $structure->behaviors['XF:Webhook'] queues the standard events.
  • $structure->contentType must match the content type you registered. The behavior uses it to find the webhook handler and to set content_type in the payload.

Define the payload​

Add a setupApiResultData() method to the entity class to control what a webhook receives. This example includes all columns and a summary of the related user:

Entity/Note.php
protected function setupApiResultData(
\XF\Api\Result\EntityResult $result,
$verbosity = self::VERBOSITY_NORMAL,
array $options = []
)
{
$result->includeColumn(['note_id', 'title', 'content', 'user_id', 'created_date']);

$user = $this->User;
if ($user)
{
$result->includeExtra([
'User' => [
'user_id' => $user->user_id,
'username' => $user->username,
'custom_title' => $user->custom_title,
],
]);
}
}
  • includeColumn() adds the columns that you list.
  • includeExtra() adds data that is not a column of the entity, such as relations.
  • The User relation can be null when the note has no user or the relation cannot be loaded, such as after the entity is deleted, so check it first.

Trigger standard events​

Saving or deleting an entity triggers the events that the behavior queues:

// Queues the insert event.
$note = \XF::em()->create('Vendor\Addon:Note');
$note->title = 'A new note';
$note->content = 'This note is new.';
$note->user_id = \XF::visitor()->user_id;
$note->save();

// Queues the update event.
$note->content = 'This note has changed.';
$note->save();

// Queues the delete event.
$note->delete();

Send a custom event​

To send an event that the behavior does not queue, call queueWebhook() in your own code. Register the event with getWebhookEvents(); otherwise it is only sent to webhooks configured to send all events.

This repository method queues a report event with statistics about recent notes:

Repository/NoteRepository.php
<?php

namespace Vendor\Addon\Repository;

use XF\Mvc\Entity\Repository;
use XF\Repository\WebhookRepository;

class NoteRepository extends Repository
{
public function sendActivityReport(): void
{
$stats = $this->db()->fetchRow("
SELECT COUNT(*) AS total_notes,
MAX(note_id) AS newest_note_id,
MIN(created_date) AS oldest_note_date,
MAX(created_date) AS newest_note_date,
COUNT(DISTINCT user_id) AS unique_users
FROM xf_note
WHERE created_date > ?
", [\XF::$time - (7 * 86400)]);

if (!$stats['newest_note_id'])
{
return;
}

$this->repository(WebhookRepository::class)->queueWebhook(
'vendor_addon_notes',
(int) $stats['newest_note_id'],
'report',
[
'report_date' => \XF::$time,
'stats' => [
'total_notes' => (int) $stats['total_notes'],
'oldest_note_date' => (int) $stats['oldest_note_date'],
'newest_note_date' => (int) $stats['newest_note_date'],
'unique_users' => (int) $stats['unique_users'],
],
]
);
}
}

queueWebhook() takes the following arguments:

ArgumentTypeDescription
contentTypestringThe content type that you registered.
contentIdintThe ID of the content that the event relates to. Use a non-zero ID, because the send job throws an exception for a falsy content ID. This example uses the newest note.
eventstringThe event name. It must match the name that getWebhookEvents() returns.
entityOrData\XF\Mvc\Entity\Entity or arrayAn entity to use its webhook result, or an array to send a fixed payload.
extraDataarrayOptional. Merged into the top level of the payload.

Call sendActivityReport() from a cron entry or another service when you want to send the event.