Si vous avez déjà mis à jour WordPress et découvert trop tard qu’un hook a changé de comportement, que votre code déclenche un notice PHP 8.1, ou qu’un bloc Gutenberg “disparaît” parce qu’un filtre s’exécute au mauvais moment, vous savez pourquoi la rubrique “What’s new for developers?” vaut de l’or. Le souci, c’est que la plupart des sites lisent ces notes… une fois que ça casse.

Le problème / Le besoin

Vous développez (ou maintenez) un site WordPress en avril 2026, avec WordPress 6.9.4 et PHP 8.1+. Vous utilisez un thème enfant, quelques plugins maison, et parfois un builder (Divi 5, Elementor, Avada). Votre problème n’est pas “comment coder”, mais “comment ne pas vous faire surprendre” par les changements core, et comment transformer les notes “What’s new for developers?” en actions concrètes.

Dans ma pratique, la casse arrive rarement à cause d’une nouveauté spectaculaire. Elle vient plutôt de détails : un script chargé trop tôt, une requête devenue plus lente, un changement de markup dans un composant, une dépréciation PHP qui remonte en logs, ou une API REST qui renvoie un champ légèrement différent.

À la fin, vous saurez :

  • mettre en place un “radar développeur” dans WordPress (sans dépendre d’un plugin tiers opaque),
  • journaliser proprement les changements et régressions après mise à jour,
  • détecter les dépréciations (WordPress + PHP) avant qu’elles ne deviennent des erreurs,
  • valider rapidement vos points sensibles (hooks, REST, blocs, assets) avec une checklist reproductible.

Résumé rapide

  • On va créer un mini-plugin “Developer Radar” (mu-plugin possible) compatible WordPress 6.9.4 / PHP 8.1+.
  • Il ajoute une page Outils > Developer Radar avec un rapport : versions, thèmes/plugins, santé, erreurs PHP, dépréciations, requêtes lentes, endpoints REST clés.
  • Il active une journalisation contrôlée (WP_DEBUG_LOG) et un handler d’erreurs qui capture les dépréciations sans casser le front.
  • Il fournit une commande WP-CLI (optionnelle) pour exécuter les checks en CI.
  • On voit des variantes : ciblage Gutenberg (blocs), ciblage builders (Divi/Elementor/Avada), et mode “pré-prod” plus strict.

Quand utiliser cette solution

  • Vous maintenez plusieurs sites et vous voulez un contrôle rapide après chaque mise à jour core/plugin.
  • Vous avez des snippets (functions.php, plugin de snippets) et vous voulez détecter les dépréciations avant qu’un futur WordPress/PHP les transforme en fatal.
  • Vous travaillez avec un builder et vous suspectez des conflits d’assets (scripts/styles) ou des hooks exécutés au mauvais moment.
  • Vous avez une pré-prod et vous voulez un rapport “OK / KO” reproductible (idéalement via WP-CLI).

Quand ne PAS utiliser cette solution

  • Vous cherchez un outil de monitoring serveur (CPU, RAM, MySQL). Prenez plutôt un APM (New Relic, Datadog) ou les logs d’hébergement.
  • Vous voulez un scanner de sécurité. Utilisez un plugin dédié et maintenu (Wordfence, Solid Security, etc.) et suivez les recommandations de l’administration avancée sécurité.
  • Vous n’avez pas d’accès admin / pas le droit d’ajouter du code. Dans ce cas, passez par un environnement de staging géré par l’hébergeur.
  • Vous êtes sur un site ultra-sensible (e-commerce à fort trafic) sans staging : ne jouez pas avec la journalisation en production. Faites d’abord une pré-prod.

Prérequis / avant de commencer

Prérequis techniques :

  • WordPress 6.9.4 (ou plus récent) et PHP 8.1+.
  • Accès FTP/SSH pour déposer un fichier dans wp-content/mu-plugins/ ou wp-content/plugins/.
  • Idéalement WP-CLI (pour la variante CI).

Avant de toucher au code :

  • Faites une sauvegarde (fichiers + base). Un snapshot d’hébergeur suffit souvent.
  • Testez sur staging. J’ai vu trop de sites casser parce que le snippet a été copié dans le mauvais functions.php (thème parent au lieu du thème enfant) puis écrasé à la mise à jour.
  • Vérifiez que WP_DEBUG et WP_DEBUG_LOG sont désactivés en production, ou au minimum que les logs ne sont pas exposés publiquement.

Ressources officielles utiles (vous y reviendrez souvent) :

L’approche naïve (et pourquoi l’éviter)

L’approche que je vois le plus : “je lis en diagonale le post de release, j’update, et si ça casse je désactive des plugins”. Ça marche… jusqu’au jour où la casse est silencieuse (dégradation perf, dépréciations, REST qui renvoie 403 sur un endpoint, bloc qui ne s’affiche plus).

Exemple naïf : activer WP_DEBUG partout et laisser le log traîner

<?php
// Mauvaise pratique : ne faites pas ça en production.
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', true);

