Registry shape at last inspection, by hook name. */ private $shapes = array(); /** * True when the caller must walk this hook's registry: either it changed * since the last inspection, or its shape cannot be read cheaply. */ public function needsInspection(string $hookName): bool { $shape = self::shape($hookName); return $shape === self::UNPROBEABLE || ($this->shapes[$hookName] ?? null) !== $shape; } /** * Remember the shape a completed inspection leaves behind. Read after the * walk rather than before it, because instrumenting a by-reference callback * adds a marker entry and therefore changes the very count being recorded. */ public function recordInspected(string $hookName): void { $shape = self::shape($hookName); if ($shape !== self::UNPROBEABLE) { $this->shapes[$hookName] = $shape; } } /** Forget everything: the scope that made these observations has ended. */ public function clear(): void { $this->shapes = array(); } /** * The current shape of one hook's callback registry, or UNPROBEABLE. * * Pure array reads only, per invariant 1 above. */ private static function shape(string $hookName): string { $filters = $GLOBALS['wp_filter'] ?? null; if (!is_array($filters)) { return self::UNPROBEABLE; } if (!array_key_exists($hookName, $filters)) { return self::ABSENT; } $hookObject = $filters[$hookName]; if (!is_object($hookObject)) { return self::UNPROBEABLE; } $callbacks = get_object_vars($hookObject)['callbacks'] ?? null; if (!is_array($callbacks)) { return self::UNPROBEABLE; } $shape = (string)spl_object_id($hookObject); foreach ($callbacks as $priority => $entries) { $shape .= '|' . $priority . ':' . (is_array($entries) ? count($entries) : -1); } return $shape; } }