options = get_option( MODULES_OPTION, [] ); $this->includes(); $this->runtime_loader = new Runtime_Loader(); add_action( 'admin_enqueue_scripts', [ $this, 'register_scripts' ], 1 ); $this->load_standalone_modules(); $this->load_sections(); $this->init_admin_menu(); $this->init_module_activation(); $this->init_quick_start(); $this->init_command_palette(); } /** * Include required files * * @return void */ private function includes() { require_once ADMINOPTIMIZER_PATH . 'includes/functions.php'; } /** * Boot modules that have no owning Section. * * Form Builder is currently the only such module: it is registered in * `Module_Registry` with `'section' => 'forms'`, but no `Forms` Section * class exists yet to declare it through `section_nav_modules()`, so it * is booted directly here rather than through a Section (ARCHITECTURE_V3.md * Section 7.8). * * @return void */ private function load_standalone_modules() { $this->runtime_loader->boot_module( 'form_builder' ); } /** * Initialize all sections * * @return void */ private function load_sections() { Notification::get_instance(); $sections = []; $sections['overview'] = new Overview(); $sections['performance'] = new Performance( $this->runtime_loader ); $sections['security'] = new Security( $this->runtime_loader ); $sections['seo'] = new SEO( $this->runtime_loader ); $sections['content'] = new Content( $this->runtime_loader ); $sections['media'] = new Media( $this->runtime_loader ); $sections['post_types_fields'] = new Post_Types_Fields( $this->runtime_loader ); $sections['users'] = new Users( $this->runtime_loader ); $sections['system'] = new System( $this->runtime_loader ); foreach ( $sections as $section ) { $section->init(); } } /** * Setup admin menu * * @return void */ private function init_admin_menu() { add_action( 'admin_menu', [ $this, 'add_menu' ] ); } /** * Add admin menu * * @return void */ public function add_menu() { $menu_title = __( 'Optimize', 'admin-optimizer' ); $page_title = __( 'Admin Optimizer', 'admin-optimizer' ); add_menu_page( $page_title, $menu_title, 'manage_options', ADMINOPTIMIZER_MODULES_MENU_SLUG, '', self::MENU_SVG_ICON ); do_action( 'adminoptimizer_add_submenu_page' ); self::add_pinned_menu(); } /** * Register one submenu entry per pinned module still available, below * every real Section route (every Section's own `add_submenu()` has * already run by the time this is called from `add_menu()`, immediately * after `adminoptimizer_add_submenu_page`). * * Each pinned module gets a distinct, `adminoptim-pinned-`-prefixed slug * so it can never collide with a real Section route — not the module's * real resolved URL. A slug containing `://` is a known WordPress trick * for an external-style menu link, but it did not render reliably here * (every such entry collapsed to the parent Optimize page's own URL), so * each entry instead gets a real internal page whose only job is to * redirect. That redirect must fire on `load-{$hook_suffix}`, not in the * page callback: by the time WordPress calls the callback it has already * `include`d admin-header.php and sent output, so `wp_safe_redirect()` * there fails silently ("headers already sent"). `load-{$hook_suffix}` * fires earlier, while admin.php is still resolving the page. * * A pinned module that has since become unavailable is silently dropped * rather than surfaced as an error. Static and independent of `$this` so * it stays directly callable/testable without constructing the * Bootstrap singleton (which has no dedicated test coverage today). * * @return void */ public static function add_pinned_menu(): void { $first_slug = ''; foreach ( (array) get_option( Module_Activation::PIN_OPTION, [] ) as $module_id ) { $module_id = sanitize_key( (string) $module_id ); if ( '' === $module_id || ! Module_Registry::is_available( $module_id ) || ! current_user_can( Module_Registry::get_settings_capability( $module_id ) ) ) { continue; } $module = Module_Registry::get_module( $module_id ); if ( null === $module ) { continue; } $submenu_slug = 'adminoptim-pinned-' . $module_id; $hook_suffix = add_submenu_page( ADMINOPTIMIZER_MODULES_MENU_SLUG, $module['name'], $module['name'], 'manage_options', $submenu_slug, '__return_null' ); if ( is_string( $hook_suffix ) ) { add_action( "load-{$hook_suffix}", static function () use ( $module_id ) { wp_safe_redirect( Module_Registry::get_module_url( $module_id ) ); exit; } ); } if ( '' === $first_slug ) { $first_slug = $submenu_slug; } } if ( '' !== $first_slug ) { add_action( 'admin_head', static function () use ( $first_slug ) { ?> register(); } /** * Lifecycle point for the Overview Quick Start card's AJAX endpoints * (hide/restore, review-popup data, apply). Registered unconditionally; * each handler enforces its own capability/nonce checks. * * @return void */ private function init_quick_start(): void { ( new Quick_Start() )->register(); } /** * Lifecycle point for registering every navigable module into the * WordPress admin-wide command palette (Cmd/Ctrl+K). * * @return void */ private function init_command_palette(): void { ( new Command_Palette() )->register(); } /** * Register shared admin script/style handles for later enqueueing. * * @return void */ public function register_scripts() { wp_register_script( 'adminoptim-select2', ADMINOPTIMIZER_ADMIN_URI . 'assets/js/select2.min.js', [], ADMINOPTIMIZER_VERSION, true ); wp_register_style( 'adminoptim-select2-style', ADMINOPTIMIZER_ADMIN_URI . 'assets/css/select2.min.css', [], ADMINOPTIMIZER_VERSION ); wp_register_style( 'adminoptim-modules-pro-settings', ADMINOPTIMIZER_ADMIN_URI . 'assets/css/modules-pro-settings.min.css', [], ADMINOPTIMIZER_VERSION ); wp_register_style( 'adminoptim-module-settings', ADMINOPTIMIZER_ADMIN_URI . 'assets/css/module-settings.min.css', [], ADMINOPTIMIZER_VERSION ); } }