store = $store; } /** * Settings storage. * * @since 1.0.0 * @return Settings */ public function store() { return $this->store; } /** * Built-in modules. * * @since 1.0.0 * @return string[] */ private function core_modules() { return array( GuestAccounts\Module::class, AddressSync\Module::class, AdvancedOrderSearch\Module::class, AutocompleteOrders\Module::class, PaymentLink\Module::class, ThankYouPreview\Module::class, SwitchUser\Module::class, OrderMeta\Module::class, UserAddressColumns\Module::class, CustomerTags\Module::class, AdvancedCustomerSearch\Module::class, DuplicateCustomers\Module::class, EmailLog\Module::class, EmailPreview\Module::class, EmailRouting\Module::class, PaymentRules\Module::class, ShippingRules\Module::class, Blocklist\Module::class, HolidayMode\Module::class, HighValueOrders\Module::class, QuickStock\Module::class, AnalyticsFilters\Module::class, WebhookHealth\Module::class, ScheduledActions\Module::class, CronInspector\Module::class, AutoloadAnalyzer\Module::class, TestData\Module::class, DatabaseCleanup\Module::class, ); } /** * Instantiate every registered module (without booting any). * * @since 1.0.0 * @return void */ public function instantiate() { if ( $this->modules ) { return; } /** * Filter the list of module class names. * * @since 1.0.0 * @param string[] $classes Fully-qualified class names extending AbstractModule. */ $classes = (array) apply_filters( 'apwc_modules', $this->core_modules() ); foreach ( $classes as $class_name ) { if ( ! is_string( $class_name ) || ! class_exists( $class_name ) || ! is_subclass_of( $class_name, AbstractModule::class ) ) { continue; } $module = new $class_name( $this->store ); $this->modules[ $module->id() ] = $module; } } /** * Instantiate all modules and boot the enabled ones. * * @since 1.0.0 * @return void */ public function load() { $this->instantiate(); if ( $this->store->was_migrated() ) { $this->sync_autoload( true ); } // Enabled modules that do not autoload their settings: fetch them all in one query on admin screens. if ( is_admin() && function_exists( 'wp_prime_option_caches' ) ) { $names = array(); foreach ( $this->modules as $module ) { if ( $module->is_enabled() && ! $module->autoload_settings() ) { $names[] = Settings::option_name( $module->id() ); } } if ( $names ) { wp_prime_option_caches( $names ); } } foreach ( $this->modules as $module ) { if ( $module->is_enabled() && '' === $module->unmet_requirement() ) { $module->boot(); /** * Fires after a module has booted. * * @since 1.0.0 * @param AbstractModule $module The module. */ do_action( 'apwc_module_booted', $module ); } } } /** * Set every settings option's autoload flag in one query. * * @since 1.0.0 * @param bool $plugin_active False on deactivation: nothing of ours should autoload then. * @param bool $lifecycle Also run switched-on power-ups' activate() / deactivate(). False * only flips the flags (WooCommerce missing or below the floor). * @return void */ public function sync_autoload( $plugin_active = true, $lifecycle = true ) { $this->instantiate(); $values = array( Settings::STATE => $plugin_active ); foreach ( $this->modules as $module ) { if ( $lifecycle && $module->is_enabled() ) { $plugin_active ? $module->activate() : $module->deactivate(); } if ( $plugin_active && $module->should_autoload() ) { $this->store->ensure_module_option( $module->id() ); } $values[ Settings::option_name( $module->id() ) ] = $plugin_active && $module->should_autoload(); foreach ( $module->autoload_options() as $option ) { $values[ $option ] = $plugin_active && $module->is_enabled(); } } wp_set_option_autoload_values( $values ); } /** * After a plugin update (stored version differs from APWC_VERSION), run the activation steps * again: activate() for switched-on power-ups (tables, scheduled jobs) and the autoload flags. * Runs on `admin_init` only, never on AJAX or cron requests (schema changes commit open * transactions), only for a store manager (admin-post.php fires `admin_init` for logged-out * visitors too), and once per version. * * @since 1.0.0 * @return void */ public function maybe_upgrade() { if ( wp_doing_ajax() || wp_doing_cron() || APWC_VERSION === $this->store->version() || ! current_user_can( 'manage_woocommerce' ) ) { return; } $this->store->set_version( APWC_VERSION ); // First, so a failing step can't make every request retry. $this->sync_autoload( true ); /** * Fires after the plugin's activation steps ran again for a new version, or after * WooCommerce reached the minimum version again (same version): handlers must be safe to * run more than once. * * @since 1.0.0 * @param string $version Current plugin version. */ do_action( 'apwc_upgraded', APWC_VERSION ); } /** * Drop cached state and settings (after changes made outside this request's objects). * * @since 1.0.0 * @return void */ public function flush() { $this->store->flush(); foreach ( $this->modules as $module ) { $module->refresh(); } } /** * All modules. * * @since 1.0.0 * @return AbstractModule[] */ public function all() { return $this->modules; } /** * Modules the current user may see and use (AbstractModule::user_can_access()): what the * Powerups list, its counts and `GET apwc/v1/modules` show. all() and get() stay unfiltered for * internal use (mask rules, uninstall, autoload sync). * * @since 1.0.0 * @return AbstractModule[] Keyed by ID. */ public function visible() { return array_filter( $this->modules, static fn( AbstractModule $module ) => $module->user_can_access() ); } /** * A module by ID. * * @since 1.0.0 * @param string $id Module ID. * @return AbstractModule|null */ public function get( $id ) { return $this->modules[ $id ] ?? null; } /** * Switch a module on or off. * * @since 1.0.0 * @param string $id Module ID. * @param bool $enabled New state. * @return void */ public function set_enabled( $id, $enabled ) { $this->store->set_enabled( $id, $enabled ); $module = $this->get( $id ); if ( $module ) { $enabled ? $module->activate() : $module->deactivate(); if ( $module->should_autoload() ) { $this->store->ensure_module_option( $id ); } $values = array( Settings::option_name( $id ) => $module->should_autoload() ); foreach ( $module->autoload_options() as $option ) { $values[ $option ] = (bool) $enabled; } wp_set_option_autoload_values( $values ); } /** * Fires when a module is toggled. * * @since 1.0.0 * @param string $id Module ID. * @param bool $enabled New state. */ do_action( 'apwc_module_toggled', $id, (bool) $enabled ); } /** * Category labels in display order. * * @since 1.0.0 * @return array */ public static function categories() { /** * Filter the category list (key => label), in display order. * * @since 1.0.0 * @param array $categories Categories. */ return (array) apply_filters( 'apwc_categories', array( 'orders' => __( 'Orders', 'admin-powerups-for-woocommerce' ), 'customers' => __( 'Customers', 'admin-powerups-for-woocommerce' ), 'emails' => __( 'Emails', 'admin-powerups-for-woocommerce' ), 'operations' => __( 'Store operations', 'admin-powerups-for-woocommerce' ), 'tools' => __( 'Tools', 'admin-powerups-for-woocommerce' ), 'developer' => __( 'Developer', 'admin-powerups-for-woocommerce' ), 'performance' => __( 'Performance', 'admin-powerups-for-woocommerce' ), ) ); } /** * Categories whose power-ups are for site admins only (`manage_options`; store managers never * see them). Developer and Performance always are: the filter can add categories, never remove * these two. Keys are lowercased and trimmed (AbstractModule::is_admin_only() compares the * same way). * * @since 1.0.0 * @return string[] */ public static function admin_only_categories() { /** * Filter the extra categories whose power-ups only site admins may see and use. Developer * and Performance are always admin-only; returning a list without them doesn't change that. * * @since 1.0.0 * @param string[] $categories Extra category keys (empty by default). */ $core = array( 'developer', 'performance' ); if ( ! has_filter( 'apwc_admin_only_categories' ) ) { return $core; // Asked once per power-up per check: skip the filter call when nobody hooks it. } $extra = array_map( static fn( $key ) => strtolower( trim( $key ) ), array_filter( (array) apply_filters( 'apwc_admin_only_categories', array() ), 'is_string' ) ); return array_values( array_unique( array_merge( $core, array_filter( $extra, static fn( string $key ): bool => '' !== $key ) ) ) ); } }