Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions classes/Admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,20 @@ public static function init() {
PUM_Admin_Notices::init();
}

PUM_Admin_Popups::init();
PUM_Admin_Themes::init();

if ( is_admin() ) {
PUM_Admin_Popups::init();
PUM_Admin_Themes::init();
PUM_Admin_Subscribers::init();
PUM_Admin_Settings::init();
PUM_Admin_Tools::init();
PUM_Admin_Shortcode_UI::init();
PUM_Upsell::init();
PUM_Admin_Onboarding::init();
} else {
// Preserve programmatic saves without loading the editor classes on every frontend request.
add_action( 'save_post', [ 'PUM_Admin_Popups', 'save' ], 10, 2 );
add_filter( 'wp_insert_post_data', [ 'PUM_Admin_Popups', 'set_slug' ], 99, 2 );
add_action( 'save_post', [ 'PUM_Admin_Themes', 'save' ], 10, 2 );
}

add_filter( 'user_has_cap', [ __CLASS__, 'prevent_default_theme_deletion' ], 10, 3 );
Expand Down
8 changes: 4 additions & 4 deletions classes/Plugin/Core.php
Original file line number Diff line number Diff line change
Expand Up @@ -348,14 +348,14 @@ protected function init_services() {
$form_conversion_tracking->init();

/*
* Defer notifications orchestrator init until WordPress's `init`
* Defer notification bootstrap until WordPress's `init`
* action. Core loads on plugins_loaded@11, but addons (Pro, Pro+,
* integrations) load at priority 12+ and need a window to register
* their own providers via the `popup_maker/notification_providers`
* filter before the Manager resolves the provider list.
* their own providers and deferred trigger hooks before the Manager
* resolves the provider list or registers frontend lazy boot hooks.
*/
add_action( 'init', function () {
$this->get( 'notifications' )->init();
$this->get( 'notifications' )->register_lazy_boot();
}, 5 );
}

Expand Down
69 changes: 69 additions & 0 deletions classes/Services/Notifications/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public function init() {
if ( $this->booted ) {
return;
}

$this->booted = true;

$this->providers = $this->resolve_providers();
Expand All @@ -78,6 +79,72 @@ public function init() {
}
}

/**
* Boot notifications immediately in wp-admin or defer them to relevant frontend hooks.
*
* @return void
*/
public function register_lazy_boot() {
if ( is_admin() ) {
$this->init();
return;
}

foreach ( $this->get_deferred_boot_hooks() as $hook ) {
if ( did_action( $hook ) || did_filter( $hook ) ) {
// The event already fired earlier in this request — e.g. an
// upgrade dispatches popup_maker/update_version during
// plugins_loaded, before this registration runs on init.
$this->init();
return;
}

add_filter( $hook, [ $this, 'boot_on_demand' ], PHP_INT_MIN );
}
}

/**
* Get hooks that can trigger lazy notification boot on frontend requests.
*
* Extensions (Pro, Pro+, legacy) can append their own trigger hooks via the
* `popup_maker/notifications/deferred_boot_hooks` filter. Any consumer that
* calls get_providers() boots the manager regardless, so this filter is an
* optimization and not a correctness requirement.
*
* @return string[]
*/
protected function get_deferred_boot_hooks() {
$defaults = [
'popup_maker/update_version',
'pum_alert_list',
'pum_alert_dismissed',
'save_post_popup',
'save_post_pum_cta',
'deleted_post',
'trashed_post',
'untrashed_post',
'update_option_pum_form_conversion_count',
'update_option_pum_total_conversion_count',
'update_option_pum_bypass_adblockers',
'activated_plugin',
'deactivated_plugin',
];

return apply_filters( 'popup_maker/notifications/deferred_boot_hooks', $defaults );
}

/**
* Boot notification providers when a frontend request reaches a relevant hook.
*
* @param mixed $value Current filter value, if any.
* @return mixed
*/
public function boot_on_demand( $value = null ) {
$this->init();

return $value;
}

/**
* Currently booted providers.
*
Expand All @@ -87,6 +154,8 @@ public function init() {
* @return array<int,Provider>
*/
public function get_providers() {
$this->init();

return $this->providers;
}

