Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
use core\di;
1441 ariadna 20
use core\tests\fake_plugins_test_trait;
1 efrain 21
 
22
/**
23
 * Hooks tests.
24
 *
25
 * @package   core
26
 * @author    Petr Skoda
27
 * @copyright 2022 Open LMS
28
 * @license   https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
29
 * @covers \core\hook\manager
30
 */
31
final class manager_test extends \advanced_testcase {
1441 ariadna 32
 
33
    use fake_plugins_test_trait;
34
 
1 efrain 35
    /**
36
     * Test public factory method to get hook manager.
37
     */
38
    public function test_get_instance(): void {
39
        $manager = manager::get_instance();
40
        $this->assertInstanceOf(manager::class, $manager);
41
 
42
        $this->assertSame($manager, manager::get_instance());
43
    }
44
 
45
    /**
46
     * Test getting of manager test instance.
47
     */
48
    public function test_phpunit_get_instance(): void {
49
        $testmanager = manager::phpunit_get_instance([]);
50
        $this->assertSame([], $testmanager->get_hooks_with_callbacks());
51
 
52
        // We get a new instance every time.
53
        $this->assertNotSame($testmanager, manager::phpunit_get_instance([]));
54
 
55
        $componentfiles = [
56
            'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_valid.php',
57
        ];
58
        $testmanager = manager::phpunit_get_instance($componentfiles);
59
        $this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
60
    }
61
 
62
    /**
63
     * Test loading and parsing of callbacks from files.
64
     */
65
    public function test_callbacks(): void {
66
        $componentfiles = [
67
            'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_valid.php',
68
            'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_valid.php',
69
        ];
70
        $testmanager = manager::phpunit_get_instance($componentfiles);
71
        $this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
72
        $callbacks = $testmanager->get_callbacks_for_hook('test_plugin\\hook\\hook');
73
        $this->assertCount(2, $callbacks);
74
        $this->assertSame([
75
            'callback' => 'test_plugin\\callbacks::test2',
76
            'component' => 'test_plugin2',
77
            'disabled' => false,
78
            'priority' => 200,
79
        ], $callbacks[0]);
80
        $this->assertSame([
81
            'callback' => 'test_plugin\\callbacks::test1',
82
            'component' => 'test_plugin1',
83
            'disabled' => false,
84
            'priority' => 100,
85
        ], $callbacks[1]);
86
 
87
        $this->assertDebuggingNotCalled();
88
        $componentfiles = [
89
            'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_broken.php',
90
        ];
91
        $testmanager = manager::phpunit_get_instance($componentfiles);
92
        $this->assertSame([], $testmanager->get_hooks_with_callbacks());
93
        $debuggings = $this->getDebuggingMessages();
94
        $this->resetDebugging();
95
        $this->assertSame(
96
            'Hook callback definition requires \'hook\' name in \'test_plugin1\'',
97
            $debuggings[0]->message
98
        );
99
        $this->assertSame(
100
            'Hook callback definition requires \'callback\' callable in \'test_plugin1\'',
101
            $debuggings[1]->message
102
        );
103
        $this->assertSame(
104
            'Hook callback definition contains invalid \'callback\' static class method string in \'test_plugin1\'',
105
            $debuggings[2]->message
106
        );
107
        $this->assertCount(3, $debuggings);
108
    }
109
 
110
    /**
111
     * Test hook dispatching, that is callback execution.
112
     */
113
    public function test_dispatch(): void {
114
        require_once(__DIR__ . '/../fixtures/hook/hook.php');
115
        require_once(__DIR__ . '/../fixtures/hook/callbacks.php');
116
 
117
        $componentfiles = [
118
            'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_valid.php',
119
            'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_valid.php',
120
        ];
121
        $testmanager = manager::phpunit_get_instance($componentfiles);
122
        \test_plugin\callbacks::$calls = [];
123
        $hook = new \test_plugin\hook\hook();
124
        $result = $testmanager->dispatch($hook);
125
        $this->assertSame($hook, $result);
126
        $this->assertSame(['test2', 'test1'], \test_plugin\callbacks::$calls);
127
        \test_plugin\callbacks::$calls = [];
128
        $this->assertDebuggingNotCalled();
129
    }
130
 
131
    /**
132
     * Test hook dispatching, that is callback execution.
133
     */
134
    public function test_dispatch_with_exception(): void {
135
        require_once(__DIR__ . '/../fixtures/hook/hook.php');
136
        require_once(__DIR__ . '/../fixtures/hook/callbacks.php');
137
 
138
        $componentfiles = [
139
            'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_exception.php',
140
            'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_valid.php',
141
        ];
142
        $testmanager = manager::phpunit_get_instance($componentfiles);
143
 
144
        $hook = new \test_plugin\hook\hook();
145
 
146
        $this->expectException(\Exception::class);
147
        $this->expectExceptionMessage('grrr');
148
 
149
        $testmanager->dispatch($hook);
150
    }
151
 
152
    /**
153
     * Test hook dispatching, that is callback execution.
154
     */
155
    public function test_dispatch_with_invalid(): void {
156
        require_once(__DIR__ . '/../fixtures/hook/hook.php');
157
        require_once(__DIR__ . '/../fixtures/hook/callbacks.php');
158
 
159
        // Missing callbacks is ignored.
160
        $componentfiles = [
161
            'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_missing.php',
162
            'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_valid.php',
163
        ];
164
        $testmanager = manager::phpunit_get_instance($componentfiles);
165
        \test_plugin\callbacks::$calls = [];
166
 
167
        $hook = new \test_plugin\hook\hook();
168
 
169
        $testmanager->dispatch($hook);
170
        $this->assertDebuggingCalled(
171
            "Hook callback definition contains invalid 'callback' method name in 'test_plugin1'. Callback method not found.",
172
        );
173
        $this->assertSame(['test2'], \test_plugin\callbacks::$calls);
174
    }
175
 
176
    /**
177
     * Test stoppping of hook dispatching.
178
     */
179
    public function test_dispatch_stoppable(): void {
180
        require_once(__DIR__ . '/../fixtures/hook/stoppablehook.php');
181
        require_once(__DIR__ . '/../fixtures/hook/callbacks.php');
182
 
183
        $componentfiles = [
184
            'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_stoppable.php',
185
            'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_stoppable.php',
186
        ];
187
        $testmanager = manager::phpunit_get_instance($componentfiles);
188
        \test_plugin\callbacks::$calls = [];
189
        $hook = new \test_plugin\hook\stoppablehook();
190
        $result = $testmanager->dispatch($hook);
191
        $this->assertSame($hook, $result);
192
        $this->assertSame(['stop1'], \test_plugin\callbacks::$calls);
193
        \test_plugin\callbacks::$calls = [];
194
        $this->assertDebuggingNotCalled();
195
    }
196
 
197
    /**
198
     * Tests callbacks can be overridden via CFG settings.
199
     */
200
    public function test_callback_overriding(): void {
201
        global $CFG;
202
        $this->resetAfterTest();
203
 
204
        $componentfiles = [
205
            'test_plugin1' => __DIR__ . '/../fixtures/hook/hooks1_valid.php',
206
            'test_plugin2' => __DIR__ . '/../fixtures/hook/hooks2_valid.php',
207
        ];
208
 
209
        $testmanager = manager::phpunit_get_instance($componentfiles);
210
        $this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
211
        $callbacks = $testmanager->get_callbacks_for_hook('test_plugin\\hook\\hook');
212
        $this->assertCount(2, $callbacks);
213
        $this->assertSame([
214
            'callback' => 'test_plugin\\callbacks::test2',
215
            'component' => 'test_plugin2',
216
            'disabled' => false,
217
            'priority' => 200,
218
        ], $callbacks[0]);
219
        $this->assertSame([
220
            'callback' => 'test_plugin\\callbacks::test1',
221
            'component' => 'test_plugin1',
222
            'disabled' => false,
223
            'priority' => 100,
224
        ], $callbacks[1]);
225
 
226
        $CFG->hooks_callback_overrides = [
227
            'test_plugin\\hook\\hook' => [
228
                'test_plugin\\callbacks::test2' => ['priority' => 33],
229
            ],
230
        ];
231
 
232
        $testmanager = manager::phpunit_get_instance($componentfiles);
233
        $this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
234
        $callbacks = $testmanager->get_callbacks_for_hook('test_plugin\\hook\\hook');
235
        $this->assertCount(2, $callbacks);
236
        $this->normalise_callbacks($callbacks);
237
        $this->assertSame([
238
            'callback' => 'test_plugin\\callbacks::test1',
239
            'component' => 'test_plugin1',
240
            'disabled' => false,
241
            'priority' => 100,
242
        ], $callbacks[0]);
243
        $this->assertSame([
244
            'callback' => 'test_plugin\\callbacks::test2',
245
            'component' => 'test_plugin2',
246
            'defaultpriority' => 200,
247
            'disabled' => false,
248
            'priority' => 33,
249
        ], $callbacks[1]);
250
 
251
        $CFG->hooks_callback_overrides = [
252
            'test_plugin\\hook\\hook' => [
253
                'test_plugin\\callbacks::test2' => ['priority' => 33, 'disabled' => true],
254
            ],
255
        ];
256
        $testmanager = manager::phpunit_get_instance($componentfiles);
257
        $this->assertSame(['test_plugin\\hook\\hook'], $testmanager->get_hooks_with_callbacks());
258
        $callbacks = $testmanager->get_callbacks_for_hook('test_plugin\\hook\\hook');
259
        $this->assertCount(2, $callbacks);
260
        $this->normalise_callbacks($callbacks);
261
        $this->assertSame([
262
            'callback' => 'test_plugin\\callbacks::test1',
263
            'component' => 'test_plugin1',
264
            'disabled' => false,
265
            'priority' => 100,
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
        $CFG->hooks_callback_overrides = [];
308
    }
309
 
310
    /**
311
     * Register a fake plugin called hooktest in the component manager.
312
     *
313
     * Tests consuming this helpers must run in a separate process.
314
     */
315
    protected function setup_hooktest_plugin(): void {
316
        global $CFG;
317
 
318
        $this->add_mocked_plugintype('fake', "{$CFG->dirroot}/lib/tests/fixtures/hook/fakeplugins");
319
        $this->add_mocked_plugin('fake', 'hooktest', "{$CFG->dirroot}/lib/tests/fixtures/hook/fakeplugins/hooktest");
320
    }
321
 
322
    /**
323
     * Call a plugin callback that has been replaced by a hook, but has no hook callback.
324
     *
325
     * The original callback should be called, but a debugging message should be output.
326
     *
327
     * @runInSeparateProcess
328
     */
329
    public function test_migrated_callback(): void {
330
        $this->resetAfterTest(true);
331
        // Include plugin hook discovery agent, and the hook that replaces the callback.
332
        require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hooks.php');
333
        require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hook/hook_replacing_callback.php');
334
        // Register the fake plugin with the component manager.
335
        $this->setup_hooktest_plugin();
336
 
337
        // Register the fake plugin with the hook manager, but don't define any hook callbacks.
338
        di::set(
339
            manager::class,
340
            manager::phpunit_get_instance(
341
                [
342
                    'fake_hooktest' => __DIR__ . '/../fixtures/hook/fakeplugins/hooktest/db/hooks_nocallbacks.php',
343
                ],
344
            ),
345
        );
346
 
347
        // Confirm a non-deprecated callback is called as expected.
348
        $this->assertEquals('Called current callback', component_callback('fake_hooktest', 'current_callback'));
349
 
350
        // Confirm the deprecated callback is called as expected.
351
        $this->assertEquals(
352
            'Called deprecated callback',
353
            component_callback('fake_hooktest', 'old_callback', [], null, true)
354
        );
1441 ariadna 355
        $this->assertDebuggingNotCalled();
356
 
357
        // Forcefully modify the PHPUnit flag on the manager to ensure the debugging message is output.
358
        $manager = di::get(manager::class);
359
        $rp = new \ReflectionProperty($manager, 'phpunit');
360
        $rp->setValue($manager, false);
361
 
362
        component_callback('fake_hooktest', 'old_callback', [], null, true);
363
 
1 efrain 364
        $this->assertDebuggingCalled(
365
            'Callback old_callback in fake_hooktest component should be migrated to new hook ' .
1441 ariadna 366
                'callback for fake_hooktest\hook\hook_replacing_callback',
1 efrain 367
        );
368
    }
369
 
370
    /**
371
     * Call a plugin callback that has been replaced by a hook, and has a hook callback.
372
     *
373
     * The original callback should not be called, and no debugging should be output.
374
     *
375
     * @runInSeparateProcess
376
     */
377
    public function test_migrated_callback_with_replacement(): void {
378
        $this->resetAfterTest(true);
379
        // Include plugin hook discovery agent, and the hook that replaces the callback, and a hook callback for the hook.
380
        require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hooks.php');
381
        require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hook/hook_replacing_callback.php');
382
        require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hook_callbacks.php');
383
        // Register the fake plugin with the component manager.
384
        $this->setup_hooktest_plugin();
385
 
386
        // Register the fake plugin with the hook manager, including the hook callback.
387
        di::set(
388
            manager::class,
389
            manager::phpunit_get_instance([
390
                'fake_hooktest' => __DIR__ . '/../fixtures/hook/fakeplugins/hooktest/db/hooks.php',
391
            ]),
392
        );
393
 
394
        // Confirm a non-deprecated callback is called as expected.
395
        $this->assertEquals('Called current callback', component_callback('fake_hooktest', 'current_callback'));
396
 
397
        // Confirm the deprecated callback is not called, as expected.
398
        $this->assertNull(component_callback('fake_hooktest', 'old_callback', [], null, true));
399
        $this->assertDebuggingNotCalled();
400
    }
401
 
402
    /**
403
     * Call a plugin class callback that has been replaced by a hook, but has no hook callback.
404
     *
405
     * The original class callback should be called, but a debugging message should be output.
406
     *
407
     * @runInSeparateProcess
408
     */
409
    public function test_migrated_class_callback(): void {
410
        $this->resetAfterTest(true);
411
        // Include plugin hook discovery agent, the class containing callbacks, and the hook that replaces the class callback.
412
        require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/callbacks.php');
413
        require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hooks.php');
414
        require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hook/hook_replacing_class_callback.php');
415
        // Register the fake plugin with the component manager.
416
        $this->setup_hooktest_plugin();
417
 
418
        // Register the fake plugin with the hook manager, but don't define any hook callbacks.
419
        di::set(
420
            manager::class,
421
            manager::phpunit_get_instance([
422
                'fake_hooktest' => __DIR__ . '/../fixtures/hook/fakeplugins/hooktest/db/hooks_nocallbacks.php',
423
            ]),
424
        );
425
 
426
        // Confirm a non-deprecated class callback is called as expected.
427
        $this->assertEquals(
428
            'Called current class callback',
429
            component_class_callback('fake_hooktest\callbacks', 'current_class_callback', [])
430
        );
431
 
432
        // Confirm the deprecated class callback is called as expected.
433
        $this->assertEquals(
434
            'Called deprecated class callback',
435
            component_class_callback('fake_hooktest\callbacks', 'old_class_callback', [], null, true)
436
        );
1441 ariadna 437
        $this->assertDebuggingNotCalled();
438
 
439
        // Forcefully modify the PHPUnit flag on the manager to ensure the debugging message is output.
440
        $manager = di::get(manager::class);
441
        $rp = new \ReflectionProperty($manager, 'phpunit');
442
        $rp->setValue($manager, false);
443
 
444
        component_class_callback('fake_hooktest\callbacks', 'old_class_callback', [], null, true);
1 efrain 445
        $this->assertDebuggingCalled(
446
            'Callback callbacks::old_class_callback in fake_hooktest component should be migrated to new hook ' .
1441 ariadna 447
                'callback for fake_hooktest\hook\hook_replacing_class_callback',
1 efrain 448
        );
449
    }
450
 
451
    /**
452
     * Call a plugin class callback that has been replaced by a hook, and has a hook callback.
453
     *
454
     * The original callback should not be called, and no debugging should be output.
455
     *
456
     * @runInSeparateProcess
457
     */
458
    public function test_migrated_class_callback_with_replacement(): void {
459
        $this->resetAfterTest(true);
460
        // Include plugin hook discovery agent, the class containing callbacks, the hook that replaces the class callback,
461
        // and a hook callback for the new hook.
462
        require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/callbacks.php');
463
        require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hooks.php');
464
        require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hook/hook_replacing_class_callback.php');
465
        require_once(__DIR__ . '/../fixtures/hook/fakeplugins/hooktest/classes/hook_callbacks.php');
466
        // Register the fake plugin with the component manager.
467
        $this->setup_hooktest_plugin();
468
 
469
        // Register the fake plugin with the hook manager, including the hook callback.
470
        di::set(
471
            manager::class,
472
            manager::phpunit_get_instance([
473
                'fake_hooktest' => __DIR__ . '/../fixtures/hook/fakeplugins/hooktest/db/hooks.php',
474
            ]),
475
        );
476
 
477
        // Confirm a non-deprecated class callback is called as expected.
478
        $this->assertEquals(
479
            'Called current class callback',
480
            component_class_callback('fake_hooktest\callbacks', 'current_class_callback', [])
481
        );
482
 
483
        // Confirm the deprecated class callback is not called, as expected.
484
        $this->assertNull(component_class_callback('fake_hooktest\callbacks', 'old_class_callback', [], null, true));
485
        $this->assertDebuggingNotCalled();
486
    }
487
 
488
    /**
1441 ariadna 489
     * Test verifying that callbacks for deprecated plugins are not returned and hook dispatching won't call into these plugins.
490
     *
491
     * @runInSeparateProcess
492
     * @return void
493
     */
494
    public function test_get_callbacks_for_hook_deprecated_plugintype(): void {
495
        $this->resetAfterTest();
496
 
497
        // Inject the fixture 'fake' plugin type into component sources, which includes a single 'fake_fullfeatured' plugin.
498
        // This 'fake_fullfeatured' plugin is an available plugin at this stage (not yet deprecated).
499
        $this->add_full_mocked_plugintype(
500
            plugintype: 'fake',
501
            path: 'lib/tests/fixtures/fakeplugins/fake',
502
        );
503
 
504
        // Force reset the static instance cache \core\hook\manager::$instance so that a fresh instance is instantiated, ensuring
505
        // the component lists are re-run and the hook manager can see the injected mock plugin and it's callbacks.
506
        // Note: we can't use \core\hook\manager::phpunit_get_instance() because that doesn't load in component callbacks from disk.
507
        $hookmanrc = new \ReflectionClass(\core\hook\manager::class);
508
        $hookmanrc->setStaticPropertyValue('instance', null);
509
        $manager = \core\hook\manager::get_instance();
510
 
511
        // Get all registered callbacks for the hook listened to by the mock plugin (after_course_created).
512
        $listeners = $manager->get_callbacks_for_hook(\core_course\hook\after_course_created::class);
513
        $componentswithcallbacks = array_column($listeners, 'component');
514
 
515
        // Verify the available mock plugin is returned as a listener.
516
        $this->assertContains('fake_fullfeatured', $componentswithcallbacks);
517
 
518
        // Deprecate the 'fake' plugin type.
519
        $this->deprecate_full_mocked_plugintype('fake');
520
 
521
        // Force a fresh plugin manager instance, again to ensure the up-to-date component lists are used.
522
        $hookmanrc->setStaticPropertyValue('instance', null);
523
        $manager = \core\hook\manager::get_instance();
524
 
525
        // And verify the plugin is now not returned as a listener, since it's deprecated.
526
        $listeners = $manager->get_callbacks_for_hook(\core_course\hook\after_course_created::class);
527
        $componentswithcallbacks = array_column($listeners, 'component');
528
        $this->assertNotContains('fake_fullfeatured', $componentswithcallbacks);
529
    }
530
 
531
    /**
1 efrain 532
     * Normalise the sort order of callbacks to help with asserts.
533
     *
534
     * @param array $callbacks
535
     */
536
    private function normalise_callbacks(array &$callbacks): void {
537
        foreach ($callbacks as &$callback) {
538
            ksort($callback);
539
        }
540
    }
541
}