Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.
   1  <?php
   2  // This file is part of Moodle - https://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <https://www.gnu.org/licenses/>.
  16  
  17  namespace core\hook;
  18  
  19  /**
  20   * Hooks tests.
  21   *
  22   * @coversDefaultClass \core\hook\manager
  23   *
  24   * @package   core
  25   * @author    Petr Skoda
  26   * @copyright 2022 Open LMS
  27   * @license   https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  28   */
  29  class manager_test extends \advanced_testcase {
  30      /**
  31       * Test public factory method to get hook manager.
  32       * @covers ::get_instance
  33       */
  34      public function test_get_instance() {
  35          $manager = manager::get_instance();
  36          $this->assertInstanceOf(manager::class, $manager);
  37  
  38          $this->assertSame($manager, manager::get_instance());
  39      }
  40  
  41      /**
  42       * Test getting of manager test instance.
  43       * @covers ::phpunit_get_instance
  44       */
  45      public function test_phpunit_get_instance() {
  46          $testmanager = manager::phpunit_get_instance([]);
  47          $this->assertSame([], $testmanager->get_hooks_with_callbacks());
  48  
  49          // We get a new instance every time.
  50          $this->assertNotSame($testmanager, manager::phpunit_get_instance([]));
  51  
  52          $componentfiles = [
  53              'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_valid.php',
  54          ];
  55          $testmanager = manager::phpunit_get_instance($componentfiles);
  56          $this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
  57      }
  58  
  59      /**
  60       * Test loading and parsing of callbacks from files.
  61       *
  62       * @covers ::get_callbacks_for_hook
  63       * @covers ::get_hooks_with_callbacks
  64       * @covers ::load_callbacks
  65       * @covers ::add_component_callbacks
  66       */
  67      public function test_callbacks() {
  68          $componentfiles = [
  69              'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_valid.php',
  70              'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_valid.php',
  71          ];
  72          $testmanager = manager::phpunit_get_instance($componentfiles);
  73          $this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
  74          $callbacks = $testmanager->get_callbacks_for_hook('test_plugin\\hook\\hook');
  75          $this->assertCount(2, $callbacks);
  76          $this->assertSame([
  77              'callback' => 'test_plugin\\callbacks::test2',
  78              'component' => 'test_plugin2',
  79              'disabled' => false,
  80              'priority' => 200,
  81          ], $callbacks[0]);
  82          $this->assertSame([
  83              'callback' => 'test_plugin\\callbacks::test1',
  84              'component' => 'test_plugin1',
  85              'disabled' => false,
  86              'priority' => 100,
  87          ], $callbacks[1]);
  88  
  89          $this->assertDebuggingNotCalled();
  90          $componentfiles = [
  91              'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_broken.php',
  92          ];
  93          $testmanager = manager::phpunit_get_instance($componentfiles);
  94          $this->assertSame([], $testmanager->get_hooks_with_callbacks());
  95          $debuggings = $this->getDebuggingMessages();
  96          $this->resetDebugging();
  97          $this->assertSame('Hook callback definition requires \'hook\' name in \'test_plugin1\'',
  98              $debuggings[0]->message);
  99          $this->assertSame('Hook callback definition requires \'callback\' callable in \'test_plugin1\'',
 100              $debuggings[1]->message);
 101          $this->assertSame('Hook callback definition contains invalid \'callback\' static class method string in \'test_plugin1\'',
 102              $debuggings[2]->message);
 103          $this->assertCount(3, $debuggings);
 104      }
 105  
 106      /**
 107       * Test hook dispatching, that is callback execution.
 108       * @covers ::dispatch
 109       */
 110      public function test_dispatch(): void {
 111          require_once (__DIR__ . '/../fixtures/hook/hook.php');
 112          require_once (__DIR__ . '/../fixtures/hook/callbacks.php');
 113  
 114          $componentfiles = [
 115              'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_valid.php',
 116              'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_valid.php',
 117          ];
 118          $testmanager = manager::phpunit_get_instance($componentfiles);
 119          \test_plugin\callbacks::$calls = [];
 120          $hook = new \test_plugin\hook\hook();
 121          $result = $testmanager->dispatch($hook);
 122          $this->assertSame($hook, $result);
 123          $this->assertSame(['test2', 'test1'], \test_plugin\callbacks::$calls);
 124          \test_plugin\callbacks::$calls = [];
 125          $this->assertDebuggingNotCalled();
 126      }
 127  
 128      /**
 129       * Test hook dispatching, that is callback execution.
 130       * @covers ::dispatch
 131       */
 132      public function test_dispatch_with_exception(): void {
 133          require_once (__DIR__ . '/../fixtures/hook/hook.php');
 134          require_once (__DIR__ . '/../fixtures/hook/callbacks.php');
 135  
 136          $componentfiles = [
 137              'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_exception.php',
 138              'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_valid.php',
 139          ];
 140          $testmanager = manager::phpunit_get_instance($componentfiles);
 141  
 142          $hook = new \test_plugin\hook\hook();
 143  
 144          $this->expectException(\Exception::class);
 145          $this->expectExceptionMessage('grrr');
 146  
 147          $testmanager->dispatch($hook);
 148      }
 149  
 150      /**
 151       * Test hook dispatching, that is callback execution.
 152       * @covers ::dispatch
 153       */
 154      public function test_dispatch_with_invalid(): void {
 155          // Missing callbacks is ignored.
 156          $componentfiles = [
 157              'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_missing.php',
 158              'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_valid.php',
 159          ];
 160          $testmanager = manager::phpunit_get_instance($componentfiles);
 161          \test_plugin\callbacks::$calls = [];
 162  
 163          $hook = new \test_plugin\hook\hook();
 164  
 165          $testmanager->dispatch($hook);
 166          $this->assertDebuggingCalled(
 167              "Hook callback definition contains invalid 'callback' method name in 'test_plugin1'. Callback method not found.",
 168          );
 169          $this->assertSame(['test2'], \test_plugin\callbacks::$calls);
 170      }
 171  
 172      /**
 173       * Test stoppping of hook dispatching.
 174       * @covers ::dispatch
 175       */
 176      public function test_dispatch_stoppable() {
 177          require_once (__DIR__ . '/../fixtures/hook/stoppablehook.php');
 178          require_once (__DIR__ . '/../fixtures/hook/callbacks.php');
 179  
 180          $componentfiles = [
 181              'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_stoppable.php',
 182              'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_stoppable.php',
 183          ];
 184          $testmanager = manager::phpunit_get_instance($componentfiles);
 185          \test_plugin\callbacks::$calls = [];
 186          $hook = new \test_plugin\hook\stoppablehook();
 187          $result = $testmanager->dispatch($hook);
 188          $this->assertSame($hook, $result);
 189          $this->assertSame(['stop1'], \test_plugin\callbacks::$calls);
 190          \test_plugin\callbacks::$calls = [];
 191          $this->assertDebuggingNotCalled();
 192      }
 193  
 194      /**
 195       * Tests callbacks can be overridden via CFG settings.
 196       * @covers ::load_callbacks
 197       * @covers ::dispatch
 198       */
 199      public function test_callback_overriding() {
 200          global $CFG;
 201          $this->resetAfterTest();
 202  
 203          $componentfiles = [
 204              'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_valid.php',
 205              'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_valid.php',
 206          ];
 207  
 208          $testmanager = manager::phpunit_get_instance($componentfiles);
 209          $this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
 210          $callbacks = $testmanager->get_callbacks_for_hook('test_plugin\\hook\\hook');
 211          $this->assertCount(2, $callbacks);
 212          $this->assertSame([
 213              'callback' => 'test_plugin\\callbacks::test2',
 214              'component' => 'test_plugin2',
 215              'disabled' => false,
 216              'priority' => 200,
 217          ], $callbacks[0]);
 218          $this->assertSame([
 219              'callback' => 'test_plugin\\callbacks::test1',
 220              'component' => 'test_plugin1',
 221              'disabled' => false,
 222              'priority' => 100,
 223          ], $callbacks[1]);
 224  
 225          $CFG->hooks_callback_overrides = [
 226              'test_plugin\\hook\\hook' => [
 227                  'test_plugin\\callbacks::test2' => ['priority' => 33]
 228              ]
 229          ];
 230  
 231          $testmanager = manager::phpunit_get_instance($componentfiles);
 232          $this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
 233          $callbacks = $testmanager->get_callbacks_for_hook('test_plugin\\hook\\hook');
 234          $this->assertCount(2, $callbacks);
 235          $this->normalise_callbacks($callbacks);
 236          $this->assertSame([
 237              'callback' => 'test_plugin\\callbacks::test1',
 238              'component' => 'test_plugin1',
 239              'disabled' => false,
 240              'priority' => 100,
 241          ], $callbacks[0]);
 242          $this->assertSame([
 243              'callback' => 'test_plugin\\callbacks::test2',
 244              'component' => 'test_plugin2',
 245              'defaultpriority' => 200,
 246              'disabled' => false,
 247              'priority' => 33,
 248          ], $callbacks[1]);
 249  
 250          $CFG->hooks_callback_overrides = [
 251              'test_plugin\\hook\\hook' => [
 252                  'test_plugin\\callbacks::test2' => ['priority' => 33, 'disabled' => true]
 253              ]
 254          ];
 255          $testmanager = manager::phpunit_get_instance($componentfiles);
 256          $this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
 257          $callbacks = $testmanager->get_callbacks_for_hook('test_plugin\\hook\\hook');
 258          $this->assertCount(2, $callbacks);
 259          $this->normalise_callbacks($callbacks);
 260          $this->assertSame([
 261              'callback' => 'test_plugin\\callbacks::test1',
 262              'component' => 'test_plugin1',
 263              'disabled' => false,
 264              'priority' => 100,
 265          ],
 266          $callbacks[0]);
 267          $this->assertSame([
 268              'callback' => 'test_plugin\\callbacks::test2',
 269              'component' => 'test_plugin2',
 270              'defaultpriority' => 200,
 271              'disabled' => true,
 272              'priority' => 33,
 273          ], $callbacks[1]);
 274  
 275          $CFG->hooks_callback_overrides = [
 276              'test_plugin\\hook\\hook' => [
 277                  'test_plugin\\callbacks::test2' => ['disabled' => true],
 278              ]
 279          ];
 280          $testmanager = manager::phpunit_get_instance($componentfiles);
 281          $this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
 282          $callbacks = $testmanager->get_callbacks_for_hook('test_plugin\\hook\\hook');
 283          $this->assertCount(2, $callbacks);
 284          $this->assertSame([
 285              'callback' => 'test_plugin\\callbacks::test2',
 286              'component' => 'test_plugin2',
 287              'disabled' => true,
 288              'priority' => 200,
 289          ], $callbacks[0]);
 290          $this->assertSame([
 291              'callback' => 'test_plugin\\callbacks::test1',
 292              'component' => 'test_plugin1',
 293              'disabled' => false,
 294              'priority' => 100,
 295          ], $callbacks[1]);
 296  
 297          require_once (__DIR__ . '/../fixtures/hook/hook.php');
 298          require_once (__DIR__ . '/../fixtures/hook/callbacks.php');
 299  
 300          \test_plugin\callbacks::$calls = [];
 301          $hook = new \test_plugin\hook\hook();
 302          $result = $testmanager->dispatch($hook);
 303          $this->assertSame($hook, $result);
 304          $this->assertSame(['test1'], \test_plugin\callbacks::$calls);
 305          \test_plugin\callbacks::$calls = [];
 306          $this->assertDebuggingNotCalled();
 307      }
 308  
 309      /**
 310       * Normalise the sort order of callbacks to help with asserts.
 311       *
 312       * @param array $callbacks
 313       * @return void
 314       */
 315      private function normalise_callbacks(array &$callbacks): void {
 316          foreach ($callbacks as &$callback) {
 317              ksort($callback);
 318          }
 319      }
 320  }