Skip to content

Commit cc32dea

Browse files
authored
Merge pull request #235 from wpengine/feat-webhooks-revamp-event-dispatch
feat: refactor codebase to implement simple webhook action handling and and dispatching
2 parents a7e84a9 + e0d2f3a commit cc32dea

File tree

12 files changed

+661
-256
lines changed

12 files changed

+661
-256
lines changed

plugins/wp-graphql-headless-webhooks/access-functions.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
function register_webhook_type( string $type, array $args = [] ): void {
3434
/** @psalm-suppress HookNotFound */
3535
if ( did_action( 'graphql_register_webhooks' ) > 0 ) {
36-
_doing_it_wrong( 'register_webhook_type', __( 'Call this before WebhookRegistry::init', 'wp-graphql-headless-webhooks' ), '0.1.0' );
36+
_doing_it_wrong( 'register_webhook_type', 'Call this before WebhookRegistry::init', '0.1.0' );
3737

3838
return;
3939
}
@@ -137,7 +137,7 @@ function register_graphql_event( Event $event ): void {
137137
if ( did_action( 'graphql_register_events' ) ) {
138138
_doing_it_wrong(
139139
__FUNCTION__,
140-
esc_html__( 'Call this before EventRegistry::init', 'wp-graphql-webhooks' ),
140+
'Call this before EventRegistry::init',
141141
'0.0.1'
142142
);
143143
return;

plugins/wp-graphql-headless-webhooks/src/Autoloader.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ protected static function require_autoloader( string $autoloader_file ): bool {
6060
* Displays a notice if the autoloader is missing.
6161
*/
6262
protected static function missing_autoloader_notice(): void {
63-
$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' );
63+
$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`.';
6464

6565
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
6666
error_log( esc_html( $error_message ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- This is a development notice.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
namespace WPGraphQL\Webhooks\Entity;
4+
5+
/**
6+
* Class Webhook
7+
*
8+
* Represents a Webhook entity (Data Transfer Object).
9+
*
10+
* @package WPGraphQL\Webhooks
11+
*/
12+
class Webhook {
13+
/** @var int Webhook post ID. */
14+
public int $id;
15+
16+
/** @var string Webhook name (post title). */
17+
public string $name;
18+
19+
/** @var string Event the webhook listens to. */
20+
public string $event;
21+
22+
/** @var string Destination URL for the webhook. */
23+
public string $url;
24+
25+
/** @var string HTTP method used for the webhook request. */
26+
public string $method;
27+
28+
/** @var array HTTP headers to be sent with the webhook request. */
29+
public array $headers;
30+
31+
/**
32+
* Webhook constructor.
33+
*
34+
* @param int $id Webhook post ID.
35+
* @param string $name Webhook name.
36+
* @param string $event Event the webhook listens to.
37+
* @param string $url Destination URL for the webhook.
38+
* @param string $method HTTP method used for the webhook request. Defaults to 'POST'.
39+
* @param array $headers HTTP headers to be sent with the request.
40+
*/
41+
public function __construct( int $id, string $name, string $event, string $url, string $method = 'POST', array $headers = [] ) {
42+
$this->id = $id;
43+
$this->name = $name;
44+
$this->event = $event;
45+
$this->url = $url;
46+
$this->method = $method;
47+
$this->headers = $headers;
48+
}
49+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
namespace WPGraphQL\Webhooks\Events\Interfaces;
3+
4+
/**
5+
* Interface EventManager
6+
*
7+
* Defines the contract for managing and registering event hooks in the WPGraphQL Webhooks system.
8+
* Implementations should set up the necessary WordPress hooks to listen for relevant events and trigger webhooks.
9+
*
10+
* @package WPGraphQL\Webhooks\Events\Interfaces
11+
*/
12+
interface EventManager {
13+
14+
/**
15+
* Register WordPress action and filter hooks for webhook events.
16+
*
17+
* This method should bind handlers to the desired WordPress events that
18+
* the webhook system listens to and dispatches payloads for.
19+
*
20+
* @return void
21+
*/
22+
public function register_hooks(): void;
23+
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
<?php
2+
3+
namespace WPGraphQL\Webhooks\Events;
4+
5+
use WPGraphQL\Webhooks\Events\Interfaces\EventManager;
6+
use WPGraphQL\Webhooks\Repository\Interfaces\WebhookRepositoryInterface;
7+
use WPGraphQL\Webhooks\Handlers\Interfaces\Handler;
8+
9+
/**
10+
* Webhook Event Manager
11+
*
12+
* Manages WordPress events and triggers matching webhooks.
13+
*/
14+
class WebhookEventManager implements EventManager {
15+
16+
private WebhookRepositoryInterface $repository;
17+
private Handler $handler;
18+
19+
/**
20+
* Constructor
21+
*
22+
* @param WebhookRepositoryInterface $repository
23+
* @param Handler $sender
24+
*/
25+
public function __construct( WebhookRepositoryInterface $repository, $handler ) {
26+
$this->repository = $repository;
27+
$this->handler = $handler;
28+
}
29+
30+
/**
31+
* Register specific WordPress event hooks.
32+
*/
33+
public function register_hooks(): void {
34+
add_action( 'transition_post_status', [ $this, 'on_transition_post_status' ], 10, 3 );
35+
add_action( 'post_updated', [ $this, 'on_post_updated' ], 10, 3 );
36+
add_action( 'deleted_post', [ $this, 'on_deleted_post' ], 10, 2 );
37+
add_action( 'added_post_meta', [ $this, 'on_post_meta_change' ], 10, 4 );
38+
add_action( 'created_term', [ $this, 'on_term_created' ], 10, 3 );
39+
add_action( 'set_object_terms', [ $this, 'on_term_assigned' ], 10, 6 );
40+
add_action( 'delete_term_relationships', [ $this, 'on_term_unassigned' ], 10, 3 );
41+
add_action( 'delete_term', [ $this, 'on_term_deleted' ], 10, 4 );
42+
add_action( 'added_term_meta', [ $this, 'on_term_meta_change' ], 10, 4 );
43+
add_action( 'user_register', [ $this, 'on_user_created' ], 10, 1 );
44+
add_action( 'deleted_user', [ $this, 'on_user_deleted' ], 10, 2 );
45+
add_action( 'add_attachment', [ $this, 'on_media_uploaded' ], 10, 1 );
46+
add_action( 'edit_attachment', [ $this, 'on_media_updated' ], 10, 1 );
47+
add_action( 'delete_attachment', [ $this, 'on_media_deleted' ], 10, 1 );
48+
add_action( 'wp_insert_comment', [ $this, 'on_comment_inserted' ], 10, 2 );
49+
add_action( 'transition_comment_status', [ $this, 'on_comment_status' ], 10, 3 );
50+
}
51+
52+
/**
53+
* Triggers webhooks for a given event if it is allowed.
54+
*
55+
* @param string $event
56+
* @param array $payload
57+
*/
58+
private function trigger_webhooks( string $event, array $payload ): void {
59+
$allowed_events = $this->repository->get_allowed_events();
60+
if ( ! array_key_exists( $event, $allowed_events ) ) {
61+
error_log( 'Event ' . $event . ' is not allowed. Allowed events: ' . implode( ', ', $allowed_events ) );
62+
return;
63+
}
64+
65+
do_action( 'graphql_webhooks_before_trigger', $event, $payload );
66+
foreach ( $this->repository->get_all() as $webhook ) {
67+
if ( $webhook->event === $event ) {
68+
$this->handler->handle( $webhook, $payload );
69+
}
70+
}
71+
72+
do_action( 'graphql_webhooks_after_trigger', $event, $payload );
73+
}
74+
75+
/** Event Handlers **/
76+
77+
public function on_transition_post_status( $new_status, $old_status, $post ) {
78+
if ( $old_status !== 'publish' && $new_status === 'publish' ) {
79+
$this->trigger_webhooks( 'post_published', [ 'post_id' => $post->ID ] );
80+
}
81+
}
82+
83+
public function on_post_updated( $post_ID, $post_after, $post_before ) {
84+
$this->trigger_webhooks( 'post_updated', [ 'post_id' => $post_ID ] );
85+
86+
if ( $post_after->post_author !== $post_before->post_author ) {
87+
$this->trigger_webhooks( 'user_assigned', [
88+
'post_id' => $post_ID,
89+
'author_id' => $post_after->post_author,
90+
] );
91+
92+
$this->trigger_webhooks( 'user_reassigned', [
93+
'post_id' => $post_ID,
94+
'old_author_id' => $post_before->post_author,
95+
'new_author_id' => $post_after->post_author,
96+
] );
97+
}
98+
}
99+
100+
public function on_deleted_post( $post_ID, $post ) {
101+
$this->trigger_webhooks( 'post_deleted', [ 'post_id' => $post_ID ] );
102+
}
103+
104+
public function on_post_meta_change( $meta_id, $post_id, $meta_key, $meta_value ) {
105+
$this->trigger_webhooks( 'post_meta_change', [
106+
'post_id' => $post_id,
107+
'meta_key' => $meta_key,
108+
] );
109+
}
110+
111+
public function on_term_created( $term_id, $tt_id, $taxonomy ) {
112+
$this->trigger_webhooks( 'term_created', [
113+
'term_id' => $term_id,
114+
'taxonomy' => $taxonomy,
115+
] );
116+
}
117+
118+
public function on_term_assigned( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) {
119+
foreach ( (array) $terms as $term_id ) {
120+
$this->trigger_webhooks( 'term_assigned', [
121+
'object_id' => $object_id,
122+
'term_id' => $term_id,
123+
'taxonomy' => $taxonomy,
124+
] );
125+
}
126+
}
127+
128+
public function on_term_unassigned( $object_id, $taxonomy, $term_ids ) {
129+
$this->trigger_webhooks( 'term_unassigned', [
130+
'object_id' => $object_id,
131+
'taxonomy' => $taxonomy,
132+
'term_ids' => $term_ids,
133+
] );
134+
}
135+
136+
public function on_term_deleted( $term, $tt_id, $taxonomy, $deleted_term ) {
137+
$this->trigger_webhooks( 'term_deleted', [
138+
'term_id' => $term,
139+
'taxonomy' => $taxonomy,
140+
] );
141+
}
142+
143+
public function on_term_meta_change( $meta_id, $term_id, $meta_key, $meta_value ) {
144+
$this->trigger_webhooks( 'term_meta_change', [
145+
'term_id' => $term_id,
146+
'meta_key' => $meta_key,
147+
] );
148+
}
149+
150+
public function on_user_created( $user_id ) {
151+
$this->trigger_webhooks( 'user_created', [ 'user_id' => $user_id ] );
152+
}
153+
154+
public function on_user_deleted( $user_id, $reassign ) {
155+
$this->trigger_webhooks( 'user_deleted', [ 'user_id' => $user_id ] );
156+
}
157+
158+
public function on_media_uploaded( $post_id ) {
159+
$this->trigger_webhooks( 'media_uploaded', [ 'post_id' => $post_id ] );
160+
}
161+
162+
public function on_media_updated( $post_id ) {
163+
$this->trigger_webhooks( 'media_updated', [ 'post_id' => $post_id ] );
164+
}
165+
166+
public function on_media_deleted( $post_id ) {
167+
$this->trigger_webhooks( 'media_deleted', [ 'post_id' => $post_id ] );
168+
}
169+
170+
public function on_comment_inserted( $comment_id, $comment_object ) {
171+
$this->trigger_webhooks( 'comment_inserted', [ 'comment_id' => $comment_id ] );
172+
}
173+
174+
public function on_comment_status( $new_status, $old_status, $comment ) {
175+
$this->trigger_webhooks( 'comment_status', [
176+
'comment_id' => $comment->comment_ID,
177+
'new_status' => $new_status,
178+
] );
179+
}
180+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
namespace WPGraphQL\Webhooks\Handlers\Interfaces;
3+
4+
use WPGraphQL\Webhooks\Entity\Webhook;
5+
6+
/**
7+
* Interface Handler
8+
*
9+
* Defines the contract for event handlers in the WPGraphQL Webhooks system.
10+
* Implementations should process the given payload when an event is triggered.
11+
*
12+
* @package WPGraphQL\Webhooks\Handlers\Interfaces
13+
*/
14+
interface Handler {
15+
/**
16+
* Handle the event payload for a specific webhook.
17+
*
18+
* @param Webhook $webhook The Webhook entity instance.
19+
* @param array $payload The event payload data.
20+
*
21+
* @return void
22+
*/
23+
public function handle(Webhook $webhook, array $payload): void;
24+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
namespace WPGraphQL\Webhooks\Handlers;
3+
4+
use WPGraphQL\Webhooks\Entity\Webhook;
5+
use WPGraphQL\Webhooks\Handlers\Interfaces\Handler;
6+
7+
/**
8+
* Class WebhookHandler
9+
*
10+
* Sends the webhook to the configured URL when an event is triggered.
11+
*/
12+
class WebhookHandler implements Handler {
13+
14+
/**
15+
* Handle the event payload for a specific webhook.
16+
*
17+
* @param Webhook $webhook The Webhook entity instance.
18+
* @param array $payload The event payload data.
19+
*
20+
* @return void
21+
*/
22+
public function handle( Webhook $webhook, array $payload ): void {
23+
$args = [
24+
'headers' => $webhook->headers ?: [ 'Content-Type' => 'application/json' ],
25+
'timeout' => 5,
26+
'blocking' => false,
27+
];
28+
$payload = apply_filters( 'graphql_webhooks_payload', $payload, $webhook );
29+
30+
if ( strtoupper( $webhook->method ) === 'GET' ) {
31+
$url = add_query_arg( $payload, $webhook->url );
32+
$args['method'] = 'GET';
33+
} else {
34+
$url = $webhook->url;
35+
$args['method'] = 'POST';
36+
$args['body'] = wp_json_encode( $payload );
37+
if ( empty( $args['headers']['Content-Type'] ) ) {
38+
$args['headers']['Content-Type'] = 'application/json';
39+
}
40+
}
41+
wp_remote_request( $url, $args );
42+
do_action( 'graphql_webhooks_sent', $webhook, $payload );
43+
}
44+
}

0 commit comments

Comments
 (0)