aboutsummaryrefslogtreecommitdiffstats
path: root/includes/HookContainer
diff options
context:
space:
mode:
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>2023-06-06 06:53:54 +0000
committerGerrit Code Review <gerrit@wikimedia.org>2023-06-06 06:53:54 +0000
commit82b378c3b03f95370cc25103808f03d73af5e256 (patch)
tree830e8bc64192c2e1262666fe1902cacbc9d7dc88 /includes/HookContainer
parent5f628a03e53448190304af982d62d3570a5c532b (diff)
parentd139eb07fe2505fe4b83598e879b15856e0c6372 (diff)
downloadmediawikicore-82b378c3b03f95370cc25103808f03d73af5e256.tar.gz
mediawikicore-82b378c3b03f95370cc25103808f03d73af5e256.zip
Merge "Simplify HookContainer"
Diffstat (limited to 'includes/HookContainer')
-rw-r--r--includes/HookContainer/FauxHookHandlerArray.php8
-rw-r--r--includes/HookContainer/HookContainer.php710
2 files changed, 436 insertions, 282 deletions
diff --git a/includes/HookContainer/FauxHookHandlerArray.php b/includes/HookContainer/FauxHookHandlerArray.php
index 5b97b79ece9f..9e088498343a 100644
--- a/includes/HookContainer/FauxHookHandlerArray.php
+++ b/includes/HookContainer/FauxHookHandlerArray.php
@@ -72,7 +72,9 @@ class FauxHookHandlerArray implements \ArrayAccess, \IteratorAggregate {
private function getHandler( $offset ) {
if ( $this->handlers === null ) {
- $this->handlers = $this->hookContainer->getLegacyHandlers( $this->name );
+ // NOTE: getHandlerCallbacks() only exists to support this.
+ // It should be deleted when we no longer need it here.
+ $this->handlers = $this->hookContainer->getHandlerCallbacks( $this->name );
}
return $this->handlers[$offset] ?? null;
@@ -81,7 +83,9 @@ class FauxHookHandlerArray implements \ArrayAccess, \IteratorAggregate {
#[\ReturnTypeWillChange]
public function getIterator() {
if ( $this->handlers === null ) {
- $this->handlers = $this->hookContainer->getLegacyHandlers( $this->name );
+ // NOTE: getHandlerCallbacks() only exists to support this.
+ // It should be deleted when we no longer need it here.
+ $this->handlers = $this->hookContainer->getHandlerCallbacks( $this->name );
}
return new \ArrayIterator( $this->handlers );
diff --git a/includes/HookContainer/HookContainer.php b/includes/HookContainer/HookContainer.php
index 6d150a67097f..8fb605cac267 100644
--- a/includes/HookContainer/HookContainer.php
+++ b/includes/HookContainer/HookContainer.php
@@ -26,6 +26,8 @@
namespace MediaWiki\HookContainer;
use Closure;
+use InvalidArgumentException;
+use LogicException;
use MWDebug;
use MWException;
use UnexpectedValueException;
@@ -34,6 +36,16 @@ use Wikimedia\NonSerializable\NonSerializableTrait;
use Wikimedia\ObjectFactory\ObjectFactory;
use Wikimedia\ScopedCallback;
use Wikimedia\Services\SalvageableService;
+use function array_filter;
+use function array_keys;
+use function array_merge;
+use function array_shift;
+use function array_unique;
+use function is_array;
+use function is_object;
+use function is_string;
+use function strpos;
+use function strtr;
/**
* HookContainer class.
@@ -45,19 +57,19 @@ use Wikimedia\Services\SalvageableService;
class HookContainer implements SalvageableService {
use NonSerializableTrait;
- /** @var array Hooks and their callbacks registered through $this->register() */
- private $dynamicHandlers = [];
+ public const NOOP = '*no-op*';
/**
- * @var array Tombstone count by hook name
+ * Normalized hook handlers, as a 3D array:
+ * - the first level maps hook names to lists of handlers
+ * - the second is a list of handlers
+ * - each handler is an associative array with some well known keys, as returned by normalizeHandler()
+ * @var array<array>
*/
- private $tombstones = [];
+ private $handlers = [];
- /** @var string magic value for use in $dynamicHandlers */
- private const TOMBSTONE = 'TOMBSTONE';
-
- /** @var array handler name and their handler objects */
- private $handlersByName = [];
+ /** @var array<object> handler name and their handler objects */
+ private $handlerObjects = [];
/** @var HookRegistry */
private $registry;
@@ -92,12 +104,11 @@ class HookContainer implements SalvageableService {
*/
public function salvage( SalvageableService $other ) {
Assert::parameterType( self::class, $other, '$other' );
- if ( $this->dynamicHandlers || $this->handlersByName ) {
+ if ( $this->handlers || $this->handlerObjects ) {
throw new MWException( 'salvage() must be called immediately after construction' );
}
- $this->handlersByName = $other->handlersByName;
- $this->dynamicHandlers = $other->dynamicHandlers;
- $this->tombstones = $other->tombstones;
+ $this->handlerObjects = $other->handlerObjects;
+ $this->handlers = $other->handlers;
}
/**
@@ -123,53 +134,44 @@ class HookContainer implements SalvageableService {
* @throws UnexpectedValueException if handlers return an invalid value
*/
public function run( string $hook, array $args = [], array $options = [] ): bool {
- $legacyHandlers = $this->getLegacyHandlers( $hook );
- $options = array_merge(
- $this->registry->getDeprecatedHooks()->getDeprecationInfo( $hook ) ?? [],
- $options
- );
- // Equivalent of legacy Hooks::runWithoutAbort()
- $notAbortable = ( isset( $options['abortable'] ) && $options['abortable'] === false );
- foreach ( $legacyHandlers as $handler ) {
- $normalizedHandler = $this->normalizeHandler( $handler, $hook );
- if ( $normalizedHandler ) {
- $functionName = $normalizedHandler['functionName'];
- $return = $this->callLegacyHook( $hook, $normalizedHandler, $args, $options );
- if ( $notAbortable && $return !== null && $return !== true ) {
- throw new UnexpectedValueException( "Invalid return from $functionName" .
- " for unabortable $hook." );
- }
- if ( $return === false ) {
- return false;
- }
- if ( is_string( $return ) ) {
- wfDeprecatedMsg(
- "Returning a string from a hook handler is deprecated since MediaWiki 1.35 ' .
- '(done by $functionName for $hook)",
- '1.35', false, false
- );
- throw new UnexpectedValueException( $return );
- }
+ $checkDeprecation = isset( $options['deprecatedVersion'] );
+
+ $abortable = $options['abortable'] ?? true;
+ foreach ( $this->getHandlers( $hook, $options ) as $handler ) {
+ // The handler is "shadowed" by a scoped hook handler.
+ if ( ( $handler['skip'] ?? 0 ) > 0 ) {
+ continue;
}
- }
- $handlers = $this->getHandlers( $hook, $options );
- $funcName = 'on' . strtr( ucfirst( $hook ), ':-', '__' );
+ if ( $checkDeprecation ) {
+ $this->checkDeprecation( $hook, $handler['functionName'], $options );
+ }
- foreach ( $handlers as $handler ) {
- $return = $handler->$funcName( ...$args );
- if ( $notAbortable && $return !== null && $return !== true ) {
- throw new UnexpectedValueException(
- "Invalid return from " . $funcName . " for unabortable $hook."
- );
+ // Compose callback arguments.
+ if ( !empty( $handler['args'] ) ) {
+ $callbackArgs = array_merge( $handler['args'], $args );
+ } else {
+ $callbackArgs = $args;
}
+ // Call the handler.
+ $callback = $handler['callback'];
+ $return = $callback( ...$callbackArgs );
+
+ // Handler returned false, signal abort to caller
if ( $return === false ) {
+ if ( !$abortable ) {
+ throw new UnexpectedValueException( "Handler {$handler['functionName']}" .
+ " return false for unabortable $hook." );
+ }
+
return false;
- }
- if ( $return !== null && !is_bool( $return ) ) {
- throw new UnexpectedValueException( "Invalid return from " . $funcName . " for $hook." );
+ } elseif ( $return !== null && $return !== true ) {
+ throw new UnexpectedValueException(
+ "Hook handlers can only return null or a boolean. Got an unexpected value from " .
+ "handler {$handler['functionName']} for $hook" );
}
}
+
return true;
}
@@ -189,14 +191,7 @@ class HookContainer implements SalvageableService {
throw new MWException( 'Cannot reset hooks in operation.' );
}
- // The tombstone logic makes it so the clear() operation can be reversed reliably,
- // and does not affect global state.
- // $this->tombstones[$hook]>0 suppresses any handlers from the HookRegistry,
- // see getHandlers().
- // The TOMBSTONE value in $this->dynamicHandlers[$hook] means that all handlers
- // that precede it in the array are ignored, see getLegacyHandlers().
- $this->dynamicHandlers[$hook][] = self::TOMBSTONE;
- $this->tombstones[$hook] = ( $this->tombstones[$hook] ?? 0 ) + 1;
+ $this->handlers[$hook] = [];
}
/**
@@ -204,141 +199,221 @@ class HookContainer implements SalvageableService {
* Intended for use in temporary registration e.g. testing
*
* @param string $hook Name of hook
- * @param callable|string|array $callback Handler object to attach
- * @param bool $replace (optional) By default adds callback to handler array.
- * Set true to remove all existing callbacks for the hook.
+ * @param callable|string|array $handler Handler to attach
+ * @param bool $replace (optional) Set true to disable existing handlers for the hook while
+ * the scoped handler is active.
* @return ScopedCallback
*/
- public function scopedRegister( string $hook, $callback, bool $replace = false ): ScopedCallback {
- // Use a known key to register the handler, so we can later remove it
- // from $this->dynamicHandlers using that key.
+ public function scopedRegister( string $hook, $handler, bool $replace = false ): ScopedCallback {
+ $handler = $this->normalizeHandler( $hook, $handler );
+ if ( !$handler ) {
+ throw new InvalidArgumentException( 'Bad hook handler!' );
+ }
+
+ $this->checkDeprecation( $hook, $handler['functionName'] );
+
$id = 'TemporaryHook_' . $this->nextScopedRegisterId++;
+ $this->getHandlers( $hook );
+
+ $this->handlers[$hook][$id] = $handler;
if ( $replace ) {
- // Use a known key for the tombstone, so we can later remove it
- // from $this->dynamicHandlers using that key.
- $ts = "{$id}_TOMBSTONE";
-
- // See comment in clear() for the tombstone logic.
- $this->dynamicHandlers[$hook][$ts] = self::TOMBSTONE;
- $this->dynamicHandlers[$hook][$id] = $callback;
- $this->tombstones[$hook] = ( $this->tombstones[$hook] ?? 0 ) + 1;
-
- return new ScopedCallback(
- function () use ( $hook, $id, $ts ) {
- unset( $this->dynamicHandlers[$hook][$ts] );
- unset( $this->dynamicHandlers[$hook][$id] );
- $this->tombstones[$hook]--;
- }
- );
- } else {
- $this->dynamicHandlers[$hook][$id] = $callback;
- return new ScopedCallback( function () use ( $hook, $id ) {
- unset( $this->dynamicHandlers[$hook][$id] );
- } );
+ $this->updateSkipCounter( $hook, 1, $id );
+ }
+
+ return new ScopedCallback( function () use ( $hook, $id, $replace ) {
+ if ( $replace ) {
+ $this->updateSkipCounter( $hook, -1, $id );
+ }
+ unset( $this->handlers[$hook][$id] );
+ } );
+ }
+
+ /**
+ * Utility for scopedRegister() for updating the counter in the 'skip'
+ * field of each handler of the given hook, up to the given index.
+ *
+ * The idea is that handlers with a non-zero 'skip' count will be ignored.
+ * This allows a call to scopedRegister to temporarily disable all previously
+ * registered handlers, and automatically re-enable them when the temporary
+ * handler goes out of scope.
+ *
+ * @param string $hook
+ * @param int $n The amount by which to update the count (1 to disable handlers,
+ * -1 to re-enable them)
+ * @param int|string $upTo The index at which to stop the update. This will be the
+ * index of the temporary handler registered with scopedRegister.
+ *
+ * @return void
+ */
+ private function updateSkipCounter( string $hook, $n, $upTo ) {
+ foreach ( $this->handlers[$hook] as $i => $unused ) {
+ if ( $i === $upTo ) {
+ break;
+ }
+
+ $current = $this->handlers[$hook][$i]['skip'] ?? 0;
+ $this->handlers[$hook][$i]['skip'] = $current + $n;
}
}
/**
+ * Returns a callable array based on the handler specification provided.
+ * This will find the appropriate handler object to call a method on,
+ * instantiating it if it doesn't exist yet.
+ *
+ * @param string $hook The name of the hook the handler was registered for
+ * @param array $handler A hook handler specification as given in an extension.json file.
+ * @param array $options Options to apply. If the 'noServices' option is set and the
+ * handler requires service injection, this method will throw an
+ * UnexpectedValueException.
+ *
+ * @return array
+ */
+ private function makeExtensionHandlerCallback( string $hook, array $handler, array $options = [] ): array {
+ $spec = $handler['handler'];
+ $name = $spec['name'];
+
+ if (
+ !empty( $options['noServices'] ) && (
+ !empty( $spec['services'] ) ||
+ !empty( $spec['optional_services'] )
+ )
+ ) {
+ throw new UnexpectedValueException(
+ "The handler for the hook $hook registered in " .
+ "{$handler['extensionPath']} has a service dependency, " .
+ "but this hook does not allow it." );
+ }
+
+ if ( !isset( $this->handlerObjects[$name] ) ) {
+ // @phan-suppress-next-line PhanTypeInvalidCallableArraySize
+ $this->handlerObjects[$name] = $this->objectFactory->createObject( $spec );
+ }
+
+ $obj = $this->handlerObjects[$name];
+ $method = $this->getHookMethodName( $hook );
+
+ return [ $obj, $method ];
+ }
+
+ /**
* Normalize/clean up format of argument passed as hook handler
*
- * @param array|callable $handler Executable handler function
* @param string $hook Hook name
+ * @param string|array|callable $handler Executable handler function. See register() for supported structures.
+ * @param array $options
+ *
* @return array|false
- * - handler: (callable) Executable handler function
+ * - callback: (callable) Executable handler function
* - functionName: (string) Handler name for passing to wfDeprecated() or Exceptions thrown
- * - args: (array) handler function arguments
+ * - args: (array) Extra handler function arguments (omitted when not needed)
*/
- private function normalizeHandler( $handler, string $hook ) {
- $normalizedHandler = $handler;
- if ( !is_array( $handler ) ) {
- $normalizedHandler = [ $normalizedHandler ];
+ private function normalizeHandler( string $hook, $handler, array $options = [] ) {
+ if ( is_object( $handler ) && !$handler instanceof Closure ) {
+ $handler = [ $handler, $this->getHookMethodName( $hook ) ];
}
- // Empty array or array filled with null/false/empty.
- if ( !array_filter( $normalizedHandler ) ) {
- return false;
+ // Backwards compatibility with old-style callable that uses a qualified method name.
+ if ( is_array( $handler ) && is_object( $handler[0] ?? false ) && is_string( $handler[1] ?? false ) ) {
+ $ofs = strpos( $handler[1], '::' );
+
+ if ( $ofs !== false ) {
+ $handler[1] = substr( $handler[1], $ofs + 2 );
+ }
}
- if ( is_array( $normalizedHandler[0] ) ) {
- // First element is an array, meaning the developer intended
- // the first element to be a callback. Merge it in so that
- // processing can be uniform.
- $normalizedHandler = array_merge( $normalizedHandler[0], array_slice( $normalizedHandler, 1 ) );
+ // Backwards compatibility: support objects wrapped in an array but no method name.
+ if ( is_array( $handler ) && is_object( $handler[0] ?? false ) && !isset( $handler[1] ) ) {
+ if ( !$handler[0] instanceof Closure ) {
+ $handler[1] = $this->getHookMethodName( $hook );
+ }
}
- $firstArg = $normalizedHandler[0];
+ // The empty callback is used to represent a no-op handler in some test cases.
+ if ( $handler === [] || $handler === null || $handler === false || $handler === self::NOOP ) {
+ return [
+ 'callback' => static function () {
+ // no-op
+ },
+ 'functionName' => self::NOOP,
+ ];
+ }
- // Extract function name, handler callback, and any arguments for the callback
- if ( $firstArg instanceof Closure ) {
- $functionName = "hook-$hook-closure";
- $callback = array_shift( $normalizedHandler );
- } elseif ( is_object( $firstArg ) ) {
- $object = array_shift( $normalizedHandler );
- $functionName = array_shift( $normalizedHandler );
+ // Plain callback
+ if ( is_callable( $handler ) ) {
+ return [
+ 'callback' => $handler,
+ 'functionName' => self::callableToString( $handler ),
+ ];
+ }
- // If no method was specified, default to on$event
- if ( $functionName === null ) {
- $functionName = "on$hook";
- } else {
- $colonPos = strpos( $functionName, '::' );
- if ( $colonPos !== false ) {
- // Some extensions use [ $object, 'Class::func' ] which
- // worked with call_user_func_array() but doesn't work now
- // that we use a plain variadic call
- $functionName = substr( $functionName, $colonPos + 2 );
+ // Not callable and not an array. Something is wrong.
+ if ( !is_array( $handler ) ) {
+ return false;
+ }
+
+ // Empty array or array filled with null/false/empty.
+ if ( !array_filter( $handler ) ) {
+ return false;
+ }
+
+ // ExtensionRegistry style handler
+ if ( isset( $handler['handler'] ) ) {
+ // Skip hooks that both acknowledge deprecation and are deprecated in core
+ if ( $handler['deprecated'] ?? false ) {
+ $deprecatedHooks = $this->registry->getDeprecatedHooks();
+ $deprecated = $deprecatedHooks->isHookDeprecated( $hook );
+ if ( $deprecated ) {
+ return false;
}
}
- $callback = [ $object, $functionName ];
- } elseif ( is_string( $firstArg ) ) {
- if ( is_callable( $normalizedHandler, true, $functionName )
- && class_exists( $firstArg ) // $firstArg can be a function in global scope
- ) {
- $callback = $normalizedHandler;
- $normalizedHandler = []; // Can't pass arguments here
- } else {
- $functionName = $callback = array_shift( $normalizedHandler );
+ $callback = $this->makeExtensionHandlerCallback( $hook, $handler, $options );
+ return [
+ 'callback' => $callback,
+ 'functionName' => self::callableToString( $callback ),
+ ];
+ }
+
+ // Not an indexed array, something is wrong.
+ if ( !isset( $handler[0] ) ) {
+ return false;
+ }
+
+ // Backwards compatibility: support for arrays of the form [ $object, $method, $data... ]
+ if ( is_object( $handler[0] ) && is_string( $handler[1] ?? false ) && array_key_exists( 2, $handler ) ) {
+ $obj = $handler[0];
+ if ( !$obj instanceof Closure ) {
+ // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
+ $method = $handler[1];
+ $handler = array_merge(
+ [ [ $obj, $method ] ],
+ array_slice( $handler, 2 )
+ );
}
- } else {
- throw new UnexpectedValueException( 'Unknown datatype in hooks for ' . $hook );
+ }
+
+ // The only option left is an array of the form [ $callable, $data... ]
+ $callback = array_shift( $handler );
+
+ // Backwards-compatibility for callbacks in the form [ [ $function ], $data ]
+ if ( is_array( $callback ) && count( $callback ) === 1 && is_string( $callback[0] ?? null ) ) {
+ $callback = $callback[0];
+ }
+
+ if ( !is_callable( $callback ) ) {
+ return false;
}
return [
'callback' => $callback,
- 'args' => $normalizedHandler,
- 'functionName' => $functionName,
+ 'functionName' => self::callableToString( $callback ),
+ 'args' => $handler,
];
}
/**
- * Run legacy hooks
- * Hook can be: a function, an object, an array of $function and
- * $data, an array of just a function, an array of object and
- * method, or an array of object, method, and data
- * (See hooks.txt for more details)
- *
- * @param string $hook
- * @param array|callable $handler The name of the hooks handler function
- * @param array $args Arguments for hook handler function
- * @param array $options
- * @return null|string|bool
- */
- private function callLegacyHook( string $hook, $handler, array $args, array $options ) {
- $callback = $handler['callback'];
- $hookArgs = array_merge( $handler['args'], $args );
- if ( isset( $options['deprecatedVersion'] ) && empty( $options['silent'] ) ) {
- wfDeprecated(
- "$hook hook (used in " . $handler['functionName'] . ")",
- $options['deprecatedVersion'] ?? false,
- $options['component'] ?? false
- );
- }
- // Call the hooks
- return $callback( ...$hookArgs );
- }
-
- /**
* Return whether hook has any handlers registered to it.
* The function may have been registered via Hooks::register or in extension.json
*
@@ -346,173 +421,181 @@ class HookContainer implements SalvageableService {
* @return bool Whether the hook has a handler registered to it
*/
public function isRegistered( string $hook ): bool {
- if ( $this->tombstones[$hook] ?? false ) {
- // If a tombstone is set, we only care about dynamically registered hooks,
- // and leave it to getLegacyHandlers() to handle the cut-off.
- return !empty( $this->getLegacyHandlers( $hook ) );
- }
-
- // If no tombstone is set, we just check if any of the three arrays contains handlers.
- if ( !empty( $this->registry->getGlobalHooks()[$hook] ) ||
- !empty( $this->dynamicHandlers[$hook] ) ||
- !empty( $this->registry->getExtensionHooks()[$hook] )
- ) {
- return true;
- }
-
- return false;
+ return !empty( $this->getHandlers( $hook ) );
}
/**
* Attach an event handler to a given hook.
*
+ * The handler should be given in one of the following forms:
+ *
+ * 1) A callable (string, array, or closure)
+ * 2) An extension hook handler spec in the form returned by
+ * HookRegistry::getExtensionHooks
+ *
+ * Several other forms are supported for backwards compatibility, but
+ * should not be used when calling this method directly.
+ *
* @param string $hook Name of hook
- * @param mixed $callback handler object to attach
+ * @param string|array|callable $handler handler
*/
- public function register( string $hook, $callback ) {
- $deprecatedHooks = $this->registry->getDeprecatedHooks();
- $deprecated = $deprecatedHooks->isHookDeprecated( $hook );
- if ( $deprecated ) {
- $info = $deprecatedHooks->getDeprecationInfo( $hook );
- if ( empty( $info['silent'] ) ) {
- $handler = $this->normalizeHandler( $callback, $hook );
- wfDeprecated(
- "$hook hook (used in " . $handler['functionName'] . ")",
- $info['deprecatedVersion'] ?? false,
- $info['component'] ?? false
- );
- }
+ public function register( string $hook, $handler ) {
+ $normalized = $this->normalizeHandler( $hook, $handler );
+ if ( !$normalized ) {
+ throw new InvalidArgumentException( 'Bad hook handler!' );
}
- $this->dynamicHandlers[$hook][] = $callback;
+
+ $this->checkDeprecation( $hook, $normalized['functionName'] );
+
+ $this->getHandlers( $hook );
+ $this->handlers[$hook][] = $normalized;
}
/**
- * Get all handlers for legacy hooks system, plus any handlers added
- * using register().
+ * Get handler callbacks.
*
- * @internal For use by Hooks.php
+ * @internal For use by FauxHookHandlerArray. Delete when no longer needed.
* @param string $hook Name of hook
* @return callable[]
*/
- public function getLegacyHandlers( string $hook ): array {
- if ( $this->tombstones[$hook] ?? false ) {
- // If there is at least one tombstone set for the hook,
- // ignore all handlers from the registry, and
- // only consider handlers registered after the tombstone
- // was set.
- $handlers = $this->dynamicHandlers[$hook] ?? [];
- $keys = array_keys( $handlers );
-
- // Loop over the handlers backwards, to find the last tombstone.
- for ( $i = count( $keys ) - 1; $i >= 0; $i-- ) {
- $k = $keys[$i];
- $v = $handlers[$k];
-
- if ( $v === self::TOMBSTONE ) {
- break;
- }
+ public function getHandlerCallbacks( string $hook ): array {
+ $handlers = $this->getHandlers( $hook );
+
+ $callbacks = [];
+ foreach ( $handlers as $h ) {
+ if ( ( $h['skip'] ?? 0 ) > 0 ) {
+ continue;
}
- // Return the part of $this->dynamicHandlers[$hook] after the TOMBSTONE
- // marker, preserving keys.
- $keys = array_slice( $keys, $i + 1 );
- $handlers = array_intersect_key( $handlers, array_fill_keys( $keys, true ) );
- } else {
- // If no tombstone is set, just merge the two arrays.
- $handlers = array_merge(
- $this->registry->getGlobalHooks()[$hook] ?? [],
- $this->dynamicHandlers[$hook] ?? []
- );
+ $callback = $h['callback'];
+
+ if ( isset( $h['args'] ) ) {
+ // Needs curry in order to pass extra arguments.
+ // NOTE: This does not support reference parameters!
+ $extraArgs = $h['args'];
+ $callbacks[] = static function ( ...$hookArgs ) use ( $callback, $extraArgs ) {
+ return $callback( ...$extraArgs, ...$hookArgs );
+ };
+ } else {
+ $callbacks[] = $callback;
+ }
}
- return $handlers;
+ return $callbacks;
}
/**
* Returns the names of all hooks that have at least one handler registered.
- * @return array
+ * @return string[]
*/
public function getHookNames(): array {
$names = array_merge(
- array_keys( $this->dynamicHandlers ),
- array_keys( $this->registry->getGlobalHooks() ),
- array_keys( $this->registry->getExtensionHooks() )
+ array_keys( array_filter( $this->handlers ) ),
+ array_keys( array_filter( $this->registry->getGlobalHooks() ) ),
+ array_keys( array_filter( $this->registry->getExtensionHooks() ) )
);
return array_unique( $names );
}
/**
- * Returns the names of all hooks that have handlers registered.
- * Note that this may include hook handlers that have been disabled using clear().
+ * Return the array of handlers for the given hook.
*
- * @internal
- * @return string[]
+ * @param string $hook Name of the hook
+ * @param array $options Handler options, which may include:
+ * - noServices: Do not allow hook handlers with service dependencies
+ * @return array[] A list of handler entries
*/
- public function getRegisteredHooks(): array {
- $names = array_merge(
- array_keys( $this->dynamicHandlers ),
- array_keys( $this->registry->getExtensionHooks() ),
- array_keys( $this->registry->getGlobalHooks() ),
- );
+ private function getHandlers( string $hook, array $options = [] ): array {
+ if ( !isset( $this->handlers[$hook] ) ) {
+ $handlers = [];
+ $registeredHooks = $this->registry->getExtensionHooks();
+ $configuredHooks = $this->registry->getGlobalHooks();
+
+ $rawHandlers = array_merge(
+ $configuredHooks[ $hook ] ?? [],
+ $registeredHooks[ $hook ] ?? []
+ );
- return array_unique( $names );
+ foreach ( $rawHandlers as $raw ) {
+ $handler = $this->normalizeHandler( $hook, $raw, $options );
+ if ( !$handler ) {
+ // XXX: log this?!
+ // NOTE: also happens for deprecated hooks, which is fine!
+ continue;
+ }
+
+ $handlers[] = $handler;
+ }
+
+ $this->handlers[ $hook ] = $handlers;
+ }
+
+ return $this->handlers[ $hook ];
}
/**
- * Return array of handler objects registered with given hook in the new system.
- * This does not include handlers registered dynamically using register(), nor does
- * it include hooks registered via the old mechanism using $wgHooks.
+ * Return the array of strings that describe the handler registered with the given hook.
*
- * @internal For use by Hooks.php
+ * @internal Only public for use by ApiQuerySiteInfo.php and SpecialVersion.php
* @param string $hook Name of the hook
- * @param array $options Handler options, which may include:
- * - noServices: Do not allow hook handlers with service dependencies
- * @return array non-deprecated handler objects
+ * @return string[] A list of handler descriptions
*/
- public function getHandlers( string $hook, array $options = [] ): array {
- if ( $this->tombstones[$hook] ?? false ) {
- // There is at least one tombstone for the hook, so suppress all new-style hooks.
- return [];
- }
- $handlers = [];
- $deprecatedHooks = $this->registry->getDeprecatedHooks();
+ public function getHandlerDescriptions( string $hook ): array {
+ $descriptions = [];
$registeredHooks = $this->registry->getExtensionHooks();
- if ( isset( $registeredHooks[$hook] ) ) {
- foreach ( $registeredHooks[$hook] as $hookReference ) {
- // Non-legacy hooks have handler attributes
- $handlerSpec = $hookReference['handler'];
- // Skip hooks that both acknowledge deprecation and are deprecated in core
- $flaggedDeprecated = !empty( $hookReference['deprecated'] );
- $deprecated = $deprecatedHooks->isHookDeprecated( $hook );
- if ( $deprecated && $flaggedDeprecated ) {
- continue;
- }
- $handlerName = $handlerSpec['name'];
- if (
- !empty( $options['noServices'] ) && (
- isset( $handlerSpec['services'] ) ||
- isset( $handlerSpec['optional_services'] )
- )
- ) {
- throw new UnexpectedValueException(
- "The handler for the hook $hook registered in " .
- "{$hookReference['extensionPath']} has a service dependency, " .
- "but this hook does not allow it." );
- }
- if ( !isset( $this->handlersByName[$handlerName] ) ) {
- $this->handlersByName[$handlerName] =
- $this->objectFactory->createObject( $handlerSpec );
- }
- $handlers[] = $this->handlersByName[$handlerName];
+ $configuredHooks = $this->registry->getGlobalHooks();
+
+ $rawHandlers = array_merge(
+ $configuredHooks[ $hook ] ?? [],
+ $registeredHooks[ $hook ] ?? [],
+ $this->handlers[ $hook ] ?? []
+ );
+
+ foreach ( $rawHandlers as $raw ) {
+ $descr = $this->describeHandler( $hook, $raw );
+
+ if ( $descr ) {
+ $descriptions[] = $descr;
}
}
- return $handlers;
+
+ return $descriptions;
+ }
+
+ /**
+ * Returns a human-readable description of the given handler.
+ *
+ * @param string $hook
+ * @param string|array|callable $handler
+ *
+ * @return ?string
+ */
+ private function describeHandler( string $hook, $handler ): ?string {
+ if ( is_array( $handler ) ) {
+ // already normalized
+ if ( isset( $handler['functionName'] ) ) {
+ return $handler['functionName'];
+ }
+
+ if ( isset( $handler['callback'] ) ) {
+ return self::callableToString( $handler['callback'] );
+ }
+
+ if ( isset( $handler['handler']['class'] ) ) {
+ // New style hook. Avoid instantiating the handler object
+ $method = $this->getHookMethodName( $hook );
+ return $handler['handler']['class'] . '::' . $method;
+ }
+ }
+
+ $handler = $this->normalizeHandler( $hook, $handler );
+ return $handler ? $handler['functionName'] : null;
}
/**
- * Will log a deprecation warning if:
- * 1. the hook is marked deprecated
+ * For each hook handler of each hook, this will log a deprecation if:
+ * 1. the hook is marked deprecated and
* 2. the "silent" flag is absent or false, and
* 3. an extension registers a handler in the new way but does not acknowledge deprecation
*/
@@ -538,4 +621,71 @@ class HookContainer implements SalvageableService {
}
}
}
+
+ /**
+ * Will trigger a deprecation warning if the given hook is deprecated and the deprecation
+ * is not marked as silent.
+ *
+ * @param string $hook The name of the hook.
+ * @param string $functionName The human-readable description of the handler
+ * @param array|null $deprecationInfo Deprecation info if the caller already knows it.
+ * If not given, it will be looked up from the hook registry.
+ *
+ * @return void
+ */
+ private function checkDeprecation( string $hook, string $functionName, array $deprecationInfo = null ): void {
+ if ( !$deprecationInfo ) {
+ $deprecatedHooks = $this->registry->getDeprecatedHooks();
+ $deprecationInfo = $deprecatedHooks->getDeprecationInfo( $hook );
+ }
+
+ if ( $deprecationInfo && empty( $deprecationInfo['silent'] ) ) {
+ wfDeprecated(
+ "$hook hook (used in " . $functionName . ")",
+ $deprecationInfo['deprecatedVersion'] ?? false,
+ $deprecationInfo['component'] ?? false
+ );
+ }
+ }
+
+ /**
+ * Returns a human-readable representation of the given callable.
+ *
+ * @param callable $callable
+ *
+ * @return string
+ */
+ private static function callableToString( $callable ): string {
+ if ( is_string( $callable ) ) {
+ return $callable;
+ }
+
+ if ( $callable instanceof Closure ) {
+ return '*closure*';
+ }
+
+ if ( is_array( $callable ) ) {
+ [ $on, $func ] = $callable;
+
+ if ( is_object( $on ) ) {
+ $on = get_class( $on );
+ }
+
+ return "$on::$func";
+ }
+
+ throw new LogicException( 'Unexpected kind of callable' );
+ }
+
+ /**
+ * Returns the default handler method name for the given hook.
+ *
+ * @param string $hook
+ *
+ * @return string
+ */
+ private function getHookMethodName( string $hook ): string {
+ $hook = strtr( $hook, ':\\', '__' );
+ return "on$hook";
+ }
}