Problèmes :

  • Sécurité : afficher les erreurs à l’écran peut révéler chemins, versions, requêtes.
  • Performance : loguer tout et n’importe quoi sur un site actif peut faire grossir les fichiers et ralentir.
  • Signal/bruit : vous noyez les vrais problèmes sous des notices sans contexte.

Exemple naïf : “je teste un hook au hasard”

<?php
add_action('init', function () {
    // Trop tôt pour certains contextes, et pas de contrôle d’accès.
    file_put_contents(WP_CONTENT_DIR . '/debug.txt', "initn", FILE_APPEND);
});

Problèmes :

  • Hook inadapté : vous capturez trop tôt (pas de requête, pas de thème, pas d’admin), ou trop souvent.
  • Permissions : n’importe quel visiteur déclenche l’écriture disque.
  • Compatibilité : sur certains hébergements, l’écriture directe est bloquée.

La bonne approche — tutoriel pas à pas

Objectif : un outil simple, lisible, désactivable, qui vous donne un rapport et capte les signaux faibles (dépréciations, erreurs, lenteurs) sans transformer votre site en sapin de Noël.

Étape 1 — Choisir l’emplacement : mu-plugin recommandé

Je recommande un MU-plugin pour ce type d’outil, car il ne dépend pas du thème et ne peut pas être désactivé “par accident”. Créez le dossier :

mkdir -p wp-content/mu-plugins

Puis créez un fichier :

wp-content/mu-plugins/developer-radar.php

Étape 2 — Coller le squelette du plugin et sécuriser l’accès

On va :

  • ajouter une page admin (Outils),
  • capturer les dépréciations PHP/WordPress uniquement pour les admins (ou en staging),
  • exécuter quelques checks “post-update”.

Étape 3 — Ajouter un handler d’erreurs non-invasif

Le point délicat : ne pas casser les erreurs normales. On va écouter E_DEPRECATED et E_USER_DEPRECATED (et optionnellement E_NOTICE en staging), les stocker en mémoire, puis les afficher dans le rapport.

On évite d’écrire un fichier custom à chaque hit. On préfère afficher dans l’admin, et éventuellement déclencher un log uniquement en mode explicite.

Étape 4 — Construire le rapport “What’s new for developers? → actions”

Le rapport ne doit pas paraphraser les notes. Il doit répondre :

  • quelles versions exactes tournent (core, PHP, thème, plugins),
  • est-ce qu’on a des dépréciations qui vont devenir des problèmes dans 1-2 versions,
  • est-ce que des endpoints REST essentiels répondent,
  • est-ce que l’exécution montre des lenteurs anormales (requêtes SQL lentes si SAVEQUERIES est activé en staging).

Étape 5 — (Optionnel) Ajouter une commande WP-CLI pour l’intégrer en CI

Sur des sites pro, je déclenche ce genre de checks sur staging après mise à jour (GitHub Actions, GitLab CI). WP-CLI est parfait pour ça.

Code complet

Copiez-collez ce fichier tel quel dans wp-content/mu-plugins/developer-radar.php. Il est autonome.

<?php
/**
 * Plugin Name: Developer Radar (MU)
 * Description: Rapport post-mise-à-jour orienté développeurs (WP 6.9.4+, PHP 8.1+). Capture dépréciations, checks REST, infos versions.
 * Author: Votre équipe
 * Version: 1.0.0
 *
 * Déposez ce fichier dans wp-content/mu-plugins/developer-radar.php
 */

declare(strict_types=1);

if (!defined('ABSPATH')) {
	exit;
}

final class BPCAB_Developer_Radar {
	private const CAPABILITY = 'manage_options';
	private const OPTION_KEY = 'bpcab_dev_radar_settings';

	/** @var array<array{type:string,message:string,file:string,line:int}> */
	private static array $captured = [];

	public static function bootstrap(): void {
		add_action('admin_menu', [__CLASS__, 'register_admin_page']);
		add_action('admin_init', [__CLASS__, 'maybe_register_settings']);

		// Capture des dépréciations uniquement en admin, et uniquement pour les utilisateurs autorisés.
		add_action('admin_init', [__CLASS__, 'maybe_enable_error_capture'], 1);

		// Endpoint REST simple pour vérifier que le site répond (utile en CI).
		add_action('rest_api_init', [__CLASS__, 'register_rest_routes']);

		// Commande WP-CLI optionnelle.
		if (defined('WP_CLI') && WP_CLI) {
			self::register_wp_cli();
		}
	}

	public static function register_admin_page(): void {
		add_management_page(
			'Developer Radar',
			'Developer Radar',
			self::CAPABILITY,
			'bpcab-developer-radar',
			[__CLASS__, 'render_admin_page']
		);
	}

	public static function maybe_register_settings(): void {
		register_setting(
			'bpcab_dev_radar',
			self::OPTION_KEY,
			[
				'type' => 'array',
				'sanitize_callback' => [__CLASS__, 'sanitize_settings'],
				'default' => [
					'capture_deprecations' => true,
					'capture_notices' => false,
					'enable_savequeries_hint' => true,
					'rest_checks' => [
						'/wp/v2/types/post',
						'/wp/v2/users/me',
					],
				],
			]
		);
	}

