Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
69.57% |
48 / 69 |
|
35.71% |
5 / 14 |
CRAP | |
0.00% |
0 / 1 |
EventManager | |
69.57% |
48 / 69 |
|
35.71% |
5 / 14 |
75.59 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
init | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
initialize_rest_endpoint | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
initialize_cron | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
rest_api_init | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
add_minutely_schedule | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
shutdown | |
76.92% |
10 / 13 |
|
0.00% |
0 / 1 |
7.60 | |||
add_subscriber | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
get_subscribers | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
get_listeners | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
initialize_listeners | |
40.00% |
2 / 5 |
|
0.00% |
0 / 1 |
7.46 | |||
push | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
send_request_events | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
5.03 | |||
send_saved_events_batch | |
89.47% |
17 / 19 |
|
0.00% |
0 / 1 |
8.07 |
1 | <?php |
2 | |
3 | namespace NewfoldLabs\WP\Module\Data; |
4 | |
5 | use Exception; |
6 | use NewfoldLabs\WP\Module\Data\EventQueue\EventQueue; |
7 | use NewfoldLabs\WP\Module\Data\Listeners\Listener; |
8 | use WP_Error; |
9 | |
10 | /** |
11 | * Class to manage event subscriptions |
12 | */ |
13 | class EventManager { |
14 | |
15 | /** |
16 | * List of default listener category classes |
17 | * |
18 | * @var Listener[] |
19 | */ |
20 | const LISTENERS = array( |
21 | '\\NewfoldLabs\\WP\\Module\\Data\\Listeners\\Admin', |
22 | '\\NewfoldLabs\\WP\\Module\\Data\\Listeners\\Content', |
23 | '\\NewfoldLabs\\WP\\Module\\Data\\Listeners\\Cron', |
24 | '\\NewfoldLabs\\WP\\Module\\Data\\Listeners\\Jetpack', |
25 | '\\NewfoldLabs\\WP\\Module\\Data\\Listeners\\Plugin', |
26 | '\\NewfoldLabs\\WP\\Module\\Data\\Listeners\\BluehostPlugin', |
27 | '\\NewfoldLabs\\WP\\Module\\Data\\Listeners\\SiteHealth', |
28 | '\\NewfoldLabs\\WP\\Module\\Data\\Listeners\\Theme', |
29 | '\\NewfoldLabs\\WP\\Module\\Data\\Listeners\\Commerce', |
30 | '\\NewfoldLabs\\WP\\Module\\Data\\Listeners\\Yoast', |
31 | '\\NewfoldLabs\\WP\\Module\\Data\\Listeners\\WonderCart', |
32 | '\\NewfoldLabs\\WP\\Module\\Data\\Listeners\\WPMail', |
33 | ); |
34 | |
35 | /** |
36 | * @var EventQueue |
37 | */ |
38 | private $event_queue; |
39 | |
40 | /** |
41 | * List of subscribers receiving event data |
42 | * |
43 | * @var array |
44 | */ |
45 | private $subscribers = array(); |
46 | |
47 | /** |
48 | * The queue of events logged in the current request |
49 | * |
50 | * @var Event[] |
51 | */ |
52 | private $queue = array(); |
53 | |
54 | /** |
55 | * The maximum number of attempts to send an event |
56 | * |
57 | * @var int |
58 | */ |
59 | private $attempts_limit = 3; |
60 | |
61 | /** |
62 | * Constructor |
63 | * |
64 | * Inject or instantiate required objects. |
65 | * |
66 | * @param ?EventQueue $event_queue |
67 | */ |
68 | public function __construct( |
69 | ?EventQueue $event_queue = null |
70 | ) { |
71 | |
72 | $this->event_queue = $event_queue ?? EventQueue::getInstance(); |
73 | } |
74 | |
75 | /** |
76 | * Initialize the Event Manager |
77 | */ |
78 | public function init(): void { |
79 | $this->initialize_listeners(); |
80 | $this->initialize_cron(); |
81 | |
82 | // Register the shutdown hook which sends or saves all queued events |
83 | add_action( 'shutdown', array( $this, 'shutdown' ) ); |
84 | } |
85 | |
86 | /** |
87 | * Initialize the REST API endpoint. |
88 | * |
89 | * @see Data::init() |
90 | */ |
91 | public function initialize_rest_endpoint() { |
92 | // Register REST endpoint. |
93 | add_action( 'rest_api_init', array( $this, 'rest_api_init' ) ); |
94 | } |
95 | |
96 | /** |
97 | * Handle setting up the scheduled job for sending updates |
98 | */ |
99 | protected function initialize_cron(): void { |
100 | // Ensure there is a minutely option in the cron schedules |
101 | // phpcs:disable WordPress.WP.CronInterval.CronSchedulesInterval |
102 | add_filter( 'cron_schedules', array( $this, 'add_minutely_schedule' ) ); |
103 | |
104 | // Minutely cron hook |
105 | add_action( 'nfd_data_sync_cron', array( $this, 'send_saved_events_batch' ) ); |
106 | |
107 | // Register the cron task |
108 | if ( ! wp_next_scheduled( 'nfd_data_sync_cron' ) ) { |
109 | wp_schedule_event( time() + constant( 'MINUTE_IN_SECONDS' ), 'minutely', 'nfd_data_sync_cron' ); |
110 | } |
111 | } |
112 | |
113 | /** |
114 | * Register the event route. |
115 | */ |
116 | public function rest_api_init() { |
117 | $controller = new API\Events( Data::$instance->hiive, $this ); |
118 | $controller->register_routes(); |
119 | } |
120 | |
121 | /** |
122 | * Add the weekly option to cron schedules if it doesn't exist |
123 | * |
124 | * @hooked cron_schedules |
125 | * |
126 | * @param array<string, array{interval:int, display:string}> $schedules List of defined cron schedule options. |
127 | * |
128 | * @return array<string, array{interval:int, display:string}> |
129 | */ |
130 | public function add_minutely_schedule( $schedules ) { |
131 | if ( ! array_key_exists( 'minutely', $schedules ) || |
132 | MINUTE_IN_SECONDS !== $schedules['minutely']['interval'] |
133 | ) { |
134 | $schedules['minutely'] = array( |
135 | 'interval' => MINUTE_IN_SECONDS, |
136 | 'display' => __( 'Once Every Minute' ), |
137 | ); |
138 | } |
139 | |
140 | return $schedules; |
141 | } |
142 | |
143 | /** |
144 | * Sends or saves all queued events at the end of the request |
145 | * |
146 | * @hooked shutdown |
147 | */ |
148 | public function shutdown(): void { |
149 | |
150 | // Due to a bug sending too many events, we are temporarily disabling these. |
151 | $disabled_events = array( 'pageview', 'page_view', 'wp_mail', 'plugin_updated' ); |
152 | foreach ( $this->queue as $index => $event ) { |
153 | if ( in_array( $event->key, $disabled_events, true ) ) { |
154 | unset( $this->queue[ $index ] ); |
155 | } |
156 | } |
157 | |
158 | // Separate out the async events |
159 | $async = array(); |
160 | foreach ( $this->queue as $index => $event ) { |
161 | if ( 'pageview' === $event->key ) { |
162 | $async[] = $event; |
163 | unset( $this->queue[ $index ] ); |
164 | } |
165 | } |
166 | |
167 | // Save any async events for sending later |
168 | if ( ! empty( $async ) ) { |
169 | $this->event_queue->queue()->push( $async ); |
170 | } |
171 | |
172 | // Any remaining items in the queue should be sent now |
173 | if ( ! empty( $this->queue ) ) { |
174 | $this->send_request_events( $this->queue ); |
175 | } |
176 | } |
177 | |
178 | /** |
179 | * Register a new event subscriber |
180 | * |
181 | * @param SubscriberInterface $subscriber Class subscribing to event updates |
182 | */ |
183 | public function add_subscriber( SubscriberInterface $subscriber ): void { |
184 | $this->subscribers[] = $subscriber; |
185 | } |
186 | |
187 | /** |
188 | * Returns filtered list of registered event subscribers |
189 | * |
190 | * @return array<SubscriberInterface> List of subscriber classes |
191 | */ |
192 | public function get_subscribers() { |
193 | return apply_filters( 'newfold_data_subscribers', $this->subscribers ); |
194 | } |
195 | |
196 | /** |
197 | * Return an array of listener classes |
198 | * |
199 | * @return Listener[] List of listener classes |
200 | */ |
201 | public function get_listeners() { |
202 | return apply_filters( 'newfold_data_listeners', $this::LISTENERS ); |
203 | } |
204 | |
205 | /** |
206 | * Initialize event listener classes |
207 | */ |
208 | protected function initialize_listeners(): void { |
209 | if ( defined( 'BURST_SAFETY_MODE' ) && constant( 'BURST_SAFETY_MODE' ) ) { |
210 | // Disable listeners when site is under heavy load |
211 | return; |
212 | } |
213 | foreach ( $this->get_listeners() as $listener ) { |
214 | $class = new $listener( $this ); |
215 | $class->register_hooks(); |
216 | } |
217 | } |
218 | |
219 | /** |
220 | * Push event data onto the queue |
221 | * |
222 | * @param Event $event Details about the action taken |
223 | */ |
224 | public function push( Event $event ): void { |
225 | /** |
226 | * The `nfd_event_log` action is handled in the notification module. |
227 | * |
228 | * @see wp-module-notifications/notifications.php |
229 | */ |
230 | do_action( 'nfd_event_log', $event->key, $event ); |
231 | $this->queue[] = $event; |
232 | } |
233 | |
234 | /** |
235 | * Send queued events to all subscribers; store them if they fail |
236 | * |
237 | * @used-by EventManager::shutdown() |
238 | * |
239 | * @param Event[] $events A list of events |
240 | */ |
241 | protected function send_request_events( array $events ): void { |
242 | |
243 | foreach ( $this->get_subscribers() as $subscriber ) { |
244 | /** |
245 | * @var array{succeededEvents:array,failedEvents:array}|WP_Error $response |
246 | */ |
247 | $response = $subscriber->notify( $events ); |
248 | |
249 | if ( ! ( $subscriber instanceof HiiveConnection ) ) { |
250 | continue; |
251 | } |
252 | |
253 | if ( is_wp_error( $response ) ) { |
254 | $this->event_queue->queue()->push( $events ); |
255 | continue; |
256 | } |
257 | |
258 | if ( ! empty( $response['failedEvents'] ) ) { |
259 | $this->event_queue->queue()->push( $response['failedEvents'] ); |
260 | } |
261 | } |
262 | } |
263 | |
264 | /** |
265 | * Send stored events to all subscribers; remove/release them from the store aftewards. |
266 | * |
267 | * @hooked nfd_data_sync_cron |
268 | */ |
269 | public function send_saved_events_batch(): void { |
270 | |
271 | $queue = $this->event_queue->queue(); |
272 | |
273 | $queue->remove_events_exceeding_attempts_limit( $this->attempts_limit ); |
274 | |
275 | /** |
276 | * Array indexed by the table row id. |
277 | * |
278 | * @var array<int,Event> $events |
279 | */ |
280 | $events = $queue->pull( 50 ); |
281 | |
282 | // If queue is empty, do nothing. |
283 | if ( empty( $events ) ) { |
284 | return; |
285 | } |
286 | |
287 | // Reserve the events in the queue so they are not processed by another instance. |
288 | if ( ! $queue->reserve( array_keys( $events ) ) ) { |
289 | // If the events fail to reserve, they will be repeatedly retried. |
290 | // It would be good to log this somewhere. |
291 | return; |
292 | } |
293 | |
294 | $queue->increment_attempt( array_keys( $events ) ); |
295 | |
296 | foreach ( $this->get_subscribers() as $subscriber ) { |
297 | /** |
298 | * @var array{succeededEvents:array,failedEvents:array}|WP_Error $response |
299 | */ |
300 | $response = $subscriber->notify( $events ); |
301 | |
302 | if ( ! ( $subscriber instanceof HiiveConnection ) ) { |
303 | continue; |
304 | } |
305 | |
306 | if ( is_wp_error( $response ) ) { |
307 | $queue->release( array_keys( $events ) ); |
308 | continue; |
309 | } |
310 | |
311 | // Remove from the queue. |
312 | if ( ! empty( $response['succeededEvents'] ) ) { |
313 | $queue->remove( array_keys( $response['succeededEvents'] ) ); |
314 | } |
315 | |
316 | // Release the 'reserve' we placed on the entry, so it will be tried again later. |
317 | if ( ! empty( $response['failedEvents'] ) ) { |
318 | $queue->release( array_keys( $response['failedEvents'] ) ); |
319 | } |
320 | } |
321 | } |
322 | } |