Skip to content

Commit f3afa97

Browse files
committed
Initial work on creating an event manager and subscribing/publishing events.
1 parent 327e397 commit f3afa97

File tree

4 files changed

+315
-16
lines changed

4 files changed

+315
-16
lines changed
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WPGraphQL\Logging\Events;
6+
7+
/**
8+
* Simple pub/sub Event Manager for WPGraphQL Logging
9+
*
10+
* Provides a lightweight event bus with optional WordPress bridge.
11+
*
12+
* Users can:
13+
* - subscribe to events using subscribe()
14+
* - publish events using publish()
15+
* - also listen via WordPress hooks: `wpgraphql_logging_event_{event_name}`
16+
*/
17+
final class EventManager {
18+
/**
19+
* In-memory map of event name to priority to listeners.
20+
*
21+
* @var array<string, array<int, array<int, callable>>>
22+
*/
23+
private static array $events = [];
24+
25+
/**
26+
* Transform listeners that can modify a payload.
27+
*
28+
* @var array<string, array<int, array<int, callable>>>
29+
*/
30+
private static array $transforms = [];
31+
32+
/**
33+
* Subscribe a listener to an event.
34+
*
35+
* @param string $event_name Event name (see Events constants).
36+
* @param callable $listener Listener callable: function(array $payload): void {}.
37+
* @param int $priority Lower runs earlier.
38+
*/
39+
public static function subscribe(string $event_name, callable $listener, int $priority = 10): void {
40+
if ( ! isset( self::$events[ $event_name ] ) ) {
41+
self::$events[ $event_name ] = [];
42+
}
43+
if ( ! isset( self::$events[ $event_name ][ $priority ] ) ) {
44+
self::$events[ $event_name ][ $priority ] = [];
45+
}
46+
47+
self::$events[ $event_name ][ $priority ][] = $listener;
48+
}
49+
50+
/**
51+
* Publish an event to all subscribers and a WordPress action bridge.
52+
*
53+
* @param string $event_name Event name (see Events constants).
54+
* @param array<string, mixed> $payload Arbitrary payload for listeners.
55+
*/
56+
public static function publish(string $event_name, array $payload = []): void {
57+
58+
$ordered_listeners = self::get_ordered_listeners( $event_name );
59+
60+
if ( [] === $ordered_listeners ) {
61+
/** @psalm-suppress HookNotFound */
62+
do_action( 'wpgraphql_logging_event_' . $event_name, $payload );
63+
return;
64+
}
65+
66+
foreach ( $ordered_listeners as $listener ) {
67+
self::invoke_listener( $listener, $payload );
68+
}
69+
70+
/** @psalm-suppress HookNotFound */
71+
do_action( 'wpgraphql_logging_event_' . $event_name, $payload );
72+
}
73+
74+
/**
75+
* Subscribe a transformer to modify the payload before it is used by core code.
76+
*
77+
* @param string $event_name Event name.
78+
* @param callable $transform function(array $payload): array {}.
79+
* @param int $priority Lower runs earlier.
80+
*/
81+
public static function subscribe_to_transform(string $event_name, callable $transform, int $priority = 10): void {
82+
if ( ! isset( self::$transforms[ $event_name ] ) ) {
83+
self::$transforms[ $event_name ] = [];
84+
}
85+
if ( ! isset( self::$transforms[ $event_name ][ $priority ] ) ) {
86+
self::$transforms[ $event_name ][ $priority ] = [];
87+
}
88+
89+
self::$transforms[ $event_name ][ $priority ][] = $transform;
90+
}
91+
92+
/**
93+
* Transform a payload by running transform subscribers and a WordPress filter bridge.
94+
*
95+
* @param string $event_name Event name.
96+
* @param array<string, mixed> $payload Initial payload.
97+
*
98+
* @return array<string, mixed> Modified payload.
99+
*/
100+
public static function transform(string $event_name, array $payload): array {
101+
102+
$ordered_transforms = self::get_ordered_transforms( $event_name );
103+
if ( [] === $ordered_transforms ) {
104+
/** @psalm-suppress HookNotFound */
105+
return apply_filters( 'wpgraphql_logging_filter_' . $event_name, $payload );
106+
}
107+
108+
foreach ( $ordered_transforms as $transform ) {
109+
$payload = self::invoke_transform( $transform, $payload );
110+
}
111+
112+
/** @psalm-suppress HookNotFound */
113+
return apply_filters( 'wpgraphql_logging_filter_' . $event_name, $payload );
114+
}
115+
116+
/**
117+
* Return listeners for an event flattened and ordered by priority (ascending).
118+
*
119+
* @param string $event_name Event name.
120+
*
121+
* @return array<int, callable>
122+
*/
123+
private static function get_ordered_listeners(string $event_name): array {
124+
if ( ! isset( self::$events[ $event_name ] ) ) {
125+
return [];
126+
}
127+
128+
$priority_to_listeners = self::$events[ $event_name ];
129+
ksort( $priority_to_listeners );
130+
131+
$ordered = [];
132+
foreach ( $priority_to_listeners as $listeners_at_priority ) {
133+
foreach ( $listeners_at_priority as $listener ) {
134+
$ordered[] = $listener;
135+
}
136+
}
137+
138+
return $ordered;
139+
}
140+
141+
/**
142+
* Return transforms for an event flattened and ordered by priority (ascending).
143+
*
144+
* @param string $event_name Event name.
145+
*
146+
* @return array<int, callable>
147+
*/
148+
private static function get_ordered_transforms(string $event_name): array {
149+
if ( ! isset( self::$transforms[ $event_name ] ) ) {
150+
return [];
151+
}
152+
153+
$priority_to_transforms = self::$transforms[ $event_name ];
154+
ksort( $priority_to_transforms );
155+
156+
$ordered = [];
157+
foreach ( $priority_to_transforms as $transforms_at_priority ) {
158+
foreach ( $transforms_at_priority as $transform ) {
159+
$ordered[] = $transform;
160+
}
161+
}
162+
163+
return $ordered;
164+
}
165+
166+
/**
167+
* Invoke a listener safely; errors are logged and do not break the pipeline.
168+
*
169+
* @param callable $listener Listener.
170+
* @param array<string, mixed> $payload Payload for listener.
171+
*/
172+
private static function invoke_listener(callable $listener, array $payload): void {
173+
try {
174+
$listener( $payload );
175+
} catch ( \Throwable $e ) {
176+
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
177+
error_log( 'WPGraphQL Logging EventManager listener error: ' . $e->getMessage() );
178+
}
179+
}
180+
181+
/**
182+
* Invoke a transform safely; returns the updated payload if valid, otherwise the original.
183+
*
184+
* @param callable $transform Transform callable.
185+
* @param array<string, mixed> $payload Current payload.
186+
*
187+
* @return array<string, mixed> Updated payload.
188+
*/
189+
private static function invoke_transform(callable $transform, array $payload): array {
190+
try {
191+
$result = $transform( $payload );
192+
if ( is_array( $result ) ) {
193+
return $result;
194+
}
195+
} catch ( \Throwable $e ) {
196+
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
197+
error_log( 'WPGraphQL Logging EventManager transform error: ' . $e->getMessage() );
198+
}
199+
200+
return $payload;
201+
}
202+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace WPGraphQL\Logging\Events;
6+
7+
/**
8+
* List of available events that users can subscribe to with the EventManager.
9+
*/
10+
final class Events {
11+
/**
12+
* Before the request is processed.
13+
*
14+
* @var string
15+
*/
16+
public const PRE_REQUEST = 'pre_request';
17+
18+
/**
19+
* After the request is processed.
20+
*
21+
* @var string
22+
*/
23+
public const POST_REQUEST = 'post_request';
24+
}

plugins/wpgraphql-logging/src/Events/QueryEventLifecycle.php

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,22 @@ class QueryEventLifecycle {
3030
/**
3131
* The logger service instance.
3232
*
33-
* @param \WPGraphQL\Logging\Logger\LoggerService $logger
33+
* @var \WPGraphQL\Logging\Logger\LoggerService
3434
*/
35-
protected function __construct( readonly LoggerService $logger ) {
36-
}
35+
protected LoggerService $logger;
36+
37+
/**
38+
* @param \WPGraphQL\Logging\Logger\LoggerService $logger
39+
*/
40+
protected function __construct( LoggerService $logger ) {
41+
$this->logger = $logger;
42+
}
3743

3844
/**
3945
* Get or create the single instance of the class.
4046
*/
4147
public static function init(): QueryEventLifecycle {
4248
if ( null === self::$instance ) {
43-
// @TODO - Add filter to allow for custom logger service.
4449
$logger = LoggerService::get_instance();
4550
self::$instance = new self( $logger );
4651
self::$instance->setup();
@@ -65,15 +70,31 @@ public function log_pre_request( string $query, ?string $operation_name, ?array
6570
'operation_name' => $operation_name,
6671
];
6772

68-
$context = apply_filters( 'wpgraphql_logging_pre_request_context', $context, $query, $variables, $operation_name );
69-
$level = apply_filters( 'wpgraphql_logging_pre_request_level', Level::Info, $query, $variables, $operation_name );
70-
$this->logger->log( $level, 'WPGraphQL Incoming Request', $context );
73+
// Allow subscribers to transform context/level via EventManager
74+
$payload = EventManager::transform( Events::PRE_REQUEST, [
75+
'query' => $query,
76+
'variables' => $variables,
77+
'operation_name' => $operation_name,
78+
'context' => $context,
79+
'level' => Level::Info,
80+
] );
81+
82+
$this->logger->log( $payload['level'], 'WPGraphQL Incoming Request', $payload['context'] );
83+
84+
// Publish event for subscribers
85+
EventManager::publish( Events::PRE_REQUEST, [
86+
'query' => $query,
87+
'variables' => $variables,
88+
'operation_name' => $operation_name,
89+
'level' => (string) $level->getName(),
90+
] );
7191
} catch ( \Throwable $e ) {
7292
// @TODO - Handle logging errors gracefully.
7393
error_log( 'Error in log_pre_request: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
7494
}
7595
}
7696

97+
7798
/**
7899
* Logs the post-request event for a GraphQL query.
79100
* This method is now hooked into 'graphql_after_execute'.
@@ -84,7 +105,7 @@ public function log_pre_request( string $query, ?string $operation_name, ?array
84105
*/
85106
public function log_post_request( $response, Request $request_instance ): void {
86107
// Extract relevant data from the WPGraphQL Request instance
87-
$params = $request_instance->get_params(); // Can be OperationParams or array of OperationParams
108+
$params = $request_instance->get_params();
88109
$query = null;
89110
$operation_name = null;
90111
$variables = null;
@@ -99,6 +120,9 @@ public function log_post_request( $response, Request $request_instance ): void {
99120
$query = $params[0]->query;
100121
$operation_name = $params[0]->operation;
101122
$variables = $params[0]->variables;
123+
} else {
124+
// Do nothing if the params are not an OperationParams object or an array of OperationParams objects
125+
return;
102126
}
103127

104128
// Determine status code if available (WPGraphQL Router sets this)
@@ -134,11 +158,15 @@ public function log_post_request( $response, Request $request_instance ): void {
134158
];
135159
$level = Level::Info;
136160

137-
// Apply filters for context and level
138-
$context = apply_filters( 'wpgraphql_logging_post_request_context', $context, $response, $request_instance );
139-
$level = apply_filters( 'wpgraphql_logging_post_request_level', $level, $response, $request_instance );
161+
$payload = EventManager::transform( Events::POST_REQUEST, [
162+
'query' => $query,
163+
'variables' => $variables,
164+
'operation_name' => $operation_name,
165+
'context' => $context,
166+
'level' => Level::Info,
167+
] );
140168

141-
$this->logger->log( $level, 'WPGraphQL Outgoing Response', $context );
169+
$this->logger->log( $payload['level'], 'WPGraphQL Response', $payload['context'] );
142170

143171
// Log errors specifically if present in the response
144172
if ( ! empty( $response_errors ) ) {
@@ -163,14 +191,25 @@ public function log_post_request( $response, Request $request_instance ): void {
163191
* Register actions and filters.
164192
*/
165193
protected function setup(): void {
194+
195+
add_action( 'do_graphql_request', [ $this, 'log_pre_request' ], 10, 3 );
196+
197+
198+
166199
/**
167-
* @psalm-suppress HookNotFound
200+
* @TODO
201+
* Pre-request: do_graphql_request
202+
* Before Query Execution: pre_graphql_execute_request
203+
* Before Field Resolve: graphql_pre_resolve_field
204+
* After Field Resolve: graphql_resolve_field
205+
* After Query Execution: graphql_execute
206+
* Response: graphql_after_execute
207+
* Failure: graphql_after_execute or graphql_request_results
168208
*/
169-
add_action( 'do_graphql_request', [ $this, 'log_pre_request' ], 10, 3 );
170209

171210
/**
172211
* @psalm-suppress HookNotFound
173212
*/
174-
add_action( 'graphql_after_execute', [ $this, 'log_post_request' ], 10, 2 );
213+
//add_action( 'graphql_after_execute', [ $this, 'log_post_request' ], 10, 2 );
175214
}
176215
}

0 commit comments

Comments
 (0)