Skip to content

feat: refactor codebase to implement simple webhook action handling and and dispatching #235

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions plugins/wp-graphql-headless-webhooks/access-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
function register_webhook_type( string $type, array $args = [] ): void {
/** @psalm-suppress HookNotFound */
if ( did_action( 'graphql_register_webhooks' ) > 0 ) {
_doing_it_wrong( 'register_webhook_type', __( 'Call this before WebhookRegistry::init', 'wp-graphql-headless-webhooks' ), '0.1.0' );
_doing_it_wrong( 'register_webhook_type', 'Call this before WebhookRegistry::init', '0.1.0' );

return;
}
Expand Down Expand Up @@ -137,7 +137,7 @@ function register_graphql_event( Event $event ): void {
if ( did_action( 'graphql_register_events' ) ) {
_doing_it_wrong(
__FUNCTION__,
esc_html__( 'Call this before EventRegistry::init', 'wp-graphql-webhooks' ),
'Call this before EventRegistry::init',
'0.0.1'
);
return;
Expand Down
2 changes: 1 addition & 1 deletion plugins/wp-graphql-headless-webhooks/src/Autoloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ protected static function require_autoloader( string $autoloader_file ): bool {
* Displays a notice if the autoloader is missing.
*/
protected static function missing_autoloader_notice(): void {
$error_message = __( 'Headless Webhooks for WPGraphQL: The Composer autoloader was not found. If you installed the plugin from the GitHub source, make sure to run `composer install`.', 'wp-graphql-headless-webhooks' );
$error_message = 'Headless Webhooks for WPGraphQL: The Composer autoloader was not found. If you installed the plugin from the GitHub source, make sure to run `composer install`.';

if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( esc_html( $error_message ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- This is a development notice.
Expand Down
49 changes: 49 additions & 0 deletions plugins/wp-graphql-headless-webhooks/src/Entity/Webhook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace WPGraphQL\Webhooks\Entity;

/**
* Class Webhook
*
* Represents a Webhook entity (Data Transfer Object).
*
* @package WPGraphQL\Webhooks
*/
class Webhook {
/** @var int Webhook post ID. */
public int $id;

/** @var string Webhook name (post title). */
public string $name;

/** @var string Event the webhook listens to. */
public string $event;

/** @var string Destination URL for the webhook. */
public string $url;

/** @var string HTTP method used for the webhook request. */
public string $method;

/** @var array HTTP headers to be sent with the webhook request. */
public array $headers;

/**
* Webhook constructor.
*
* @param int $id Webhook post ID.
* @param string $name Webhook name.
* @param string $event Event the webhook listens to.
* @param string $url Destination URL for the webhook.
* @param string $method HTTP method used for the webhook request. Defaults to 'POST'.
* @param array $headers HTTP headers to be sent with the request.
*/
public function __construct( int $id, string $name, string $event, string $url, string $method = 'POST', array $headers = [] ) {
$this->id = $id;
$this->name = $name;
$this->event = $event;
$this->url = $url;
$this->method = $method;
$this->headers = $headers;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
namespace WPGraphQL\Webhooks\Events\Interfaces;

/**
* Interface EventManager
*
* Defines the contract for managing and registering event hooks in the WPGraphQL Webhooks system.
* Implementations should set up the necessary WordPress hooks to listen for relevant events and trigger webhooks.
*
* @package WPGraphQL\Webhooks\Events\Interfaces
*/
interface EventManager {

/**
* Register WordPress action and filter hooks for webhook events.
*
* This method should bind handlers to the desired WordPress events that
* the webhook system listens to and dispatches payloads for.
*
* @return void
*/
public function register_hooks(): void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
<?php

namespace WPGraphQL\Webhooks\Events;

use WPGraphQL\Webhooks\Events\Interfaces\EventManager;
use WPGraphQL\Webhooks\Repository\Interfaces\WebhookRepositoryInterface;
use WPGraphQL\Webhooks\Handlers\Interfaces\Handler;

/**
* Webhook Event Manager
*
* Manages WordPress events and triggers matching webhooks.
*/
class WebhookEventManager implements EventManager {

private WebhookRepositoryInterface $repository;
private Handler $handler;

/**
* Constructor
*
* @param WebhookRepositoryInterface $repository
* @param Handler $sender
*/
public function __construct( WebhookRepositoryInterface $repository, $handler ) {
$this->repository = $repository;
$this->handler = $handler;
}

/**
* Register specific WordPress event hooks.
*/
public function register_hooks(): void {
add_action( 'transition_post_status', [ $this, 'on_transition_post_status' ], 10, 3 );
add_action( 'post_updated', [ $this, 'on_post_updated' ], 10, 3 );
add_action( 'deleted_post', [ $this, 'on_deleted_post' ], 10, 2 );
add_action( 'added_post_meta', [ $this, 'on_post_meta_change' ], 10, 4 );
add_action( 'created_term', [ $this, 'on_term_created' ], 10, 3 );
add_action( 'set_object_terms', [ $this, 'on_term_assigned' ], 10, 6 );
add_action( 'delete_term_relationships', [ $this, 'on_term_unassigned' ], 10, 3 );
add_action( 'delete_term', [ $this, 'on_term_deleted' ], 10, 4 );
add_action( 'added_term_meta', [ $this, 'on_term_meta_change' ], 10, 4 );
add_action( 'user_register', [ $this, 'on_user_created' ], 10, 1 );
add_action( 'deleted_user', [ $this, 'on_user_deleted' ], 10, 2 );
add_action( 'add_attachment', [ $this, 'on_media_uploaded' ], 10, 1 );
add_action( 'edit_attachment', [ $this, 'on_media_updated' ], 10, 1 );
add_action( 'delete_attachment', [ $this, 'on_media_deleted' ], 10, 1 );
add_action( 'wp_insert_comment', [ $this, 'on_comment_inserted' ], 10, 2 );
add_action( 'transition_comment_status', [ $this, 'on_comment_status' ], 10, 3 );
}

/**
* Triggers webhooks for a given event if it is allowed.
*
* @param string $event
* @param array $payload
*/
private function trigger_webhooks( string $event, array $payload ): void {
$allowed_events = $this->repository->get_allowed_events();
if ( ! array_key_exists( $event, $allowed_events ) ) {
error_log( 'Event ' . $event . ' is not allowed. Allowed events: ' . implode( ', ', $allowed_events ) );
return;
}

do_action( 'graphql_webhooks_before_trigger', $event, $payload );
foreach ( $this->repository->get_all() as $webhook ) {
if ( $webhook->event === $event ) {
$this->handler->handle( $webhook, $payload );
}
}

do_action( 'graphql_webhooks_after_trigger', $event, $payload );
}

/** Event Handlers **/

public function on_transition_post_status( $new_status, $old_status, $post ) {
if ( $old_status !== 'publish' && $new_status === 'publish' ) {
$this->trigger_webhooks( 'post_published', [ 'post_id' => $post->ID ] );
}
}

public function on_post_updated( $post_ID, $post_after, $post_before ) {
$this->trigger_webhooks( 'post_updated', [ 'post_id' => $post_ID ] );

if ( $post_after->post_author !== $post_before->post_author ) {
$this->trigger_webhooks( 'user_assigned', [
'post_id' => $post_ID,
'author_id' => $post_after->post_author,
] );

$this->trigger_webhooks( 'user_reassigned', [
'post_id' => $post_ID,
'old_author_id' => $post_before->post_author,
'new_author_id' => $post_after->post_author,
] );
}
}

public function on_deleted_post( $post_ID, $post ) {
$this->trigger_webhooks( 'post_deleted', [ 'post_id' => $post_ID ] );
}

public function on_post_meta_change( $meta_id, $post_id, $meta_key, $meta_value ) {
$this->trigger_webhooks( 'post_meta_change', [
'post_id' => $post_id,
'meta_key' => $meta_key,
] );
}

public function on_term_created( $term_id, $tt_id, $taxonomy ) {
$this->trigger_webhooks( 'term_created', [
'term_id' => $term_id,
'taxonomy' => $taxonomy,
] );
}

public function on_term_assigned( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) {
foreach ( (array) $terms as $term_id ) {
$this->trigger_webhooks( 'term_assigned', [
'object_id' => $object_id,
'term_id' => $term_id,
'taxonomy' => $taxonomy,
] );
}
}

public function on_term_unassigned( $object_id, $taxonomy, $term_ids ) {
$this->trigger_webhooks( 'term_unassigned', [
'object_id' => $object_id,
'taxonomy' => $taxonomy,
'term_ids' => $term_ids,
] );
}

public function on_term_deleted( $term, $tt_id, $taxonomy, $deleted_term ) {
$this->trigger_webhooks( 'term_deleted', [
'term_id' => $term,
'taxonomy' => $taxonomy,
] );
}

public function on_term_meta_change( $meta_id, $term_id, $meta_key, $meta_value ) {
$this->trigger_webhooks( 'term_meta_change', [
'term_id' => $term_id,
'meta_key' => $meta_key,
] );
}

public function on_user_created( $user_id ) {
$this->trigger_webhooks( 'user_created', [ 'user_id' => $user_id ] );
}

public function on_user_deleted( $user_id, $reassign ) {
$this->trigger_webhooks( 'user_deleted', [ 'user_id' => $user_id ] );
}

public function on_media_uploaded( $post_id ) {
$this->trigger_webhooks( 'media_uploaded', [ 'post_id' => $post_id ] );
}

public function on_media_updated( $post_id ) {
$this->trigger_webhooks( 'media_updated', [ 'post_id' => $post_id ] );
}

public function on_media_deleted( $post_id ) {
$this->trigger_webhooks( 'media_deleted', [ 'post_id' => $post_id ] );
}

public function on_comment_inserted( $comment_id, $comment_object ) {
$this->trigger_webhooks( 'comment_inserted', [ 'comment_id' => $comment_id ] );
}

public function on_comment_status( $new_status, $old_status, $comment ) {
$this->trigger_webhooks( 'comment_status', [
'comment_id' => $comment->comment_ID,
'new_status' => $new_status,
] );
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php
namespace WPGraphQL\Webhooks\Handlers\Interfaces;

use WPGraphQL\Webhooks\Entity\Webhook;

/**
* Interface Handler
*
* Defines the contract for event handlers in the WPGraphQL Webhooks system.
* Implementations should process the given payload when an event is triggered.
*
* @package WPGraphQL\Webhooks\Handlers\Interfaces
*/
interface Handler {
/**
* Handle the event payload for a specific webhook.
*
* @param Webhook $webhook The Webhook entity instance.
* @param array $payload The event payload data.
*
* @return void
*/
public function handle(Webhook $webhook, array $payload): void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php
namespace WPGraphQL\Webhooks\Handlers;

use WPGraphQL\Webhooks\Entity\Webhook;
use WPGraphQL\Webhooks\Handlers\Interfaces\Handler;

/**
* Class WebhookHandler
*
* Sends the webhook to the configured URL when an event is triggered.
*/
class WebhookHandler implements Handler {

/**
* Handle the event payload for a specific webhook.
*
* @param Webhook $webhook The Webhook entity instance.
* @param array $payload The event payload data.
*
* @return void
*/
public function handle( Webhook $webhook, array $payload ): void {
$args = [
'headers' => $webhook->headers ?: [ 'Content-Type' => 'application/json' ],
'timeout' => 5,
'blocking' => false,
];
$payload = apply_filters( 'graphql_webhooks_payload', $payload, $webhook );

if ( strtoupper( $webhook->method ) === 'GET' ) {
$url = add_query_arg( $payload, $webhook->url );
$args['method'] = 'GET';
} else {
$url = $webhook->url;
$args['method'] = 'POST';
$args['body'] = wp_json_encode( $payload );
if ( empty( $args['headers']['Content-Type'] ) ) {
$args['headers']['Content-Type'] = 'application/json';
}
}
wp_remote_request( $url, $args );
do_action( 'graphql_webhooks_sent', $webhook, $payload );
}
}
Loading