	/**
	 * @param mixed $value
	 * @return array
	 */
	public static function sanitize_settings($value): array {
		$value = is_array($value) ? $value : [];

		$rest_checks = [];
		if (!empty($value['rest_checks']) && is_array($value['rest_checks'])) {
			foreach ($value['rest_checks'] as $route) {
				$route = is_string($route) ? trim($route) : '';
				if ($route !== '' && str_starts_with($route, '/')) {
					$rest_checks[] = $route;
				}
			}
		}

		return [
			'capture_deprecations' => !empty($value['capture_deprecations']),
			'capture_notices' => !empty($value['capture_notices']),
			'enable_savequeries_hint' => !empty($value['enable_savequeries_hint']),
			'rest_checks' => $rest_checks,
		];
	}

	private static function get_settings(): array {
		$defaults = [
			'capture_deprecations' => true,
			'capture_notices' => false,
			'enable_savequeries_hint' => true,
			'rest_checks' => [
				'/wp/v2/types/post',
				'/wp/v2/users/me',
			],
		];

		$settings = get_option(self::OPTION_KEY, []);
		return array_replace($defaults, is_array($settings) ? $settings : []);
	}

	public static function maybe_enable_error_capture(): void {
		if (!current_user_can(self::CAPABILITY)) {
			return;
		}

		$settings = self::get_settings();
		if (empty($settings['capture_deprecations']) && empty($settings['capture_notices'])) {
			return;
		}

		// Respecte un éventuel handler déjà en place (plugin de monitoring).
		$previous = set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) use ($settings) {
			$should_capture = false;
			$type = 'other';

			if (($errno & (E_DEPRECATED | E_USER_DEPRECATED)) !== 0 && !empty($settings['capture_deprecations'])) {
				$should_capture = true;
				$type = 'deprecated';
			}

			if (($errno & (E_NOTICE | E_USER_NOTICE)) !== 0 && !empty($settings['capture_notices'])) {
				$should_capture = true;
				$type = 'notice';
			}

			if ($should_capture) {
				self::$captured[] = [
					'type' => $type,
					'message' => $errstr,
					'file' => $errfile,
					'line' => $errline,
				];
			}

			// Retourne false pour laisser PHP/WordPress gérer aussi (ne masque pas).
			return false;
		});

		// Si un handler existait, on le restaure à la fin de la requête.
		if (is_callable($previous)) {
			add_action('shutdown', function () use ($previous) {
				// On restaure le handler précédent pour limiter les effets de bord.
				set_error_handler($previous);
			}, 0);
		}
	}

	public static function register_rest_routes(): void {
		register_rest_route('bpcab-dev-radar/v1', '/ping', [
			'methods' => 'GET',
			'permission_callback' => function () {
				return current_user_can(self::CAPABILITY);
			},
			'callback' => function () {
				return rest_ensure_response([
					'ok' => true,
					'time' => time(),
					'wp' => get_bloginfo('version'),
					'php' => PHP_VERSION,
				]);
			},
		]);
	}

	public static function render_admin_page(): void {
		if (!current_user_can(self::CAPABILITY)) {
			wp_die(esc_html__('Accès refusé.', 'default'));
		}

		$settings = self::get_settings();

		// Traitement du formulaire (nonce + sanitize via register_setting).
		if (isset($_POST['bpcab_dev_radar_submit'])) {
			check_admin_referer('bpcab_dev_radar_save');

			$posted = [
				'capture_deprecations' => !empty($_POST['bpcab_capture_deprecations']),
				'capture_notices' => !empty($_POST['bpcab_capture_notices']),
				'enable_savequeries_hint' => !empty($_POST['bpcab_enable_savequeries_hint']),
				'rest_checks' => [],
			];

			$raw_routes = isset($_POST['bpcab_rest_checks']) ? (string) $_POST['bpcab_rest_checks'] : '';
			$lines = preg_split('/R/', $raw_routes) ?: [];
			foreach ($lines as $line) {
				$line = trim($line);
				if ($line !== '') {
					$posted['rest_checks'][] = $line;
				}
			}

			update_option(self::OPTION_KEY, self::sanitize_settings($posted));
			$settings = self::get_settings();
			echo '<div class="notice notice-success"><p>' . esc_html__('Paramètres enregistrés.', 'default') . '</p></div>';
		}

		$report = self::build_report($settings);

		echo '<div class="wrap">';
		echo '<h1>' . esc_html__('Developer Radar', 'default') . '</h1>';

		echo '<p>Ce rapport est pensé comme un pont entre les notes “What’s new for developers?” et votre réalité : dépréciations, compatibilité, endpoints REST, perf. Je l’utilise souvent juste après une mise à jour core/plugins sur staging.</p>';

		self::render_settings_form($settings);

		echo '<hr />';

		self::render_report($report);

		echo '</div>';
	}

	private static function render_settings_form(array $settings): void {
		$routes_text = implode("n", array_map('strval', $settings['rest_checks'] ?? []));

		echo '<h2>Paramètres</h2>';
		echo '<form method="post">';
		wp_nonce_field('bpcab_dev_radar_save');

		echo '<p><label><input type="checkbox" name="bpcab_capture_deprecations" value="1" ' . checked(!empty($settings['capture_deprecations']), true, false) . ' /> Capturer les dépréciations (E_DEPRECATED, E_USER_DEPRECATED)</label></p>';
		echo '<p><label><input type="checkbox" name="bpcab_capture_notices" value="1" ' . checked(!empty($settings['capture_notices']), true, false) . ' /> Capturer aussi les notices (plus bruyant, plutôt staging)</label></p>';
		echo '<p><label><input type="checkbox" name="bpcab_enable_savequeries_hint" value="1" ' . checked(!empty($settings['enable_savequeries_hint']), true, false) . ' /> Afficher un rappel SAVEQUERIES (pour diagnostiquer des requêtes lentes)</label></p>';

		echo '<h3>Routes REST à vérifier (une par ligne)</h3>';
		echo '<p><textarea name="bpcab_rest_checks" rows="6" style="width:100%;max-width:900px">' . esc_textarea($routes_text) . '</textarea></p>';

		echo '<p><button type="submit" class="button button-primary" name="bpcab_dev_radar_submit" value="1">Enregistrer</button></p>';
		echo '</form>';
	}

	/**
	 * @return array{
	 *   environment: array,
	 *   plugins: array,
	 *   theme: array,
	 *   rest: array,
	 *   errors: array,
	 *   performance: array
	 * }
	 */
	private static function build_report(array $settings): array {
		global $wpdb;

		$environment = [
			'wp_version' => get_bloginfo('version'),
			'php_version' => PHP_VERSION,
			'wp_debug' => (defined('WP_DEBUG') && WP_DEBUG) ? 'true' : 'false',
			'wp_debug_log' => (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) ? 'true' : 'false',
			'wp_debug_display' => (defined('WP_DEBUG_DISPLAY') && WP_DEBUG_DISPLAY) ? 'true' : 'false',
			'script_debug' => (defined('SCRIPT_DEBUG') && SCRIPT_DEBUG) ? 'true' : 'false',
			'memory_limit' => (string) ini_get('memory_limit'),
			'wp_memory_limit' => defined('WP_MEMORY_LIMIT') ? (string) WP_MEMORY_LIMIT : '(non défini)',
		];

		$theme = [];
		$theme_obj = wp_get_theme();
		if ($theme_obj->exists()) {
			$theme = [
				'name' => $theme_obj->get('Name'),
				'version' => $theme_obj->get('Version'),
				'stylesheet' => $theme_obj->get_stylesheet(),
				'template' => $theme_obj->get_template(),
				'is_child' => $theme_obj->parent() ? 'true' : 'false',
			];
		}

		$plugins = [
			'active' => [],
		];

		$active_plugins = (array) get_option('active_plugins', []);
		foreach ($active_plugins as $plugin_file) {
			$data = get_plugin_data(WP_PLUGIN_DIR . '/' . $plugin_file, false, false);
			$plugins['active'][] = [
				'file' => $plugin_file,
				'name' => $data['Name'] ?? $plugin_file,
				'version' => $data['Version'] ?? '',
			];
		}

		$rest_results = [];
		$routes = $settings['rest_checks'] ?? [];
		if (is_array($routes)) {
			foreach ($routes as $route) {
				$route = is_string($route) ? $route : '';
				if ($route === '') {
					continue;
				}

				// Appel interne REST : pas de HTTP, évite les problèmes de boucle/cURL.
				$request = new WP_REST_Request('GET', $route);
				$request->set_header('Accept', 'application/json');

				// Pour /users/me, il faut un utilisateur connecté : on simule le contexte admin actuel.
				$response = rest_do_request($request);

				$status = $response->get_status();
				$rest_results[] = [
					'route' => $route,
					'status' => $status,
					'ok' => ($status >= 200 && $status < 300) ? 'true' : 'false',
				];
			}
		}

		$errors = [
			'captured_count' => count(self::$captured),
			'captured' => self::$captured,
		];

		$performance = [
			'savequeries_defined' => (defined('SAVEQUERIES') && SAVEQUERIES) ? 'true' : 'false',
			'query_count' => null,
			'slow_queries' => [],
		];

		// Si SAVEQUERIES est activé, WordPress stocke $wpdb->queries.
		if (defined('SAVEQUERIES') && SAVEQUERIES && isset($wpdb->queries) && is_array($wpdb->queries)) {
			$performance['query_count'] = count($wpdb->queries);

			// Détection basique des requêtes lentes.
			foreach ($wpdb->queries as $q) {
				// Format typique: [sql, time, caller, ...]
				$sql = isset($q[0]) ? (string) $q[0] : '';
				$time = isset($q[1]) ? (float) $q[1] : 0.0;
				$caller = isset($q[2]) ? (string) $q[2] : '';

				if ($time >= 0.2) {
					$performance['slow_queries'][] = [
						'time' => $time,
						'caller' => $caller,
						'sql' => $sql,
					];
				}
			}
		}

		return [
			'environment' => $environment,
			'plugins' => $plugins,
			'theme' => $theme,
			'rest' => $rest_results,
			'errors' => $errors,
			'performance' => $performance,
		];
	}

	private static function render_report(array $report): void {
		echo '<h2>Rapport</h2>';

		echo '<h3>Environnement</h3>';
		echo '<table class="widefat striped"><tbody>';
		foreach ($report['environment'] as $k => $v) {
			echo '<tr><th style="width:260px">' . esc_html($k) . '</th><td><code>' . esc_html((string) $v) . '</code></td></tr>';
		}
		echo '</tbody></table>';

		echo '<h3>Thème actif</h3>';
		echo '<table class="widefat striped"><tbody>';
		foreach ($report['theme'] as $k => $v) {
			echo '<tr><th style="width:260px">' . esc_html($k) . '</th><td><code>' . esc_html((string) $v) . '</code></td></tr>';
		}
		echo '</tbody></table>';

		echo '<h3>Plugins actifs</h3>';
		echo '<table class="widefat striped">';
		echo '<thead><tr><th>Plugin</th><th>Fichier</th><th>Version</th></tr></thead><tbody>';
		foreach ($report['plugins']['active'] as $p) {
			echo '<tr>';
			echo '<td>' . esc_html((string) ($p['name'] ?? '')) . '</td>';
			echo '<td><code>' . esc_html((string) ($p['file'] ?? '')) . '</code></td>';
			echo '<td><code>' . esc_html((string) ($p['version'] ?? '')) . '</code></td>';
			echo '</tr>';
		}
		echo '</tbody></table>';

		echo '<h3>Checks REST (appel interne)</h3>';
		echo '<table class="widefat striped">';
		echo '<thead><tr><th>Route</th><th>HTTP</th><th>OK</th></tr></thead><tbody>';
		foreach ($report['rest'] as $r) {
			$ok = ($r['ok'] ?? 'false') === 'true';
			echo '<tr>';
			echo '<td><code>' . esc_html((string) ($r['route'] ?? '')) . '</code></td>';
			echo '<td><code>' . esc_html((string) ($r['status'] ?? '')) . '</code></td>';
			echo '<td>' . ($ok ? '✅' : '❌') . '</td>';
			echo '</tr>';
		}
		echo '</tbody></table>';

		echo '<h3>Dépréciations / notices capturées</h3>';
		echo '<p><strong>Total capturé :</strong> ' . esc_html((string) ($report['errors']['captured_count'] ?? 0)) . '</p>';

		$captured = $report['errors']['captured'] ?? [];
		if (empty($captured)) {
			echo '<p>Aucune dépréciation/notices capturée sur cette requête admin. Si vous venez d’updater, rechargez cette page après avoir visité 2-3 pages sensibles (front + admin).</p>';
		} else {
			echo '<table class="widefat striped">';
			echo '<thead><tr><th>Type</th><th>Message</th><th>Fichier</th><th>Ligne</th></tr></thead><tbody>';
			foreach ($captured as $e) {
				echo '<tr>';
				echo '<td><code>' . esc_html((string) ($e['type'] ?? '')) . '</code></td>';
				echo '<td>' . esc_html((string) ($e['message'] ?? '')) . '</td>';
				echo '<td><code>' . esc_html((string) ($e['file'] ?? '')) . '</code></td>';
				echo '<td><code>' . esc_html((string) ($e['line'] ?? 0)) . '</code></td>';
				echo '</tr>';
			}
			echo '</tbody></table>';
		}

		echo '<h3>Performance (staging)</h3>';
		$perf = $report['performance'] ?? [];
		echo '<table class="widefat striped"><tbody>';
		echo '<tr><th style="width:260px">SAVEQUERIES</th><td><code>' . esc_html((string) ($perf['savequeries_defined'] ?? 'false')) . '</code></td></tr>';
		echo '<tr><th>Nombre de requêtes (si activé)</th><td><code>' . esc_html((string) ($perf['query_count'] ?? 'n/a')) . '</code></td></tr>';
		echo '</tbody></table>';

		$slow = $perf['slow_queries'] ?? [];
		if (!empty($slow)) {
			echo '<p><strong>Requêtes lentes détectées (&ge; 0.2s) :</strong></p>';
			echo '<table class="widefat striped">';
			echo '<thead><tr><th>Temps</th><th>Caller</th><th>SQL</th></tr></thead><tbody>';
			foreach ($slow as $q) {
				echo '<tr>';
				echo '<td><code>' . esc_html((string) ($q['time'] ?? '')) . 's</code></td>';
				echo '<td><code>' . esc_html((string) ($q['caller'] ?? '')) . '</code></td>';
				echo '<td><code>' . esc_html((string) ($q['sql'] ?? '')) . '</code></td>';
				echo '</tr>';
			}
			echo '</tbody></table>';
		} else {
			echo '<p>Pas de requêtes lentes (ou SAVEQUERIES désactivé).</p>';
		}

		echo '<hr />';
		echo '<h3>Conseil pratique</h3>';
		echo '<p>Quand vous lisez “What’s new for developers?” sur developer.wordpress.org, notez 3 zones : (1) API/hook que vous utilisez, (2) changements REST/blocs, (3) perf. Ensuite, utilisez ce rapport pour valider ces zones sur votre site, pas dans l’abstrait.</p>';
	}

	private static function register_wp_cli(): void {
		WP_CLI::add_command('dev-radar', function (array $args, array $assoc_args) {
			$settings = self::get_settings();

			// Permet de rendre le check plus strict en CI.
			$fail_on_deprecated = isset($assoc_args['fail-on-deprecated']) ? (bool) $assoc_args['fail-on-deprecated'] : false;

			$report = self::build_report($settings);

			WP_CLI::log('WP: ' . $report['environment']['wp_version'] . ' | PHP: ' . $report['environment']['php_version']);
			WP_CLI::log('Plugins actifs: ' . count($report['plugins']['active']));
			WP_CLI::log('Dépréciations/notices capturées (sur cette exécution): ' . $report['errors']['captured_count']);

			foreach (($report['rest'] ?? []) as $r) {
				WP_CLI::log('REST ' . $r['route'] . ' => ' . $r['status']);
			}

			if ($fail_on_deprecated && !empty($report['errors']['captured_count'])) {
				WP_CLI::error('Dépréciations détectées. Corrigez-les avant de déployer.');
			}

			WP_CLI::success('Developer Radar OK.');
		}, [
			'shortdesc' => 'Exécute les checks Developer Radar.',
			'synopsis' => [
				[
					'type' => 'flag',
					'name' => 'fail-on-deprecated',
					'description' => 'Retourne une erreur si des dépréciations/notices sont capturées.',
					'optional' => true,
				],
			],
		]);
	}
}

