MDL-78332 core: fix hook discovery and use is_subclass_of()

And order hooks in admin UI - core first.
This commit is contained in:
Petr Skoda 2023-05-24 14:54:20 +02:00
parent 0dcb5c4281
commit 8a9e5aeb7a
6 changed files with 57 additions and 26 deletions

View File

@ -92,7 +92,7 @@ class hook_list_table extends flexible_table {
public function out(): void {
// All hook consumers referenced from the db/hooks.php files.
$hookmanager = \core\hook\manager::get_instance();
$allhooks = $hookmanager->get_all_callbacks();
$allhooks = (array)$hookmanager->get_all_callbacks();
// Add any unused hooks.
foreach (array_keys($this->emitters) as $classname) {
@ -102,6 +102,17 @@ class hook_list_table extends flexible_table {
$allhooks[$classname] = [];
}
// Order rows by hook name, putting core first.
\core_collator::ksort($allhooks);
$corehooks = [];
foreach ($allhooks as $classname => $consumers) {
if (str_starts_with($classname, 'core\\')) {
$corehooks[$classname] = $consumers;
unset($allhooks[$classname]);
}
}
$allhooks = array_merge($corehooks, $allhooks);
foreach ($allhooks as $classname => $consumers) {
$this->add_data_keyed(
$this->format_row((object) [

View File

@ -17,7 +17,7 @@
namespace core\hook;
/**
* Interface for hook callbacks that were deprecated by the hook.
* Interface for describing of lib.php callbacks that were deprecated by the hook.
*
* @package core
* @author Petr Skoda

View File

@ -26,7 +26,7 @@ namespace core\hook;
*/
interface described_hook {
/**
* Mandatory hook purpose description in Markdown format
* Hook purpose description in Markdown format
* used on Hooks overview page.
*
* It should include description of callback priority setting

View File

@ -17,7 +17,11 @@
namespace core\hook;
/**
* This interface describes a component which can discover hooks in its own namespace.
* This interface describes a class which can discover all hook
* classes of a plugin.
*
* To add new discovery agent in your plugin you need to add your_plugin\hooks
* class that implements this interface.
*
* @package core
* @copyright Andrew Lyons <andrew@nicols.co.uk>
@ -25,7 +29,7 @@ namespace core\hook;
*/
interface discovery_agent {
/**
* Discover hooks belonging to the component.
* Returns a list of hooks for component.
*
* @return array
*/

View File

@ -94,8 +94,6 @@ final class manager implements
*
* NOTE: this is the "Listener Provider" described in PSR-14,
* instead of instance parameter it uses real PHP class names.
* Moodle hooks should be final and parents of hook class are not
* considered when resolving callbacks.
*
* @param string $hookclassname PHP class name of hook
* @return array list of callback definitions
@ -420,10 +418,7 @@ final class manager implements
if (!class_exists($hookclassname)) {
continue;
}
// It's 2023 and PHP still doesn't provide a simple way to detect if a class implements an interface without
// that class being instantiated.
$rc = new \ReflectionClass($hookclassname);
if (!$rc->implementsInterface(\core\hook\deprecated_callback_replacement::class)) {
if (!is_subclass_of($hookclassname, \core\hook\deprecated_callback_replacement::class)) {
continue;
}
$deprecations = $hookclassname::get_deprecated_plugin_callbacks();
@ -558,18 +553,19 @@ final class manager implements
}
/**
* Returns list of hooks discovered through standardised Moodle methods.
* Returns list of hooks discovered through hook namespaces or discovery agents.
*
* Note that the exact discovery logic may change in the future,
* for now this looks for hooks mentioned in callback registrations
* and non-abstract classes in \component_name\hook namespaces that
* implement described_hook interface.
* The hooks overview page includes also all other classes that are
* referenced in callback registrations in db/hooks.php files, those
* are not included here.
*
* @return array hook class names
*/
public static function discover_known_hooks(): array {
// All classes in hook namespace of core and plugins, unless plugin has a discovery agent.
$hooks = \core\hooks::discover_hooks();
// Look for hooks classes in all plugins that implement discovery agent interface.
foreach (\core_component::get_component_names() as $component) {
$classname = "{$component}\\hooks";
@ -577,8 +573,7 @@ final class manager implements
continue;
}
$rc = new \ReflectionClass($classname);
if (!$rc->implementsInterface(\core\hook\hook_discover_agent::class)) {
if (!is_subclass_of($classname, discovery_agent::class)) {
continue;
}

View File

@ -17,22 +17,45 @@
namespace core;
/**
* Hook discovery agent for core.
* Standard hook discovery agent for Moodle which lists
* all non-abstract classes in hooks namespace of core and all plugins
* unless there is a hook discovery agent in a plugin.
*
* @package core
* @copyright Andrew Lyons <andrew@nicols.co.uk>
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class hooks implements \core\hook\discovery_agent {
final class hooks implements \core\hook\discovery_agent {
/**
* Returns all Moodle hooks in standard hook namespace.
*
* @return array list of hook classes
*/
public static function discover_hooks(): array {
// Describe any hard-coded hooks which can't be easily discovered by namespace.
// Look for hooks in hook namespace in core and all components.
$hooks = [];
$hooks = array_merge($hooks, self::discover_hooks_in_namespace('core', 'hook'));
foreach (\core_component::get_component_names() as $component) {
$agent = "$component\\hooks";
if (class_exists($agent) && is_subclass_of($agent, hook\discovery_agent::class)) {
// Let the plugin supply the list of hooks instead.
continue;
}
$hooks = array_merge($hooks, self::discover_hooks_in_namespace($component, 'hook'));
}
return $hooks;
}
/**
* Look up all non-abstract classes in "$component\$namespace" namespace.
*
* @param string $component
* @param string $namespace
* @return array list of hook classes
*/
public static function discover_hooks_in_namespace(string $component, string $namespace): array {
$classes = \core_component::get_component_classes_in_namespace($component, $namespace);
@ -44,8 +67,8 @@ class hooks implements \core\hook\discovery_agent {
continue;
}
if (is_a($classname, \core\hook\manager::class, true)) {
// Skip the manager.
if ($classname === \core\hook\manager::class) {
// Skip the manager in core.
continue;
}
@ -55,11 +78,9 @@ class hooks implements \core\hook\discovery_agent {
'tags' => [],
];
if ($rc->implementsInterface(\core\hook\described_hook::class)) {
if (is_subclass_of($classname, \core\hook\described_hook::class)) {
$hooks[$classname]['description'] = $classname::get_hook_description();
}
}
return $hooks;