PluginProbe
WooCommerce / 11.1.0-rc.2
WooCommerce v11.1.0-rc.2
11.1.0 11.1.0-rc.2 11.1.0-rc.1 11.1.0-beta.2 11.1.0-beta.1 11.0.1 11.0.0 11.0.0-rc.3 11.0.0-rc.2 11.0.0-rc.1 11.0.0-beta.2 11.0.0-beta.1 10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 All 648 releases
woocommerce / src / Utilities / CallbackUtil.php
CallbackUtil.php
163 lines 5.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 declare(strict_types=1);
3
4 namespace Automattic\WooCommerce\Utilities;
5
6 /**
7 * Utility class for working with WordPress hooks and callbacks.
8 *
9 * @since 10.5.0
10 */
11 final class CallbackUtil {
12
13 /**
14 * Get a stable signature for a callback that can be used for hashing.
15 *
16 * This method normalizes callbacks into consistent string representations,
17 * regardless of changes in dynamic properties in callback instances.
18 *
19 * @param callable|mixed $callback A PHP callback.
20 * @return string Normalized callback signature.
21 *
22 * @since 10.5.0
23 */
24 public static function get_callback_signature( $callback ): string {
25 if ( is_string( $callback ) ) {
26 // Standalone function.
27 return $callback;
28 }
29
30 if ( is_array( $callback ) && 2 === count( $callback ) ) {
31 $target = $callback[0];
32 $method = $callback[1];
33
34 if ( ( is_object( $target ) || is_string( $target ) ) && is_string( $method ) ) {
35 // Array callback (class method).
36 $class = is_object( $target ) ? get_class( $target ) : $target;
37 return "{$class}::{$method}";
38 }
39 }
40
41 if ( $callback instanceof \Closure ) {
42 // Closure.
43 try {
44 return self::get_closure_signature( $callback );
45 } catch ( \Exception $e ) {
46 return 'Closure@' . spl_object_hash( $callback );
47 }
48 }
49
50 if ( is_object( $callback ) ) {
51 // Invokable object.
52 try {
53 return self::get_invokable_signature( $callback );
54 } catch ( \Exception $e ) {
55 return get_class( $callback ) . '::__invoke';
56 }
57 }
58
59 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- Fallback for unknown callback types.
60 return serialize( $callback );
61 }
62
63 /**
64 * Get signatures for all callbacks attached to a specific hook.
65 *
66 * Returns an array of callback signatures for all callbacks registered
67 * with the specified hook name, organized by priority. This is useful
68 * for generating cache keys or comparing hook state.
69 *
70 * Closure signatures are based on their file location and line numbers,
71 * providing consistent hashes across requests for the same closure code.
72 *
73 * Memoized per hook for the request, recomputed only when the registered
74 * callbacks change, so repeated calls skip the reflection work that closure
75 * and invokable callbacks require.
76 *
77 * @param string $hook_name The name of the hook to inspect.
78 * @return array<int, array<string>> Array of priority => array( signatures ), empty if hook has no callbacks.
79 *
80 * @since 10.5.0
81 */
82 public static function get_hook_callback_signatures( string $hook_name ): array {
83 global $wp_filter;
84
85 static $cache = array();
86
87 if ( ! isset( $wp_filter[ $hook_name ] ) ) {
88 unset( $cache[ $hook_name ] );
89 return array();
90 }
91
92 $callbacks_by_priority = $wp_filter[ $hook_name ]->callbacks;
93
94 // Objects inside compare by identity, so a replaced instance or closure is
95 // a mismatch; holding the array also stops their ids being reused.
96 if ( isset( $cache[ $hook_name ] ) && $cache[ $hook_name ]['callbacks'] === $callbacks_by_priority ) {
97 return $cache[ $hook_name ]['signatures'];
98 }
99
100 $result = array();
101
102 foreach ( $callbacks_by_priority as $priority => $priority_callbacks ) {
103 $result[ $priority ] = array_map(
104 fn( $callback_data ) => self::get_callback_signature( $callback_data['function'] ),
105 array_values( $priority_callbacks )
106 );
107 }
108
109 $cache[ $hook_name ] = array(
110 'callbacks' => $callbacks_by_priority,
111 'signatures' => $result,
112 );
113
114 return $result;
115 }
116
117 /**
118 * Get a stable signature for a closure based on its file path and line numbers.
119 *
120 * @param \Closure $closure The closure to generate a signature for.
121 * @return string Signature in the format 'Closure@filename:startLine-endLine'.
122 * @throws \ReflectionException If reflection fails.
123 */
124 private static function get_closure_signature( \Closure $closure ): string {
125 $reflection = new \ReflectionFunction( $closure );
126 $file = $reflection->getFileName();
127 $start = $reflection->getStartLine();
128 $end = $reflection->getEndLine();
129
130 if ( false === $file || false === $start || false === $end ) {
131 throw new \ReflectionException( 'Unable to get closure location information' );
132 }
133
134 return sprintf( 'Closure@%s:%d-%d', $file, $start, $end );
135 }
136
137 /**
138 * Get a stable signature for an invokable object based on its class and __invoke method location.
139 *
140 * For regular classes, returns 'ClassName::__invoke' since the class name is stable.
141 * For anonymous classes, includes file location since the class name varies between requests.
142 *
143 * @param object $invokable The invokable object to generate a signature for.
144 * @return string Signature in format 'ClassName::__invoke' or 'class@anonymous[hash]::__invoke@filename:startLine-endLine'.
145 */
146 private static function get_invokable_signature( object $invokable ): string {
147 $method = new \ReflectionMethod( $invokable, '__invoke' );
148 $class = $method->getDeclaringClass();
149
150 if ( ! $class->isAnonymous() ) {
151 return $class->getName() . '::__invoke';
152 }
153
154 return sprintf(
155 'class@anonymous[%s]::__invoke@%s:%d-%d',
156 md5( $class->getName() ),
157 $method->getFileName(),
158 $method->getStartLine(),
159 $method->getEndLine()
160 );
161 }
162 }
163