MediaWiki REL1_35
HookContainer.php
Go to the documentation of this file.
1<?php
27
28use Closure;
29use MWDebug;
30use MWException;
31use UnexpectedValueException;
32use Wikimedia\Assert\Assert;
33use Wikimedia\ObjectFactory;
34use Wikimedia\ScopedCallback;
35use Wikimedia\Services\SalvageableService;
36
44class HookContainer implements SalvageableService {
45
48
50 private $handlersByName = [];
51
53 private $registry;
54
57
60
63
68 public function __construct(
69 HookRegistry $hookRegistry,
70 ObjectFactory $objectFactory
71 ) {
72 $this->registry = $hookRegistry;
73 $this->objectFactory = $objectFactory;
74 }
75
86 public function salvage( SalvageableService $other ) {
87 Assert::parameterType( self::class, $other, '$other' );
88 if ( $this->legacyRegisteredHandlers || $this->handlersByName ) {
89 throw new MWException( 'salvage() must be called immediately after construction' );
90 }
91 $this->handlersByName = $other->handlersByName;
92 $this->legacyRegisteredHandlers = $other->legacyRegisteredHandlers;
93 }
94
120 public function run( string $hook, array $args = [], array $options = [] ) : bool {
121 $legacyHandlers = $this->getLegacyHandlers( $hook );
122 $options = array_merge(
123 $this->registry->getDeprecatedHooks()->getDeprecationInfo( $hook ) ?? [],
124 $options
125 );
126 // Equivalent of legacy Hooks::runWithoutAbort()
127 $notAbortable = ( isset( $options['abortable'] ) && $options['abortable'] === false );
128 foreach ( $legacyHandlers as $handler ) {
129 $normalizedHandler = $this->normalizeHandler( $handler, $hook );
130 if ( $normalizedHandler ) {
131 $functionName = $normalizedHandler['functionName'];
132 $return = $this->callLegacyHook( $hook, $normalizedHandler, $args, $options );
133 if ( $notAbortable && $return !== null && $return !== true ) {
134 throw new UnexpectedValueException( "Invalid return from $functionName" .
135 " for unabortable $hook." );
136 }
137 if ( $return === false ) {
138 return false;
139 }
140 if ( is_string( $return ) ) {
142 "Returning a string from a hook handler is deprecated since MediaWiki 1.35 ' .
143 '(done by $functionName for $hook)",
144 '1.35', false, false
145 );
146 throw new UnexpectedValueException( $return );
147 }
148 }
149 }
150
151 $handlers = $this->getHandlers( $hook, $options );
152 $funcName = 'on' . strtr( ucfirst( $hook ), ':-', '__' );
153
154 foreach ( $handlers as $handler ) {
155 $return = $handler->$funcName( ...$args );
156 if ( $notAbortable && $return !== null && $return !== true ) {
157 throw new UnexpectedValueException(
158 "Invalid return from " . $funcName . " for unabortable $hook."
159 );
160 }
161 if ( $return === false ) {
162 return false;
163 }
164 if ( $return !== null && !is_bool( $return ) ) {
165 throw new UnexpectedValueException( "Invalid return from " . $funcName . " for $hook." );
166 }
167 }
168 return true;
169 }
170
182 public function clear( string $hook ) : void {
183 if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MW_PARSER_TEST' ) ) {
184 throw new MWException( 'Cannot reset hooks in operation.' );
185 }
186 unset( $this->legacyRegisteredHandlers[$hook] ); // dynamically registered legacy handlers
187 }
188
199 public function scopedRegister( string $hook, $callback, bool $replace = false ) : ScopedCallback {
200 if ( $replace ) {
201 // Stash any previously registered hooks
202 if ( !isset( $this->originalHooks[$hook] ) &&
203 isset( $this->legacyRegisteredHandlers[$hook] )
204 ) {
205 $this->originalHooks[$hook] = $this->legacyRegisteredHandlers[$hook];
206 }
207 $this->legacyRegisteredHandlers[$hook] = [ $callback ];
208 return new ScopedCallback( function () use ( $hook ) {
209 unset( $this->legacyRegisteredHandlers[$hook] );
210 } );
211 }
212 $id = $this->nextScopedRegisterId++;
213 $this->legacyRegisteredHandlers[$hook][$id] = $callback;
214 return new ScopedCallback( function () use ( $hook, $id ) {
215 unset( $this->legacyRegisteredHandlers[$hook][$id] );
216 } );
217 }
218
226 public function getOriginalHooksForTest() {
227 if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MW_PARSER_TEST' ) ) {
228 throw new MWException( 'Cannot get original hooks outside when not in test mode' );
229 }
230 return $this->originalHooks ?? [];
231 }
232
243 private function normalizeHandler( $handler, string $hook ) {
244 $normalizedHandler = $handler;
245 if ( !is_array( $handler ) ) {
246 $normalizedHandler = [ $normalizedHandler ];
247 }
248
249 // Empty array or array filled with null/false/empty.
250 if ( !array_filter( $normalizedHandler ) ) {
251 return false;
252 }
253
254 if ( is_array( $normalizedHandler[0] ) ) {
255 // First element is an array, meaning the developer intended
256 // the first element to be a callback. Merge it in so that
257 // processing can be uniform.
258 $normalizedHandler = array_merge( $normalizedHandler[0], array_slice( $normalizedHandler, 1 ) );
259 }
260
261 $firstArg = $normalizedHandler[0];
262
263 // Extract function name, handler callback, and any arguments for the callback
264 if ( $firstArg instanceof Closure ) {
265 $functionName = "hook-$hook-closure";
266 $callback = array_shift( $normalizedHandler );
267 } elseif ( is_object( $firstArg ) ) {
268 $object = array_shift( $normalizedHandler );
269 $functionName = array_shift( $normalizedHandler );
270
271 // If no method was specified, default to on$event
272 if ( $functionName === null ) {
273 $functionName = "on$hook";
274 } else {
275 $colonPos = strpos( $functionName, '::' );
276 if ( $colonPos !== false ) {
277 // Some extensions use [ $object, 'Class::func' ] which
278 // worked with call_user_func_array() but doesn't work now
279 // that we use a plain variadic call
280 $functionName = substr( $functionName, $colonPos + 2 );
281 }
282 }
283
284 $callback = [ $object, $functionName ];
285 } elseif ( is_string( $firstArg ) ) {
286 if ( is_callable( $normalizedHandler, true, $functionName )
287 && class_exists( $firstArg ) // $firstArg can be a function in global scope
288 ) {
289 $callback = $normalizedHandler;
290 $normalizedHandler = []; // Can't pass arguments here
291 } else {
292 $functionName = $callback = array_shift( $normalizedHandler );
293 }
294 } else {
295 throw new UnexpectedValueException( 'Unknown datatype in hooks for ' . $hook );
296 }
297
298 return [
299 'callback' => $callback,
300 'args' => $normalizedHandler,
301 'functionName' => $functionName,
302 ];
303 }
304
318 private function callLegacyHook( string $hook, $handler, array $args, array $options ) {
319 $callback = $handler['callback'];
320 $hookArgs = array_merge( $handler['args'], $args );
321 if ( isset( $options['deprecatedVersion'] ) && empty( $options['silent'] ) ) {
323 "$hook hook (used in " . $handler['functionName'] . ")",
324 $options['deprecatedVersion'] ?? false,
325 $options['component'] ?? false
326 );
327 }
328 // Call the hooks
329 return $callback( ...$hookArgs );
330 }
331
339 public function isRegistered( string $hook ) : bool {
340 $legacyRegisteredHook = !empty( $this->registry->getGlobalHooks()[$hook] ) ||
341 !empty( $this->legacyRegisteredHandlers[$hook] );
342 $registeredHooks = $this->registry->getExtensionHooks();
343 return !empty( $registeredHooks[$hook] ) || $legacyRegisteredHook;
344 }
345
352 public function register( string $hook, $callback ) {
353 $deprecatedHooks = $this->registry->getDeprecatedHooks();
354 $deprecated = $deprecatedHooks->isHookDeprecated( $hook );
355 if ( $deprecated ) {
356 $info = $deprecatedHooks->getDeprecationInfo( $hook );
357 if ( empty( $info['silent'] ) ) {
358 $deprecatedVersion = $info['deprecatedVersion'] ?? false;
359 $component = $info['component'] ?? false;
361 "$hook hook", $deprecatedVersion, $component
362 );
363 }
364 }
365 $this->legacyRegisteredHandlers[$hook][] = $callback;
366 }
367
375 public function getLegacyHandlers( string $hook ) : array {
376 $handlers = array_merge(
377 $this->legacyRegisteredHandlers[$hook] ?? [],
378 $this->registry->getGlobalHooks()[$hook] ?? []
379 );
380 return $handlers;
381 }
382
391 public function getHandlers( string $hook, array $options = [] ) : array {
392 $handlers = [];
393 $deprecatedHooks = $this->registry->getDeprecatedHooks();
394 $registeredHooks = $this->registry->getExtensionHooks();
395 if ( isset( $registeredHooks[$hook] ) ) {
396 foreach ( $registeredHooks[$hook] as $hookReference ) {
397 // Non-legacy hooks have handler attributes
398 $handlerSpec = $hookReference['handler'];
399 // Skip hooks that both acknowledge deprecation and are deprecated in core
400 $flaggedDeprecated = !empty( $hookReference['deprecated'] );
401 $deprecated = $deprecatedHooks->isHookDeprecated( $hook );
402 if ( $deprecated && $flaggedDeprecated ) {
403 continue;
404 }
405 $handlerName = $handlerSpec['name'];
406 if ( !empty( $options['noServices'] ) && isset( $handlerSpec['services'] ) ) {
407 throw new UnexpectedValueException(
408 "The handler for the hook $hook registered in " .
409 "{$hookReference['extensionPath']} has a service dependency, " .
410 "but this hook does not allow it." );
411 }
412 if ( !isset( $this->handlersByName[$handlerName] ) ) {
413 $this->handlersByName[$handlerName] =
414 $this->objectFactory->createObject( $handlerSpec );
415 }
416 $handlers[] = $this->handlersByName[$handlerName];
417 }
418 }
419 return $handlers;
420 }
421
428 public function emitDeprecationWarnings() {
429 $deprecatedHooks = $this->registry->getDeprecatedHooks();
430 $registeredHooks = $this->registry->getExtensionHooks();
431 foreach ( $registeredHooks as $name => $handlers ) {
432 if ( $deprecatedHooks->isHookDeprecated( $name ) ) {
433 $deprecationInfo = $deprecatedHooks->getDeprecationInfo( $name );
434 if ( !empty( $deprecationInfo['silent'] ) ) {
435 continue;
436 }
437 $version = $deprecationInfo['deprecatedVersion'] ?? '';
438 $component = $deprecationInfo['component'] ?? 'MediaWiki';
439 foreach ( $handlers as $handler ) {
440 if ( !isset( $handler['deprecated'] ) || !$handler['deprecated'] ) {
441 MWDebug::sendRawDeprecated(
442 "Hook $name was deprecated in $component $version " .
443 "but is registered in " . $handler['extensionPath']
444 );
445 }
446 }
447 }
448 }
449 }
450}
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
if(ini_get('mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition Setup.php:85
New debugger system that outputs a toolbar on page view.
Definition MWDebug.php:33
MediaWiki exception.
getHandlers(string $hook, array $options=[])
Return array of handler objects registered with given hook in the new system.
__construct(HookRegistry $hookRegistry, ObjectFactory $objectFactory)
clear(string $hook)
Clear hooks registered via Hooks::register().
getOriginalHooksForTest()
Return hooks that were set before being potentially overridden by scopedRegister().
isRegistered(string $hook)
Return whether hook has any handlers registered to it.
salvage(SalvageableService $other)
Salvage the state of HookContainer by retaining existing handler objects and hooks registered via Hoo...
normalizeHandler( $handler, string $hook)
Normalize/clean up format of argument passed as hook handler.
callLegacyHook(string $hook, $handler, array $args, array $options)
Run legacy hooks Hook can be: a function, an object, an array of $function and $data,...
getLegacyHandlers(string $hook)
Get all handlers for legacy hooks system.
scopedRegister(string $hook, $callback, bool $replace=false)
Register hook and handler, allowing for easy removal.
array $handlersByName
handler name and their handler objects
emitDeprecationWarnings()
Will log a deprecation warning if:
run(string $hook, array $args=[], array $options=[])
Call registered hook functions through either the legacy $wgHooks or extension.json.
array $originalHooks
existing hook names and their handlers to restore between tests
int $nextScopedRegisterId
The next ID to be used by scopedRegister()
array $legacyRegisteredHandlers
Hooks and their callbacks registered through $this->register()
if( $line===false) $args
Definition mcc.php:124