BPCAB_Developer_Radar::bootstrap();

Explication du code

Lecture “simple” :

  • Le MU-plugin ajoute une page dans Outils.
  • Quand un admin charge l’admin, on installe un handler d’erreurs PHP qui écoute les dépréciations et les garde en mémoire.
  • La page affiche un rapport : versions, thème, plugins actifs, checks REST, et (si staging) quelques infos perf.
  • Optionnel : une commande WP-CLI exécute le même rapport, utile en CI.

Points techniques importants

  • Contrôle d’accès : tout est derrière manage_options. Ça limite l’exposition de données sensibles (liste plugins/versions, chemins fichiers dans les erreurs).
  • Sanitization : le paramètre “routes REST” est nettoyé : on garde uniquement des routes qui commencent par /. On évite d’accepter une URL complète (sinon on finirait par faire du SSRF si on passait en HTTP externe).
  • Capture d’erreurs : on utilise set_error_handler() et on retourne false pour ne pas masquer le comportement normal de WordPress/PHP. C’est un piège classique : retourner true “avale” l’erreur et vous cachez un problème réel.
  • REST checks : on utilise rest_do_request() avec WP_REST_Request. Ça évite les appels HTTP sortants, le cache, et les soucis de DNS/SSL. C’est plus fiable pour un check fonctionnel.
  • Performance : on ne force pas SAVEQUERIES (qui se définit dans wp-config.php). On affiche juste un état et on exploite les données si elles existent.

