MediaWiki master
EventDispatchEngine.php
Go to the documentation of this file.
1<?php
3
4use InvalidArgumentException;
8use Wikimedia\ObjectFactory\ObjectFactory;
10
24
29 private array $listeners = [];
30
36 private array $pendingSubscribers = [];
37
38 private HookContainer $hookContainer;
39
40 private ObjectFactory $objectFactory;
41
42 public function __construct( ObjectFactory $objectFactory, HookContainer $hookContainer ) {
43 $this->hookContainer = $hookContainer;
44 $this->objectFactory = $objectFactory;
45 }
46
56 public function dispatch( DomainEvent $event, IConnectionProvider $dbProvider ): void {
57 $this->hookContainer->run(
58 $event->getEventType(),
59 [ $event, $dbProvider, ]
60 );
61 }
62
69 public function registerListener( string $eventType, $listener ): void {
70 if ( !is_callable( $listener ) ) {
71 throw new InvalidArgumentException( '$listener must be callable' );
72 }
73
74 $this->registerTriggerIfNeeded( $eventType );
75 $this->listeners[$eventType][] = $listener;
76 }
77
81 public function registerSubscriber( $subscriber ): void {
82 if ( $subscriber instanceof DomainEventSubscriber ) {
83 // If we have a DomainEventSubscriber, apply it immediately.
84 // We can't wait until later, because we don't know what events
85 // it wants to register for, so we don't know on what event to
86 // call registerListeners().
87 $subscriber->registerListeners( $this );
88 return;
89 }
90
91 if ( !is_array( $subscriber ) ) {
92 throw new InvalidArgumentException(
93 '$subscriber must be a DomainEventSubscriber or an array'
94 );
95 }
96
97 // NOTE: we could make the 'events' key optional, and just call
98 // applySubscriberSpec() immediately if it's not given. But that would
99 // make it too easy to just forget about providing it. Callers that want
100 // to apply the subscriber immediately can just create the object instead
101 // of passing a spec array.
102 if ( !isset( $subscriber['events'] ) ) {
103 throw new InvalidArgumentException(
104 '$subscriber must contain the key "events" to specify which ' .
105 'events will trigger instantiation of the subscriber'
106 );
107 }
108
109 // Register the spec for lazy instantiation when any of the relevant
110 // events is triggered.
111 foreach ( $subscriber['events'] as $eventType ) {
112 $this->registerTriggerIfNeeded( $eventType );
113 // NOTE: must be by reference, so the spec can be resolved for all
114 // events that trigger instantiation at once.
115 $this->pendingSubscribers[$eventType][] =& $subscriber;
116 }
117 }
118
123 private function resolveSubscribers( string $eventType ) {
124 // Copy the list of pending subscribers, since applying subscribers
125 // may modify $this->pendingSubscribers again.
126 $pending = $this->pendingSubscribers[$eventType] ?? [];
127
128 // Blank subscribers for this event. Once the subscribers have been
129 // applied, all their listeners are registered, and the subscribers
130 // are no longer relevant.
131 $this->pendingSubscribers[$eventType] = [];
132
133 // NOTE: entries in $this->subscribers are by reference!
134 foreach ( $pending as &$spec ) {
135 // NOTE: $spec may be empty if it was previously resolved through
136 // a different event type.
137 if ( !$spec ) {
138 continue;
139 }
140
141 $this->applySubscriberSpec( $spec );
142
143 // Empty the spec, so it's not re-triggered though another event
144 // that also references it.
145 $spec = [];
146 }
147
148 if ( $this->pendingSubscribers[$eventType] ) {
149 // If more pending subscribers got added, recurse!
150 $this->resolveSubscribers( $eventType );
151 }
152 }
153
154 private function applySubscriberSpec( array $spec ) {
156 $subscriber = $this->objectFactory->createObject(
157 $spec,
158 [ 'assertClass' => DomainEventSubscriber::class, ]
159 );
160
161 if ( $subscriber instanceof InitializableDomainEventSubscriber ) {
162 $subscriber->initSubscriber( $spec );
163 }
164
165 $subscriber->registerListeners( $this );
166 }
167
172 private function registerTriggerIfNeeded( string $eventName ) {
173 if (
174 isset( $this->listeners[ $eventName ] ) ||
175 isset( $this->pendingSubscribers[ $eventName ] )
176 ) {
177 // If there is already a listener or subscriber, then we assume
178 // we already registered a trigger.
179 return;
180 }
181
182 $this->hookContainer->register(
183 $eventName,
184 function ( DomainEvent $event, IConnectionProvider $dbProvider ) {
185 $this->dispatchInternal( $event, $dbProvider );
186 }
187 );
188 }
189
194 private function dispatchInternal( DomainEvent $event, IConnectionProvider $dbProvider ) {
195 $this->resolveSubscribers( $event->getEventType() );
196 $listeners = $this->listeners[ $event->getEventType() ] ?? [];
197
198 foreach ( $listeners as $callback ) {
199 $this->push( $callback, $event, $dbProvider );
200 }
201 }
202
207 private function push(
208 callable $callback,
209 DomainEvent $event,
210 IConnectionProvider $dbProvider
211 ) {
212 // TODO: DeferredUpdates should take a more abstract representation of
213 // the current transactional context!
214 $dbw = $dbProvider->getPrimaryDatabase();
215 DeferredUpdates::addUpdate( new MWCallableUpdate(
216 function () use ( $callback, $event, $dbProvider ) {
217 $this->invoke( $callback, $event, $dbProvider );
218 },
219 __METHOD__,
220 [ $dbw ]
221 ) );
222 }
223
227 private function invoke(
228 callable $callback,
229 DomainEvent $event,
230 IConnectionProvider $dbProvider
231 ) {
232 $callback( $event, $dbProvider );
233 }
234
235}
run()
Run the job.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
Defer callable updates to run later in the PHP process.
DeferrableUpdate for closure/callable.
Base class for domain event objects to be used with DomainEventDispatcher.
Implementation of DomainEventDispatcher and DomainEventSource based on HookContainer and DeferredUpda...
dispatch(DomainEvent $event, IConnectionProvider $dbProvider)
Emit the given event to any listeners that have been registered for the respective event type.
registerListener(string $eventType, $listener)
Add a listener that will be notified on events of the given type.
__construct(ObjectFactory $objectFactory, HookContainer $hookContainer)
Service for sending domain events to registered listeners.
Service object for registering listeners for domain events.
Objects implementing DomainEventSubscriber represent a collection of related event listeners.
Provide primary and replica IDatabase connections.