diff --git a/includes/Admin/Settings/Repository/LegacySettingsRepository.php b/includes/Admin/Settings/Repository/LegacySettingsRepository.php index 5bf68812aa..25f1145a8f 100644 --- a/includes/Admin/Settings/Repository/LegacySettingsRepository.php +++ b/includes/Admin/Settings/Repository/LegacySettingsRepository.php @@ -30,6 +30,9 @@ public function __construct( ) { $this->new_repo = $new_repo ?? new SettingsRepository(); $this->bridge = $bridge ?? new LegacySettingsBridge(); + add_action( 'init', [ $this, 'init' ] ); + } + public function init(): void { foreach ( $this->known_sections() as $section ) { add_action( "update_option_{$section}", [ $this, 'on_section_changed' ] ); diff --git a/includes/Shortcodes/FullWidthVendorLayout.php b/includes/Shortcodes/FullWidthVendorLayout.php index 8fb53a168e..90bb533246 100644 --- a/includes/Shortcodes/FullWidthVendorLayout.php +++ b/includes/Shortcodes/FullWidthVendorLayout.php @@ -27,13 +27,29 @@ class FullWidthVendorLayout implements Hookable { */ public function register_hooks(): void { add_action( 'dokan_setup_wizard_styles', [ $this, 'update_layout_style' ] ); - // Register vendor dashboard assets if the vendor layout is not legacy. - $vendor_layout = dokan_get_option( 'vendor_layout_style', 'dokan_appearance', 'legacy' ); - if ( 'latest' === $vendor_layout ) { - add_action( 'init', [ $this, 'register_vendor_dashboard_assets' ], 99 ); - add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_vendor_dashboard_assets' ] ); - add_filter( 'template_include', [ $this, 'rewrite_vendor_dashboard_template' ] ); - } + add_action( 'init', [ $this, 'register_vendor_dashboard_assets' ], 99 ); + add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_vendor_dashboard_assets' ] ); + add_filter( 'template_include', [ $this, 'rewrite_vendor_dashboard_template' ] ); + } + + /** + * Whether the marketplace runs the React (latest) vendor dashboard layout. + * + * Deliberately resolved per callback instead of once in `register_hooks()`: + * hooks are registered on `plugins_loaded`, and reading a Dokan setting + * walks the legacy settings bridge, which builds the translated admin + * settings schema. Doing that before `init` — where + * `load_plugin_textdomain()` runs — trips WordPress 6.7+'s + * `_load_textdomain_just_in_time` notice on every request. Every callback + * below fires on `init` or later, and the repository memoizes the section + * snapshot, so the deferred read costs nothing. + * + * @since DOKAN_SINCE + * + * @return bool + */ + protected function is_latest_layout(): bool { + return 'latest' === dokan_get_option( 'vendor_layout_style', 'dokan_appearance', 'legacy' ); } /** @@ -70,6 +86,10 @@ public function update_layout_style(): void { * @return string Modified template path */ public function rewrite_vendor_dashboard_template( $template ) { + if ( ! $this->is_latest_layout() ) { + return $template; + } + // Check if we should load the fullwidth template. if ( ! dokan_is_seller_dashboard() ) { return $template; @@ -94,6 +114,10 @@ public function rewrite_vendor_dashboard_template( $template ) { * @return void */ public function register_vendor_dashboard_assets() { + if ( ! $this->is_latest_layout() ) { + return; + } + $admin_dashboard_file = DOKAN_DIR . '/assets/js/vendor-dashboard/layout/index.asset.php'; if ( file_exists( $admin_dashboard_file ) ) { $dashboard_script = require $admin_dashboard_file; @@ -213,6 +237,10 @@ public function register_vendor_dashboard_assets() { * @return void */ public function enqueue_vendor_dashboard_assets() { + if ( ! $this->is_latest_layout() ) { + return; + } + if ( ! is_user_logged_in() || ! dokan_is_seller_dashboard() ) { return; } diff --git a/tests/php/src/Admin/Settings/Repository/LegacySettingsRepositoryTest.php b/tests/php/src/Admin/Settings/Repository/LegacySettingsRepositoryTest.php index ffc07fbfd7..c6a7872d73 100644 --- a/tests/php/src/Admin/Settings/Repository/LegacySettingsRepositoryTest.php +++ b/tests/php/src/Admin/Settings/Repository/LegacySettingsRepositoryTest.php @@ -53,6 +53,47 @@ public function test_class_implements_interface(): void { $this->assertInstanceOf( LegacySettingsRepositoryInterface::class, $repo ); } + /** + * The repository is resolved from the container by `dokan_get_option()`, + * which callers reach as early as `plugins_loaded` (hook registration). + * Building the settings schema there runs hundreds of `esc_html__()` + * calls before `init` — WordPress 6.7+ answers that with a + * `_load_textdomain_just_in_time` doing-it-wrong notice, and every + * front-end request pays to assemble an admin-only schema. + */ + public function test_constructor_does_not_build_the_settings_schema(): void { + $calls = 0; + $counter = function ( $elements ) use ( &$calls ) { + ++$calls; + return $elements; + }; + + add_filter( 'dokan_get_admin_settings_schema', $counter ); + + try { + new LegacySettingsRepository(); + } finally { + remove_filter( 'dokan_get_admin_settings_schema', $counter ); + } + + $this->assertSame( 0, $calls ); + } + + /** + * Cache invalidation must not depend on the section being present in the + * bridge mapping — any warmed snapshot is stale once its option changes. + */ + public function test_write_to_unmapped_section_flushes_its_snapshot(): void { + update_option( 'dokan_unmapped_section', [ 'foo' => 'before' ] ); + + $repo = new LegacySettingsRepository(); + $repo->all( 'dokan_unmapped_section' ); + + update_option( 'dokan_unmapped_section', [ 'foo' => 'after' ] ); + + $this->assertSame( 'after', $repo->get( 'dokan_unmapped_section', 'foo' ) ); + } + public function test_all_returns_empty_array_when_option_missing(): void { delete_option( 'dokan_general' ); diff --git a/tests/php/src/Shortcodes/FullWidthVendorLayoutTest.php b/tests/php/src/Shortcodes/FullWidthVendorLayoutTest.php new file mode 100644 index 0000000000..f49ce2d27a --- /dev/null +++ b/tests/php/src/Shortcodes/FullWidthVendorLayoutTest.php @@ -0,0 +1,96 @@ +get( LegacySettingsRepository::class )->flush_cache( null ); + } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch + unset( $e ); + } + } + } + + /** + * Count `dokan_appearance` reads while running `$callback`. + * + * @param callable $callback Code under test. + * + * @return int + */ + private function count_appearance_reads( callable $callback ): int { + $reads = 0; + $counter = function ( $pre ) use ( &$reads ) { + ++$reads; + return $pre; + }; + + add_filter( 'pre_option_dokan_appearance', $counter ); + + try { + $callback(); + } finally { + remove_filter( 'pre_option_dokan_appearance', $counter ); + } + + return $reads; + } + + /** + * `register_hooks()` runs on `plugins_loaded` (see `WeDevs_Dokan::init_hooks()`). + * Reading a Dokan setting there resolves the legacy settings bridge, which + * builds the translated admin settings schema — before `init`, where + * `load_plugin_textdomain()` runs. WordPress 6.7+ answers that with a + * `_load_textdomain_just_in_time` doing-it-wrong notice on every request. + */ + public function test_register_hooks_does_not_read_settings(): void { + $layout = new FullWidthVendorLayout(); + + $this->assertSame( 0, $this->count_appearance_reads( [ $layout, 'register_hooks' ] ) ); + } + + /** + * Gating moved from hook registration into the callbacks, so the template + * override must still be inert while the marketplace runs the legacy layout. + */ + public function test_template_is_untouched_for_legacy_layout(): void { + update_option( 'dokan_appearance', [ 'vendor_layout_style' => 'legacy' ] ); + + $layout = new FullWidthVendorLayout(); + $layout->register_hooks(); + + $this->assertSame( 'theme-template.php', $layout->rewrite_vendor_dashboard_template( 'theme-template.php' ) ); + } + + /** + * The deferred gate must still consult the setting when a callback runs. + */ + public function test_layout_setting_is_read_when_a_callback_runs(): void { + update_option( 'dokan_appearance', [ 'vendor_layout_style' => 'legacy' ] ); + + $layout = new FullWidthVendorLayout(); + $layout->register_hooks(); + + $reads = $this->count_appearance_reads( + function () use ( $layout ) { + $layout->rewrite_vendor_dashboard_template( 'theme-template.php' ); + } + ); + + $this->assertGreaterThan( 0, $reads ); + } +}