Pourquoi ça colle à l’esprit “What’s new for developers?”

La rubrique “What’s new for developers?” vous donne des signaux : dépréciations, changements d’API, modifications d’assets, évolutions du REST, etc. Le radar, lui, vous force à répondre à la seule question qui compte : “sur ce site, est-ce que ça casse, est-ce que ça dégrade, est-ce que ça va casser bientôt ?”.

J’ai souvent vu des équipes lire les notes de release, décider “rien ne nous concerne”, puis découvrir une avalanche de E_DEPRECATED quand l’hébergeur passe PHP à la version suivante. Ce radar limite ce scénario.

Variantes et cas d’usage

Variante 1 — Mode staging strict : fail si dépréciations

Si vous avez WP-CLI sur staging :

wp dev-radar --fail-on-deprecated

Couplez ça avec votre pipeline de déploiement : si une mise à jour plugin introduit une dépréciation, vous le voyez avant production.

Variante 2 — Ajouter des checks ciblés “Gutenberg / blocs”

Quand un site utilise beaucoup de blocs, une régression classique est un bloc qui ne se rend plus comme prévu (attribut renommé, markup filtré, style non chargé). Vous pouvez ajouter un check simple : compter les blocs présents sur une page clé.

Ajoutez dans build_report() un test sur une page connue (ID à adapter) :

