Skip to content
90 changes: 89 additions & 1 deletion inc/Engine/Admin/RocketInsights/Recommendations/DataManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,90 @@ public function get_status(): string {
return $data['status'];
}

/**
* Check if required metrics are available for recommendations.
*
* @return bool True if metrics exist, false otherwise.
*/
public function has_required_metrics(): bool {
$average_metrics = $this->get_average_metrics();

if ( null === $average_metrics ) {
return false;
}

// Verify core metrics exist.
$required = [ 'lcp', 'ttfb', 'cls', 'tbt' ];
foreach ( $required as $metric ) {
if ( ! isset( $average_metrics[ $metric ] ) ) {
return false;
}
}

return true;
}

/**
* Determine if recommendations should be fetched.
*
* Compares current metrics hash with cached hash.
*
* @return bool True if should fetch, false if cache is valid.
*/
public function should_fetch_recommendations(): bool {
// Get cached data.
$cached_data = $this->get_recommendations();

// If no cache, always fetch.
if ( false === $cached_data ) {
return true;
}

// Calculate current hash.
$current_hash = $this->calculate_metrics_hash();

// Get cached hash.
$cached_hash = $cached_data['metrics_hash'] ?? null;

// If hash differs or doesn't exist, fetch new recommendations.
if ( null === $cached_hash || $current_hash !== $cached_hash ) {
$this->logger::debug(
'Recommendations: Metrics hash changed',
[
'cached_hash' => $cached_hash,
'current_hash' => $current_hash,
]
);
return true;
}

return false;
}

/**
* Calculate hash from current metrics and enabled options.
*
* @return string Hash of current state.
*/
private function calculate_metrics_hash(): string {
// Get global score data.
$global_score_data = $this->global_score->get_global_score_data();
$score = $global_score_data['score'] ?? 0;
$average_metrics = $global_score_data['average_metrics'] ?? [];

// Get enabled options.
$enabled_options = $this->get_enabled_options();

// Combine all data that affects recommendations.
$hash_data = [
'score' => $score,
'average_metrics' => $average_metrics,
'enabled_options' => $enabled_options,
];

return md5( (string) wp_json_encode( $hash_data ) );
}

/**
* Set loading status in transient.
*
Expand All @@ -261,12 +345,16 @@ private function set_loading_status(): void {
* @return void
*/
private function save_recommendations( array $data ): void {
// Add current metrics hash.
$data['metrics_hash'] = $this->calculate_metrics_hash();

set_transient( self::TRANSIENT_NAME, $data, self::CACHE_EXPIRATION );

$this->logger::debug(
'Recommendations: Saved to cache',
[
'status' => $data['status'],
'status' => $data['status'],
'metrics_hash' => $data['metrics_hash'],
]
);
}
Expand Down
133 changes: 133 additions & 0 deletions inc/Engine/Admin/RocketInsights/Recommendations/SettingsSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);

namespace WP_Rocket\Engine\Admin\RocketInsights\Recommendations;

use WP_Rocket\Event_Management\Subscriber_Interface;
use WP_Rocket\Logger\LoggerAware;
use WP_Rocket\Logger\LoggerAwareInterface;

