PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.5.0
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.5.0
5.5.0 5.4.0 5.3.2 5.3.1 5.1.6 5.1.5 trunk 2.1.5 2.11 2.12 2.13 2.15 3.0.0 3.0.1 3.0.2 3.0.3 3.0.5 3.0.51 3.0.60 3.0.61 3.0.62 3.0.70 3.0.71 3.0.72 3.1.0 All 34 releases
double-opt-in / src / EventSystem / EventDispatcher.php

EventDispatcher.php in Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification 5.5.0, at src/EventSystem/EventDispatcher.php

246 lines 5.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Event Dispatcher Implementation
4 *
5 * @package Forge12\DoubleOptIn\EventSystem
6 * @since 4.0.0
7 */
8
9 namespace Forge12\DoubleOptIn\EventSystem;
10
11 use Forge12\Shared\LoggerInterface;
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 /**
18 * Class EventDispatcher
19 *
20 * Dispatches events to registered listeners with WordPress hook bridging support.
21 */
22 class EventDispatcher implements EventDispatcherInterface {
23
24 /**
25 * Registered listeners grouped by event name and priority.
26 *
27 * @var array<string, array<int, callable[]>>
28 */
29 private array $listeners = array();
30
31 /**
32 * Sorted listeners cache.
33 *
34 * @var array<string, callable[]>
35 */
36 private array $sorted = array();
37
38 /**
39 * Logger instance.
40 *
41 * @var LoggerInterface
42 */
43 private LoggerInterface $logger;
44
45 /**
46 * Whether to bridge events to WordPress hooks.
47 *
48 * @var bool
49 */
50 private bool $bridgeToWordPress;
51
52 /**
53 * Constructor.
54 *
55 * @param LoggerInterface $logger The logger instance.
56 * @param bool $bridgeToWordPress Whether to bridge to WordPress hooks (default: true).
57 */
58 public function __construct( LoggerInterface $logger, bool $bridgeToWordPress = true ) {
59 $this->logger = $logger;
60 $this->bridgeToWordPress = $bridgeToWordPress;
61 }
62
63 /**
64 * {@inheritdoc}
65 */
66 public function dispatch( object $event ): object {
67 $eventName = get_class( $event );
68
69 $this->logger->debug(
70 'Dispatching event',
71 array(
72 'plugin' => 'double-opt-in',
73 'component' => 'event-dispatcher',
74 'event' => $eventName,
75 'triggered_by' => $event instanceof Event ? $event->getTriggeredBy() : null,
76 )
77 );
78
79 // Bridge to WordPress hooks for backward compatibility
80 if ( $this->bridgeToWordPress && $event instanceof Event && $event->shouldBridgeToWordPress() ) {
81 $wpHookName = $event::getWordPressHookName();
82 if ( ! empty( $wpHookName ) ) {
83 /**
84 * Fire the WordPress action for backward compatibility.
85 *
86 * This allows existing code using add_action() to continue working.
87 */
88 do_action( $wpHookName, $event );
89
90 $this->logger->debug(
91 'WordPress hook bridged',
92 array(
93 'plugin' => 'double-opt-in',
94 'component' => 'event-dispatcher',
95 'wp_hook' => $wpHookName,
96 )
97 );
98 }
99 }
100
101 // Dispatch to typed event listeners
102 $listeners = $this->getListeners( $eventName );
103
104 foreach ( $listeners as $listener ) {
105 if ( $event instanceof StoppableEventInterface && $event->isPropagationStopped() ) {
106 $this->logger->debug(
107 'Event propagation stopped',
108 array(
109 'plugin' => 'double-opt-in',
110 'component' => 'event-dispatcher',
111 'event' => $eventName,
112 )
113 );
114 break;
115 }
116
117 try {
118 $listener( $event );
119 } catch ( \Throwable $e ) {
120 $this->logger->error(
121 'Event listener threw exception',
122 array(
123 'plugin' => 'double-opt-in',
124 'component' => 'event-dispatcher',
125 'event' => $eventName,
126 'exception' => $e->getMessage(),
127 )
128 );
129 }
130 }
131
132 return $event;
133 }
134
135 /**
136 * {@inheritdoc}
137 */
138 public function addListener( string $eventName, callable $listener, int $priority = 10 ): void {
139 $this->listeners[ $eventName ][ $priority ][] = $listener;
140 unset( $this->sorted[ $eventName ] );
141
142 $this->logger->debug(
143 'Listener registered',
144 array(
145 'plugin' => 'double-opt-in',
146 'component' => 'event-dispatcher',
147 'event' => $eventName,
148 'priority' => $priority,
149 )
150 );
151 }
152
153 /**
154 * {@inheritdoc}
155 */
156 public function removeListener( string $eventName, callable $listener ): void {
157 if ( ! isset( $this->listeners[ $eventName ] ) ) {
158 return;
159 }
160
161 foreach ( $this->listeners[ $eventName ] as $priority => &$listeners ) {
162 $key = array_search( $listener, $listeners, true );
163 if ( $key !== false ) {
164 unset( $listeners[ $key ] );
165 unset( $this->sorted[ $eventName ] );
166 }
167 }
168 }
169
170 /**
171 * {@inheritdoc}
172 */
173 public function hasListeners( string $eventName ): bool {
174 return ! empty( $this->listeners[ $eventName ] );
175 }
176
177 /**
178 * {@inheritdoc}
179 */
180 public function getListeners( string $eventName ): array {
181 if ( ! isset( $this->listeners[ $eventName ] ) ) {
182 return array();
183 }
184
185 if ( ! isset( $this->sorted[ $eventName ] ) ) {
186 $this->sortListeners( $eventName );
187 }
188
189 return $this->sorted[ $eventName ];
190 }
191
192 /**
193 * Sort listeners by priority.
194 *
195 * Higher priority numbers execute first (like WordPress hooks).
196 *
197 * @param string $eventName The event name.
198 *
199 * @return void
200 */
201 private function sortListeners( string $eventName ): void {
202 $this->sorted[ $eventName ] = array();
203
204 if ( isset( $this->listeners[ $eventName ] ) ) {
205 krsort( $this->listeners[ $eventName ] ); // Higher priority first
206 foreach ( $this->listeners[ $eventName ] as $listeners ) {
207 foreach ( $listeners as $listener ) {
208 $this->sorted[ $eventName ][] = $listener;
209 }
210 }
211 }
212 }
213
214 /**
215 * Register a listener from a WordPress hook.
216 *
217 * This method allows bridging existing WordPress hooks to typed events.
218 *
219 * @param string $wpHookName The WordPress hook name.
220 * @param string $eventClass The event class to listen for.
221 *
222 * @return void
223 */
224 public function bridgeWordPressHook( string $wpHookName, string $eventClass ): void {
225 add_action(
226 $wpHookName,
227 function ( ...$args ) use ( $eventClass ) {
228 // If the first argument is already our typed event, skip
229 if ( isset( $args[0] ) && $args[0] instanceof Event ) {
230 return;
231 }
232
233 $this->logger->debug(
234 'WordPress hook received (legacy)',
235 array(
236 'plugin' => 'double-opt-in',
237 'component' => 'event-dispatcher',
238 'wp_hook' => $eventClass::getWordPressHookName(),
239 )
240 );
241 },
242 1
243 );
244 }
245 }
246