<?php
// Exemple : compter les blocs sur une page clé.
// À placer dans build_report(), avec un ID réel.
$page_id = 123;
$post = get_post($page_id);
$block_stats = [];

if ($post instanceof WP_Post) {
	$blocks = parse_blocks($post->post_content);
	$block_stats['total'] = count($blocks);
	$block_stats['names'] = [];

	$walk = function (array $blocks) use (&$walk, &$block_stats): void {
		foreach ($blocks as $b) {
			if (!empty($b['blockName'])) {
				$name = (string) $b['blockName'];
				$block_stats['names'][$name] = ($block_stats['names'][$name] ?? 0) + 1;
			}
			if (!empty($b['innerBlocks']) && is_array($b['innerBlocks'])) {
				$walk($b['innerBlocks']);
			}
		}
	};

	$walk($blocks);
}

Ce n’est pas un test visuel, mais c’est un détecteur de “page vidée” (par exemple si un contenu a été remplacé par un shortcode cassé, ou si un filtre a supprimé des blocs).

Variante 3 — Ajouter des routes REST propres à votre métier

Ajoutez :

  • /wp/v2/posts?per_page=1 (test lecture)
  • une route de votre plugin (ex: /mon-plugin/v1/status)
  • si vous avez du headless : une route auth (mais attention aux permissions)