/**
* Recommendations Settings Subscriber.
*
* Detects WP Rocket settings changes and triggers recommendation updates.
*/
class SettingsSubscriber implements Subscriber_Interface, LoggerAwareInterface {
use LoggerAware;

/**
* Recommendations data manager instance.
*
* @var DataManager
*/
private $data_manager;

/**
* Constructor.
*
* @param DataManager $data_manager Data manager instance.
*/
public function __construct( DataManager $data_manager ) {
$this->data_manager = $data_manager;
}

/**
* Return an array of events that this subscriber wants to listen to.
*
* @return array
*/
public static function get_subscribed_events(): array {
return [
'rocket_after_save_options' => [ 'maybe_fetch_after_settings_change', 10, 2 ],
];
}

/**
* Maybe fetch recommendations after settings change.
*
* Only fetches if:
* - Status is completed or failed
* - Changed settings affect recommendations
* - Metrics hash would change
*
* @param array $old_options Previous settings.
* @param array $new_options New settings.
* @return void
*/
public function maybe_fetch_after_settings_change( array $old_options, array $new_options ): void {
// Check current status.
$status = $this->data_manager->get_status();

// Only proceed if recommendations exist.
if ( ! in_array( $status, [ 'completed', 'failed' ], true ) ) {
$this->logger::debug(
'Recommendations: Settings changed but status not ready',
[ 'status' => $status ]
);
return;
}

// Check if relevant settings changed.
if ( ! $this->has_relevant_changes( $old_options, $new_options ) ) {
$this->logger::debug( 'Recommendations: Settings changed but none affect recommendations' );
return;
}

// Check if hash would change (prevents redundant fetch).
if ( ! $this->data_manager->should_fetch_recommendations() ) {
$this->logger::debug( 'Recommendations: Settings changed but hash unchanged' );
return;
}

$this->logger::info( 'Recommendations: Relevant settings changed, fetching new recommendations' );

// Fetch new recommendations.
$this->data_manager->fetch_recommendations();
}

/**
* Check if changed settings affect recommendations.
*
* @param array $old_options Previous settings.
* @param array $new_options New settings.
* @return bool True if relevant changes detected.
*/
private function has_relevant_changes( array $old_options, array $new_options ): bool {
// Get list of recommendation-related options.
$relevant_keys = [
'minify_css',
'minify_js',
'minify_concatenate_css',
'minify_concatenate_js',
'defer_all_js',
'delay_js',
'async_css',
'critical_css',
'remove_unused_css',
'lazyload',
'lazyload_iframes',
'lazyload_youtube',
'image_dimensions',
'cdn',
'do_caching_mobile_files',
'cache_logged_user',
'cache_webp',
'manual_preload',
'sitemap_preload',
'control_heartbeat',
'minify_google_fonts',
];

// Check if any relevant setting changed.
foreach ( $relevant_keys as $key ) {
$old_value = $old_options[ $key ] ?? false;
$new_value = $new_options[ $key ] ?? false;

if ( $old_value !== $new_value ) {
return true;
}
}

return false;
}
}
6 changes: 6 additions & 0 deletions inc/Engine/Admin/RocketInsights/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
Queue\Queue as RIQueue,
Recommendations\APIClient as RecommendationsAPIClient,
Recommendations\DataManager,
Recommendations\SettingsSubscriber as RecommendationsSettingsSubscriber,
URLLimit\Subscriber as URLLimitSubscriber,
Settings\Controller as SettingsController,
Settings\Subscriber as SettingsSubscriber,
Expand Down Expand Up @@ -60,6 +61,7 @@ class ServiceProvider extends AbstractServiceProvider {
'ri_global_metrics_calculator',
'ri_global_metrics_subscriber',
'ri_recommendations_data_manager',
'ri_recommendations_settings_subscriber',
];

/**
Expand Down Expand Up @@ -206,6 +208,10 @@ public function register(): void {
]
);

// Recommendations Settings Subscriber.
$this->getContainer()->addShared( 'ri_recommendations_settings_subscriber', RecommendationsSettingsSubscriber::class )
->addArgument( 'ri_recommendations_data_manager' );

// Subscriber.
$this->getContainer()->addShared( 'ri_subscriber', Subscriber::class )
->addArguments(
Expand Down
1 change: 1 addition & 0 deletions inc/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@ private function init_common_subscribers() {
'ri_url_limit_subscriber',
'ri_post_listing_subscriber',
'ri_global_metrics_subscriber',
'ri_recommendations_settings_subscriber',
'post_subscriber',
'tracking_subscriber',
'logger_subscriber',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

return [
'test_data' => [
'shouldReturnTrueWhenAllMetricsExist' => [
'config' => [
'global_score_data' => [
'score' => 75,
'average_metrics' => [
'lcp' => 2.5,
'ttfb' => 0.8,
'cls' => 0.1,
'tbt' => 200,
],
],
],
'expected' => [
'has_metrics' => true,
],
],

'shouldReturnFalseWhenAverageMetricsNull' => [
'config' => [
'global_score_data' => [
'score' => 75,
],
],
'expected' => [
'has_metrics' => false,
],
],

'shouldReturnFalseWhenAverageMetricsEmpty' => [
'config' => [
'global_score_data' => [
'score' => 75,
'average_metrics' => [],
],
],
'expected' => [
'has_metrics' => false,
],
],

'shouldReturnFalseWhenMissingLCP' => [
'config' => [
'global_score_data' => [
'score' => 75,
'average_metrics' => [
'ttfb' => 0.8,
'cls' => 0.1,
'tbt' => 200,
],
],
],
'expected' => [
'has_metrics' => false,
],
],

'shouldReturnFalseWhenMissingTTFB' => [
'config' => [
'global_score_data' => [
'score' => 75,
'average_metrics' => [
'lcp' => 2.5,
'cls' => 0.1,
'tbt' => 200,
],
],
],
'expected' => [
'has_metrics' => false,
],
],

'shouldReturnFalseWhenMissingCLS' => [
'config' => [
'global_score_data' => [
'score' => 75,
'average_metrics' => [
'lcp' => 2.5,
'ttfb' => 0.8,
'tbt' => 200,
],
],
],
'expected' => [
'has_metrics' => false,
],
],

'shouldReturnFalseWhenMissingTBT' => [
'config' => [
'global_score_data' => [
'score' => 75,
'average_metrics' => [
'lcp' => 2.5,
'ttfb' => 0.8,
'cls' => 0.1,
],
],
],
'expected' => [
'has_metrics' => false,
],
],
],
];
Loading
Loading