* * This is an mu-plugin. It lives in a subdirectory and is loaded by the stub in * `loader/studiou-wc-mail-queue.php` — see that file for install instructions. * * @package studiou-wc-mail-queue * @copyright 2026 QUADARAX * @author Dalibor Votruba * @license GPL-2.0-or-later */ defined( 'ABSPATH' ) || exit; define( 'STUDIOU_WCMQ_VERSION', '1.0.0' ); define( 'STUDIOU_WCMQ_FILE', __FILE__ ); define( 'STUDIOU_WCMQ_DIR', plugin_dir_path( __FILE__ ) ); define( 'STUDIOU_WCMQ_URL', plugin_dir_url( __FILE__ ) ); define( 'STUDIOU_WCMQ_BASENAME', plugin_basename( __FILE__ ) ); /** * Declare HPOS compatibility. * * Named (not a closure) so a test harness or another plugin can remove_action it. */ function studiou_wcmq_declare_hpos_compat() { if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) { \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', STUDIOU_WCMQ_FILE, true ); } } add_action( 'before_woocommerce_init', 'studiou_wcmq_declare_hpos_compat' ); /** * Main plugin class. * * Its only jobs: guard on WooCommerce, load dependencies, instantiate managers. * Managers register their own hooks in their constructors / init(). */ final class Studiou_WC_Mail_Queue { /** * Worker instance. Held so its hooks stay attached. * * @var Studiou_WCMQ_Worker|null */ private $worker = null; /** * Admin instance. * * @var Studiou_WCMQ_Admin|null */ private $admin = null; /** * Boot entry point. */ public static function init_plugin() { $class = __CLASS__; new $class(); } /** * Constructor. */ public function __construct() { if ( ! class_exists( 'WooCommerce' ) ) { add_action( 'admin_notices', array( $this, 'woocommerce_missing_notice' ) ); return; } $this->load_dependencies(); Studiou_WCMQ_DB::maybe_upgrade_db(); Studiou_WCMQ_Utils_Log::init(); Studiou_WCMQ_Context::init(); Studiou_WCMQ_Interceptor::init(); Studiou_WCMQ_State::init(); Studiou_WCMQ_Reaper::init(); Studiou_WCMQ_Retention::init(); $this->worker = new Studiou_WCMQ_Worker(); if ( is_admin() ) { $this->admin = new Studiou_WCMQ_Admin(); } // Action Scheduler only exists once WooCommerce has loaded it. An mu-plugin // runs before regular plugins, so nothing AS-dependent may happen at file // scope or at plugins_loaded. Defer scheduling to `init`. add_action( 'init', array( $this, 'schedule_recurring_actions' ), 20 ); } /** * Load class files. */ private function load_dependencies() { $files = array( 'utils-log.php', 'class-wcmq-settings.php', 'class-wcmq-db.php', 'class-wcmq-state.php', 'class-wcmq-context.php', 'class-wcmq-queue.php', 'class-wcmq-interceptor.php', 'class-wcmq-notifier.php', 'class-wcmq-worker.php', 'class-wcmq-reaper.php', 'class-wcmq-retention.php', ); if ( is_admin() ) { $files[] = 'class-wcmq-admin.php'; } foreach ( $files as $file ) { require_once STUDIOU_WCMQ_DIR . 'includes/' . $file; } } /** * Register the recurring reaper + retention actions, exactly once each. * * Gated to admin / cron / WP-CLI. The `as_next_scheduled_action()` pre-check * inside each is what keeps this idempotent, but it is still two DB queries, * and running them on every front-end page view buys nothing: those contexts * are exactly the ones that never need to bootstrap a schedule. */ public function schedule_recurring_actions() { if ( ! function_exists( 'as_schedule_recurring_action' ) ) { return; } $should_schedule = is_admin() || wp_doing_cron() || ( defined( 'WP_CLI' ) && WP_CLI ); if ( ! $should_schedule ) { return; } Studiou_WCMQ_Reaper::schedule(); Studiou_WCMQ_Retention::schedule(); } /** * Admin notice shown when WooCommerce is absent. */ public function woocommerce_missing_notice() { ?>