PluginProbe
404 Solution / trunk
404 Solution vtrunk
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / diagnostics / HookInstrumentationRegistry.php

HookInstrumentationRegistry.php in 404 Solution trunk, at includes/diagnostics/HookInstrumentationRegistry.php

126 lines 4.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * The instrumenter's memory of what it changed, indexed for O(1) answers.
9 *
10 * ABJ_404_Solution_HookCallbackInstrumenter owns registry MUTATION: wrapping a
11 * callback, marking one it must not wrap, putting the original back. This
12 * collaborator owns the bookkeeping that mutation needs to be reversible, and
13 * the three questions asked of it on the hot path:
14 *
15 * - which registrations belong to hook X (staleness check, once per walk);
16 * - is this exact entry one I installed (per entry examined);
17 * - is this entry a wrapper or marker closure of mine (per entry examined).
18 *
19 * All three used to be linear scans over every registration the instrumenter
20 * held, and the last two ran INSIDE the per-entry loop, so inspecting one hook
21 * cost O(entries x registrations). On the owner's plugin-heavy localhost (4,024
22 * registered callbacks) that is the difference between a bounded cost and a
23 * request that takes tens of seconds with debug_mode on. Keeping the indexes
24 * beside the table -- rather than deriving them per question -- is what makes
25 * the quadratic shape unrepresentable here.
26 *
27 * The registration shapes live here rather than in the instrumenter because
28 * this class is what stores them; the instrumenter imports them back, so there
29 * is one definition of what a registration IS and a `mode` discriminator that
30 * tells wrapper and marker apart wherever one is read.
31 *
32 * @phpstan-type WrapperRegistration array{mode: 'wrapper', hook: string, priority: int, ordinal: int, id: string, original: callable, wrapper: callable}
33 * @phpstan-type MarkerRegistration array{mode: 'marker', hook: string, priority: int, ordinal: int, id: string, original: callable, before_id: string, before: callable}
34 * @phpstan-type Registration WrapperRegistration|MarkerRegistration
35 */
36 final class ABJ_404_Solution_HookInstrumentationRegistry {
37
38 /** @var array<string, Registration> Registration by collision-safe key. */
39 private $registrations = array();
40
41 /** @var array<string, array<string, true>> Registration keys by hook name. */
42 private $keysByHook = array();
43
44 /**
45 * Object ids of every wrapper/marker closure currently registered.
46 *
47 * spl_object_id() recycles ids once an object is freed, which would make a
48 * stale entry here answer "yes, mine" about an unrelated object. It cannot
49 * go stale: the closure is reachable from its own registration for exactly
50 * as long as the id is in this set, so the id cannot be reissued while the
51 * set still holds it.
52 *
53 * @var array<int, true>
54 */
55 private $ownedCallbackIds = array();
56
57 /** @param Registration $registration */
58 public function add(string $key, array $registration): void {
59 $this->forget($key);
60 $this->registrations[$key] = $registration;
61 $hook = $registration['hook'];
62 $this->keysByHook[$hook][$key] = true;
63 foreach (array('wrapper', 'before') as $field) {
64 $callback = $registration[$field] ?? null;
65 if (is_object($callback)) {
66 $this->ownedCallbackIds[spl_object_id($callback)] = true;
67 }
68 }
69 }
70
71 public function forget(string $key): void {
72 if (!array_key_exists($key, $this->registrations)) {
73 return;
74 }
75 $registration = $this->registrations[$key];
76 $hook = $registration['hook'];
77 unset($this->keysByHook[$hook][$key], $this->registrations[$key]);
78 if (($this->keysByHook[$hook] ?? null) === array()) {
79 unset($this->keysByHook[$hook]);
80 }
81 foreach (array('wrapper', 'before') as $field) {
82 $callback = $registration[$field] ?? null;
83 if (is_object($callback)) {
84 unset($this->ownedCallbackIds[spl_object_id($callback)]);
85 }
86 }
87 }
88
89 /** @return Registration|null */
90 public function get(string $key): ?array {
91 return $this->registrations[$key] ?? null;
92 }
93
94 /** @return array<string, Registration> Registrations of one hook, keyed by key. */
95 public function forHook(string $hook): array {
96 $found = array();
97 foreach (array_keys($this->keysByHook[$hook] ?? array()) as $key) {
98 if (array_key_exists($key, $this->registrations)) {
99 $found[$key] = $this->registrations[$key];
100 }
101 }
102 return $found;
103 }
104
105 /** @return array<string, Registration> Every registration, keyed by key. */
106 public function all(): array {
107 return $this->registrations;
108 }
109
110 /**
111 * Is this WP_Hook entry's callable one of the closures we installed?
112 *
113 * @param mixed $callback
114 */
115 public function ownsCallback($callback): bool {
116 return is_object($callback)
117 && isset($this->ownedCallbackIds[spl_object_id($callback)]);
118 }
119
120 public function clear(): void {
121 $this->registrations = array();
122 $this->keysByHook = array();
123 $this->ownedCallbackIds = array();
124 }
125 }
126