Astuce : privilégiez des routes GET sans effet de bord. Les POST/PUT en check automatique finissent souvent par polluer votre base.

Compatibilité Divi 5 / Elementor / Avada

Le radar est volontairement “core only” : admin + REST + erreurs PHP. Il ne dépend d’aucun builder, donc il est compatible par défaut.

Divi 5

Sur Divi, je rencontre surtout :

  • des conflits d’assets (scripts/styles minifiés) après une mise à jour,
  • des caches à purger (Divi + cache serveur + navigateur) qui masquent la correction.

Action pratique : après une mise à jour, purgez le cache Divi et rechargez la page Radar pour voir si des dépréciations apparaissent lors du rendu admin.

Elementor

Avec Elementor, les régressions sont souvent liées à :

  • un widget custom qui utilise une API dépréciée,
  • un script non enqueued correctement (mauvais hook, mauvaise condition).

Ajoutez une route REST de votre plugin Elementor custom (si vous en avez) dans les checks. Et si vous avez des widgets maison, activez temporairement “capturer les notices” en staging : les notices PHP révèlent souvent des appels à des propriétés non définies.

Avada (Fusion Builder)

Avada est souvent couplé à une optimisation agressive (minification, defer, concat). Si le front “clignote” ou casse après update, ce n’est pas forcément WordPress : c’est parfois l’ordre de chargement des scripts.

Le radar n’analyse pas vos assets front, mais il vous donne un point de départ : si vous avez des dépréciations qui viennent d’un plugin Avada-related, vous le voyez immédiatement (chemin fichier + ligne).

Vérifications après mise en place

  1. Allez dans Outils > Developer Radar.
  2. Vérifiez que WordPress affiche bien la version (6.9.4+) et PHP 8.1+.
  3. Rechargez la page après avoir visité :
    • une page front “complexe” (builder, page d’accueil),
    • l’éditeur d’un article,
    • une page de réglages d’un plugin important.
  4. Regardez la section “Dépréciations / notices capturées”. Si vous voyez des chemins de plugin/thème, notez-les.
  5. Vérifiez que les checks REST renvoient 200. Un 401/403 sur /wp/v2/users/me peut être normal si le contexte utilisateur n’est pas reconnu, mais en admin ça doit passer.

Tableau de diagnostic rapide

Symptôme Cause probable Vérification Solution
La page “Developer Radar” n’apparaît pas Fichier au mauvais endroit, ou MU-plugins non chargés Vérifiez wp-content/mu-plugins/developer-radar.php Créer le dossier mu-plugins et y déposer le fichier
Écran blanc / erreur 500 Erreur de syntaxe (parenthèse/point-virgule) lors d’une modification Consultez les logs PHP, ou activez l’affichage en staging Revenir au fichier original, corriger la syntaxe
Aucune dépréciation capturée alors que vos logs en montrent Vous n’êtes pas admin, ou la capture est désactivée Vérifiez le rôle et la case “Capturer les dépréciations” Activer la capture, tester en admin
Checks REST en 403 Plugin de sécurité / restriction REST / règles WAF Désactiver temporairement la restriction sur staging Autoriser les routes nécessaires, ajuster les permission_callback
Requêtes lentes non listées SAVEQUERIES désactivé Constante absente dans wp-config.php Activer SAVEQUERIES en staging uniquement

Si ça ne marche pas

  1. Vérifiez l’emplacement : un MU-plugin doit être directement dans wp-content/mu-plugins/, pas dans un sous-dossier (sinon il ne sera pas chargé).
  2. Vérifiez la version PHP : si vous êtes en PHP 7.4/8.0, certains sites ont encore ça en 2026. Ici on cible PHP 8.1+. Regardez Outils > Santé du site ou le rapport.
  3. Regardez les logs : si vous avez un fatal, il sera dans les logs serveur. Sur beaucoup d’hébergeurs, c’est dans un panneau “Logs”.
  4. Désactivez temporairement les plugins de monitoring sur staging : certains installent déjà un set_error_handler() et peuvent interférer.
  5. Conflit cache : si vous avez un cache d’admin (rare) ou un cache objet persistant, videz-le. Et videz votre cache navigateur (j’ai déjà perdu 30 minutes sur une page admin servie depuis un cache agressif).
  6. Permaliens : si vos checks REST échouent en 404, régénérez les permaliens (Réglages > Permaliens > Enregistrer). Ça corrige des réécritures cassées après migration.

Pièges et erreurs courantes

