Simplify HookContainer
This converts all hook handlers to the same internal representation. This is done lazily, when the hook is run for the first time. The logic for temporarily disabling handlers by calling scopedRegister() with the $replace parameter set has been greatly simplified. There are some minor changes to the class's interface and behavior, none of which should be breaking changes: * run() will emit deprecation warnings if and only if it was called with the deprecationVersion option set, for all kinds of handlers. The idea is that deprecated hooks should emit a warning either from run(), or from emitDeprecationWarnings(). The latter happens if the hook is listed in DeprecatedHooks. * register() now also accepts hook handlers declared in the way that extensions register hooks. * Attempts to call register() with an invalid hook definition now result in an invalidArgumentException. * Attempts to call register() for a deprecated hook will consistently result in a deprecation warning. * The internal getRegisteredHooks() method has been removed in favor of the identical getHookNames() method. * The internal getLegacyHandlers method has been removed in favor of getHandlerDescriptions() and getHandlerCallbacks(). * The call order changed so that dynamically registered handlers are called last, instead of getting called before handler objects from extensions. Change-Id: I7d690a1172af44a90b957b2274d68e51b7f09938
This commit is contained in:
parent
f6dc03a075
commit
d139eb07fe
14 changed files with 771 additions and 481 deletions
|
|
@ -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 );
|
||||
|
|
|
|||
|
|
@ -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'] );
|
||||
|
||||
$handlers = $this->getHandlers( $hook, $options );
|
||||
$funcName = 'on' . strtr( ucfirst( $hook ), ':-', '__' );
|
||||
|
||||
foreach ( $handlers as $handler ) {
|
||||
$return = $handler->$funcName( ...$args );
|
||||
if ( $notAbortable && $return !== null && $return !== true ) {
|
||||
throw new UnexpectedValueException(
|
||||
"Invalid return from " . $funcName . " for unabortable $hook."
|
||||
);
|
||||
$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;
|
||||
}
|
||||
|
||||
if ( $checkDeprecation ) {
|
||||
$this->checkDeprecation( $hook, $handler['functionName'], $options );
|
||||
}
|
||||
|
||||
// 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,140 +199,220 @@ 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 ) ) {
|
||||
// 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 );
|
||||
}
|
||||
}
|
||||
|
||||
// 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 );
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
];
|
||||
}
|
||||
|
||||
// Plain callback
|
||||
if ( is_callable( $handler ) ) {
|
||||
return [
|
||||
'callback' => $handler,
|
||||
'functionName' => self::callableToString( $handler ),
|
||||
];
|
||||
}
|
||||
|
||||
// Not callable and not an array. Something is wrong.
|
||||
if ( !is_array( $handler ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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 ) );
|
||||
// Empty array or array filled with null/false/empty.
|
||||
if ( !array_filter( $handler ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$firstArg = $normalizedHandler[0];
|
||||
|
||||
// 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 );
|
||||
|
||||
// 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 );
|
||||
// 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 );
|
||||
public function getHandlerCallbacks( string $hook ): array {
|
||||
$handlers = $this->getHandlers( $hook );
|
||||
|
||||
// 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;
|
||||
}
|
||||
$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[]
|
||||
*/
|
||||
public function getRegisteredHooks(): array {
|
||||
$names = array_merge(
|
||||
array_keys( $this->dynamicHandlers ),
|
||||
array_keys( $this->registry->getExtensionHooks() ),
|
||||
array_keys( $this->registry->getGlobalHooks() ),
|
||||
);
|
||||
|
||||
return array_unique( $names );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @internal For use by Hooks.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 array[] A list of handler entries
|
||||
*/
|
||||
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();
|
||||
$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 ) {
|
||||
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 ] ?? []
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
$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];
|
||||
|
||||
$handlers[] = $handler;
|
||||
}
|
||||
|
||||
$this->handlers[ $hook ] = $handlers;
|
||||
}
|
||||
return $handlers;
|
||||
|
||||
return $this->handlers[ $hook ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Will log a deprecation warning if:
|
||||
* 1. the hook is marked deprecated
|
||||
* Return the array of strings that describe the handler registered with the given hook.
|
||||
*
|
||||
* @internal Only public for use by ApiQuerySiteInfo.php and SpecialVersion.php
|
||||
* @param string $hook Name of the hook
|
||||
* @return string[] A list of handler descriptions
|
||||
*/
|
||||
public function getHandlerDescriptions( string $hook ): array {
|
||||
$descriptions = [];
|
||||
$registeredHooks = $this->registry->getExtensionHooks();
|
||||
$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 $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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,12 +100,7 @@ class Hooks {
|
|||
public static function getHandlers( $name ) {
|
||||
wfDeprecated( __METHOD__, '1.35' );
|
||||
$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
|
||||
$handlers = $hookContainer->getLegacyHandlers( $name );
|
||||
$funcName = 'on' . strtr( ucfirst( $name ), ':-', '__' );
|
||||
foreach ( $hookContainer->getHandlers( $name ) as $obj ) {
|
||||
$handlers[] = [ $obj, $funcName ];
|
||||
}
|
||||
return $handlers;
|
||||
return $hookContainer->getHandlerCallbacks( $name );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1012,11 +1012,11 @@ class ApiQuerySiteinfo extends ApiQueryBase {
|
|||
|
||||
$data = [];
|
||||
foreach ( $hookNames as $name ) {
|
||||
$subscribers = $hookContainer->getLegacyHandlers( $name );
|
||||
$subscribers = $hookContainer->getHandlerDescriptions( $name );
|
||||
|
||||
$arr = [
|
||||
'name' => $name,
|
||||
'subscribers' => array_map( [ SpecialVersion::class, 'arrayToString' ], $subscribers ),
|
||||
'subscribers' => $subscribers,
|
||||
];
|
||||
|
||||
ApiResult::setArrayType( $arr['subscribers'], 'array' );
|
||||
|
|
|
|||
|
|
@ -1084,41 +1084,44 @@ class SpecialVersion extends SpecialPage {
|
|||
* @return string HTML
|
||||
*/
|
||||
private function getHooks() {
|
||||
if ( $this->getConfig()->get( MainConfigNames::SpecialVersionShowHooks ) ) {
|
||||
$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
|
||||
$hookNames = $hookContainer->getHookNames();
|
||||
sort( $hookNames );
|
||||
|
||||
$ret = [];
|
||||
$this->addTocSection( 'version-hooks', 'mw-version-hooks' );
|
||||
$ret[] = Html::element(
|
||||
'h2',
|
||||
[ 'id' => 'mw-version-hooks' ],
|
||||
$this->msg( 'version-hooks' )->text()
|
||||
);
|
||||
$ret[] = Html::openElement( 'table', [ 'class' => 'wikitable', 'id' => 'sv-hooks' ] );
|
||||
$ret[] = Html::openElement( 'tr' );
|
||||
$ret[] = Html::element( 'th', [], $this->msg( 'version-hook-name' )->text() );
|
||||
$ret[] = Html::element( 'th', [], $this->msg( 'version-hook-subscribedby' )->text() );
|
||||
$ret[] = Html::closeElement( 'tr' );
|
||||
|
||||
foreach ( $hookNames as $hook ) {
|
||||
$hooks = $hookContainer->getLegacyHandlers( $hook );
|
||||
if ( !$hooks ) {
|
||||
continue;
|
||||
}
|
||||
$ret[] = Html::openElement( 'tr' );
|
||||
$ret[] = Html::element( 'td', [], $hook );
|
||||
$ret[] = Html::element( 'td', [], $this->listToText( $hooks ) );
|
||||
$ret[] = Html::closeElement( 'tr' );
|
||||
}
|
||||
|
||||
$ret[] = Html::closeElement( 'table' );
|
||||
|
||||
return implode( "\n", $ret );
|
||||
if ( !$this->getConfig()->get( MainConfigNames::SpecialVersionShowHooks ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return '';
|
||||
$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
|
||||
$hookNames = $hookContainer->getHookNames();
|
||||
|
||||
if ( !$hookNames ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
sort( $hookNames );
|
||||
|
||||
$ret = [];
|
||||
$this->addTocSection( 'version-hooks', 'mw-version-hooks' );
|
||||
$ret[] = Html::element(
|
||||
'h2',
|
||||
[ 'id' => 'mw-version-hooks' ],
|
||||
$this->msg( 'version-hooks' )->text()
|
||||
);
|
||||
$ret[] = Html::openElement( 'table', [ 'class' => 'wikitable', 'id' => 'sv-hooks' ] );
|
||||
$ret[] = Html::openElement( 'tr' );
|
||||
$ret[] = Html::element( 'th', [], $this->msg( 'version-hook-name' )->text() );
|
||||
$ret[] = Html::element( 'th', [], $this->msg( 'version-hook-subscribedby' )->text() );
|
||||
$ret[] = Html::closeElement( 'tr' );
|
||||
|
||||
foreach ( $hookNames as $name ) {
|
||||
$handlers = $hookContainer->getHandlerDescriptions( $name );
|
||||
|
||||
$ret[] = Html::openElement( 'tr' );
|
||||
$ret[] = Html::element( 'td', [], $name );
|
||||
$ret[] = Html::element( 'td', [], $this->listToText( $handlers ) );
|
||||
$ret[] = Html::closeElement( 'tr' );
|
||||
}
|
||||
|
||||
$ret[] = Html::closeElement( 'table' );
|
||||
|
||||
return implode( "\n", $ret );
|
||||
}
|
||||
|
||||
private function openExtType( string $text = null, string $name = null ) {
|
||||
|
|
|
|||
|
|
@ -2542,7 +2542,7 @@ abstract class MediaWikiIntegrationTestCase extends PHPUnit\Framework\TestCase {
|
|||
* @since 1.40
|
||||
*/
|
||||
protected function clearHooks( ?array $hookNames = null ) {
|
||||
$hookNames ??= $this->localServices->getHookContainer()->getRegisteredHooks();
|
||||
$hookNames ??= $this->localServices->getHookContainer()->getHookNames();
|
||||
foreach ( $hookNames as $name ) {
|
||||
$this->clearHook( $name );
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,14 +43,14 @@ namespace MediaWiki\HookContainer {
|
|||
$hookContainer->register( 'FooHook', static function () {
|
||||
return true;
|
||||
} );
|
||||
$handlersBeforeScopedRegister = $hookContainer->getLegacyHandlers( 'FooHook' );
|
||||
$handlersBeforeScopedRegister = $hookContainer->getHandlerCallbacks( 'FooHook' );
|
||||
$this->assertCount( 2, $handlersBeforeScopedRegister );
|
||||
|
||||
// Wipe out the 2 existing handlers and add a new scoped handler
|
||||
$reset2 = $hookContainer->scopedRegister( 'FooHook', static function () {
|
||||
return true;
|
||||
}, true );
|
||||
$handlersAfterScopedRegister = $hookContainer->getLegacyHandlers( 'FooHook' );
|
||||
$handlersAfterScopedRegister = $hookContainer->getHandlerCallbacks( 'FooHook' );
|
||||
$this->assertCount( 1, $handlersAfterScopedRegister );
|
||||
|
||||
ScopedCallback::consume( $reset2 );
|
||||
|
|
@ -58,7 +58,7 @@ namespace MediaWiki\HookContainer {
|
|||
// Teardown causes the original handlers to be re-applied
|
||||
$this->mediaWikiTearDown();
|
||||
|
||||
$handlersAfterTearDown = $hookContainer->getLegacyHandlers( 'FooHook' );
|
||||
$handlersAfterTearDown = $hookContainer->getHandlerCallbacks( 'FooHook' );
|
||||
$this->assertCount( 2, $handlersAfterTearDown );
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<?php
|
||||
|
||||
use Wikimedia\ScopedCallback;
|
||||
|
||||
class HooksTest extends MediaWikiIntegrationTestCase {
|
||||
|
||||
private const MOCK_HOOK_NAME = 'MediaWikiHooksTest001';
|
||||
|
|
@ -9,23 +11,23 @@ class HooksTest extends MediaWikiIntegrationTestCase {
|
|||
}
|
||||
|
||||
public static function provideHooks() {
|
||||
$i = new HookTestDummyHookHandlerClass();
|
||||
$obj = new HookTestDummyHookHandlerClass();
|
||||
|
||||
return [
|
||||
[
|
||||
'Object and method',
|
||||
[ $i, 'someNonStatic' ],
|
||||
[ $obj, 'someNonStatic' ],
|
||||
'changed-nonstatic',
|
||||
'changed-nonstatic'
|
||||
],
|
||||
[ 'Object and no method', [ $i ], 'changed-onevent', 'original' ],
|
||||
[ 'Object and no method', [ $obj ], 'changed-onevent', 'original' ],
|
||||
[
|
||||
'Object and method with data',
|
||||
[ $i, 'someNonStaticWithData', 'data' ],
|
||||
[ $obj, 'someNonStaticWithData', 'data' ],
|
||||
'data',
|
||||
'original'
|
||||
],
|
||||
[ 'Object and static method', [ $i, 'someStatic' ], 'changed-static', 'original' ],
|
||||
[ 'Object and static method', [ $obj, 'someStatic' ], 'changed-static', 'original' ],
|
||||
[
|
||||
'Class::method static call',
|
||||
[ 'HookTestDummyHookHandlerClass::someStatic' ],
|
||||
|
|
@ -69,6 +71,50 @@ class HooksTest extends MediaWikiIntegrationTestCase {
|
|||
$this->assertSame( $expectedBar, $bar, $msg );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers Hooks::getHandlers
|
||||
*/
|
||||
public function testGetHandlers() {
|
||||
global $wgHooks;
|
||||
$hookContainer = $this->getServiceContainer()->getHookContainer();
|
||||
|
||||
$this->filterDeprecated( '/\$wgHooks/' );
|
||||
$this->hideDeprecated( 'Hooks::getHandlers' );
|
||||
|
||||
$this->assertSame(
|
||||
[],
|
||||
Hooks::getHandlers( 'MediaWikiHooksTest001' ),
|
||||
'No hooks registered'
|
||||
);
|
||||
|
||||
$a = [ new HookTestDummyHookHandlerClass(), 'someStatic' ];
|
||||
$b = [ new HookTestDummyHookHandlerClass(), 'onMediaWikiHooksTest001' ];
|
||||
|
||||
$wgHooks['MediaWikiHooksTest001'][] = $a;
|
||||
|
||||
$this->assertSame(
|
||||
[ $a ],
|
||||
array_values( Hooks::getHandlers( 'MediaWikiHooksTest001' ) ),
|
||||
'Hook registered by $wgHooks'
|
||||
);
|
||||
$reset = $hookContainer->scopedRegister( 'MediaWikiHooksTest001', $b );
|
||||
$this->assertSame(
|
||||
[ $a, $b ],
|
||||
array_values( Hooks::getHandlers( 'MediaWikiHooksTest001' ) ),
|
||||
'Hooks::getHandlers() should return hooks registered via wgHooks as well as Hooks::register'
|
||||
);
|
||||
|
||||
ScopedCallback::consume( $reset );
|
||||
unset( $wgHooks['MediaWikiHooksTest001'] );
|
||||
|
||||
$hookContainer->register( 'MediaWikiHooksTest001', $b );
|
||||
$this->assertSame(
|
||||
[ $b ],
|
||||
array_values( Hooks::getHandlers( 'MediaWikiHooksTest001' ) ),
|
||||
'Hook registered by Hook::register'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers Hooks::isRegistered
|
||||
* @covers Hooks::getHandlers
|
||||
|
|
@ -123,8 +169,9 @@ class HooksTest extends MediaWikiIntegrationTestCase {
|
|||
public function testUncallableFunction() {
|
||||
$this->hideDeprecated( 'Hooks::run' );
|
||||
$hookContainer = $this->getServiceContainer()->getHookContainer();
|
||||
|
||||
$this->expectException( InvalidArgumentException::class );
|
||||
$hookContainer->register( self::MOCK_HOOK_NAME, 'ThisFunctionDoesntExist' );
|
||||
$this->expectExceptionMessage( 'Call to undefined function ThisFunctionDoesntExist' );
|
||||
Hooks::run( self::MOCK_HOOK_NAME, [] );
|
||||
}
|
||||
|
||||
|
|
@ -188,8 +235,8 @@ class HooksTest extends MediaWikiIntegrationTestCase {
|
|||
public function testCallHook_UnknownDatatype() {
|
||||
$this->hideDeprecated( 'Hooks::run' );
|
||||
$hookContainer = $this->getServiceContainer()->getHookContainer();
|
||||
$this->expectException( InvalidArgumentException::class );
|
||||
$hookContainer->register( self::MOCK_HOOK_NAME, 12345 );
|
||||
$this->expectException( UnexpectedValueException::class );
|
||||
Hooks::run( self::MOCK_HOOK_NAME );
|
||||
}
|
||||
|
||||
|
|
@ -199,9 +246,10 @@ class HooksTest extends MediaWikiIntegrationTestCase {
|
|||
public function testCallHook_Deprecated() {
|
||||
$hookContainer = $this->getServiceContainer()->getHookContainer();
|
||||
$hookContainer->register( self::MOCK_HOOK_NAME, 'HookTestDummyHookHandlerClass::someStatic' );
|
||||
$this->expectDeprecation();
|
||||
$this->expectDeprecationAndContinue( '/Use of MediaWikiHooksTest001 hook/' );
|
||||
|
||||
$a = $b = 0;
|
||||
$this->hideDeprecated( 'Hooks::run' );
|
||||
Hooks::run( self::MOCK_HOOK_NAME, [ $a, $b ], '1.31' );
|
||||
}
|
||||
|
||||
|
|
@ -245,9 +293,7 @@ class HooksTest extends MediaWikiIntegrationTestCase {
|
|||
$foo = 'original';
|
||||
|
||||
$this->expectException( UnexpectedValueException::class );
|
||||
$this->expectExceptionMessage( 'Invalid return from hook-MediaWikiHooksTest001-closure for ' .
|
||||
'unabortable MediaWikiHooksTest001'
|
||||
);
|
||||
$this->expectExceptionMessage( 'unabortable MediaWikiHooksTest001' );
|
||||
Hooks::runWithoutAbort( self::MOCK_HOOK_NAME, [ &$foo ] );
|
||||
}
|
||||
|
||||
|
|
@ -260,7 +306,7 @@ class HooksTest extends MediaWikiIntegrationTestCase {
|
|||
$hookContainer->register( self::MOCK_HOOK_NAME, static function () {
|
||||
return 'test';
|
||||
} );
|
||||
$this->expectDeprecation();
|
||||
$this->expectException( UnexpectedValueException::class );
|
||||
Hooks::run( self::MOCK_HOOK_NAME, [] );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,8 +55,6 @@ class AuthManagerTest extends \MediaWikiIntegrationTestCase {
|
|||
|
||||
/** @var HookContainer */
|
||||
private $hookContainer;
|
||||
/** @var array */
|
||||
private $authHooks;
|
||||
|
||||
/** @var UserNameUtils */
|
||||
protected $userNameUtils;
|
||||
|
|
@ -116,7 +114,7 @@ class AuthManagerTest extends \MediaWikiIntegrationTestCase {
|
|||
$mock = $this->getMockBuilder( $hookInterface )
|
||||
->onlyMethods( [ "on$hook" ] )
|
||||
->getMock();
|
||||
$this->authHooks[$hook][] = $mock;
|
||||
$this->hookContainer->register( $hook, $mock );
|
||||
return $mock->expects( $expect )->method( "on$hook" );
|
||||
}
|
||||
|
||||
|
|
@ -125,7 +123,7 @@ class AuthManagerTest extends \MediaWikiIntegrationTestCase {
|
|||
* @param string $hook
|
||||
*/
|
||||
protected function unhook( $hook ) {
|
||||
$this->authHooks[$hook] = [];
|
||||
$this->hookContainer->clear( $hook );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -226,21 +224,9 @@ class AuthManagerTest extends \MediaWikiIntegrationTestCase {
|
|||
$this->watchlistManager = $this->getServiceContainer()->getWatchlistManager();
|
||||
}
|
||||
if ( $regen || !$this->hookContainer ) {
|
||||
// Set up a HookContainer similar to the normal one except that it
|
||||
// gets global hooks from $this->authHooks instead of $wgHooks
|
||||
// Set up a HookContainer we control
|
||||
$this->hookContainer = new HookContainer(
|
||||
new class( $this->authHooks ) extends StaticHookRegistry {
|
||||
private $hooks;
|
||||
|
||||
public function __construct( &$hooks ) {
|
||||
parent::__construct();
|
||||
$this->hooks =& $hooks;
|
||||
}
|
||||
|
||||
public function getGlobalHooks() {
|
||||
return $this->hooks;
|
||||
}
|
||||
},
|
||||
new StaticHookRegistry( [], [], [] ),
|
||||
$this->objectFactory
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ class LanguageIntegrationTest extends LanguageClassesTestCase {
|
|||
// Don't allow installed hooks to run, except if a test restores them via origHooks (needed
|
||||
// for testIsKnownLanguageTag_cldr)
|
||||
$this->origHandlers = $this->getServiceContainer()->getHookContainer()
|
||||
->getLegacyHandlers( 'LanguageGetTranslatedLanguageNames' );
|
||||
->getHandlerCallbacks( 'LanguageGetTranslatedLanguageNames' );
|
||||
|
||||
$this->clearHook( 'LanguageGetTranslatedLanguageNames' );
|
||||
$this->overrideConfigValue( MainConfigNames::UsePigLatinVariant, true );
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ class ExtensionRegistrationTest extends MediaWikiIntegrationTestCase {
|
|||
|
||||
private $autoloaderState;
|
||||
|
||||
/** @var ?ExtensionRegistry */
|
||||
private $originalExtensionRegistry = null;
|
||||
|
||||
protected function setUp(): void {
|
||||
global $wgHooks;
|
||||
|
||||
|
|
@ -43,6 +46,11 @@ class ExtensionRegistrationTest extends MediaWikiIntegrationTestCase {
|
|||
|
||||
protected function tearDown(): void {
|
||||
AutoLoader::restoreState( $this->autoloaderState );
|
||||
|
||||
if ( $this->originalExtensionRegistry ) {
|
||||
$this->setExtensionRegistry( $this->originalExtensionRegistry );
|
||||
}
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
|
|
@ -75,27 +83,47 @@ class ExtensionRegistrationTest extends MediaWikiIntegrationTestCase {
|
|||
$this->assertArrayHasKey( 1300, $GLOBALS['wgNamespaceContentModels'] );
|
||||
}
|
||||
|
||||
private function setExtensionRegistry( ExtensionRegistry $registry ) {
|
||||
$class = new \ReflectionClass( ExtensionRegistry::class );
|
||||
|
||||
if ( !$this->originalExtensionRegistry ) {
|
||||
$this->originalExtensionRegistry = $class->getStaticPropertyValue( 'instance' );
|
||||
}
|
||||
|
||||
$class->setStaticPropertyValue( 'instance', $registry );
|
||||
}
|
||||
|
||||
public static function onAnEvent() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
public static function onBooEvent() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
public function testExportHooks() {
|
||||
$manifest = [
|
||||
'Hooks' => [
|
||||
'AnEvent' => 'FooBarClass::onAnEvent',
|
||||
'AnEvent' => self::class . '::onAnEvent',
|
||||
'BooEvent' => 'main',
|
||||
],
|
||||
'HookHandler' => [
|
||||
'main' => [ 'class' => 'Whatever' ]
|
||||
'HookHandlers' => [
|
||||
'main' => [ 'class' => self::class ]
|
||||
],
|
||||
];
|
||||
|
||||
$file = $this->makeManifestFile( $manifest );
|
||||
|
||||
$registry = new ExtensionRegistry();
|
||||
$this->setExtensionRegistry( $registry );
|
||||
|
||||
$registry->queue( $file );
|
||||
$registry->loadFromQueue();
|
||||
|
||||
$this->resetServices();
|
||||
$hookContainer = $this->getServiceContainer()->getHookContainer();
|
||||
$this->assertTrue( $hookContainer->isRegistered( 'AnEvent' ) );
|
||||
$this->assertTrue( $hookContainer->isRegistered( 'BooEvent' ) );
|
||||
$this->assertTrue( $hookContainer->isRegistered( 'AnEvent' ), 'AnEvent' );
|
||||
$this->assertTrue( $hookContainer->isRegistered( 'BooEvent' ), 'BooEvent' );
|
||||
}
|
||||
|
||||
public function testExportAutoload() {
|
||||
|
|
|
|||
|
|
@ -25,32 +25,6 @@ class TestLocalisationCache extends LocalisationCache {
|
|||
$this->selfAccess = TestingAccessWrapper::newFromObject( $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* Recurse through the given array and replace every object by a scalar value that can be
|
||||
* serialized as JSON to use as a hash key.
|
||||
*
|
||||
* @param array $arr
|
||||
* @return array
|
||||
*/
|
||||
private static function hashiblifyArray( array $arr ): array {
|
||||
foreach ( $arr as $key => $val ) {
|
||||
if ( is_array( $val ) ) {
|
||||
$arr[$key] = self::hashiblifyArray( $val );
|
||||
} elseif ( is_object( $val ) ) {
|
||||
// spl_object_hash() may return duplicate values if an object is destroyed and a new
|
||||
// one gets its hash and happens to be registered in the same hook in the same
|
||||
// location. This seems unlikely, but let's be safe and maintain a reference so it
|
||||
// can't happen. (In practice, there are probably no objects in the hooks at all.)
|
||||
static $objects = [];
|
||||
if ( !in_array( $val, $objects, true ) ) {
|
||||
$objects[] = $val;
|
||||
}
|
||||
$arr[$key] = spl_object_hash( $val );
|
||||
}
|
||||
}
|
||||
return $arr;
|
||||
}
|
||||
|
||||
public function recache( $code ) {
|
||||
$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
|
||||
// Test run performance is killed if we have to regenerate l10n for every test
|
||||
|
|
@ -58,11 +32,8 @@ class TestLocalisationCache extends LocalisationCache {
|
|||
$code,
|
||||
$this->selfAccess->options->get( MainConfigNames::ExtensionMessagesFiles ),
|
||||
$this->selfAccess->options->get( MainConfigNames::MessagesDirs ),
|
||||
// json_encode doesn't handle objects well
|
||||
self::hashiblifyArray( $hookContainer->getLegacyHandlers( 'LocalisationCacheRecacheFallback' ) ),
|
||||
self::hashiblifyArray( $hookContainer->getHandlers( 'LocalisationCacheRecacheFallback' ) ),
|
||||
self::hashiblifyArray( $hookContainer->getLegacyHandlers( 'LocalisationCacheRecache' ) ),
|
||||
self::hashiblifyArray( $hookContainer->getHandlers( 'LocalisationCacheRecache' ) ),
|
||||
$hookContainer->getHandlerDescriptions( 'LocalisationCacheRecacheFallback' ),
|
||||
$hookContainer->getHandlerDescriptions( 'LocalisationCacheRecache' ),
|
||||
] ) );
|
||||
if ( isset( self::$testingCache[$cacheKey] ) ) {
|
||||
$this->data[$code] = self::$testingCache[$cacheKey];
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class FauxGlobalHookArrayTest extends \MediaWikiUnitTestCase {
|
|||
$hooks['FirstHook'][] = $handler;
|
||||
|
||||
$this->assertTrue( $container->isRegistered( 'FirstHook' ) );
|
||||
$this->assertCount( 1, $container->getLegacyHandlers( 'FirstHook' ) );
|
||||
$this->assertCount( 1, $container->getHandlerCallbacks( 'FirstHook' ) );
|
||||
|
||||
$this->assertTrue( isset( $hooks['FirstHook'] ) );
|
||||
$this->assertCount( 1, $hooks['FirstHook'] );
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace MediaWiki\HookContainer {
|
||||
|
||||
use InvalidArgumentException;
|
||||
use MediaWiki\Tests\Unit\DummyServicesTrait;
|
||||
use MediaWikiUnitTestCase;
|
||||
use stdClass;
|
||||
|
|
@ -57,11 +58,13 @@ namespace MediaWiki\HookContainer {
|
|||
|
||||
public static function provideRegister() {
|
||||
return [
|
||||
// not yet supported: 'function' => [ 'strtoupper', 'strtoupper' ],
|
||||
// not yet supported: 'object' => [ new \FooExtension\Hooks(), 'FooExtension\Hooks::onFooActionComplete' ],
|
||||
'function' => [ 'strtoupper', 'strtoupper' ],
|
||||
'object' => [ new \FooExtension\Hooks(), 'FooExtension\Hooks::onFooActionComplete' ],
|
||||
'object and method' => [ [ new FooClass(), 'fooMethod' ], 'MediaWiki\HookContainer\FooClass::fooMethod' ],
|
||||
'object in array with no method' => [ new \FooExtension\Hooks(), 'FooExtension\Hooks::onFooActionComplete' ],
|
||||
'extension' => [
|
||||
self::HANDLER_REGISTRATION,
|
||||
'FooExtension\Hooks'
|
||||
'FooExtension\Hooks::onFooActionComplete'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
|
@ -76,20 +79,21 @@ namespace MediaWiki\HookContainer {
|
|||
'FooActionComplete' => [ $handler ]
|
||||
], [] );
|
||||
|
||||
$handlers = $hookContainer->getHandlers( 'FooActionComplete' );
|
||||
$handlers = $hookContainer->getHandlerDescriptions( 'FooActionComplete' );
|
||||
|
||||
$this->assertSame( $expected, get_class( $handlers[0] ) );
|
||||
$this->assertSame( $expected, $handlers[0] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Values returned: hook, handlersToRegister, expectedReturn
|
||||
*/
|
||||
public static function provideGetHandlers() {
|
||||
public static function provideGetHandlerDescriptions() {
|
||||
return [
|
||||
'NoHandlersExist' => [
|
||||
'MWTestHook',
|
||||
null,
|
||||
[],
|
||||
0,
|
||||
0
|
||||
],
|
||||
'SuccessfulHandlerReturn' => [
|
||||
'FooActionComplete',
|
||||
|
|
@ -100,9 +104,8 @@ namespace MediaWiki\HookContainer {
|
|||
'services' => [],
|
||||
],
|
||||
],
|
||||
[
|
||||
new \FooExtension\Hooks(),
|
||||
],
|
||||
1,
|
||||
1
|
||||
],
|
||||
'SkipDeprecated' => [
|
||||
'FooActionCompleteDeprecated',
|
||||
|
|
@ -114,7 +117,8 @@ namespace MediaWiki\HookContainer {
|
|||
],
|
||||
'deprecated' => true,
|
||||
],
|
||||
[],
|
||||
1,
|
||||
0
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
@ -235,13 +239,13 @@ namespace MediaWiki\HookContainer {
|
|||
} );
|
||||
|
||||
// handlers registered in 2 different ways
|
||||
$this->assertCount( 2, $hookContainer->getLegacyHandlers( 'MWTestHook' ) );
|
||||
$this->assertCount( 2, $hookContainer->getHandlerCallbacks( 'MWTestHook' ) );
|
||||
$hookContainer->run( 'MWTestHook' );
|
||||
$this->assertEquals( 2, $numCalls );
|
||||
|
||||
// Remove one of the handlers that increments $called
|
||||
ScopedCallback::consume( $reset );
|
||||
$this->assertCount( 1, $hookContainer->getLegacyHandlers( 'MWTestHook' ) );
|
||||
$this->assertCount( 1, $hookContainer->getHandlerCallbacks( 'MWTestHook' ) );
|
||||
|
||||
$numCalls = 0;
|
||||
$hookContainer->run( 'MWTestHook' );
|
||||
|
|
@ -249,10 +253,16 @@ namespace MediaWiki\HookContainer {
|
|||
}
|
||||
|
||||
/**
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::getHandlers
|
||||
* @dataProvider provideGetHandlers
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::getHandlerDescriptions
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::getHandlerCallbacks
|
||||
* @dataProvider provideGetHandlerDescriptions
|
||||
*/
|
||||
public function testGetHandlers( string $hook, ?array $handlerToRegister, array $expectedReturn ) {
|
||||
public function testGetHandlers(
|
||||
string $hook,
|
||||
?array $handlerToRegister,
|
||||
int $expectedDescriptions,
|
||||
int $expectedCallbacks
|
||||
) {
|
||||
if ( $handlerToRegister ) {
|
||||
$hooks = [ $hook => [ $handlerToRegister ] ];
|
||||
} else {
|
||||
|
|
@ -262,37 +272,24 @@ namespace MediaWiki\HookContainer {
|
|||
'FooActionCompleteDeprecated' => [ 'deprecatedVersion' => '1.35' ]
|
||||
];
|
||||
$hookContainer = $this->newHookContainer( [], $hooks, $fakeDeprecatedHooks );
|
||||
$handlers = $hookContainer->getHandlers( $hook );
|
||||
$this->assertArrayEquals(
|
||||
$handlers,
|
||||
$expectedReturn,
|
||||
'HookContainer::getHandlers() should return array of handler functions'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::getLegacyHandlers
|
||||
*/
|
||||
public function testGetLegacyHandler() {
|
||||
$hookContainer = $this->newHookContainer( [], [], [] );
|
||||
$hookContainer->register(
|
||||
'FooActionComplete',
|
||||
[ new FooClass(),
|
||||
'fooMethod'
|
||||
]
|
||||
$descriptions = $hookContainer->getHandlerDescriptions( $hook );
|
||||
$this->assertCount(
|
||||
$expectedDescriptions,
|
||||
$descriptions,
|
||||
'getHandlerDescriptions()'
|
||||
);
|
||||
$expectedHandlers = [ [ new FooClass(),
|
||||
'fooMethod'
|
||||
] ];
|
||||
$callbacks = $hookContainer->getLegacyHandlers( 'FooActionComplete' );
|
||||
$this->assertIsCallable( $callbacks[0] );
|
||||
$this->assertArrayEquals(
|
||||
|
||||
$callbacks = $hookContainer->getHandlerCallbacks( $hook );
|
||||
$this->assertCount(
|
||||
$expectedCallbacks,
|
||||
$callbacks,
|
||||
$expectedHandlers,
|
||||
true
|
||||
'getHandlerCallbacks()'
|
||||
);
|
||||
|
||||
// TODO: Test support for handlers with extra args once that is fixed.
|
||||
foreach ( $callbacks as $clbk ) {
|
||||
$this->assertIsCallable( $clbk );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -300,6 +297,9 @@ namespace MediaWiki\HookContainer {
|
|||
*/
|
||||
public static function provideRunConfigured() {
|
||||
$fooObj = new FooClass();
|
||||
$closure = static function ( &$count ) {
|
||||
$count++;
|
||||
};
|
||||
$extra = 10;
|
||||
return [
|
||||
// Callables
|
||||
|
|
@ -307,11 +307,10 @@ namespace MediaWiki\HookContainer {
|
|||
'Object and method' => [ [ $fooObj, 'fooMethod' ] ],
|
||||
'Class name and static method' => [ [ 'MediaWiki\HookContainer\FooClass', 'fooStaticMethod' ] ],
|
||||
'static method' => [ 'MediaWiki\HookContainer\FooClass::fooStaticMethod' ],
|
||||
'Closure' => [
|
||||
static function ( &$count ) {
|
||||
$count++;
|
||||
}
|
||||
],
|
||||
'Closure' => [ $closure ],
|
||||
|
||||
// Shorthand
|
||||
'Object' => [ $fooObj ],
|
||||
|
||||
// Handlers with extra data attached
|
||||
'static method with extra data' => [
|
||||
|
|
@ -320,7 +319,6 @@ namespace MediaWiki\HookContainer {
|
|||
],
|
||||
'Object and method with extra data' => [ [ [ $fooObj, 'fooMethodWithExtra' ], $extra ], 11 ],
|
||||
'Function extra data' => [ [ 'fooGlobalFunctionWithExtra', $extra ], 11 ],
|
||||
'Function in array with extra data' => [ [ [ 'fooGlobalFunctionWithExtra' ], $extra ], 11 ],
|
||||
'Closure with extra data' => [
|
||||
[
|
||||
static function ( int $inc, &$count ) {
|
||||
|
|
@ -331,8 +329,18 @@ namespace MediaWiki\HookContainer {
|
|||
11
|
||||
],
|
||||
|
||||
// No-ops
|
||||
'empty array' => [ [], 1 ],
|
||||
'null' => [ null, 1 ],
|
||||
'false' => [ false, 1 ],
|
||||
'NOOP' => [ HookContainer::NOOP, 1 ],
|
||||
|
||||
// Strange edge cases
|
||||
'Object in array without method' => [ [ $fooObj ] ],
|
||||
'Callable in array' => [ [ [ $fooObj, 'fooMethod' ] ] ],
|
||||
'Closure in array with no extra data' => [ [ $closure ] ],
|
||||
'Function in array' => [ [ 'fooGlobalFunction' ] ],
|
||||
'Function in array in array' => [ [ [ 'fooGlobalFunction' ] ] ],
|
||||
'static method as array in array' => [
|
||||
[ [ 'MediaWiki\HookContainer\FooClass', 'fooStaticMethod' ] ]
|
||||
],
|
||||
|
|
@ -344,7 +352,6 @@ namespace MediaWiki\HookContainer {
|
|||
|
||||
/**
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::run
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::callLegacyHook
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::normalizeHandler
|
||||
* @dataProvider provideRunConfigured
|
||||
*/
|
||||
|
|
@ -359,9 +366,9 @@ namespace MediaWiki\HookContainer {
|
|||
|
||||
/**
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::run
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::callLegacyHook
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::normalizeHandler
|
||||
* @dataProvider provideRunConfigured
|
||||
* @dataProvider provideRunExtensionHook
|
||||
*/
|
||||
public function testRegisterAndRun( $handler, $expectedCount = 2 ) {
|
||||
$hookContainer = $this->newHookContainer( [], [] );
|
||||
|
|
@ -373,6 +380,45 @@ namespace MediaWiki\HookContainer {
|
|||
$this->assertSame( $expectedCount, $count );
|
||||
}
|
||||
|
||||
/**
|
||||
* Values returned: hook, handler, handler arguments, options
|
||||
*/
|
||||
public static function provideRegisterAndRunCallback() {
|
||||
$fooObj = new FooClass();
|
||||
return [
|
||||
// Callables
|
||||
'Function' => [ 'fooGlobalFunction' ],
|
||||
'Object and method' => [ [ $fooObj, 'fooMethod' ] ],
|
||||
'Class name and static method' => [ [ 'MediaWiki\HookContainer\FooClass', 'fooStaticMethod' ] ],
|
||||
'static method' => [ 'MediaWiki\HookContainer\FooClass::fooStaticMethod' ],
|
||||
'Closure' => [
|
||||
static function ( &$count ) {
|
||||
$count++;
|
||||
}
|
||||
],
|
||||
|
||||
// Extension-style handler
|
||||
'Extension handler' => [ self::HANDLER_REGISTRATION ],
|
||||
|
||||
// NOTE: hook handlers with extra data are not supported for callbacks!
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::getHandlerCallbacks
|
||||
* @dataProvider provideRegisterAndRunCallback
|
||||
*/
|
||||
public function testRegisterAndRunCallback( $handler, $expectedCount = 2 ) {
|
||||
$hookContainer = $this->newHookContainer( [], [] );
|
||||
$hookContainer->register( 'Increment', $handler );
|
||||
|
||||
$count = 1;
|
||||
foreach ( $hookContainer->getHandlerCallbacks( 'Increment' ) as $callback ) {
|
||||
$callback( $count );
|
||||
}
|
||||
$this->assertSame( $expectedCount, $count );
|
||||
}
|
||||
|
||||
/**
|
||||
* Values returned: hook, handler, handler arguments, options
|
||||
*/
|
||||
|
|
@ -384,15 +430,14 @@ namespace MediaWiki\HookContainer {
|
|||
|
||||
/**
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::run
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::callLegacyHook
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::normalizeHandler
|
||||
* @dataProvider provideRunExtensionHook
|
||||
*/
|
||||
public function testRunExtensionHook( array $handler, $expectedCount = 1 ) {
|
||||
$hookContainer = $this->newHookContainer( [], [ 'Increment' => [ $handler ] ] );
|
||||
$hookContainer = $this->newHookContainer( [], [ 'X\\Y::Increment' => [ $handler ] ] );
|
||||
|
||||
$count = 0;
|
||||
$hookValue = $hookContainer->run( 'Increment', [ &$count ] );
|
||||
$hookValue = $hookContainer->run( 'X\\Y::Increment', [ &$count ] );
|
||||
$this->assertTrue( $hookValue );
|
||||
$this->assertSame( $expectedCount, $count );
|
||||
}
|
||||
|
|
@ -411,7 +456,6 @@ namespace MediaWiki\HookContainer {
|
|||
|
||||
/**
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::run
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::callLegacyHook
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::normalizeHandler
|
||||
* @dataProvider provideRunFailsWithNoService
|
||||
*/
|
||||
|
|
@ -451,7 +495,7 @@ namespace MediaWiki\HookContainer {
|
|||
$seq = [ 'start' ];
|
||||
$hookContainer->run( 'Append', [ &$seq ] );
|
||||
|
||||
$expected = [ 'start', 'configured1', 'configured2', 'registered', 'FooExtension' ];
|
||||
$expected = [ 'start', 'configured1', 'configured2', 'FooExtension', 'registered' ];
|
||||
$this->assertSame( $expected, $seq );
|
||||
}
|
||||
|
||||
|
|
@ -527,21 +571,16 @@ namespace MediaWiki\HookContainer {
|
|||
/**
|
||||
* Test running deprecated hooks from $wgHooks with the deprecation declared in HookContainer.
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::run
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::callLegacyHook
|
||||
* @dataProvider provideRegisterDeprecated
|
||||
*/
|
||||
public function testRunConfiguredDeprecated( array $deprecationInfo, bool $expectWarning ) {
|
||||
public function testRunConfiguredDeprecated( array $deprecationInfo ) {
|
||||
$hookContainer = $this->newHookContainer(
|
||||
[ 'Increment' => [ self::HANDLER_FUNCTION ] ],
|
||||
[],
|
||||
[ 'Increment' => $deprecationInfo ]
|
||||
);
|
||||
|
||||
// Expected deprecation?
|
||||
if ( $expectWarning ) {
|
||||
$this->expectDeprecationAndContinue( '/Use of Increment hook/' );
|
||||
}
|
||||
|
||||
// No warning expected when running the hook!
|
||||
// Deprecated hooks should still be functional!
|
||||
$count = 0;
|
||||
$hookContainer->run( 'Increment', [ &$count ] );
|
||||
|
|
@ -551,7 +590,6 @@ namespace MediaWiki\HookContainer {
|
|||
/**
|
||||
* Test running deprecated hooks from $wgHooks with the deprecation passed in the options parameter.
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::run
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::callLegacyHook
|
||||
* @dataProvider provideRegisterDeprecated
|
||||
*/
|
||||
public function testRunConfiguredDeprecatedWithOption( array $deprecationInfo, bool $expectWarning ) {
|
||||
|
|
@ -588,21 +626,41 @@ namespace MediaWiki\HookContainer {
|
|||
[ 'Increment' => $deprecationInfo ]
|
||||
);
|
||||
|
||||
// NOTE: Currently expected to NOT trigger a deprecation warning. May change.
|
||||
|
||||
// Deprecated hooks should be functional, the handle that acknowledges deprecation should be skipped.
|
||||
// We do not expect deprecation warnings here. They are covered by emitDeprecationWarnings()
|
||||
$count = 0;
|
||||
$hookContainer->run( 'Increment', [ &$count ] );
|
||||
$this->assertSame( 1, $count );
|
||||
}
|
||||
|
||||
// Try again, with $deprecationInfo as $options
|
||||
/**
|
||||
* Test running deprecated hooks from extensions.
|
||||
*
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::run
|
||||
*/
|
||||
public function testRunHandlerObjectDeprecatedWithOption() {
|
||||
$deprecationInfo = [ 'deprecatedVersion' => '1.0' ];
|
||||
|
||||
// If the handler acknowledges deprecation, it should be skipped
|
||||
$knownDeprecated = self::HANDLER_REGISTRATION + [ 'deprecated' => true ];
|
||||
|
||||
$hookContainer = $this->newHookContainer(
|
||||
[],
|
||||
[ 'Increment' => [ self::HANDLER_REGISTRATION, $knownDeprecated ] ],
|
||||
[ 'Increment' => $deprecationInfo ]
|
||||
);
|
||||
|
||||
// We do expect deprecation warnings when the 'deprecationVersion' key is provided in the $options parameter.
|
||||
$this->expectDeprecationAndContinue( '/Use of Increment hook/' );
|
||||
|
||||
// Deprecated hooks should be functional, the handle that acknowledges deprecation should be skipped.
|
||||
$count = 0;
|
||||
$hookContainer->run( 'Increment', [ &$count ], $deprecationInfo );
|
||||
$this->assertSame( 2, $count );
|
||||
$this->assertSame( 1, $count );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::getHookNames
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::getRegisteredHooks
|
||||
*/
|
||||
public function testGetHookNames() {
|
||||
$fooHandler = [ 'handler' => [
|
||||
|
|
@ -625,15 +683,18 @@ namespace MediaWiki\HookContainer {
|
|||
|
||||
$container->register( 'C', 'strtoupper' );
|
||||
|
||||
// Ask for a few hooks that have no handlers.
|
||||
// Negative caching inside HookHandler should not cause them to be returned from getHookNames
|
||||
$container->isRegistered( 'X' );
|
||||
$container->getHandlerCallbacks( 'Y' );
|
||||
|
||||
$this->assertArrayEquals( [ 'A', 'B', 'C' ], $container->getHookNames() );
|
||||
$this->assertArrayEquals( [ 'A', 'B', 'C' ], $container->getRegisteredHooks() );
|
||||
|
||||
// make sure we are getting each hook name only once
|
||||
$container->register( 'B', 'strtoupper' );
|
||||
$container->register( 'A', 'strtoupper' );
|
||||
|
||||
$this->assertArrayEquals( [ 'A', 'B', 'C' ], $container->getHookNames() );
|
||||
$this->assertArrayEquals( [ 'A', 'B', 'C' ], $container->getRegisteredHooks() );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -642,10 +703,6 @@ namespace MediaWiki\HookContainer {
|
|||
public static function provideRunErrors() {
|
||||
// XXX: should also fail: non-function string, empty array
|
||||
return [
|
||||
'a number' => [
|
||||
123,
|
||||
[]
|
||||
],
|
||||
'return a string' => [
|
||||
static function () {
|
||||
return 'string';
|
||||
|
|
@ -674,6 +731,49 @@ namespace MediaWiki\HookContainer {
|
|||
$hookContainer->run( 'MWTestHook', [], $options );
|
||||
}
|
||||
|
||||
/**
|
||||
* Values returned: hook, handlersToRegister, options
|
||||
*/
|
||||
public static function provideRegisterErrors() {
|
||||
// XXX: should also fail: non-function string, empty array
|
||||
return [
|
||||
'a number' => [ 123 ],
|
||||
'non-callable string' => [ 'a, b, c' ],
|
||||
'array referencing an unknown method' => [ [ self::class, 'thisMethodDoesNotExist' ] ],
|
||||
'empty string' => [ '' ],
|
||||
'zero' => [ 0 ],
|
||||
'true' => [ true ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideRegisterErrors
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::normalizeHandler
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::register
|
||||
*/
|
||||
public function testRegisterErrors( $badHandler ) {
|
||||
$hookContainer = $this->newHookContainer();
|
||||
|
||||
$this->expectException( InvalidArgumentException::class );
|
||||
$hookContainer->register( 'MWTestHook', $badHandler );
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideRegisterErrors
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::normalizeHandler
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::run
|
||||
*/
|
||||
public function testRunWithBadHandlers( $badHandler ) {
|
||||
$goodHandler = self::HANDLER_FUNCTION;
|
||||
$hookContainer = $this->newHookContainer( [ 'MWTestHook' => [ $badHandler, $goodHandler ] ] );
|
||||
|
||||
// Bad handlers from the constructor should fail silently
|
||||
$count = 0;
|
||||
$hookContainer->run( 'MWTestHook', [ &$count ] );
|
||||
|
||||
$this->assertSame( 1, $count );
|
||||
}
|
||||
|
||||
public static function provideEmitDeprecationWarnings() {
|
||||
yield 'Deprecated extension hook' => [
|
||||
'$oldHooks' => [],
|
||||
|
|
@ -755,8 +855,7 @@ namespace MediaWiki\HookContainer {
|
|||
|
||||
/**
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::isRegistered
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::getHandlers
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::getLegacyHandlers
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::getHandlerCallbacks
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::register
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::clear
|
||||
*/
|
||||
|
|
@ -785,11 +884,11 @@ namespace MediaWiki\HookContainer {
|
|||
$this->assertTrue( $hookContainer->isRegistered( 'XyzHook' ) );
|
||||
$this->assertTrue( $hookContainer->isRegistered( 'FooActionComplete' ) );
|
||||
|
||||
$this->assertSame( [], $hookContainer->getLegacyHandlers( 'Increment' ) );
|
||||
$this->assertSame( [], $hookContainer->getHandlers( 'Increment' ) );
|
||||
$this->assertNotEmpty( $hookContainer->getLegacyHandlers( 'AbcHook' ) );
|
||||
$this->assertNotEmpty( $hookContainer->getHandlers( 'FooActionComplete' ) );
|
||||
$this->assertNotEmpty( $hookContainer->getLegacyHandlers( 'XyzHook' ) );
|
||||
$this->assertSame( [], $hookContainer->getHandlerCallbacks( 'Increment' ) );
|
||||
$this->assertSame( [], $hookContainer->getHandlerCallbacks( 'Increment' ) );
|
||||
$this->assertNotEmpty( $hookContainer->getHandlerCallbacks( 'AbcHook' ) );
|
||||
$this->assertNotEmpty( $hookContainer->getHandlerCallbacks( 'FooActionComplete' ) );
|
||||
$this->assertNotEmpty( $hookContainer->getHandlerCallbacks( 'XyzHook' ) );
|
||||
|
||||
// No more increment!
|
||||
$hookContainer->run( 'Increment', [ &$count ] );
|
||||
|
|
@ -884,6 +983,10 @@ namespace MediaWiki\HookContainer {
|
|||
return true;
|
||||
}
|
||||
|
||||
public function onIncrement( &$count ) {
|
||||
$count++;
|
||||
}
|
||||
|
||||
public static function fooStaticMethod( &$count ) {
|
||||
$count++;
|
||||
return null;
|
||||
|
|
@ -941,6 +1044,10 @@ namespace FooExtension {
|
|||
$count++;
|
||||
}
|
||||
|
||||
public function onX_Y__Increment( &$count ) {
|
||||
$count++;
|
||||
}
|
||||
|
||||
public function onAppend( &$list ) {
|
||||
$list[] = 'FooExtension';
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue