PluginProbe
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More / 2.2.0
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More v2.2.0
2.3.0 2.2.0 2.1.1 2.1.0 2.0.0 1.10.0 1.9.1 1.9.0 1.2.1 1.2.2 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 All 59 releases
storeengine / includes / classes / event-stream-server.php

event-stream-server.php in StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More 2.2.0, at includes/classes/event-stream-server.php

206 lines 5.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * A lightweight server-sent event (SSE) handler for streaming real-time data to browsers using EventSource.
4 *
5 * @version 1.0.0
6 */
7
8 namespace StoreEngine\Classes;
9
10 use StoreEngine\Utils\Helper;
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit;
14 }
15
16 /**
17 * Class EventStreamServer
18 *
19 * A lightweight server-sent event (SSE) handler for streaming real-time data to browsers using EventSource.
20 *
21 * @example
22 * ```php
23 * $sse = new EventStreamServer();
24 * $sse->listen(function () use ($sse) {
25 * $sse->emitEvent([
26 * 'event' => 'ping',
27 * 'message' => 'hello',
28 * 'time' => current_time('mysql'),
29 * ]);
30 *
31 * // Optionally close stream under a condition
32 * if ( some_condition() ) {
33 * $sse->emitEvent([
34 * 'event' => 'end',
35 * 'message' => 'done',
36 * ], true);
37 * }
38 * });
39 * ```
40 */
41 class EventStreamServer {
42
43 /**
44 * Whether the connection should remain open.
45 *
46 * @var bool
47 */
48 private bool $connected = true;
49
50 /**
51 * @var int
52 */
53 private int $id = 0;
54
55 private bool $is_reconnect = false;
56
57 /**
58 * Prepares the HTTP headers and environment for a Server-Sent Events stream.
59 *
60 * Disables buffering and compression, sets correct headers, and ends any existing output buffers.
61 */
62 protected function setupHeaders(): void {
63 // phpcs:disable
64 $previous = error_reporting( error_reporting() ^ E_WARNING ); // Disable warnings temporarily
65
66 // Required headers for SSE
67 header( 'Content-Type: text/event-stream' );
68 header( 'Cache-Control: no-cache' );
69 header( 'Connection: keep-alive' );
70
71 // Prevent Apache buffering
72 if ( function_exists( 'apache_setenv' ) ) {
73 @apache_setenv( 'no-gzip', 1 );
74 }
75
76 // Disable PHP buffering
77 @ini_set( 'output_buffering', 'off' );
78 @ini_set( 'zlib.output_compression', 0 );
79 @ini_set( 'implicit_flush', 1 );
80
81 // NGINX-specific buffering control
82 if ( ! empty( $_SERVER['SERVER_SOFTWARE'] ) && stripos( $_SERVER['SERVER_SOFTWARE'], 'nginx' ) !== false ) {
83 header( 'X-Accel-Buffering: no' );
84 header( 'Content-Encoding: none' );
85 }
86
87
88 $this->id = intval( wp_unslash( $_SERVER['HTTP_LAST_EVENT_ID'] ?? 0 ) );
89 $this->is_reconnect = isset( $_SERVER['HTTP_LAST_EVENT_ID'] );
90
91 // Restore error reporting previous state.
92 error_reporting( $previous );
93
94 // Prevent script timeout
95 set_time_limit( 0 );
96
97 // Clean existing output buffers
98 while ( ob_get_level() != 0 ) {
99 ob_end_flush();
100 }
101
102 ob_implicit_flush( 1 );
103 flush();
104
105 // phpcs:enable
106 }
107
108 /**
109 * Starts the event stream and repeatedly invokes the provided callback.
110 *
111 * The callback should use emitEvent() to send data to the client.
112 *
113 * @param callable $callback The function that emits events. Called continuously while connected.
114 */
115 public function listen( callable $callback ): void {
116 $this->setupHeaders();
117
118 // Initial padding to prevent browser-side buffering (especially in IE)
119 echo ':' . str_repeat( ' ', 2048 ) . "\n\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
120 flush();
121
122 $start = time();
123
124 echo 'retry: ' . 1000 . "\n";
125
126 while ( $this->connected ) {
127 $upTime = ( time() - $start );
128
129 if ( $upTime % 300 === 0 ) {
130 // No updates needed, send a comment to keep the connection alive.
131 // From https://developer.mozilla.org/en-US/docs/Server-sent_events/Using_server-sent_events
132 echo ': ' . sha1( wp_rand() ) . "\n\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
133 }
134
135 try {
136 call_user_func( $callback );
137 } catch ( \Exception $e ) {
138 Helper::log_error( $e );
139
140 $this->emitEvent( [
141 'event' => 'error',
142 'message' => $e->getMessage(),
143 'code' => $e->getCode(),
144 ] );
145 }
146
147 @ob_flush();
148 flush();
149
150 // if the connection has been closed by the client we better exit the loop
151 if ( connection_aborted() || $upTime > 600 ) {
152 break;
153 }
154
155 // Prevent tight infinite loop
156 usleep( 100000 ); // 0.1 second
157 }
158 }
159
160 public function terminate(): void {
161 $this->connected = false;
162 sleep( 1 ); // Delay to allow client to receive final message
163 exit;
164 }
165
166 /**
167 * Emits a Server-Sent Event to the client.
168 *
169 * @param array $data {
170 * The data to send in the event.
171 *
172 * @type string $event Optional. The event name. Defaults to 'message'.
173 * @type string $type Optional. message type for js.
174 * @type mixed $n Additional fields included as JSON in the data payload.
175 * }
176 *
177 * @param bool $terminate Optional. Whether to terminate the connection after sending. Default false.
178 *
179 * @return void
180 */
181 public function emitEvent( array $data = [], bool $terminate = false ): void {
182 $event = esc_attr( $data['event'] ?? 'message' );
183 unset( $data['event'] );
184
185 echo "id: {$this->getNewId()}\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- integer escaping!
186 echo "event: {$event}\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
187 echo 'data: ' . wp_json_encode( $data ) . "\n\n";
188
189 // Browser padding for IE
190 echo ':' . str_repeat( ' ', 2048 ) . "\n\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
191
192 @ob_flush();
193 flush();
194
195 if ( $terminate ) {
196 $this->terminate();
197 }
198 }
199
200 public function getNewId(): int {
201 return $this->id ++;
202 }
203 }
204
205 // End of file event-stream-server.php.
206