From c449206c063ec8845276199e4a272aeced51e69e Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 31 Mar 2026 23:03:28 +0200 Subject: [PATCH 1/9] Add initial PHPStan config --- phpstan.neon.dist | 12 ++++++++++++ tests/phpstan/scan-files.php | 6 ++++++ 2 files changed, 18 insertions(+) create mode 100644 phpstan.neon.dist create mode 100644 tests/phpstan/scan-files.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 00000000..43d6bcac --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,12 @@ +parameters: + level: 1 + paths: + - src + - profile-command.php + scanDirectories: + - vendor/wp-cli/wp-cli/php + scanFiles: + - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php + - tests/phpstan/scan-files.php + + treatPhpDocTypesAsCertain: false diff --git a/tests/phpstan/scan-files.php b/tests/phpstan/scan-files.php new file mode 100644 index 00000000..81f3d91b --- /dev/null +++ b/tests/phpstan/scan-files.php @@ -0,0 +1,6 @@ + Date: Wed, 1 Apr 2026 10:45:44 +0200 Subject: [PATCH 2/9] Level 2 --- phpstan.neon.dist | 2 +- src/Formatter.php | 4 ++-- src/Profiler.php | 32 ++++++++++++++++---------------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 43d6bcac..1c825d92 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 1 + level: 2 paths: - src - profile-command.php diff --git a/src/Formatter.php b/src/Formatter.php index 775db154..f863aefd 100644 --- a/src/Formatter.php +++ b/src/Formatter.php @@ -68,8 +68,8 @@ public function display_items( $items, $include_total, $order, $orderby ) { * @param double $b Floating number. */ private function compare_float( $a, $b ) { - $a = number_format( $a, 4 ); - $b = number_format( $b, 4 ); + $a = round( $a, 4 ); + $b = round( $b, 4 ); if ( 0 === $a - $b ) { return 0; } elseif ( $a - $b < 0 ) { diff --git a/src/Profiler.php b/src/Profiler.php index 2658441f..df5c23aa 100644 --- a/src/Profiler.php +++ b/src/Profiler.php @@ -439,8 +439,8 @@ private function load_wordpress_with_template() { if ( 'bootstrap' === $this->focus ) { $this->set_stage_hooks( $this->stage_hooks['bootstrap'] ); } elseif ( ! $this->focus ) { - $logger = new Logger( array( 'stage' => 'bootstrap' ) ); - $logger->start(); + $bootstrap_logger = new Logger( array( 'stage' => 'bootstrap' ) ); + $bootstrap_logger->start(); } } WP_CLI::get_runner()->load_wordpress(); @@ -451,9 +451,9 @@ private function load_wordpress_with_template() { if ( 'hook' === $this->type && 'wp_loaded:after' === $this->focus ) { $this->wp_tick_profile_end(); } - if ( 'stage' === $this->type && ! $this->focus ) { - $logger->stop(); - $this->loggers[] = $logger; + if ( 'stage' === $this->type && ! $this->focus && isset( $bootstrap_logger ) ) { + $bootstrap_logger->stop(); + $this->loggers[] = $bootstrap_logger; } // Skip main_query and template stages for admin requests. @@ -466,8 +466,8 @@ private function load_wordpress_with_template() { if ( 'main_query' === $this->focus ) { $this->set_stage_hooks( $this->stage_hooks['main_query'] ); } elseif ( ! $this->focus ) { - $logger = new Logger( array( 'stage' => 'main_query' ) ); - $logger->start(); + $main_query_logger = new Logger( array( 'stage' => 'main_query' ) ); + $main_query_logger->start(); } } wp(); @@ -478,9 +478,9 @@ private function load_wordpress_with_template() { if ( 'hook' === $this->type && 'wp:after' === $this->focus ) { $this->wp_tick_profile_end(); } - if ( 'stage' === $this->type && ! $this->focus ) { - $logger->stop(); - $this->loggers[] = $logger; + if ( 'stage' === $this->type && ! $this->focus && isset( $main_query_logger ) ) { + $main_query_logger->stop(); + $this->loggers[] = $main_query_logger; } define( 'WP_USE_THEMES', true ); @@ -496,8 +496,8 @@ private function load_wordpress_with_template() { if ( 'template' === $this->focus ) { $this->set_stage_hooks( $this->stage_hooks['template'] ); } elseif ( ! $this->focus ) { - $logger = new Logger( array( 'stage' => 'template' ) ); - $logger->start(); + $template_logger = new Logger( array( 'stage' => 'template' ) ); + $template_logger->start(); } } ob_start(); @@ -510,9 +510,9 @@ private function load_wordpress_with_template() { if ( 'hook' === $this->type && 'wp_footer:after' === $this->focus ) { $this->wp_tick_profile_end(); } - if ( 'stage' === $this->type && ! $this->focus ) { - $logger->stop(); - $this->loggers[] = $logger; + if ( 'stage' === $this->type && ! $this->focus && isset( $template_logger ) ) { + $template_logger->stop(); + $this->loggers[] = $template_logger; } } @@ -577,7 +577,7 @@ private function set_stage_hooks( $hooks ) { /** * Get the callbacks for a given filter * - * @param string + * @param string $filter * @return array|false */ private static function get_filter_callbacks( $filter ) { From 571974d7e19fbd695a8fb212933973b489927856 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 1 Apr 2026 17:52:09 +0200 Subject: [PATCH 3/9] Level 4 --- phpstan.neon.dist | 2 +- src/Formatter.php | 2 +- src/Profiler.php | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 1c825d92..3108e400 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 2 + level: 4 paths: - src - profile-command.php diff --git a/src/Formatter.php b/src/Formatter.php index f863aefd..b5daf7dd 100644 --- a/src/Formatter.php +++ b/src/Formatter.php @@ -70,7 +70,7 @@ public function display_items( $items, $include_total, $order, $orderby ) { private function compare_float( $a, $b ) { $a = round( $a, 4 ); $b = round( $b, 4 ); - if ( 0 === $a - $b ) { + if ( 0.0 === $a - $b ) { return 0; } elseif ( $a - $b < 0 ) { return -1; diff --git a/src/Profiler.php b/src/Profiler.php index df5c23aa..dbfbf481 100644 --- a/src/Profiler.php +++ b/src/Profiler.php @@ -471,6 +471,7 @@ private function load_wordpress_with_template() { } } wp(); + // @phpstan-ignore-next-line if ( $this->running_hook ) { $this->loggers[ $this->running_hook ]->stop(); $this->running_hook = null; @@ -503,6 +504,7 @@ private function load_wordpress_with_template() { ob_start(); require_once ABSPATH . WPINC . '/template-loader.php'; ob_get_clean(); + // @phpstan-ignore-next-line if ( $this->running_hook ) { $this->loggers[ $this->running_hook ]->stop(); $this->running_hook = null; From 4733d2d28bb858f1735683b1aab566c0c0303098 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 2 Apr 2026 09:51:01 +0200 Subject: [PATCH 4/9] Level 5 --- phpstan.neon.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 3108e400..bf766444 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 4 + level: 5 paths: - src - profile-command.php From a0e1f6cf1735ef3426c2b925c5986226274e5da0 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 2 Apr 2026 11:27:40 +0200 Subject: [PATCH 5/9] Level 6 --- phpstan.neon.dist | 2 +- src/Command.php | 35 ++++++++++++++++++++----- src/Formatter.php | 35 +++++++++++++++++++++---- src/Logger.php | 63 ++++++++++++++++++++++++++++++++++++++++++++ src/Profiler.php | 66 ++++++++++++++++++++++++++++++++++++++++++++--- 5 files changed, 185 insertions(+), 16 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index bf766444..4f71cc92 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 5 + level: 6 paths: - src - profile-command.php diff --git a/src/Command.php b/src/Command.php index 8727938e..de65d4ad 100644 --- a/src/Command.php +++ b/src/Command.php @@ -129,6 +129,10 @@ class Command { * * @skipglobalargcheck * @when before_wp_load + * + * @param array $args + * @param array $assoc_args + * @return void */ public function stage( $args, $assoc_args ) { global $wpdb; @@ -257,6 +261,10 @@ public function stage( $args, $assoc_args ) { * * @skipglobalargcheck * @when before_wp_load + * + * @param array $args + * @param array $assoc_args + * @return void */ public function hook( $args, $assoc_args ) { @@ -357,6 +365,10 @@ public function hook( $args, $assoc_args ) { * | 0.1009s | 100% | 1 | * +---------+-------------+---------------+ * + * @param array $args + * @param array $assoc_args + * @return void + * * @subcommand eval */ public function eval_( $args, $assoc_args ) { @@ -426,6 +438,10 @@ function () use ( $statement ) { * | 0.1009s | 100% | 1 | * +---------+-------------+---------------+ * + * @param array $args + * @param array $assoc_args + * @return void + * * @subcommand eval-file */ public function eval_file( $args, $assoc_args ) { @@ -451,6 +467,12 @@ function () use ( $file ) { /** * Profile an eval or eval-file statement. + * + * @param array $assoc_args + * @param callable $profile_callback + * @param string $order + * @param string|null $orderby + * @return void */ private static function profile_eval_ish( $assoc_args, $profile_callback, $order = 'ASC', $orderby = null ) { $hook = Utils\get_flag_value( $assoc_args, 'hook' ); @@ -500,6 +522,7 @@ private static function profile_eval_ish( $assoc_args, $profile_callback, $order * Include a file without exposing it to current scope * * @param string $file + * @return void */ private static function include_file( $file ) { include $file; @@ -508,9 +531,9 @@ private static function include_file( $file ) { /** * Filter loggers with zero-ish values. * - * @param array $loggers - * @param array $metrics - * @return array + * @param array<\WP_CLI\Profile\Logger> $loggers + * @param array $metrics + * @return array<\WP_CLI\Profile\Logger> */ private static function shine_spotlight( $loggers, $metrics ) { @@ -550,9 +573,9 @@ private static function shine_spotlight( $loggers, $metrics ) { /** * Filter loggers to only those whose callback name matches a pattern. * - * @param array $loggers - * @param string $pattern - * @return array + * @param array<\WP_CLI\Profile\Logger> $loggers + * @param string $pattern + * @return array<\WP_CLI\Profile\Logger> */ private static function filter_by_callback( $loggers, $pattern ) { return array_filter( diff --git a/src/Formatter.php b/src/Formatter.php index b5daf7dd..492118b8 100644 --- a/src/Formatter.php +++ b/src/Formatter.php @@ -4,12 +4,28 @@ class Formatter { + /** + * @var \WP_CLI\Formatter + */ private $formatter; + /** + * @var array + */ private $args; + /** + * @var int|null + */ private $total_cell_index; + /** + * Formatter constructor. + * + * @param array $assoc_args + * @param array|null $fields + * @param string|bool $prefix + */ public function __construct( &$assoc_args, $fields = null, $prefix = false ) { $format_args = array( 'format' => 'table', @@ -51,7 +67,11 @@ public function __construct( &$assoc_args, $fields = null, $prefix = false ) { /** * Display multiple items according to the output arguments. * - * @param array $items + * @param array<\WP_CLI\Profile\Logger> $items + * @param bool $include_total + * @param string $order + * @param string|null $orderby + * @return void */ public function display_items( $items, $include_total, $order, $orderby ) { if ( 'table' === $this->args['format'] && empty( $this->args['field'] ) ) { @@ -64,8 +84,9 @@ public function display_items( $items, $include_total, $order, $orderby ) { /** * Function to compare floats. * - * @param double $a Floating number. - * @param double $b Floating number. + * @param float $a Floating number. + * @param float $b Floating number. + * @return int */ private function compare_float( $a, $b ) { $a = round( $a, 4 ); @@ -82,8 +103,12 @@ private function compare_float( $a, $b ) { /** * Show items in a \cli\Table. * - * @param array $items - * @param array $fields + * @param string $order + * @param string|null $orderby + * @param array<\WP_CLI\Profile\Logger> $items + * @param array $fields + * @param bool $include_total + * @return void */ private function show_table( $order, $orderby, $items, $fields, $include_total ) { $table = new \cli\Table(); diff --git a/src/Logger.php b/src/Logger.php index 0c3ab116..4d70d57b 100644 --- a/src/Logger.php +++ b/src/Logger.php @@ -2,36 +2,72 @@ namespace WP_CLI\Profile; +/** + * Logger class. + * + * @property string $callback + * @property string $location + */ class Logger { + /** @var float */ public $time = 0; + /** @var int */ public $query_count = 0; + /** @var float */ public $query_time = 0; + /** @var int */ public $cache_hits = 0; + /** @var int */ public $cache_misses = 0; + /** @var string|null */ public $cache_ratio = null; + /** @var int */ public $hook_count = 0; + /** @var float */ public $hook_time = 0; + /** @var int */ public $request_count = 0; + /** @var float */ public $request_time = 0; + /** @var float|null */ private $start_time = null; + /** @var int|null */ private $query_offset = null; + /** @var int|null */ private $cache_hit_offset = null; + /** @var int|null */ private $cache_miss_offset = null; + /** @var float|null */ private $hook_start_time = null; + /** @var int */ private $hook_depth = 0; + /** @var float|null */ private $request_start_time = null; + /** @var array */ private $definitions = array(); + /** @var array<\WP_CLI\Profile\Logger> */ public static $active_loggers = array(); + /** + * Logger constructor. + * + * @param array $definition + */ public function __construct( $definition = array() ) { foreach ( $definition as $k => $v ) { $this->definitions[ $k ] = $v; } } + /** + * Magic getter for definitions. + * + * @param string $key + * @return mixed + */ public function __get( $key ) { if ( isset( $this->definitions[ $key ] ) ) { return $this->definitions[ $key ]; @@ -40,16 +76,31 @@ public function __get( $key ) { return null; } + /** + * Magic setter for definitions. + * + * @param string $key + * @param mixed $value + * @return void + */ public function __set( $key, $value ) { $this->definitions[ $key ] = $value; } + /** + * Magic isset for definitions. + * + * @param string $key + * @return bool + */ public function __isset( $key ) { return isset( $this->definitions[ $key ] ); } /** * Start this logger + * + * @return void */ public function start() { global $wpdb, $wp_object_cache; @@ -66,6 +117,8 @@ public function start() { /** * Whether or not the logger is running + * + * @return bool */ public function running() { return ! is_null( $this->start_time ); @@ -73,6 +126,8 @@ public function running() { /** * Stop this logger + * + * @return void */ public function stop() { global $wpdb, $wp_object_cache; @@ -115,6 +170,8 @@ public function stop() { /** * Start this logger's hook timer + * + * @return void */ public function start_hook_timer() { ++$this->hook_count; @@ -128,6 +185,8 @@ public function start_hook_timer() { /** * Stop this logger's hook timer + * + * @return void */ public function stop_hook_timer() { if ( $this->hook_depth ) { @@ -142,6 +201,8 @@ public function stop_hook_timer() { /** * Start this logger's request timer + * + * @return void */ public function start_request_timer() { ++$this->request_count; @@ -150,6 +211,8 @@ public function start_request_timer() { /** * Stop this logger's request timer + * + * @return void */ public function stop_request_timer() { if ( ! is_null( $this->request_start_time ) ) { diff --git a/src/Profiler.php b/src/Profiler.php index dbfbf481..279695aa 100644 --- a/src/Profiler.php +++ b/src/Profiler.php @@ -6,9 +6,13 @@ class Profiler { + /** @var string */ private $type; + /** @var string|bool|null */ private $focus; + /** @var array> */ private $loggers = array(); + /** @var array> */ private $stage_hooks = array( 'bootstrap' => array( 'muplugins_loaded', @@ -35,26 +39,49 @@ class Profiler { ), ); + /** @var array */ private $current_stage_hooks = array(); + /** @var string|null */ private $running_hook = null; + /** @var string|null */ private $previous_filter = null; + /** @var array|null */ private $previous_filter_callbacks = null; + /** @var int */ private $filter_depth = 0; + /** @var string|null */ private $tick_callback = null; + /** @var string|null */ private $tick_location = null; + /** @var float|null */ private $tick_start_time = null; + /** @var int|null */ private $tick_query_offset = null; + /** @var int|null */ private $tick_cache_hit_offset = null; + /** @var int|null */ private $tick_cache_miss_offset = null; + /** @var bool */ private $is_admin_request = false; + /** + * Profiler constructor. + * + * @param string $type + * @param string|bool|null $focus + */ public function __construct( $type, $focus ) { $this->type = $type; $this->focus = $focus; } + /** + * Get the loggers. + * + * @return array<\WP_CLI\Profile\Logger> + */ public function get_loggers() { foreach ( $this->loggers as $i => $logger ) { if ( is_array( $logger ) ) { @@ -77,6 +104,8 @@ public function get_loggers() { /** * Run the profiler against WordPress + * + * @return void */ public function run() { $url = WP_CLI::get_runner()->config['url']; @@ -153,6 +182,9 @@ function () { /** * Start profiling function calls on the end of this filter + * + * @param mixed $value + * @return mixed */ public function wp_tick_profile_begin( $value = null ) { @@ -189,6 +221,9 @@ public function wp_tick_profile_begin( $value = null ) { /** * Stop profiling function calls at the beginning of this filter + * + * @param mixed $value + * @return mixed */ public function wp_tick_profile_end( $value = null ) { unregister_tick_function( array( $this, 'handle_function_tick' ) ); @@ -198,6 +233,8 @@ public function wp_tick_profile_end( $value = null ) { /** * Profiling verbosity at the beginning of every action and filter + * + * @return void */ public function wp_hook_begin() { @@ -247,6 +284,9 @@ public function wp_hook_begin() { /** * Wrap current filter callbacks with a timer + * + * @param string $current_filter + * @return void */ private function wrap_current_filter_callbacks( $current_filter ) { @@ -282,6 +322,9 @@ private function wrap_current_filter_callbacks( $current_filter ) { /** * Profiling verbosity at the end of every action and filter + * + * @param mixed $filter_value + * @return mixed */ public function wp_hook_end( $filter_value = null ) { @@ -313,6 +356,8 @@ public function wp_hook_end( $filter_value = null ) { /** * Handle the tick of a function + * + * @return void */ public function handle_function_tick() { global $wpdb, $wp_object_cache; @@ -399,6 +444,9 @@ public function handle_function_tick() { /** * Profiling request time for any active Loggers + * + * @param mixed $filter_value + * @return mixed */ public function wp_request_begin( $filter_value = null ) { foreach ( Logger::$active_loggers as $logger ) { @@ -409,6 +457,9 @@ public function wp_request_begin( $filter_value = null ) { /** * Profiling request time for any active Loggers + * + * @param mixed $filter_value + * @return mixed */ public function wp_request_end( $filter_value = null ) { foreach ( Logger::$active_loggers as $logger ) { @@ -419,6 +470,8 @@ public function wp_request_end( $filter_value = null ) { /** * Runs through the entirety of the WP bootstrap process + * + * @return void */ private function load_wordpress_with_template() { @@ -471,7 +524,6 @@ private function load_wordpress_with_template() { } } wp(); - // @phpstan-ignore-next-line if ( $this->running_hook ) { $this->loggers[ $this->running_hook ]->stop(); $this->running_hook = null; @@ -504,7 +556,6 @@ private function load_wordpress_with_template() { ob_start(); require_once ABSPATH . WPINC . '/template-loader.php'; ob_get_clean(); - // @phpstan-ignore-next-line if ( $this->running_hook ) { $this->loggers[ $this->running_hook ]->stop(); $this->running_hook = null; @@ -520,6 +571,9 @@ private function load_wordpress_with_template() { /** * Get a human-readable name from a callback + * + * @param mixed $callback + * @return array{0: string, 1: string} */ private static function get_name_location_from_callback( $callback ) { $location = ''; @@ -568,6 +622,9 @@ private static function get_short_location( $location ) { /** * Set the hooks for the current stage + * + * @param array $hooks + * @return void */ private function set_stage_hooks( $hooks ) { $this->current_stage_hooks = $hooks; @@ -580,7 +637,7 @@ private function set_stage_hooks( $hooks ) { * Get the callbacks for a given filter * * @param string $filter - * @return array|false + * @return array|false */ private static function get_filter_callbacks( $filter ) { global $wp_filter; @@ -604,7 +661,8 @@ private static function get_filter_callbacks( $filter ) { * Set the callbacks for a given filter * * @param string $filter - * @param mixed $callbacks + * @param mixed $callbacks + * @return void */ private static function set_filter_callbacks( $filter, $callbacks ) { global $wp_filter; From 1fb51faf5ec45a8219cbfe72973d6e20e7403336 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 2 Apr 2026 13:07:40 +0200 Subject: [PATCH 6/9] Lint fix --- src/Logger.php | 32 ++++++++++++++++---------------- src/Profiler.php | 20 ++++++++++---------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/Logger.php b/src/Logger.php index 4d70d57b..f6e53ecb 100644 --- a/src/Logger.php +++ b/src/Logger.php @@ -11,37 +11,37 @@ class Logger { /** @var float */ - public $time = 0; + public $time = 0; /** @var int */ - public $query_count = 0; + public $query_count = 0; /** @var float */ - public $query_time = 0; + public $query_time = 0; /** @var int */ - public $cache_hits = 0; + public $cache_hits = 0; /** @var int */ - public $cache_misses = 0; + public $cache_misses = 0; /** @var string|null */ - public $cache_ratio = null; + public $cache_ratio = null; /** @var int */ - public $hook_count = 0; + public $hook_count = 0; /** @var float */ - public $hook_time = 0; + public $hook_time = 0; /** @var int */ - public $request_count = 0; + public $request_count = 0; /** @var float */ - public $request_time = 0; + public $request_time = 0; /** @var float|null */ - private $start_time = null; + private $start_time = null; /** @var int|null */ - private $query_offset = null; + private $query_offset = null; /** @var int|null */ - private $cache_hit_offset = null; + private $cache_hit_offset = null; /** @var int|null */ - private $cache_miss_offset = null; + private $cache_miss_offset = null; /** @var float|null */ - private $hook_start_time = null; + private $hook_start_time = null; /** @var int */ - private $hook_depth = 0; + private $hook_depth = 0; /** @var float|null */ private $request_start_time = null; diff --git a/src/Profiler.php b/src/Profiler.php index 279695aa..f5465a9b 100644 --- a/src/Profiler.php +++ b/src/Profiler.php @@ -11,7 +11,7 @@ class Profiler { /** @var string|bool|null */ private $focus; /** @var array> */ - private $loggers = array(); + private $loggers = array(); /** @var array> */ private $stage_hooks = array( 'bootstrap' => array( @@ -40,26 +40,26 @@ class Profiler { ); /** @var array */ - private $current_stage_hooks = array(); + private $current_stage_hooks = array(); /** @var string|null */ - private $running_hook = null; + private $running_hook = null; /** @var string|null */ - private $previous_filter = null; + private $previous_filter = null; /** @var array|null */ private $previous_filter_callbacks = null; /** @var int */ - private $filter_depth = 0; + private $filter_depth = 0; /** @var string|null */ - private $tick_callback = null; + private $tick_callback = null; /** @var string|null */ - private $tick_location = null; + private $tick_location = null; /** @var float|null */ - private $tick_start_time = null; + private $tick_start_time = null; /** @var int|null */ - private $tick_query_offset = null; + private $tick_query_offset = null; /** @var int|null */ - private $tick_cache_hit_offset = null; + private $tick_cache_hit_offset = null; /** @var int|null */ private $tick_cache_miss_offset = null; From 0b4f0d2f13aeea6a13e02b79e6d473a63794b212 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 7 Apr 2026 09:40:49 +0200 Subject: [PATCH 7/9] Level 7 --- phpstan.neon.dist | 2 +- src/Command.php | 1 + src/Formatter.php | 8 +++++--- src/Profiler.php | 36 ++++++++++++++++++++++++++---------- 4 files changed, 33 insertions(+), 14 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 4f71cc92..54a544d7 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 6 + level: 7 paths: - src - profile-command.php diff --git a/src/Command.php b/src/Command.php index de65d4ad..00f30554 100644 --- a/src/Command.php +++ b/src/Command.php @@ -147,6 +147,7 @@ public function stage( $args, $assoc_args ) { WP_CLI::error( 'Invalid stage. Must be one of ' . implode( ', ', $valid_stages ) . ', or use --all.' ); } + assert( is_bool( $focus ) || is_string( $focus ) || is_null( $focus ) ); $profiler = new Profiler( 'stage', $focus ); $profiler->run(); diff --git a/src/Formatter.php b/src/Formatter.php index 492118b8..2cf62bdc 100644 --- a/src/Formatter.php +++ b/src/Formatter.php @@ -24,7 +24,7 @@ class Formatter { * * @param array $assoc_args * @param array|null $fields - * @param string|bool $prefix + * @param string|false $prefix */ public function __construct( &$assoc_args, $fields = null, $prefix = false ) { $format_args = array( @@ -134,7 +134,7 @@ function ( $a, $b ) use ( $order, $orderby ) { list( $first, $second ) = $orderby_array; if ( is_numeric( $first->$orderby ) && is_numeric( $second->$orderby ) ) { - return $this->compare_float( $first->$orderby, $second->$orderby ); + return $this->compare_float( (float) $first->$orderby, (float) $second->$orderby ); } return strcmp( $first->$orderby, $second->$orderby ); @@ -164,6 +164,7 @@ function ( $a, $b ) use ( $order, $orderby ) { } if ( stripos( $fields[ $i ], '_ratio' ) ) { if ( ! is_null( $value ) ) { + assert( is_array( $totals[ $i ] ) ); $totals[ $i ][] = $value; } } else { @@ -181,7 +182,8 @@ function ( $a, $b ) use ( $order, $orderby ) { continue; } if ( stripos( $fields[ $i ], '_time' ) || 'time' === $fields[ $i ] ) { - $totals[ $i ] = round( $value, 4 ) . 's'; + assert( is_numeric( $value ) ); + $totals[ $i ] = round( (float) $value, 4 ) . 's'; } if ( is_array( $value ) ) { if ( ! empty( $value ) ) { diff --git a/src/Profiler.php b/src/Profiler.php index f5465a9b..5f12cb3c 100644 --- a/src/Profiler.php +++ b/src/Profiler.php @@ -6,11 +6,11 @@ class Profiler { - /** @var string */ + /** @var string|false */ private $type; /** @var string|bool|null */ private $focus; - /** @var array> */ + /** @var array> */ private $loggers = array(); /** @var array> */ private $stage_hooks = array( @@ -69,7 +69,7 @@ class Profiler { /** * Profiler constructor. * - * @param string $type + * @param string|false $type * @param string|bool|null $focus */ public function __construct( $type, $focus ) { @@ -99,7 +99,9 @@ public function get_loggers() { $logger->location = self::get_short_location( $logger->location ); $this->loggers[ $i ] = $logger; } - return $this->loggers; + /** @var array<\WP_CLI\Profile\Logger> $loggers */ + $loggers = $this->loggers; + return $loggers; } /** @@ -149,7 +151,7 @@ function () { ); if ( 'hook' === $this->type && - $this->focus && + is_string( $this->focus ) && ':before' === substr( $this->focus, -7, 7 ) ) { $stage_hooks = array(); @@ -158,7 +160,7 @@ function () { } $end_hook = substr( $this->focus, 0, -7 ); $key = array_search( $end_hook, $stage_hooks, true ); - if ( isset( $stage_hooks[ $key - 1 ] ) ) { + if ( is_int( $key ) && isset( $stage_hooks[ $key - 1 ] ) ) { $start_hook = $stage_hooks[ $key - 1 ]; WP_CLI::add_wp_hook( $start_hook, array( $this, 'wp_tick_profile_begin' ), 9999 ); } else { @@ -167,7 +169,7 @@ function () { WP_CLI::add_wp_hook( $end_hook, array( $this, 'wp_tick_profile_end' ), -9999 ); } elseif ( 'hook' === $this->type && - $this->focus && + is_string( $this->focus ) && ':after' === substr( $this->focus, -6, 6 ) ) { $start_hook = substr( $this->focus, 0, -6 ); @@ -243,10 +245,14 @@ public function wp_hook_begin() { } $current_filter = current_filter(); + if ( ! is_string( $current_filter ) ) { + return; + } if ( ( 'stage' === $this->type && in_array( $current_filter, $this->current_stage_hooks, true ) ) || ( 'hook' === $this->type && ! $this->focus ) ) { $pseudo_hook = "{$current_filter}:before"; if ( isset( $this->loggers[ $pseudo_hook ] ) ) { + assert( $this->loggers[ $pseudo_hook ] instanceof Logger ); $this->loggers[ $pseudo_hook ]->stop(); } $callback_count = 0; @@ -308,6 +314,7 @@ private function wrap_current_filter_callbacks( $current_filter ) { ) ); } + assert( $this->loggers[ $i ] instanceof Logger ); $this->loggers[ $i ]->start(); $value = call_user_func_array( $the_['function'], func_get_args() ); $this->loggers[ $i ]->stop(); @@ -335,10 +342,11 @@ public function wp_hook_end( $filter_value = null ) { $current_filter = current_filter(); if ( ( 'stage' === $this->type && in_array( $current_filter, $this->current_stage_hooks, true ) ) || ( 'hook' === $this->type && ! $this->focus ) ) { + assert( $this->loggers[ $current_filter ] instanceof Logger ); $this->loggers[ $current_filter ]->stop(); if ( 'stage' === $this->type ) { $key = array_search( $current_filter, $this->current_stage_hooks, true ); - if ( false !== $key && isset( $this->current_stage_hooks[ $key + 1 ] ) ) { + if ( is_int( $key ) && isset( $this->current_stage_hooks[ $key + 1 ] ) ) { $pseudo_hook = "{$this->current_stage_hooks[$key+1]}:before"; } else { $pseudo_hook = "{$this->current_stage_hooks[$key]}:after"; @@ -379,6 +387,7 @@ public function handle_function_tick() { ); } + assert( is_array( $this->loggers[ $callback_hash ] ) ); $this->loggers[ $callback_hash ]['time'] += $time; if ( isset( $wpdb ) ) { @@ -411,7 +420,10 @@ public function handle_function_tick() { $location = ''; $callback = ''; if ( in_array( strtolower( $frame['function'] ), array( 'include', 'require', 'include_once', 'require_once' ), true ) ) { - $callback = $frame['function'] . " '" . $frame['args'][0] . "'"; + $callback = $frame['function']; + if ( isset( $frame['args'][0] ) ) { + $callback .= " '" . $frame['args'][0] . "'"; + } } elseif ( isset( $frame['object'] ) && method_exists( $frame['object'], $frame['function'] ) ) { $callback = get_class( $frame['object'] ) . '->' . $frame['function'] . '()'; } elseif ( isset( $frame['class'] ) && method_exists( $frame['class'], $frame['function'] ) ) { @@ -498,6 +510,7 @@ private function load_wordpress_with_template() { } WP_CLI::get_runner()->load_wordpress(); if ( $this->running_hook ) { + assert( $this->loggers[ $this->running_hook ] instanceof Logger ); $this->loggers[ $this->running_hook ]->stop(); $this->running_hook = null; } @@ -525,6 +538,7 @@ private function load_wordpress_with_template() { } wp(); if ( $this->running_hook ) { + assert( $this->loggers[ $this->running_hook ] instanceof Logger ); $this->loggers[ $this->running_hook ]->stop(); $this->running_hook = null; } @@ -557,6 +571,7 @@ private function load_wordpress_with_template() { require_once ABSPATH . WPINC . '/template-loader.php'; ob_get_clean(); if ( $this->running_hook ) { + assert( $this->loggers[ $this->running_hook ] instanceof Logger ); $this->loggers[ $this->running_hook ]->stop(); $this->running_hook = null; } @@ -605,7 +620,8 @@ private static function get_name_location_from_callback( $callback ) { * @return string */ private static function get_short_location( $location ) { - $abspath = rtrim( realpath( ABSPATH ), '/' ) . '/'; + $real_abspath = realpath( ABSPATH ); + $abspath = rtrim( false !== $real_abspath ? $real_abspath : ABSPATH, '/' ) . '/'; if ( defined( 'WP_PLUGIN_DIR' ) && 0 === stripos( $location, WP_PLUGIN_DIR ) ) { $location = str_replace( trailingslashit( WP_PLUGIN_DIR ), '', $location ); } elseif ( defined( 'WPMU_PLUGIN_DIR' ) && 0 === stripos( $location, WPMU_PLUGIN_DIR ) ) { From 7aae9f21140a22bff3465092260209d7ccf335ea Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 7 Apr 2026 10:58:56 +0200 Subject: [PATCH 8/9] Level 8 --- phpstan.neon.dist | 2 +- src/Formatter.php | 7 +++++-- src/Profiler.php | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 54a544d7..7f239a7e 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 7 + level: 8 paths: - src - profile-command.php diff --git a/src/Formatter.php b/src/Formatter.php index 2cf62bdc..60303d7a 100644 --- a/src/Formatter.php +++ b/src/Formatter.php @@ -27,6 +27,9 @@ class Formatter { * @param string|false $prefix */ public function __construct( &$assoc_args, $fields = null, $prefix = false ) { + if ( null === $fields ) { + $fields = []; + } $format_args = array( 'format' => 'table', 'fields' => $fields, @@ -55,9 +58,9 @@ public function __construct( &$assoc_args, $fields = null, $prefix = false ) { } } - if ( 'time' !== $fields[0] ) { + if ( ! empty( $fields ) && 'time' !== $fields[0] ) { $index = array_search( $fields[0], $format_args['fields'], true ); - $this->total_cell_index = ( false !== $index ) ? $index : null; + $this->total_cell_index = ( false !== $index ) ? (int) $index : null; } $this->args = $format_args; diff --git a/src/Profiler.php b/src/Profiler.php index 5f12cb3c..4a526d52 100644 --- a/src/Profiler.php +++ b/src/Profiler.php @@ -272,7 +272,8 @@ public function wp_hook_begin() { } if ( 0 === $this->filter_depth - && ! is_null( $this->previous_filter_callbacks ) ) { + && ! is_null( $this->previous_filter_callbacks ) + && ! is_null( $this->previous_filter ) ) { self::set_filter_callbacks( $this->previous_filter, $this->previous_filter_callbacks ); $this->previous_filter_callbacks = null; } From 7c99e0e51f173739d2f9c6bb850bc9feb603c13a Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 7 Apr 2026 11:46:21 +0200 Subject: [PATCH 9/9] Level 9 --- phpstan.neon.dist | 2 +- src/Command.php | 86 ++++++++++++++++++++++++---------- src/Formatter.php | 32 ++++++++++--- src/Profiler.php | 115 +++++++++++++++++++++++++++++----------------- 4 files changed, 163 insertions(+), 72 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 7f239a7e..3b5a67dd 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 8 + level: 9 paths: - src - profile-command.php diff --git a/src/Command.php b/src/Command.php index 00f30554..b27e082a 100644 --- a/src/Command.php +++ b/src/Command.php @@ -137,17 +137,20 @@ class Command { public function stage( $args, $assoc_args ) { global $wpdb; - $focus = Utils\get_flag_value( $assoc_args, 'all', isset( $args[0] ) ? $args[0] : null ); + /** @var array $typed_assoc_args */ + $typed_assoc_args = self::get_typed_assoc_args( $assoc_args ); + $focus = Utils\get_flag_value( $typed_assoc_args, 'all', isset( $args[0] ) ? $args[0] : null ); - $order = Utils\get_flag_value( $assoc_args, 'order', 'ASC' ); - $orderby = Utils\get_flag_value( $assoc_args, 'orderby', null ); + $order_val = Utils\get_flag_value( $typed_assoc_args, 'order', 'ASC' ); + $order = is_string( $order_val ) ? $order_val : 'ASC'; + $orderby_val = Utils\get_flag_value( $typed_assoc_args, 'orderby', null ); + $orderby = ( is_string( $orderby_val ) || is_null( $orderby_val ) ) ? $orderby_val : null; $valid_stages = array( 'bootstrap', 'main_query', 'template' ); if ( $focus && ( true !== $focus && ! in_array( $focus, $valid_stages, true ) ) ) { WP_CLI::error( 'Invalid stage. Must be one of ' . implode( ', ', $valid_stages ) . ', or use --all.' ); } - assert( is_bool( $focus ) || is_string( $focus ) || is_null( $focus ) ); $profiler = new Profiler( 'stage', $focus ); $profiler->run(); @@ -184,9 +187,10 @@ public function stage( $args, $assoc_args ) { ); } $fields = array_merge( $base, $metrics ); - $formatter = new Formatter( $assoc_args, $fields ); + $formatter = new Formatter( $typed_assoc_args, $fields ); $loggers = $profiler->get_loggers(); - if ( Utils\get_flag_value( $assoc_args, 'spotlight' ) ) { + /** @var array $typed_assoc_args */ + if ( Utils\get_flag_value( $typed_assoc_args, 'spotlight' ) ) { $loggers = self::shine_spotlight( $loggers, $metrics ); } @@ -269,10 +273,14 @@ public function stage( $args, $assoc_args ) { */ public function hook( $args, $assoc_args ) { - $focus = Utils\get_flag_value( $assoc_args, 'all', isset( $args[0] ) ? $args[0] : null ); + /** @var array $typed_assoc_args */ + $typed_assoc_args = self::get_typed_assoc_args( $assoc_args ); + $focus = Utils\get_flag_value( $typed_assoc_args, 'all', isset( $args[0] ) ? $args[0] : null ); - $order = Utils\get_flag_value( $assoc_args, 'order', 'ASC' ); - $orderby = Utils\get_flag_value( $assoc_args, 'orderby', null ); + $order_val = Utils\get_flag_value( $typed_assoc_args, 'order', 'ASC' ); + $order = is_string( $order_val ) ? $order_val : 'ASC'; + $orderby_val = Utils\get_flag_value( $typed_assoc_args, 'orderby', null ); + $orderby = ( is_string( $orderby_val ) || is_null( $orderby_val ) ) ? $orderby_val : null; $profiler = new Profiler( 'hook', $focus ); $profiler->run(); @@ -300,13 +308,16 @@ public function hook( $args, $assoc_args ) { 'request_count', ); $fields = array_merge( $base, $metrics ); - $formatter = new Formatter( $assoc_args, $fields ); + $formatter = new Formatter( $typed_assoc_args, $fields ); $loggers = $profiler->get_loggers(); - if ( Utils\get_flag_value( $assoc_args, 'spotlight' ) ) { + /** @var array $typed_assoc_args */ + if ( Utils\get_flag_value( $typed_assoc_args, 'spotlight' ) ) { $loggers = self::shine_spotlight( $loggers, $metrics ); } - $search = Utils\get_flag_value( $assoc_args, 'search', false ); - if ( false !== $search && '' !== $search ) { + /** @var array $typed_assoc_args */ + $search_val = Utils\get_flag_value( $typed_assoc_args, 'search', '' ); + $search = is_string( $search_val ) ? $search_val : ''; + if ( '' !== $search ) { if ( ! $focus ) { WP_CLI::error( '--search requires --all or a specific hook.' ); } @@ -375,11 +386,15 @@ public function hook( $args, $assoc_args ) { public function eval_( $args, $assoc_args ) { $statement = $args[0]; - $order = Utils\get_flag_value( $assoc_args, 'order', 'ASC' ); - $orderby = Utils\get_flag_value( $assoc_args, 'orderby', null ); + /** @var array $typed_assoc_args */ + $typed_assoc_args = self::get_typed_assoc_args( $assoc_args ); + $order_val = Utils\get_flag_value( $typed_assoc_args, 'order', 'ASC' ); + $order = is_string( $order_val ) ? $order_val : 'ASC'; + $orderby_val = Utils\get_flag_value( $typed_assoc_args, 'orderby', null ); + $orderby = ( is_string( $orderby_val ) || is_null( $orderby_val ) ) ? $orderby_val : null; self::profile_eval_ish( - $assoc_args, + $typed_assoc_args, function () use ( $statement ) { eval( $statement ); // phpcs:ignore Squiz.PHP.Eval.Discouraged -- no other way around here }, @@ -449,15 +464,20 @@ public function eval_file( $args, $assoc_args ) { $file = $args[0]; - $order = Utils\get_flag_value( $assoc_args, 'order', 'ASC' ); - $orderby = Utils\get_flag_value( $assoc_args, 'orderby', null ); + /** @var array $typed_assoc_args */ + /** @var array $typed_assoc_args */ + $typed_assoc_args = self::get_typed_assoc_args( $assoc_args ); + $order_val = Utils\get_flag_value( $typed_assoc_args, 'order', 'ASC' ); + $order = is_string( $order_val ) ? $order_val : 'ASC'; + $orderby_val = Utils\get_flag_value( $typed_assoc_args, 'orderby', null ); + $orderby = ( is_string( $orderby_val ) || is_null( $orderby_val ) ) ? $orderby_val : null; if ( ! file_exists( $file ) ) { WP_CLI::error( "'$file' does not exist." ); } self::profile_eval_ish( - $assoc_args, + $typed_assoc_args, function () use ( $file ) { self::include_file( $file ); }, @@ -476,10 +496,12 @@ function () use ( $file ) { * @return void */ private static function profile_eval_ish( $assoc_args, $profile_callback, $order = 'ASC', $orderby = null ) { - $hook = Utils\get_flag_value( $assoc_args, 'hook' ); - $focus = false; - $type = false; - $fields = array(); + /** @var array $typed_assoc_args */ + $typed_assoc_args = self::get_typed_assoc_args( $assoc_args ); + $hook = Utils\get_flag_value( $typed_assoc_args, 'hook' ); + $focus = false; + $type = false; + $fields = array(); if ( $hook ) { $type = 'hook'; if ( true !== $hook ) { @@ -515,7 +537,7 @@ private static function profile_eval_ish( $assoc_args, $profile_callback, $order 'request_count', ) ); - $formatter = new Formatter( $assoc_args, $fields ); + $formatter = new Formatter( $typed_assoc_args, $fields ); $formatter->display_items( $loggers, false, $order, $orderby ); } @@ -586,4 +608,20 @@ function ( $logger ) use ( $pattern ) { } ); } + + /** + * Get typed assoc args for get_flag_value. + * + * @param array $assoc_args + * @return array + */ + private static function get_typed_assoc_args( $assoc_args ) { + $typed = array(); + foreach ( $assoc_args as $k => $v ) { + if ( is_bool( $v ) || is_string( $v ) ) { + $typed[ $k ] = $v; + } + } + return $typed; + } } diff --git a/src/Formatter.php b/src/Formatter.php index 60303d7a..7c290c74 100644 --- a/src/Formatter.php +++ b/src/Formatter.php @@ -43,10 +43,19 @@ public function __construct( &$assoc_args, $fields = null, $prefix = false ) { } if ( ! is_array( $format_args['fields'] ) ) { - $format_args['fields'] = explode( ',', $format_args['fields'] ); + $fields_val = $format_args['fields']; + $fields_str = is_scalar( $fields_val ) ? (string) $fields_val : ''; + $format_args['fields'] = explode( ',', $fields_str ); } - $format_args['fields'] = array_filter( array_map( 'trim', $format_args['fields'] ) ); + $format_args['fields'] = array_filter( + array_map( + function ( $val ) { + return trim( is_scalar( $val ) ? (string) $val : '' ); + }, + $format_args['fields'] + ) + ); if ( isset( $assoc_args['fields'] ) ) { if ( empty( $format_args['fields'] ) ) { @@ -78,7 +87,9 @@ public function __construct( &$assoc_args, $fields = null, $prefix = false ) { */ public function display_items( $items, $include_total, $order, $orderby ) { if ( 'table' === $this->args['format'] && empty( $this->args['field'] ) ) { - $this->show_table( $order, $orderby, $items, $this->args['fields'], $include_total ); + /** @var array $fields */ + $fields = $this->args['fields']; + $this->show_table( $order, $orderby, $items, $fields, $include_total ); } else { $this->formatter->display_items( $items ); } @@ -171,10 +182,13 @@ function ( $a, $b ) use ( $order, $orderby ) { $totals[ $i ][] = $value; } } else { - $totals[ $i ] += $value; + $current_total = is_numeric( $totals[ $i ] ) ? $totals[ $i ] : 0; + $add_value = is_numeric( $value ) ? $value : 0; + $totals[ $i ] = $current_total + $add_value; } if ( stripos( $fields[ $i ], '_time' ) || 'time' === $fields[ $i ] ) { - $values[ $i ] = round( $value, 4 ) . 's'; + $value_num = is_numeric( $value ) ? (float) $value : 0.0; + $values[ $i ] = round( $value_num, 4 ) . 's'; } } $table->addRow( $values ); @@ -190,7 +204,13 @@ function ( $a, $b ) use ( $order, $orderby ) { } if ( is_array( $value ) ) { if ( ! empty( $value ) ) { - $totals[ $i ] = round( ( array_sum( array_map( 'floatval', $value ) ) / count( $value ) ), 2 ) . '%'; + $float_values = array_map( + function ( $val ) { + return floatval( is_scalar( $val ) ? $val : 0 ); + }, + $value + ); + $totals[ $i ] = round( ( array_sum( $float_values ) / count( $value ) ), 2 ) . '%'; } else { $totals[ $i ] = null; } diff --git a/src/Profiler.php b/src/Profiler.php index 4a526d52..b224bcd9 100644 --- a/src/Profiler.php +++ b/src/Profiler.php @@ -259,7 +259,9 @@ public function wp_hook_begin() { $callbacks = self::get_filter_callbacks( $current_filter ); if ( false !== $callbacks ) { foreach ( $callbacks as $priority => $cbs ) { - $callback_count += count( $cbs ); + if ( is_array( $cbs ) ) { + $callback_count += count( $cbs ); + } } } $this->loggers[ $current_filter ] = new Logger( @@ -305,24 +307,38 @@ private function wrap_current_filter_callbacks( $current_filter ) { $this->previous_filter_callbacks = $callbacks; foreach ( $callbacks as $priority => $priority_callbacks ) { - foreach ( $priority_callbacks as $i => $the_ ) { - $callbacks[ $priority ][ $i ] = array( - 'function' => function () use ( $the_, $i ) { - if ( ! isset( $this->loggers[ $i ] ) ) { - $this->loggers[ $i ] = new Logger( - array( - 'callback' => $the_['function'], - ) - ); - } - assert( $this->loggers[ $i ] instanceof Logger ); - $this->loggers[ $i ]->start(); - $value = call_user_func_array( $the_['function'], func_get_args() ); - $this->loggers[ $i ]->stop(); - return $value; - }, - 'accepted_args' => $the_['accepted_args'], - ); + if ( is_array( $priority_callbacks ) ) { + $new_priority_callbacks = $priority_callbacks; + foreach ( $priority_callbacks as $i => $the_ ) { + if ( is_array( $the_ ) && isset( $the_['function'] ) && isset( $the_['accepted_args'] ) ) { + $func = $the_['function']; + $new_priority_callbacks[ $i ] = array( + 'function' => function () use ( $func, $i ) { + if ( ! isset( $this->loggers[ $i ] ) ) { + $this->loggers[ $i ] = new Logger( + array( + 'callback' => $func, + ) + ); + } + assert( $this->loggers[ $i ] instanceof Logger ); + $this->loggers[ $i ]->start(); + + $args = func_get_args(); + if ( is_callable( $func ) ) { + $value = call_user_func_array( $func, $args ); + } else { + $value = null; + } + + $this->loggers[ $i ]->stop(); + return $value; + }, + 'accepted_args' => $the_['accepted_args'], + ); + } + } + $callbacks[ $priority ] = $new_priority_callbacks; } } self::set_filter_callbacks( $current_filter, $callbacks ); @@ -388,27 +404,41 @@ public function handle_function_tick() { ); } - assert( is_array( $this->loggers[ $callback_hash ] ) ); - $this->loggers[ $callback_hash ]['time'] += $time; - - if ( isset( $wpdb ) ) { - $total_queries = count( $wpdb->queries ); - for ( $i = $this->tick_query_offset; $i < $total_queries; $i++ ) { - $this->loggers[ $callback_hash ]['query_time'] += $wpdb->queries[ $i ][1]; - ++$this->loggers[ $callback_hash ]['query_count']; + $logger_data = $this->loggers[ $callback_hash ]; + if ( is_array( $logger_data ) ) { + $current_time = isset( $logger_data['time'] ) && is_numeric( $logger_data['time'] ) ? $logger_data['time'] : 0.0; + $logger_data['time'] = (float) $current_time + $time; + + if ( isset( $wpdb ) ) { + $total_queries = count( $wpdb->queries ); + for ( $i = $this->tick_query_offset; $i < $total_queries; $i++ ) { + $q_time = isset( $wpdb->queries[ $i ][1] ) ? $wpdb->queries[ $i ][1] : 0.0; + $current_q_time = isset( $logger_data['query_time'] ) && is_numeric( $logger_data['query_time'] ) ? $logger_data['query_time'] : 0.0; + $q_time_val = is_numeric( $q_time ) ? $q_time : 0.0; + $logger_data['query_time'] = (float) $current_q_time + (float) $q_time_val; + + $current_q_count = isset( $logger_data['query_count'] ) && is_numeric( $logger_data['query_count'] ) ? $logger_data['query_count'] : 0; + $logger_data['query_count'] = (int) $current_q_count + 1; + } } - } - if ( isset( $wp_object_cache ) ) { - $hits = ! empty( $wp_object_cache->cache_hits ) ? $wp_object_cache->cache_hits : 0; - $misses = ! empty( $wp_object_cache->cache_misses ) ? $wp_object_cache->cache_misses : 0; - $this->loggers[ $callback_hash ]['cache_hits'] = ( $hits - $this->tick_cache_hit_offset ) + $this->loggers[ $callback_hash ]['cache_hits']; - $this->loggers[ $callback_hash ]['cache_misses'] = ( $misses - $this->tick_cache_miss_offset ) + $this->loggers[ $callback_hash ]['cache_misses']; - $total = $this->loggers[ $callback_hash ]['cache_hits'] + $this->loggers[ $callback_hash ]['cache_misses']; - if ( $total ) { - $ratio = ( $this->loggers[ $callback_hash ]['cache_hits'] / $total ) * 100; - $this->loggers[ $callback_hash ]['cache_ratio'] = round( $ratio, 2 ) . '%'; + if ( isset( $wp_object_cache ) ) { + $hits = ! empty( $wp_object_cache->cache_hits ) ? $wp_object_cache->cache_hits : 0; + $misses = ! empty( $wp_object_cache->cache_misses ) ? $wp_object_cache->cache_misses : 0; + + $current_hits = isset( $logger_data['cache_hits'] ) && is_numeric( $logger_data['cache_hits'] ) ? $logger_data['cache_hits'] : 0; + $logger_data['cache_hits'] = ( $hits - $this->tick_cache_hit_offset ) + (int) $current_hits; + + $current_misses = isset( $logger_data['cache_misses'] ) && is_numeric( $logger_data['cache_misses'] ) ? $logger_data['cache_misses'] : 0; + $logger_data['cache_misses'] = ( $misses - $this->tick_cache_miss_offset ) + (int) $current_misses; + + $total = $logger_data['cache_hits'] + $logger_data['cache_misses']; + if ( $total ) { + $ratio = ( $logger_data['cache_hits'] / $total ) * 100; + $logger_data['cache_ratio'] = round( $ratio, 2 ) . '%'; + } } + $this->loggers[ $callback_hash ] = $logger_data; } } @@ -422,8 +452,8 @@ public function handle_function_tick() { $callback = ''; if ( in_array( strtolower( $frame['function'] ), array( 'include', 'require', 'include_once', 'require_once' ), true ) ) { $callback = $frame['function']; - if ( isset( $frame['args'][0] ) ) { - $callback .= " '" . $frame['args'][0] . "'"; + if ( isset( $frame['args'] ) && is_array( $frame['args'] ) && isset( $frame['args'][0] ) && is_scalar( $frame['args'][0] ) ) { + $callback .= " '" . (string) $frame['args'][0] . "'"; } } elseif ( isset( $frame['object'] ) && method_exists( $frame['object'], $frame['function'] ) ) { $callback = get_class( $frame['object'] ) . '->' . $frame['function'] . '()'; @@ -598,9 +628,11 @@ private static function get_name_location_from_callback( $callback ) { if ( is_array( $callback ) && is_object( $callback[0] ) ) { $reflection = new \ReflectionMethod( $callback[0], $callback[1] ); $name = get_class( $callback[0] ) . '->' . $callback[1] . '()'; - } elseif ( is_array( $callback ) && method_exists( $callback[0], $callback[1] ) ) { + } elseif ( is_array( $callback ) && isset( $callback[0] ) && isset( $callback[1] ) && ( is_object( $callback[0] ) || is_string( $callback[0] ) ) && is_string( $callback[1] ) && method_exists( $callback[0], $callback[1] ) ) { $reflection = new \ReflectionMethod( $callback[0], $callback[1] ); - $name = $callback[0] . '::' . $callback[1] . '()'; + /** @var string $class_name */ + $class_name = $callback[0]; + $name = $class_name . '::' . $callback[1] . '()'; } elseif ( is_object( $callback ) && is_a( $callback, 'Closure' ) ) { $reflection = new \ReflectionFunction( $callback ); $name = 'function(){}'; @@ -689,6 +721,7 @@ private static function set_filter_callbacks( $filter, $callbacks ) { } if ( is_a( $wp_filter[ $filter ], 'WP_Hook' ) ) { + /** @var array $callbacks */ $wp_filter[ $filter ]->callbacks = $callbacks; } else { $wp_filter[ $filter ] = $callbacks; // phpcs:ignore