Erreur Cause Solution
Copier le code dans le thème parent Le thème est mis à jour et écrase vos modifications Utilisez un MU-plugin ou un plugin custom, ou un thème enfant
Parse error: syntax error, unexpected... Point-virgule manquant / accolade en trop Revenir au fichier original, valider avec un linter PHP, relire la ligne indiquée
Utiliser un hook trop tôt Vous attendez REST / admin / thème alors que vous êtes sur init Ici on utilise admin_init et rest_api_init selon le contexte
Confusion action vs filtre Retourner une valeur dans une action, ou ne rien retourner dans un filtre Respectez la signature : actions = effets, filtres = retour
REST check renvoie 401/403 Permission manquante, plugin de sécurité, cookies non envoyés Tester en admin, vérifier permission_callback, ajuster la route testée
Le log grossit trop vite Vous avez activé notices + debug log en production Réservez la capture “bruyante” au staging, et n’affichez jamais les erreurs au front
“Call to undefined function …” Fonction appelée avant chargement (ou code d’un vieux tuto) Vérifier la doc officielle, déplacer le code sur le bon hook, cibler WP 6.9.4+
Snippet cassé par un plugin de snippets Ordre de chargement / erreur silencieuse Préférez un MU-plugin pour les outils transverses (radar, logs, checks)
Vous testez sur production “juste 5 minutes” Absence de staging, pas de rollback Créez une pré-prod, ou au minimum une sauvegarde restaurable

Conseils sécurité, performance et maintenance

Sécurité :

  • Ne rendez pas ce rapport public. Il liste plugins/versions, ce qui aide un attaquant à cibler des failles connues.
  • Ne stockez pas les erreurs (chemins fichiers) en option WordPress sans besoin. Ici, on garde en mémoire sur la requête.
  • Gardez WP_DEBUG_DISPLAY à false en production. Si vous devez loguer, faites-le côté serveur, pas côté navigateur.

Performance :

  • La capture d’erreurs ne doit pas tourner sur toutes les requêtes publiques. Ici, elle est limitée à l’admin et à un rôle.
  • Évitez SAVEQUERIES en prod : c’est utile, mais coûteux. Activez-le en staging pour traquer une régression après update.
  • Les checks REST via rest_do_request() sont rapides et évitent le réseau, mais ne testent pas la couche HTTP/WAF. Si vous suspectez un WAF, faites un check externe en plus.

Maintenance :

  • Après chaque update majeure, relisez les posts développeurs sur developer.wordpress.org/news, puis mettez à jour vos routes REST “critiques” et vos pages de test.
  • Quand une dépréciation apparaît, corrigez-la tout de suite. Les dépréciations sont des dettes à intérêt variable : un jour elles deviennent des fatals, souvent au pire moment (migration PHP, refonte, pic de trafic).

Ressources

FAQ

Pourquoi un MU-plugin plutôt qu’un plugin classique ?

Parce qu’un outil de diagnostic doit survivre aux changements de thème, et parce qu’il ne doit pas être désactivé “par mégarde”. Les MU-plugins se chargent automatiquement.

Est-ce que ce code est compatible WordPress 6.9.4 et PHP 8.1 ?

Oui. Le code utilise des APIs core stables (admin_menu, REST, options) et des fonctionnalités PHP 8.1+ (typed properties, strict_types).

Est-ce que la capture des dépréciations remplace un vrai monitoring ?

Non. C’est un filet de sécurité “dev”. Pour la prod, un monitoring serveur et applicatif reste recommandé.

Pourquoi ne pas écrire les dépréciations dans un fichier ?

Parce que ça devient vite bruyant, et que les fichiers de log peuvent être exposés ou grossir trop vite. Ici, on cible un usage “après update” en admin/staging.

Pourquoi certains checks REST peuvent échouer alors que le site fonctionne ?

Parce que des plugins de sécurité bloquent l’accès REST, ou parce que la route nécessite une authentification. Ajustez la liste des routes à vérifier selon votre contexte.

Je vois des chemins complets (/home/…/wp-content/plugins/…) dans les dépréciations, c’est grave ?

Ce n’est pas “grave” en staging, mais c’est sensible. Ne partagez pas ce rapport publiquement et limitez l’accès admin. En prod, évitez d’afficher ce type d’information.

Comment relier ça concrètement à “What’s new for developers?”

Quand une note annonce une dépréciation ou un changement d’API, vous cherchez dans vos plugins/thème si vous utilisez la zone concernée, puis vous validez via le radar : (1) dépréciations capturées, (2) endpoints REST critiques, (3) perf si régression suspectée.

Je n’ai aucune dépréciation, ça veut dire que je suis “clean” ?

Ça veut surtout dire que sur cette requête admin, rien n’a été capturé. Visitez les pages qui chargent votre code (front, éditeur, pages builder) puis revenez au radar.

Est-ce que ça peut casser un plugin qui installe déjà un error handler ?

En général non, car on retourne false (on ne masque pas) et on tente de restaurer le handler précédent au shutdown. Cela dit, certains plugins font des choses plus agressives. Testez sur staging si vous avez un outil de monitoring avancé.

Quelle est la prochaine amélioration utile ?

Ajouter un check d’assets : liste des scripts/styles enqueued sur une page front clé, pour repérer un script manquant après update. C’est particulièrement utile avec Divi/Avada et leurs optimisations.