Skip to content

Commit 35afecf

Browse files
committed
Move smart cache into it's own class
1 parent 3e1c4ee commit 35afecf

File tree

2 files changed

+383
-121
lines changed

2 files changed

+383
-121
lines changed
Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
1+
<?php
2+
3+
namespace WPGraphQL\Webhooks\Events;
4+
5+
use GraphQLRelay\Relay;
6+
7+
/**
8+
* Handles Smart Cache events and consolidates them before triggering webhooks
9+
*/
10+
class SmartCacheEventHandler {
11+
/**
12+
* Stores Smart Cache events temporarily to consolidate them
13+
* @var array
14+
*/
15+
private array $buffer = [];
16+
17+
/**
18+
* Timer to process buffered Smart Cache events
19+
* @var int|false
20+
*/
21+
private $timer = false;
22+
23+
/**
24+
* Callback to trigger webhooks
25+
* @var callable
26+
*/
27+
private $webhook_trigger_callback;
28+
29+
/**
30+
* Event action mapping
31+
* @var array
32+
*/
33+
private const EVENT_MAP = [
34+
'create' => 'smart_cache_created',
35+
'update' => 'smart_cache_updated',
36+
'delete' => 'smart_cache_deleted',
37+
];
38+
39+
/**
40+
* Constructor
41+
*
42+
* @param callable $webhook_trigger_callback Callback to trigger webhooks
43+
*/
44+
public function __construct( callable $webhook_trigger_callback ) {
45+
$this->webhook_trigger_callback = $webhook_trigger_callback;
46+
}
47+
48+
/**
49+
* Initialize hooks
50+
*/
51+
public function init() {
52+
add_action( 'graphql_purge', [ $this, 'handle_graphql_purge' ], 10, 3 );
53+
add_action( 'wpgraphql_cache_purge_nodes', [ $this, 'handle_cache_purge_nodes' ], 10, 2 );
54+
add_action( 'shutdown', [ $this, 'process_buffer' ] );
55+
}
56+
57+
/**
58+
* Handle graphql_purge event
59+
*
60+
* @param string $key Cache key being purged
61+
* @param string $event Event type (e.g., post_UPDATE)
62+
* @param string $graphql_endpoint GraphQL endpoint URL
63+
*/
64+
public function handle_graphql_purge( $key, $event, $graphql_endpoint ) {
65+
$parsed = $this->parse_event( $event );
66+
if ( ! $parsed ) {
67+
return;
68+
}
69+
70+
$this->buffer_event( $key, $parsed['post_type'], $parsed['action'], $graphql_endpoint );
71+
}
72+
73+
/**
74+
* Handle cache purge nodes event
75+
*
76+
* @param string $key Cache key
77+
* @param array $nodes Nodes being purged
78+
*/
79+
public function handle_cache_purge_nodes( $key, $nodes ) {
80+
$payload = [
81+
'cache_key' => $key,
82+
'nodes' => $nodes,
83+
'nodes_count' => count( $nodes ),
84+
'timestamp' => current_time( 'c' ),
85+
];
86+
87+
call_user_func( $this->webhook_trigger_callback, 'smart_cache_nodes_purged', $payload );
88+
}
89+
90+
/**
91+
* Parse event string into components
92+
*
93+
* @param string $event Event string (e.g., post_UPDATE)
94+
* @return array|null Array with 'post_type' and 'action' keys, or null if invalid
95+
*/
96+
private function parse_event( string $event ): ?array {
97+
$parts = explode( '_', $event );
98+
if ( count( $parts ) !== 2 ) {
99+
return null;
100+
}
101+
102+
return [
103+
'post_type' => $parts[0],
104+
'action' => strtolower( $parts[1] ),
105+
];
106+
}
107+
108+
/**
109+
* Buffer an event for consolidated processing
110+
*
111+
* @param string $key Cache key
112+
* @param string $post_type Post type
113+
* @param string $action Action (create, update, delete)
114+
* @param string $graphql_endpoint GraphQL endpoint URL
115+
*/
116+
private function buffer_event( string $key, string $post_type, string $action, string $graphql_endpoint ) {
117+
$buffer_key = "{$post_type}_{$action}";
118+
119+
if ( ! isset( $this->buffer[ $buffer_key ] ) ) {
120+
$this->buffer[ $buffer_key ] = [
121+
'post_type' => $post_type,
122+
'action' => $action,
123+
'graphql_endpoint' => $graphql_endpoint,
124+
'keys' => [],
125+
'objects' => [],
126+
];
127+
}
128+
129+
$key_info = $this->analyze_cache_key( $key );
130+
131+
// Extract object information if it's a Relay ID
132+
if ( $key_info['type'] === 'relay_id' && isset( $key_info['decoded'] ) ) {
133+
$this->add_object_to_buffer( $buffer_key, $key_info['decoded'], $action );
134+
}
135+
136+
$this->buffer[ $buffer_key ]['keys'][] = $key_info;
137+
$this->schedule_processing();
138+
}
139+
140+
/**
141+
* Analyze a cache key and determine its type
142+
*
143+
* @param string $key Cache key
144+
* @return array Key information with 'key', 'type', and optionally 'decoded'
145+
*/
146+
private function analyze_cache_key( string $key ): array {
147+
$info = [
148+
'key' => $key,
149+
'type' => $this->classify_key_type( $key ),
150+
];
151+
152+
// Try to decode Relay IDs
153+
if ( $info['type'] === 'relay_id' && class_exists( Relay::class ) ) {
154+
try {
155+
$decoded = Relay::fromGlobalId( $key );
156+
if ( ! empty( $decoded['type'] ) && ! empty( $decoded['id'] ) ) {
157+
$info['decoded'] = $decoded;
158+
}
159+
} catch ( \Exception $e ) {
160+
// Not a valid Relay ID after all
161+
$info['type'] = 'unknown';
162+
}
163+
}
164+
165+
return $info;
166+
}
167+
168+
/**
169+
* Classify the type of cache key
170+
*
171+
* @param string $key Cache key
172+
* @return string Key type: 'list', 'skipped', 'relay_id', or 'unknown'
173+
*/
174+
private function classify_key_type( string $key ): string {
175+
if ( strpos( $key, 'list:' ) === 0 ) {
176+
return 'list';
177+
}
178+
179+
if ( strpos( $key, 'skipped:' ) === 0 ) {
180+
return 'skipped';
181+
}
182+
183+
// Assume it might be a Relay ID if it looks like base64
184+
if ( preg_match( '/^[A-Za-z0-9+\/]+=*$/', $key ) ) {
185+
return 'relay_id';
186+
}
187+
188+
return 'unknown';
189+
}
190+
191+
/**
192+
* Add object data to buffer
193+
*
194+
* @param string $buffer_key Buffer key
195+
* @param array $decoded Decoded Relay ID data
196+
* @param string $action Action being performed
197+
*/
198+
private function add_object_to_buffer( string $buffer_key, array $decoded, string $action ) {
199+
$object_key = "{$decoded['type']}:{$decoded['id']}";
200+
201+
if ( isset( $this->buffer[ $buffer_key ]['objects'][ $object_key ] ) ) {
202+
return; // Already added
203+
}
204+
205+
$object_data = $this->fetch_object_data( $decoded['type'], (int) $decoded['id'], $action );
206+
if ( $object_data ) {
207+
$this->buffer[ $buffer_key ]['objects'][ $object_key ] = $object_data;
208+
}
209+
}
210+
211+
/**
212+
* Fetch object data based on type and ID
213+
*
214+
* @param string $type Object type
215+
* @param int $id Object ID
216+
* @param string $action The action being performed
217+
* @return array|null Object data or null if not found
218+
*/
219+
private function fetch_object_data( string $type, int $id, string $action ): ?array {
220+
// For delete actions, just return minimal data
221+
if ( $action === 'delete' ) {
222+
return [
223+
'id' => $id,
224+
'type' => $type,
225+
'deleted' => true,
226+
];
227+
}
228+
229+
$fetchers = [
230+
'post' => [ $this, 'fetch_post_data' ],
231+
'term' => [ $this, 'fetch_term_data' ],
232+
'user' => [ $this, 'fetch_user_data' ],
233+
];
234+
235+
if ( isset( $fetchers[ $type ] ) ) {
236+
return call_user_func( $fetchers[ $type ], $id );
237+
}
238+
239+
return null;
240+
}
241+
242+
/**
243+
* Fetch post data
244+
*
245+
* @param int $id Post ID
246+
* @return array|null
247+
*/
248+
private function fetch_post_data( int $id ): ?array {
249+
$post = get_post( $id );
250+
if ( ! $post ) {
251+
return null;
252+
}
253+
254+
return [
255+
'id' => $post->ID,
256+
'title' => $post->post_title,
257+
'status' => $post->post_status,
258+
'type' => $post->post_type,
259+
'url' => get_permalink( $post ),
260+
];
261+
}
262+
263+
/**
264+
* Fetch term data
265+
*
266+
* @param int $id Term ID
267+
* @return array|null
268+
*/
269+
private function fetch_term_data( int $id ): ?array {
270+
$term = get_term( $id );
271+
if ( ! $term || is_wp_error( $term ) ) {
272+
return null;
273+
}
274+
275+
return [
276+
'id' => $term->term_id,
277+
'name' => $term->name,
278+
'taxonomy' => $term->taxonomy,
279+
'url' => get_term_link( $term ),
280+
];
281+
}
282+
283+
/**
284+
* Fetch user data
285+
*
286+
* @param int $id User ID
287+
* @return array|null
288+
*/
289+
private function fetch_user_data( int $id ): ?array {
290+
$user = get_user_by( 'id', $id );
291+
if ( ! $user ) {
292+
return null;
293+
}
294+
295+
return [
296+
'id' => $user->ID,
297+
'login' => $user->user_login,
298+
'display_name' => $user->display_name,
299+
'url' => get_author_posts_url( $user->ID ),
300+
];
301+
}
302+
303+
/**
304+
* Schedule buffer processing
305+
*/
306+
private function schedule_processing() {
307+
if ( $this->timer !== false ) {
308+
return; // Already scheduled
309+
}
310+
311+
$this->timer = wp_schedule_single_event( time() + 1, 'wpgraphql_webhooks_process_smart_cache' );
312+
add_action( 'wpgraphql_webhooks_process_smart_cache', [ $this, 'process_buffer' ] );
313+
}
314+
315+
/**
316+
* Process the buffered events
317+
*/
318+
public function process_buffer() {
319+
if ( empty( $this->buffer ) ) {
320+
return;
321+
}
322+
323+
foreach ( $this->buffer as $data ) {
324+
$webhook_event = self::EVENT_MAP[ $data['action'] ] ?? null;
325+
if ( ! $webhook_event ) {
326+
continue;
327+
}
328+
329+
$payload = $this->build_payload( $data );
330+
call_user_func( $this->webhook_trigger_callback, $webhook_event, $payload );
331+
}
332+
333+
$this->buffer = [];
334+
$this->timer = false;
335+
}
336+
337+
/**
338+
* Build webhook payload from buffered data
339+
*
340+
* @param array $data Buffered event data
341+
* @return array
342+
*/
343+
private function build_payload( array $data ): array {
344+
return [
345+
'post_type' => $data['post_type'],
346+
'action' => $data['action'],
347+
'graphql_endpoint' => $data['graphql_endpoint'],
348+
'timestamp' => current_time( 'c' ),
349+
'cache_keys_purged' => count( $data['keys'] ),
350+
'objects_affected' => array_values( $data['objects'] ),
351+
'cache_key_summary' => $this->summarize_keys( $data['keys'] ),
352+
];
353+
}
354+
355+
/**
356+
* Summarize cache keys by type
357+
*
358+
* @param array $keys Array of key information
359+
* @return array Summary with counts by type
360+
*/
361+
private function summarize_keys( array $keys ): array {
362+
$summary = array_fill_keys( [ 'relay_id', 'list', 'skipped', 'unknown' ], 0 );
363+
364+
foreach ( $keys as $key_info ) {
365+
$summary[ $key_info['type'] ]++;
366+
}
367+
368+
return array_filter( $summary ); // Remove zeros
369+
}
370+
}

0 commit comments

Comments
 (0)