This commit is contained in:
Andrew Nicols
2023-05-25 10:03:22 +08:00
7 changed files with 57 additions and 68 deletions
+12 -1
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) [
@@ -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
+1 -1
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
+6 -2
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 <[email protected]>
@@ -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
*/
+8 -37
View File
@@ -89,37 +89,11 @@ final class manager implements
return $instance;
}
/**
* Reset all hook caches. This is intended to be called only
* from the admin/hooks.php page after callback override is changed.
*
* @return void
* @codeCoverageIgnore
*/
public function reset_caches(): void {
if (PHPUNIT_TEST && $this === self::$instance) {
debugging('\core\hook\manager::get_instance()->reset_caches() is not supposed to be called in PHPUnit tests',
DEBUG_DEVELOPER);
return;
}
// WARNING: This will not work when callback overrides are changed
// and multiple web nodes with local cache stores are present - in that
// case admins must purge all caches when tweaking callback overrides.
$cache = \cache::make('core', 'hookcallbacks');
$cache->delete('callbacks');
$cache->delete('deprecations');
$this->init_standard_callbacks();
}
/**
* Returns list of callbacks for given hook name.
*
* 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
@@ -444,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();
@@ -582,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";
@@ -601,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;
}
+29 -8
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 <[email protected]>
* @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;
-18
View File
@@ -56,24 +56,6 @@ class manager_test extends \advanced_testcase {
$this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
}
/**
* Test reset of test instance.
*
* NOTE: normal hook manger instance cannot be reset in PHPUnit test
* because it may be used to control the test environment itself.
*
* @covers ::reset_caches
* @covers ::init_standard_callbacks
*/
public function test_reset_caches() {
$testmanager = manager::phpunit_get_instance([]);
$this->assertSame([], $testmanager->get_hooks_with_callbacks());
$testmanager->reset_caches();
$manager = manager::get_instance();
$this->assertSame($manager->get_hooks_with_callbacks(), $testmanager->get_hooks_with_callbacks());
}
/**
* Test loading and parsing of callbacks from files.
*