Expand Down
6 changes: 3 additions & 3 deletions classes/Services/Notifications/Provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
* entries to the `pum_alert_list` filter and optionally reacts to
* dismissals, version changes, or other plugin events.
*
* Providers are booted once during plugin load via
* `Notifications::init()` — each is given the opportunity to wire its own
* hooks inside its `init()` method.
* Providers are booted once via `Notifications::init()` — each is given the
* opportunity to wire its own hooks inside its `init()` method. Frontend
* requests may defer that boot until a notification-related event.
*
* @since 1.23.0
*/
Expand Down
31 changes: 26 additions & 5 deletions includes/legacy/class-popup-maker.php
Original file line number Diff line number Diff line change
Expand Up @@ -169,21 +169,42 @@ public function init() {
PUM_Utils_Upgrades::instance();
PUM_Newsletters::init();
PUM_Integrations::init();
PUM_Privacy::init();

PUM_Utils_Alerts::init();
$this->register_deferred_hooks();

PUM_Shortcode_Popup::init();
PUM_Shortcode_PopupTrigger::init();
PUM_Shortcode_PopupClose::init();
PUM_Shortcode_PopupCookie::init();
PUM_Shortcode_CallToAction::init();

PUM_Telemetry::init();

new PUM_Extensions();
}

/**
* Register hooks whose handlers are not needed during normal frontend bootstrap.
*
* String callbacks preserve the existing hooks while allowing WordPress to
* autoload each implementation only when its hook runs.
*
* @return void
*/
private function register_deferred_hooks() {
add_filter( 'wp_privacy_personal_data_exporters', [ 'PUM_Privacy', 'register_exporter' ], 10 );
add_filter( 'wp_privacy_personal_data_erasers', [ 'PUM_Privacy', 'register_erasers' ], 10 );
add_action( 'admin_init', [ 'PUM_Privacy', 'privacy_policy_content' ], 20 );
add_action( 'pum_save_popup', [ 'PUM_Privacy', 'clear_cookie_list' ] );

add_action( 'admin_init', [ 'PUM_Utils_Alerts', 'hooks' ] );
add_action( 'admin_init', [ 'PUM_Utils_Alerts', 'php_handler' ] );
add_action( 'wp_ajax_pum_alerts_action', [ 'PUM_Utils_Alerts', 'ajax_handler' ] );
add_filter( 'pum_alert_list', [ 'PUM_Utils_Alerts', 'translation_request' ], 10 );
add_action( 'admin_menu', [ 'PUM_Utils_Alerts', 'append_alert_count' ], 999 );

add_action( 'pum_daily_scheduled_events', [ 'PUM_Telemetry', 'track_check' ] );
add_filter( 'pum_alert_list', [ 'PUM_Telemetry', 'optin_alert' ] );
add_action( 'pum_alert_dismissed', [ 'PUM_Telemetry', 'optin_alert_check' ], 10, 2 );
}

/**
* Returns true when debug mode is enabled.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php
/**
* Test provider used to verify hooks added during an active filter still run.
*
* @package Popup_Maker
*/

class PUM_Test_Deferred_Notification_Provider implements \PopupMaker\Services\Notifications\Provider {

/**
* @return void
*/
public function init() {
add_filter( 'pum_alert_list', [ $this, 'add_alert' ], PHP_INT_MIN + 1 );
}

/**
* @param array $alerts Registered alerts.
* @return array
*/
public function add_alert( $alerts ) {
$alerts[] = [ 'code' => 'deferred_test_provider' ];

return $alerts;
}
}
134 changes: 134 additions & 0 deletions tests/php/tests/Notification_Manager_Loader_Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<?php
/**
* Tests for on-demand notification provider loading.
*
* @package Popup_Maker
*/

require_once dirname( __DIR__ ) . '/fixtures/class-pum-test-deferred-notification-provider.php';

/**
* Verify frontend requests load notification providers when needed.
*/
class Notification_Manager_Loader_Test extends WP_UnitTestCase {

/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*
* @return void
*/
public function test_alert_filter_boots_deferred_providers_in_current_iteration() {
$manager = new \PopupMaker\Services\Notifications\Manager( \PopupMaker\plugin() );
$provider = new PUM_Test_Deferred_Notification_Provider();

$this->assertFalse( is_admin() );

add_filter(
'popup_maker/notification_providers',
static function ( $providers ) use ( $provider ) {
$providers[] = $provider;
return $providers;
}
);
$manager->register_lazy_boot();

$alerts = apply_filters( 'pum_alert_list', [] );
$codes = wp_list_pluck( $alerts, 'code' );

$this->assertContains( 'deferred_test_provider', $codes );
$this->assertContains( $provider, $manager->get_providers() );
}

/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*
* @return void
*/
public function test_custom_deferred_boot_hook_boots_the_manager_on_frontend() {
$manager = new \PopupMaker\Services\Notifications\Manager( \PopupMaker\plugin() );
$provider = new PUM_Test_Deferred_Notification_Provider();
$custom_hook = 'pum_test_custom_deferred_notification_boot';

$this->assertFalse( is_admin() );

add_filter(
'popup_maker/notification_providers',
static function ( $providers ) use ( $provider ) {
$providers[] = $provider;
return $providers;
}
);

add_filter(
'popup_maker/notifications/deferred_boot_hooks',
static function ( $hooks ) use ( $custom_hook ) {
$hooks[] = $custom_hook;
return $hooks;
}
);

$manager->register_lazy_boot();

$this->assertSame( 'original-value', apply_filters( $custom_hook, 'original-value' ) );
$this->assertSame( PHP_INT_MIN + 1, has_filter( 'pum_alert_list', [ $provider, 'add_alert' ] ) );
$this->assertContains( $provider, $manager->get_providers() );
}

/**
* An upgrade fires popup_maker/update_version during plugins_loaded,
* before init@5 registers lazy boot — the manager must boot immediately
* when a deferred hook already fired earlier in the request.
*
* @return void
*/
public function test_lazy_boot_inits_immediately_when_deferred_hook_already_fired() {
$manager = new \PopupMaker\Services\Notifications\Manager( \PopupMaker\plugin() );
$provider = new PUM_Test_Deferred_Notification_Provider();

$this->assertFalse( is_admin() );

add_filter(
'popup_maker/notification_providers',
static function ( $providers ) use ( $provider ) {
$providers[] = $provider;
return $providers;
}
);

// Simulate the upgrade action firing before register_lazy_boot runs.
do_action( 'popup_maker/update_version', '1.0.0', '0.9.0' );

$manager->register_lazy_boot();

$this->assertContains( $provider, $manager->get_providers() );
}

/**
* Core must wire the Manager's lazy boot on init — verify through the
* real booted plugin rather than a locally constructed manager.
*
* @return void
*/
public function test_core_registers_manager_lazy_boot_on_init() {
$manager = \PopupMaker\plugin( 'notifications' );

// Core's wiring results in one of two valid states: deferred boot
// filters registered, or an immediate boot because a deferred hook
// (e.g. popup_maker/update_version on a fresh install) already fired.
$deferred = false !== has_filter( 'pum_alert_list', [ $manager, 'boot_on_demand' ] );

$booted_prop = new ReflectionProperty( $manager, 'booted' );

if ( PHP_VERSION_ID < 80100 ) {
// Required before PHP 8.1, deprecated no-op on PHP 8.5+.
$booted_prop->setAccessible( true );
}

$this->assertTrue(
$deferred || $booted_prop->getValue( $manager ),
'Core should register the real manager for deferred boot on frontend requests.'
);
}
}
6 changes: 6 additions & 0 deletions tests/php/tests/PUM_Admin_Loader_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,18 @@ public function tearDown(): void {
}

/**
* Frontend bootstrap defers the heavy editor classes until their save hooks run.
*
* @runInSeparateProcess
* @preserveGlobalState disabled
*
* @return void
*/
public function test_frontend_loads_only_cross_context_admin_hooks() {
$this->assertFalse( is_admin() );
$this->assertSame( 10, has_action( 'save_post', [ 'PUM_Admin_Popups', 'save' ] ) );
$this->assertSame( 10, has_action( 'save_post', [ 'PUM_Admin_Themes', 'save' ] ) );
$this->assertSame( 99, has_filter( 'wp_insert_post_data', [ 'PUM_Admin_Popups', 'set_slug' ] ) );
$this->assertSame( 10, has_action( 'enqueue_block_assets', [ 'PUM_Admin_BlockEditor', 'register_block_assets' ] ) );
$this->assertFalse( has_action( 'admin_menu', [ 'PUM_Admin_Pages', 'register_pages' ] ) );
$this->assertFalse( has_action( 'wp_ajax_pum_object_search', [ 'PUM_Admin_Ajax', 'object_search' ] ) );
Expand Down
Loading