Revert "Simplify HookContainer"
This apparently caused some change in how hook handlers are called (it
now calls e.g. AbuseFilterHookHandler::onAbuseFilter-generateUserVars()
instead of AbuseFilterHookHandler::onAbuseFilter_generateUserVars()),
causing both test failures and errors on Beta.
This reverts commit d139eb07fe.
Bug: T338213
Change-Id: I50c3d1c5dfd2d7eeac59992156a8a644cf0197e5
This commit is contained in:
parent
82b378c3b0
commit
b0317287bc
14 changed files with 480 additions and 770 deletions
|
|
@ -72,9 +72,7 @@ class FauxHookHandlerArray implements \ArrayAccess, \IteratorAggregate {
|
|||
|
||||
private function getHandler( $offset ) {
|
||||
if ( $this->handlers === null ) {
|
||||
// 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 );
|
||||
$this->handlers = $this->hookContainer->getLegacyHandlers( $this->name );
|
||||
}
|
||||
|
||||
return $this->handlers[$offset] ?? null;
|
||||
|
|
@ -83,9 +81,7 @@ class FauxHookHandlerArray implements \ArrayAccess, \IteratorAggregate {
|
|||
#[\ReturnTypeWillChange]
|
||||
public function getIterator() {
|
||||
if ( $this->handlers === null ) {
|
||||
// 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 );
|
||||
$this->handlers = $this->hookContainer->getLegacyHandlers( $this->name );
|
||||
}
|
||||
|
||||
return new \ArrayIterator( $this->handlers );
|
||||
|
|
|
|||
|
|
@ -26,8 +26,6 @@
|
|||
namespace MediaWiki\HookContainer;
|
||||
|
||||
use Closure;
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
use MWDebug;
|
||||
use MWException;
|
||||
use UnexpectedValueException;
|
||||
|
|
@ -36,16 +34,6 @@ 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.
|
||||
|
|
@ -57,19 +45,19 @@ use function strtr;
|
|||
class HookContainer implements SalvageableService {
|
||||
use NonSerializableTrait;
|
||||
|
||||
public const NOOP = '*no-op*';
|
||||
/** @var array Hooks and their callbacks registered through $this->register() */
|
||||
private $dynamicHandlers = [];
|
||||
|
||||
/**
|
||||
* 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>
|
||||
* @var array Tombstone count by hook name
|
||||
*/
|
||||
private $handlers = [];
|
||||
private $tombstones = [];
|
||||
|
||||
/** @var array<object> handler name and their handler objects */
|
||||
private $handlerObjects = [];
|
||||
/** @var string magic value for use in $dynamicHandlers */
|
||||
private const TOMBSTONE = 'TOMBSTONE';
|
||||
|
||||
/** @var array handler name and their handler objects */
|
||||
private $handlersByName = [];
|
||||
|
||||
/** @var HookRegistry */
|
||||
private $registry;
|
||||
|
|
@ -104,11 +92,12 @@ class HookContainer implements SalvageableService {
|
|||
*/
|
||||
public function salvage( SalvageableService $other ) {
|
||||
Assert::parameterType( self::class, $other, '$other' );
|
||||
if ( $this->handlers || $this->handlerObjects ) {
|
||||
if ( $this->dynamicHandlers || $this->handlersByName ) {
|
||||
throw new MWException( 'salvage() must be called immediately after construction' );
|
||||
}
|
||||
$this->handlerObjects = $other->handlerObjects;
|
||||
$this->handlers = $other->handlers;
|
||||
$this->handlersByName = $other->handlersByName;
|
||||
$this->dynamicHandlers = $other->dynamicHandlers;
|
||||
$this->tombstones = $other->tombstones;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -134,44 +123,53 @@ class HookContainer implements SalvageableService {
|
|||
* @throws UnexpectedValueException if handlers return an invalid value
|
||||
*/
|
||||
public function run( string $hook, array $args = [], array $options = [] ): bool {
|
||||
$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;
|
||||
}
|
||||
|
||||
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." );
|
||||
$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 );
|
||||
}
|
||||
|
||||
return false;
|
||||
} 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" );
|
||||
}
|
||||
}
|
||||
|
||||
$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."
|
||||
);
|
||||
}
|
||||
if ( $return === false ) {
|
||||
return false;
|
||||
}
|
||||
if ( $return !== null && !is_bool( $return ) ) {
|
||||
throw new UnexpectedValueException( "Invalid return from " . $funcName . " for $hook." );
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -191,7 +189,14 @@ class HookContainer implements SalvageableService {
|
|||
throw new MWException( 'Cannot reset hooks in operation.' );
|
||||
}
|
||||
|
||||
$this->handlers[$hook] = [];
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -199,220 +204,140 @@ class HookContainer implements SalvageableService {
|
|||
* Intended for use in temporary registration e.g. testing
|
||||
*
|
||||
* @param string $hook Name of 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.
|
||||
* @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.
|
||||
* @return ScopedCallback
|
||||
*/
|
||||
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'] );
|
||||
|
||||
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.
|
||||
$id = 'TemporaryHook_' . $this->nextScopedRegisterId++;
|
||||
|
||||
$this->getHandlers( $hook );
|
||||
|
||||
$this->handlers[$hook][$id] = $handler;
|
||||
if ( $replace ) {
|
||||
$this->updateSkipCounter( $hook, 1, $id );
|
||||
// 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] );
|
||||
} );
|
||||
}
|
||||
|
||||
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
|
||||
* - callback: (callable) Executable handler function
|
||||
* - handler: (callable) Executable handler function
|
||||
* - functionName: (string) Handler name for passing to wfDeprecated() or Exceptions thrown
|
||||
* - args: (array) Extra handler function arguments (omitted when not needed)
|
||||
* - args: (array) handler function arguments
|
||||
*/
|
||||
private function normalizeHandler( string $hook, $handler, array $options = [] ) {
|
||||
if ( is_object( $handler ) && !$handler instanceof Closure ) {
|
||||
$handler = [ $handler, $this->getHookMethodName( $hook ) ];
|
||||
}
|
||||
|
||||
// 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.
|
||||
private function normalizeHandler( $handler, string $hook ) {
|
||||
$normalizedHandler = $handler;
|
||||
if ( !is_array( $handler ) ) {
|
||||
return false;
|
||||
$normalizedHandler = [ $normalizedHandler ];
|
||||
}
|
||||
|
||||
// Empty array or array filled with null/false/empty.
|
||||
if ( !array_filter( $handler ) ) {
|
||||
if ( !array_filter( $normalizedHandler ) ) {
|
||||
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;
|
||||
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 ) );
|
||||
}
|
||||
|
||||
$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 );
|
||||
}
|
||||
}
|
||||
|
||||
$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 )
|
||||
);
|
||||
$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 );
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
} else {
|
||||
throw new UnexpectedValueException( 'Unknown datatype in hooks for ' . $hook );
|
||||
}
|
||||
|
||||
return [
|
||||
'callback' => $callback,
|
||||
'functionName' => self::callableToString( $callback ),
|
||||
'args' => $handler,
|
||||
'args' => $normalizedHandler,
|
||||
'functionName' => $functionName,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
|
@ -421,181 +346,173 @@ class HookContainer implements SalvageableService {
|
|||
* @return bool Whether the hook has a handler registered to it
|
||||
*/
|
||||
public function isRegistered( string $hook ): bool {
|
||||
return !empty( $this->getHandlers( $hook ) );
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 string|array|callable $handler handler
|
||||
* @param mixed $callback handler object to attach
|
||||
*/
|
||||
public function register( string $hook, $handler ) {
|
||||
$normalized = $this->normalizeHandler( $hook, $handler );
|
||||
if ( !$normalized ) {
|
||||
throw new InvalidArgumentException( 'Bad hook 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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$this->checkDeprecation( $hook, $normalized['functionName'] );
|
||||
|
||||
$this->getHandlers( $hook );
|
||||
$this->handlers[$hook][] = $normalized;
|
||||
$this->dynamicHandlers[$hook][] = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get handler callbacks.
|
||||
* Get all handlers for legacy hooks system, plus any handlers added
|
||||
* using register().
|
||||
*
|
||||
* @internal For use by FauxHookHandlerArray. Delete when no longer needed.
|
||||
* @internal For use by Hooks.php
|
||||
* @param string $hook Name of hook
|
||||
* @return callable[]
|
||||
*/
|
||||
public function getHandlerCallbacks( string $hook ): array {
|
||||
$handlers = $this->getHandlers( $hook );
|
||||
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 );
|
||||
|
||||
$callbacks = [];
|
||||
foreach ( $handlers as $h ) {
|
||||
if ( ( $h['skip'] ?? 0 ) > 0 ) {
|
||||
continue;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
$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 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] ?? []
|
||||
);
|
||||
}
|
||||
|
||||
return $callbacks;
|
||||
return $handlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the names of all hooks that have at least one handler registered.
|
||||
* @return string[]
|
||||
* @return array
|
||||
*/
|
||||
public function getHookNames(): array {
|
||||
$names = array_merge(
|
||||
array_keys( array_filter( $this->handlers ) ),
|
||||
array_keys( array_filter( $this->registry->getGlobalHooks() ) ),
|
||||
array_keys( array_filter( $this->registry->getExtensionHooks() ) )
|
||||
array_keys( $this->dynamicHandlers ),
|
||||
array_keys( $this->registry->getGlobalHooks() ),
|
||||
array_keys( $this->registry->getExtensionHooks() )
|
||||
);
|
||||
|
||||
return array_unique( $names );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the array of handlers for the given hook.
|
||||
* Returns the names of all hooks that have handlers registered.
|
||||
* Note that this may include hook handlers that have been disabled using clear().
|
||||
*
|
||||
* @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[] A list of handler entries
|
||||
* @return array non-deprecated handler objects
|
||||
*/
|
||||
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!
|
||||
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 ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$handlers[] = $handler;
|
||||
$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];
|
||||
}
|
||||
|
||||
$this->handlers[ $hook ] = $handlers;
|
||||
}
|
||||
|
||||
return $this->handlers[ $hook ];
|
||||
return $handlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Will log a deprecation warning if:
|
||||
* 1. the hook is marked deprecated
|
||||
* 2. the "silent" flag is absent or false, and
|
||||
* 3. an extension registers a handler in the new way but does not acknowledge deprecation
|
||||
*/
|
||||
|
|
@ -621,71 +538,4 @@ 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,7 +100,12 @@ class Hooks {
|
|||
public static function getHandlers( $name ) {
|
||||
wfDeprecated( __METHOD__, '1.35' );
|
||||
$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
|
||||
return $hookContainer->getHandlerCallbacks( $name );
|
||||
$handlers = $hookContainer->getLegacyHandlers( $name );
|
||||
$funcName = 'on' . strtr( ucfirst( $name ), ':-', '__' );
|
||||
foreach ( $hookContainer->getHandlers( $name ) as $obj ) {
|
||||
$handlers[] = [ $obj, $funcName ];
|
||||
}
|
||||
return $handlers;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1012,11 +1012,11 @@ class ApiQuerySiteinfo extends ApiQueryBase {
|
|||
|
||||
$data = [];
|
||||
foreach ( $hookNames as $name ) {
|
||||
$subscribers = $hookContainer->getHandlerDescriptions( $name );
|
||||
$subscribers = $hookContainer->getLegacyHandlers( $name );
|
||||
|
||||
$arr = [
|
||||
'name' => $name,
|
||||
'subscribers' => $subscribers,
|
||||
'subscribers' => array_map( [ SpecialVersion::class, 'arrayToString' ], $subscribers ),
|
||||
];
|
||||
|
||||
ApiResult::setArrayType( $arr['subscribers'], 'array' );
|
||||
|
|
|
|||
|
|
@ -1084,44 +1084,41 @@ class SpecialVersion extends SpecialPage {
|
|||
* @return string HTML
|
||||
*/
|
||||
private function getHooks() {
|
||||
if ( !$this->getConfig()->get( MainConfigNames::SpecialVersionShowHooks ) ) {
|
||||
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 );
|
||||
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( 'td', [], $name );
|
||||
$ret[] = Html::element( 'td', [], $this->listToText( $handlers ) );
|
||||
$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 );
|
||||
}
|
||||
|
||||
$ret[] = Html::closeElement( 'table' );
|
||||
|
||||
return implode( "\n", $ret );
|
||||
return '';
|
||||
}
|
||||
|
||||
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()->getHookNames();
|
||||
$hookNames ??= $this->localServices->getHookContainer()->getRegisteredHooks();
|
||||
foreach ( $hookNames as $name ) {
|
||||
$this->clearHook( $name );
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,14 +43,14 @@ namespace MediaWiki\HookContainer {
|
|||
$hookContainer->register( 'FooHook', static function () {
|
||||
return true;
|
||||
} );
|
||||
$handlersBeforeScopedRegister = $hookContainer->getHandlerCallbacks( 'FooHook' );
|
||||
$handlersBeforeScopedRegister = $hookContainer->getLegacyHandlers( '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->getHandlerCallbacks( 'FooHook' );
|
||||
$handlersAfterScopedRegister = $hookContainer->getLegacyHandlers( '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->getHandlerCallbacks( 'FooHook' );
|
||||
$handlersAfterTearDown = $hookContainer->getLegacyHandlers( 'FooHook' );
|
||||
$this->assertCount( 2, $handlersAfterTearDown );
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
<?php
|
||||
|
||||
use Wikimedia\ScopedCallback;
|
||||
|
||||
class HooksTest extends MediaWikiIntegrationTestCase {
|
||||
|
||||
private const MOCK_HOOK_NAME = 'MediaWikiHooksTest001';
|
||||
|
|
@ -11,23 +9,23 @@ class HooksTest extends MediaWikiIntegrationTestCase {
|
|||
}
|
||||
|
||||
public static function provideHooks() {
|
||||
$obj = new HookTestDummyHookHandlerClass();
|
||||
$i = new HookTestDummyHookHandlerClass();
|
||||
|
||||
return [
|
||||
[
|
||||
'Object and method',
|
||||
[ $obj, 'someNonStatic' ],
|
||||
[ $i, 'someNonStatic' ],
|
||||
'changed-nonstatic',
|
||||
'changed-nonstatic'
|
||||
],
|
||||
[ 'Object and no method', [ $obj ], 'changed-onevent', 'original' ],
|
||||
[ 'Object and no method', [ $i ], 'changed-onevent', 'original' ],
|
||||
[
|
||||
'Object and method with data',
|
||||
[ $obj, 'someNonStaticWithData', 'data' ],
|
||||
[ $i, 'someNonStaticWithData', 'data' ],
|
||||
'data',
|
||||
'original'
|
||||
],
|
||||
[ 'Object and static method', [ $obj, 'someStatic' ], 'changed-static', 'original' ],
|
||||
[ 'Object and static method', [ $i, 'someStatic' ], 'changed-static', 'original' ],
|
||||
[
|
||||
'Class::method static call',
|
||||
[ 'HookTestDummyHookHandlerClass::someStatic' ],
|
||||
|
|
@ -71,50 +69,6 @@ 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
|
||||
|
|
@ -169,9 +123,8 @@ 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, [] );
|
||||
}
|
||||
|
||||
|
|
@ -235,8 +188,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 );
|
||||
}
|
||||
|
||||
|
|
@ -246,10 +199,9 @@ class HooksTest extends MediaWikiIntegrationTestCase {
|
|||
public function testCallHook_Deprecated() {
|
||||
$hookContainer = $this->getServiceContainer()->getHookContainer();
|
||||
$hookContainer->register( self::MOCK_HOOK_NAME, 'HookTestDummyHookHandlerClass::someStatic' );
|
||||
$this->expectDeprecationAndContinue( '/Use of MediaWikiHooksTest001 hook/' );
|
||||
$this->expectDeprecation();
|
||||
|
||||
$a = $b = 0;
|
||||
$this->hideDeprecated( 'Hooks::run' );
|
||||
Hooks::run( self::MOCK_HOOK_NAME, [ $a, $b ], '1.31' );
|
||||
}
|
||||
|
||||
|
|
@ -293,7 +245,9 @@ class HooksTest extends MediaWikiIntegrationTestCase {
|
|||
$foo = 'original';
|
||||
|
||||
$this->expectException( UnexpectedValueException::class );
|
||||
$this->expectExceptionMessage( 'unabortable MediaWikiHooksTest001' );
|
||||
$this->expectExceptionMessage( 'Invalid return from hook-MediaWikiHooksTest001-closure for ' .
|
||||
'unabortable MediaWikiHooksTest001'
|
||||
);
|
||||
Hooks::runWithoutAbort( self::MOCK_HOOK_NAME, [ &$foo ] );
|
||||
}
|
||||
|
||||
|
|
@ -306,7 +260,7 @@ class HooksTest extends MediaWikiIntegrationTestCase {
|
|||
$hookContainer->register( self::MOCK_HOOK_NAME, static function () {
|
||||
return 'test';
|
||||
} );
|
||||
$this->expectException( UnexpectedValueException::class );
|
||||
$this->expectDeprecation();
|
||||
Hooks::run( self::MOCK_HOOK_NAME, [] );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ class AuthManagerTest extends \MediaWikiIntegrationTestCase {
|
|||
|
||||
/** @var HookContainer */
|
||||
private $hookContainer;
|
||||
/** @var array */
|
||||
private $authHooks;
|
||||
|
||||
/** @var UserNameUtils */
|
||||
protected $userNameUtils;
|
||||
|
|
@ -114,7 +116,7 @@ class AuthManagerTest extends \MediaWikiIntegrationTestCase {
|
|||
$mock = $this->getMockBuilder( $hookInterface )
|
||||
->onlyMethods( [ "on$hook" ] )
|
||||
->getMock();
|
||||
$this->hookContainer->register( $hook, $mock );
|
||||
$this->authHooks[$hook][] = $mock;
|
||||
return $mock->expects( $expect )->method( "on$hook" );
|
||||
}
|
||||
|
||||
|
|
@ -123,7 +125,7 @@ class AuthManagerTest extends \MediaWikiIntegrationTestCase {
|
|||
* @param string $hook
|
||||
*/
|
||||
protected function unhook( $hook ) {
|
||||
$this->hookContainer->clear( $hook );
|
||||
$this->authHooks[$hook] = [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -224,9 +226,21 @@ class AuthManagerTest extends \MediaWikiIntegrationTestCase {
|
|||
$this->watchlistManager = $this->getServiceContainer()->getWatchlistManager();
|
||||
}
|
||||
if ( $regen || !$this->hookContainer ) {
|
||||
// Set up a HookContainer we control
|
||||
// Set up a HookContainer similar to the normal one except that it
|
||||
// gets global hooks from $this->authHooks instead of $wgHooks
|
||||
$this->hookContainer = new HookContainer(
|
||||
new StaticHookRegistry( [], [], [] ),
|
||||
new class( $this->authHooks ) extends StaticHookRegistry {
|
||||
private $hooks;
|
||||
|
||||
public function __construct( &$hooks ) {
|
||||
parent::__construct();
|
||||
$this->hooks =& $hooks;
|
||||
}
|
||||
|
||||
public function getGlobalHooks() {
|
||||
return $this->hooks;
|
||||
}
|
||||
},
|
||||
$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()
|
||||
->getHandlerCallbacks( 'LanguageGetTranslatedLanguageNames' );
|
||||
->getLegacyHandlers( 'LanguageGetTranslatedLanguageNames' );
|
||||
|
||||
$this->clearHook( 'LanguageGetTranslatedLanguageNames' );
|
||||
$this->overrideConfigValue( MainConfigNames::UsePigLatinVariant, true );
|
||||
|
|
|
|||
|
|
@ -19,9 +19,6 @@ class ExtensionRegistrationTest extends MediaWikiIntegrationTestCase {
|
|||
|
||||
private $autoloaderState;
|
||||
|
||||
/** @var ?ExtensionRegistry */
|
||||
private $originalExtensionRegistry = null;
|
||||
|
||||
protected function setUp(): void {
|
||||
global $wgHooks;
|
||||
|
||||
|
|
@ -46,11 +43,6 @@ class ExtensionRegistrationTest extends MediaWikiIntegrationTestCase {
|
|||
|
||||
protected function tearDown(): void {
|
||||
AutoLoader::restoreState( $this->autoloaderState );
|
||||
|
||||
if ( $this->originalExtensionRegistry ) {
|
||||
$this->setExtensionRegistry( $this->originalExtensionRegistry );
|
||||
}
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
|
|
@ -83,47 +75,27 @@ 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' => self::class . '::onAnEvent',
|
||||
'AnEvent' => 'FooBarClass::onAnEvent',
|
||||
'BooEvent' => 'main',
|
||||
],
|
||||
'HookHandlers' => [
|
||||
'main' => [ 'class' => self::class ]
|
||||
'HookHandler' => [
|
||||
'main' => [ 'class' => 'Whatever' ]
|
||||
],
|
||||
];
|
||||
|
||||
$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' ), 'AnEvent' );
|
||||
$this->assertTrue( $hookContainer->isRegistered( 'BooEvent' ), 'BooEvent' );
|
||||
$this->assertTrue( $hookContainer->isRegistered( 'AnEvent' ) );
|
||||
$this->assertTrue( $hookContainer->isRegistered( 'BooEvent' ) );
|
||||
}
|
||||
|
||||
public function testExportAutoload() {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,32 @@ 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
|
||||
|
|
@ -32,8 +58,11 @@ class TestLocalisationCache extends LocalisationCache {
|
|||
$code,
|
||||
$this->selfAccess->options->get( MainConfigNames::ExtensionMessagesFiles ),
|
||||
$this->selfAccess->options->get( MainConfigNames::MessagesDirs ),
|
||||
$hookContainer->getHandlerDescriptions( 'LocalisationCacheRecacheFallback' ),
|
||||
$hookContainer->getHandlerDescriptions( 'LocalisationCacheRecache' ),
|
||||
// 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' ) ),
|
||||
] ) );
|
||||
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->getHandlerCallbacks( 'FirstHook' ) );
|
||||
$this->assertCount( 1, $container->getLegacyHandlers( 'FirstHook' ) );
|
||||
|
||||
$this->assertTrue( isset( $hooks['FirstHook'] ) );
|
||||
$this->assertCount( 1, $hooks['FirstHook'] );
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
namespace MediaWiki\HookContainer {
|
||||
|
||||
use InvalidArgumentException;
|
||||
use MediaWiki\Tests\Unit\DummyServicesTrait;
|
||||
use MediaWikiUnitTestCase;
|
||||
use stdClass;
|
||||
|
|
@ -58,13 +57,11 @@ namespace MediaWiki\HookContainer {
|
|||
|
||||
public static function provideRegister() {
|
||||
return [
|
||||
'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' ],
|
||||
// not yet supported: 'function' => [ 'strtoupper', 'strtoupper' ],
|
||||
// not yet supported: 'object' => [ new \FooExtension\Hooks(), 'FooExtension\Hooks::onFooActionComplete' ],
|
||||
'extension' => [
|
||||
self::HANDLER_REGISTRATION,
|
||||
'FooExtension\Hooks::onFooActionComplete'
|
||||
'FooExtension\Hooks'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
|
@ -79,21 +76,20 @@ namespace MediaWiki\HookContainer {
|
|||
'FooActionComplete' => [ $handler ]
|
||||
], [] );
|
||||
|
||||
$handlers = $hookContainer->getHandlerDescriptions( 'FooActionComplete' );
|
||||
$handlers = $hookContainer->getHandlers( 'FooActionComplete' );
|
||||
|
||||
$this->assertSame( $expected, $handlers[0] );
|
||||
$this->assertSame( $expected, get_class( $handlers[0] ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Values returned: hook, handlersToRegister, expectedReturn
|
||||
*/
|
||||
public static function provideGetHandlerDescriptions() {
|
||||
public static function provideGetHandlers() {
|
||||
return [
|
||||
'NoHandlersExist' => [
|
||||
'MWTestHook',
|
||||
null,
|
||||
0,
|
||||
0
|
||||
[],
|
||||
],
|
||||
'SuccessfulHandlerReturn' => [
|
||||
'FooActionComplete',
|
||||
|
|
@ -104,8 +100,9 @@ namespace MediaWiki\HookContainer {
|
|||
'services' => [],
|
||||
],
|
||||
],
|
||||
1,
|
||||
1
|
||||
[
|
||||
new \FooExtension\Hooks(),
|
||||
],
|
||||
],
|
||||
'SkipDeprecated' => [
|
||||
'FooActionCompleteDeprecated',
|
||||
|
|
@ -117,8 +114,7 @@ namespace MediaWiki\HookContainer {
|
|||
],
|
||||
'deprecated' => true,
|
||||
],
|
||||
1,
|
||||
0
|
||||
[],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
|
@ -239,13 +235,13 @@ namespace MediaWiki\HookContainer {
|
|||
} );
|
||||
|
||||
// handlers registered in 2 different ways
|
||||
$this->assertCount( 2, $hookContainer->getHandlerCallbacks( 'MWTestHook' ) );
|
||||
$this->assertCount( 2, $hookContainer->getLegacyHandlers( 'MWTestHook' ) );
|
||||
$hookContainer->run( 'MWTestHook' );
|
||||
$this->assertEquals( 2, $numCalls );
|
||||
|
||||
// Remove one of the handlers that increments $called
|
||||
ScopedCallback::consume( $reset );
|
||||
$this->assertCount( 1, $hookContainer->getHandlerCallbacks( 'MWTestHook' ) );
|
||||
$this->assertCount( 1, $hookContainer->getLegacyHandlers( 'MWTestHook' ) );
|
||||
|
||||
$numCalls = 0;
|
||||
$hookContainer->run( 'MWTestHook' );
|
||||
|
|
@ -253,16 +249,10 @@ namespace MediaWiki\HookContainer {
|
|||
}
|
||||
|
||||
/**
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::getHandlerDescriptions
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::getHandlerCallbacks
|
||||
* @dataProvider provideGetHandlerDescriptions
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::getHandlers
|
||||
* @dataProvider provideGetHandlers
|
||||
*/
|
||||
public function testGetHandlers(
|
||||
string $hook,
|
||||
?array $handlerToRegister,
|
||||
int $expectedDescriptions,
|
||||
int $expectedCallbacks
|
||||
) {
|
||||
public function testGetHandlers( string $hook, ?array $handlerToRegister, array $expectedReturn ) {
|
||||
if ( $handlerToRegister ) {
|
||||
$hooks = [ $hook => [ $handlerToRegister ] ];
|
||||
} else {
|
||||
|
|
@ -272,24 +262,37 @@ namespace MediaWiki\HookContainer {
|
|||
'FooActionCompleteDeprecated' => [ 'deprecatedVersion' => '1.35' ]
|
||||
];
|
||||
$hookContainer = $this->newHookContainer( [], $hooks, $fakeDeprecatedHooks );
|
||||
|
||||
$descriptions = $hookContainer->getHandlerDescriptions( $hook );
|
||||
$this->assertCount(
|
||||
$expectedDescriptions,
|
||||
$descriptions,
|
||||
'getHandlerDescriptions()'
|
||||
$handlers = $hookContainer->getHandlers( $hook );
|
||||
$this->assertArrayEquals(
|
||||
$handlers,
|
||||
$expectedReturn,
|
||||
'HookContainer::getHandlers() should return array of handler functions'
|
||||
);
|
||||
}
|
||||
|
||||
$callbacks = $hookContainer->getHandlerCallbacks( $hook );
|
||||
$this->assertCount(
|
||||
$expectedCallbacks,
|
||||
/**
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::getLegacyHandlers
|
||||
*/
|
||||
public function testGetLegacyHandler() {
|
||||
$hookContainer = $this->newHookContainer( [], [], [] );
|
||||
$hookContainer->register(
|
||||
'FooActionComplete',
|
||||
[ new FooClass(),
|
||||
'fooMethod'
|
||||
]
|
||||
);
|
||||
$expectedHandlers = [ [ new FooClass(),
|
||||
'fooMethod'
|
||||
] ];
|
||||
$callbacks = $hookContainer->getLegacyHandlers( 'FooActionComplete' );
|
||||
$this->assertIsCallable( $callbacks[0] );
|
||||
$this->assertArrayEquals(
|
||||
$callbacks,
|
||||
'getHandlerCallbacks()'
|
||||
$expectedHandlers,
|
||||
true
|
||||
);
|
||||
|
||||
foreach ( $callbacks as $clbk ) {
|
||||
$this->assertIsCallable( $clbk );
|
||||
}
|
||||
// TODO: Test support for handlers with extra args once that is fixed.
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -297,9 +300,6 @@ namespace MediaWiki\HookContainer {
|
|||
*/
|
||||
public static function provideRunConfigured() {
|
||||
$fooObj = new FooClass();
|
||||
$closure = static function ( &$count ) {
|
||||
$count++;
|
||||
};
|
||||
$extra = 10;
|
||||
return [
|
||||
// Callables
|
||||
|
|
@ -307,10 +307,11 @@ namespace MediaWiki\HookContainer {
|
|||
'Object and method' => [ [ $fooObj, 'fooMethod' ] ],
|
||||
'Class name and static method' => [ [ 'MediaWiki\HookContainer\FooClass', 'fooStaticMethod' ] ],
|
||||
'static method' => [ 'MediaWiki\HookContainer\FooClass::fooStaticMethod' ],
|
||||
'Closure' => [ $closure ],
|
||||
|
||||
// Shorthand
|
||||
'Object' => [ $fooObj ],
|
||||
'Closure' => [
|
||||
static function ( &$count ) {
|
||||
$count++;
|
||||
}
|
||||
],
|
||||
|
||||
// Handlers with extra data attached
|
||||
'static method with extra data' => [
|
||||
|
|
@ -319,6 +320,7 @@ 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 ) {
|
||||
|
|
@ -329,18 +331,8 @@ 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' ] ]
|
||||
],
|
||||
|
|
@ -352,6 +344,7 @@ namespace MediaWiki\HookContainer {
|
|||
|
||||
/**
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::run
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::callLegacyHook
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::normalizeHandler
|
||||
* @dataProvider provideRunConfigured
|
||||
*/
|
||||
|
|
@ -366,9 +359,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( [], [] );
|
||||
|
|
@ -380,45 +373,6 @@ 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
|
||||
*/
|
||||
|
|
@ -430,14 +384,15 @@ 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( [], [ 'X\\Y::Increment' => [ $handler ] ] );
|
||||
$hookContainer = $this->newHookContainer( [], [ 'Increment' => [ $handler ] ] );
|
||||
|
||||
$count = 0;
|
||||
$hookValue = $hookContainer->run( 'X\\Y::Increment', [ &$count ] );
|
||||
$hookValue = $hookContainer->run( 'Increment', [ &$count ] );
|
||||
$this->assertTrue( $hookValue );
|
||||
$this->assertSame( $expectedCount, $count );
|
||||
}
|
||||
|
|
@ -456,6 +411,7 @@ namespace MediaWiki\HookContainer {
|
|||
|
||||
/**
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::run
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::callLegacyHook
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::normalizeHandler
|
||||
* @dataProvider provideRunFailsWithNoService
|
||||
*/
|
||||
|
|
@ -495,7 +451,7 @@ namespace MediaWiki\HookContainer {
|
|||
$seq = [ 'start' ];
|
||||
$hookContainer->run( 'Append', [ &$seq ] );
|
||||
|
||||
$expected = [ 'start', 'configured1', 'configured2', 'FooExtension', 'registered' ];
|
||||
$expected = [ 'start', 'configured1', 'configured2', 'registered', 'FooExtension' ];
|
||||
$this->assertSame( $expected, $seq );
|
||||
}
|
||||
|
||||
|
|
@ -571,16 +527,21 @@ 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 ) {
|
||||
public function testRunConfiguredDeprecated( array $deprecationInfo, bool $expectWarning ) {
|
||||
$hookContainer = $this->newHookContainer(
|
||||
[ 'Increment' => [ self::HANDLER_FUNCTION ] ],
|
||||
[],
|
||||
[ 'Increment' => $deprecationInfo ]
|
||||
);
|
||||
|
||||
// No warning expected when running the hook!
|
||||
// Expected deprecation?
|
||||
if ( $expectWarning ) {
|
||||
$this->expectDeprecationAndContinue( '/Use of Increment hook/' );
|
||||
}
|
||||
|
||||
// Deprecated hooks should still be functional!
|
||||
$count = 0;
|
||||
$hookContainer->run( 'Increment', [ &$count ] );
|
||||
|
|
@ -590,6 +551,7 @@ 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 ) {
|
||||
|
|
@ -626,41 +588,21 @@ 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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
// Try again, with $deprecationInfo as $options
|
||||
$hookContainer->run( 'Increment', [ &$count ], $deprecationInfo );
|
||||
$this->assertSame( 1, $count );
|
||||
$this->assertSame( 2, $count );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::getHookNames
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::getRegisteredHooks
|
||||
*/
|
||||
public function testGetHookNames() {
|
||||
$fooHandler = [ 'handler' => [
|
||||
|
|
@ -683,18 +625,15 @@ 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() );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -703,6 +642,10 @@ 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';
|
||||
|
|
@ -731,49 +674,6 @@ 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' => [],
|
||||
|
|
@ -855,7 +755,8 @@ namespace MediaWiki\HookContainer {
|
|||
|
||||
/**
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::isRegistered
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::getHandlerCallbacks
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::getHandlers
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::getLegacyHandlers
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::register
|
||||
* @covers \MediaWiki\HookContainer\HookContainer::clear
|
||||
*/
|
||||
|
|
@ -884,11 +785,11 @@ namespace MediaWiki\HookContainer {
|
|||
$this->assertTrue( $hookContainer->isRegistered( 'XyzHook' ) );
|
||||
$this->assertTrue( $hookContainer->isRegistered( 'FooActionComplete' ) );
|
||||
|
||||
$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' ) );
|
||||
$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' ) );
|
||||
|
||||
// No more increment!
|
||||
$hookContainer->run( 'Increment', [ &$count ] );
|
||||
|
|
@ -983,10 +884,6 @@ namespace MediaWiki\HookContainer {
|
|||
return true;
|
||||
}
|
||||
|
||||
public function onIncrement( &$count ) {
|
||||
$count++;
|
||||
}
|
||||
|
||||
public static function fooStaticMethod( &$count ) {
|
||||
$count++;
|
||||
return null;
|
||||
|
|
@ -1044,10 +941,6 @@ namespace FooExtension {
|
|||
$count++;
|
||||
}
|
||||
|
||||
public function onX_Y__Increment( &$count ) {
|
||||
$count++;
|
||||
}
|
||||
|
||||
public function onAppend( &$list ) {
|
||||
$list[] = 'FooExtension';
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue