1+ <?php
2+
3+ namespace WPGraphQL \Webhooks \Events ;
4+
5+ use GraphQLRelay \Relay ;
6+ use WPGraphQL \Webhooks \Events \Interfaces \EventManager ;
7+ use WPGraphQL \Webhooks \Repository \Interfaces \WebhookRepositoryInterface ;
8+ use WPGraphQL \Webhooks \Handlers \Interfaces \Handler ;
9+
10+ /**
11+ * Smart Cache Webhook Manager
12+ *
13+ * Listens to WPGraphQL Smart Cache purge events and triggers webhooks
14+ */
15+ class SmartCacheWebhookManager implements EventManager {
16+
17+ private WebhookRepositoryInterface $ repository ;
18+ private Handler $ handler ;
19+
20+ public function __construct ( WebhookRepositoryInterface $ repository , Handler $ handler ) {
21+ $ this ->repository = $ repository ;
22+ $ this ->handler = $ handler ;
23+ }
24+
25+ /**
26+ * Register Smart Cache purge hooks
27+ */
28+ public function register_hooks (): void {
29+ add_action ( 'wpgraphql_cache_purge_nodes ' , [ $ this , 'handle_purge_nodes ' ], 10 , 2 );
30+ add_action ( 'graphql_purge ' , [ $ this , 'handle_graphql_purge ' ], 10 , 3 );
31+ }
32+
33+ /**
34+ * Handle node purge events from Smart Cache
35+ */
36+ public function handle_purge_nodes ( string $ key , array $ nodes ): void {
37+ error_log ( "[Webhook] handle_purge_nodes - Key: $ key, Node count: " . count ( $ nodes ) );
38+
39+ // Handle empty nodes array
40+ if ( empty ( $ nodes ) ) {
41+ error_log ( "[Webhook] handle_purge_nodes - No nodes provided for key: $ key " );
42+ return ;
43+ }
44+
45+ $ node_type = $ nodes [0 ]['type ' ] ?? null ;
46+
47+ if ( empty ( $ node_type ) ) {
48+ error_log ( "[Webhook] handle_purge_nodes - No node type found in first node for key: $ key " );
49+ return ;
50+ }
51+
52+ $ event = SmartCacheEventMapper::mapEvent ( strtolower ( $ node_type ) );
53+
54+ if ( $ event === null ) {
55+ error_log ( "[Webhook] handle_purge_nodes - No mapped event found for node type: $ node_type " );
56+ return ;
57+ }
58+
59+ error_log ( "[Webhook] handle_purge_nodes - Mapped ' $ node_type' to event: $ event " );
60+
61+ $ path = $ this ->get_path_from_key ( $ key );
62+ $ smart_cache_keys = $ this ->get_smart_cache_keys ( $ nodes );
63+
64+ $ this ->trigger_webhooks ( $ event , [
65+ 'key ' => $ key ,
66+ 'path ' => $ path ,
67+ 'nodes ' => $ nodes ,
68+ 'smart_cache_keys ' => $ smart_cache_keys
69+ ] );
70+ }
71+
72+ /**
73+ * Handle general purge events from Smart Cache
74+ */
75+ public function handle_graphql_purge ( string $ key , string $ event , string $ graphql_endpoint ): void {
76+ error_log ( "[Webhook] handle_graphql_purge - Key: $ key, Event: $ event, Endpoint: $ graphql_endpoint " );
77+
78+ // Skip special prefixed keys (they're not actual entity IDs)
79+ if ( strpos ( $ key , 'skipped: ' ) === 0 || strpos ( $ key , 'list: ' ) === 0 ) {
80+ error_log ( "[Webhook] Skipping webhook trigger for special key: $ key " );
81+ return ;
82+ }
83+ $ mapped_event = SmartCacheEventMapper::mapEvent ( strtolower ( $ event ) );
84+
85+ if ( $ mapped_event === null ) {
86+ error_log ( "[Webhook] handle_graphql_purge - No mapped event found for Smart Cache event: $ event " );
87+ return ;
88+ }
89+
90+ error_log ( "[Webhook] handle_graphql_purge - Mapped ' $ event' to event: $ mapped_event " );
91+
92+ $ path = $ this ->get_path_from_key ( $ key );
93+
94+ $ this ->trigger_webhooks ( $ mapped_event , [
95+ 'key ' => $ key ,
96+ 'path ' => $ path ,
97+ 'graphql_endpoint ' => $ graphql_endpoint ,
98+ 'smart_cache_keys ' => [ $ key ]
99+ ] );
100+ }
101+
102+ /**
103+ * Trigger webhooks with Smart Cache formatted payload
104+ */
105+ private function trigger_webhooks ( string $ event , array $ payload ): void {
106+ // Event is already mapped, no need to map again
107+ $ allowed_events = $ this ->repository ->get_allowed_events ();
108+
109+ if ( ! array_key_exists ( $ event , $ allowed_events ) ) {
110+ error_log ( "[Webhook] Event ' $ event' is not in allowed events list. " );
111+ return ;
112+ }
113+
114+ // Set uri fallback if smart_cache_keys is empty
115+ if ( empty ( $ payload ['smart_cache_keys ' ] ) ) {
116+ $ payload ['uri ' ] = $ payload ['path ' ] ?? '' ;
117+ }
118+
119+ error_log ( "[Webhook] Triggering webhooks for event: $ event with payload: " . var_export ( $ payload , true ) );
120+
121+ do_action ( 'graphql_webhooks_before_trigger ' , $ event , $ payload );
122+
123+ $ webhooks = $ this ->repository ->get_all ();
124+ error_log ( "[Webhook] Found " . count ( $ webhooks ) . " webhooks for event: $ event " );
125+ $ triggered_count = 0 ;
126+
127+ foreach ( $ webhooks as $ webhook ) {
128+ if ( $ webhook ->event === $ event ) {
129+ $ this ->handler ->handle ( $ webhook , $ payload );
130+ $ triggered_count ++;
131+ }
132+ }
133+
134+ error_log ( "[Webhook] Triggered $ triggered_count webhooks for event: $ event " );
135+
136+ do_action ( 'graphql_webhooks_after_trigger ' , $ event , $ payload );
137+ }
138+
139+ /**
140+ * Extract Smart Cache keys from nodes
141+ */
142+ private function get_smart_cache_keys ( array $ nodes ): array {
143+ $ keys = [];
144+
145+ foreach ( $ nodes as $ node ) {
146+ if ( isset ( $ node ['id ' ] ) && ! empty ( $ node ['id ' ] ) ) {
147+ $ keys [] = $ node ['id ' ];
148+ } elseif ( isset ( $ node ['databaseId ' ] ) && ! empty ( $ node ['databaseId ' ] ) ) {
149+ // Fallback to databaseId if id is not available
150+ $ keys [] = $ node ['databaseId ' ];
151+ }
152+ }
153+
154+ return array_filter ( $ keys ); // Remove empty values
155+ }
156+
157+ /**
158+ * Get the path from the key
159+ *
160+ * Supports all post types, terms, users, and falls back gracefully.
161+ * Handles special prefixed keys like 'skipped:post', 'list:post', etc.
162+ *
163+ * @param string $key The key to get the path from
164+ *
165+ * @return string
166+ */
167+ public function get_path_from_key ( $ key ) {
168+ $ path = '' ;
169+
170+ if ( empty ( $ key ) ) {
171+ error_log ( "[Webhook] Empty key provided to get_path_from_key " );
172+ return $ path ;
173+ }
174+
175+ // Handle special prefixed keys (skipped:, list:, etc.)
176+ if ( strpos ( $ key , ': ' ) !== false ) {
177+ error_log ( "[Webhook] Prefixed key detected: $ key - cannot generate path for non-entity keys " );
178+ return $ path ;
179+ }
180+
181+ try {
182+ $ node_id = Relay::fromGlobalId ( $ key );
183+ } catch (Exception $ e ) {
184+ error_log ( "[Webhook] Failed to decode GraphQL global ID: $ key - " . $ e ->getMessage () );
185+ return $ path ;
186+ }
187+
188+ $ node_type = $ node_id ['type ' ] ?? null ;
189+ $ database_id = $ node_id ['id ' ] ?? null ;
190+
191+ if ( empty ( $ node_type ) || empty ( $ database_id ) ) {
192+ error_log ( "[Webhook] Invalid node ID structure for key: $ key (type: $ node_type, id: $ database_id) " );
193+ return $ path ;
194+ }
195+
196+ $ permalink = null ;
197+ error_log ( "[Webhook] Processing key: $ key (type: $ node_type, database_id: $ database_id) " );
198+
199+ switch ( $ node_type ) {
200+ case 'post ' :
201+ case 'page ' :
202+ default :
203+ $ post_id = absint ( $ database_id );
204+ if ( $ post_id > 0 ) {
205+ $ post = get_post ( $ post_id );
206+ if ( $ post && ! is_wp_error ( $ post ) ) {
207+ $ permalink = get_permalink ( $ post_id );
208+ error_log ( "[Webhook] Generated permalink for post $ post_id: $ permalink " );
209+ } else {
210+ error_log ( "[Webhook] Post not found or error for ID: $ post_id " );
211+ }
212+ }
213+ break ;
214+
215+ case 'term ' :
216+ $ term_id = absint ( $ database_id );
217+ if ( $ term_id > 0 ) {
218+ $ term = get_term ( $ term_id );
219+ if ( $ term && ! is_wp_error ( $ term ) ) {
220+ $ permalink = get_term_link ( $ term_id );
221+ error_log ( "[Webhook] Generated permalink for term $ term_id: $ permalink " );
222+ } else {
223+ error_log ( "[Webhook] Term not found or error for ID: $ term_id " );
224+ }
225+ }
226+ break ;
227+
228+ case 'user ' :
229+ $ user_id = absint ( $ database_id );
230+ if ( $ user_id > 0 ) {
231+ $ user = get_user_by ( 'id ' , $ user_id );
232+ if ( $ user instanceof \WP_User ) {
233+ $ permalink = home_url ( '/author/ ' . $ user ->user_nicename . '/ ' );
234+ error_log ( "[Webhook] Generated permalink for user $ user_id: $ permalink " );
235+ } else {
236+ error_log ( "[Webhook] User not found for ID: $ user_id " );
237+ }
238+ }
239+ break ;
240+ }
241+
242+ if ( ! empty ( $ permalink ) && is_string ( $ permalink ) && ! is_wp_error ( $ permalink ) ) {
243+ $ parsed_path = parse_url ( $ permalink , PHP_URL_PATH );
244+ if ( $ parsed_path !== false ) {
245+ $ path = $ parsed_path ;
246+ error_log ( "[Webhook] Final path for key $ key: $ path " );
247+ } else {
248+ error_log ( "[Webhook] Failed to parse URL path from permalink: $ permalink " );
249+ }
250+ } else {
251+ error_log ( "[Webhook] No valid permalink generated for key: $ key " );
252+ }
253+
254+ return $ path ;
255+ }
256+ }
0 commit comments