*/ private array $bindings = []; /** * Factories registered for classes that need constructor arguments. * * @var array */ private array $factories = []; /** * Guards against a second call to {@see self::boot()}. * * @var bool */ private bool $booted = false; /** * Use {@see self::instance()}. */ private function __construct() {} /** * Container instance. */ public static function instance(): self { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Boot the plugin. */ public function boot(): void { if ( $this->booted ) { return; } $this->booted = true; // Profiling is checked against the raw constant everywhere in the boot // path. Routing through Profiler::enabled() would autoload the profiler on // every production request purely to learn that it is switched off. $profiling = defined( 'RAHKAR_WP_PROFILE' ) && RAHKAR_WP_PROFILE; if ( $profiling ) { Profiler::mark( 'boot:start' ); } // The worker endpoint answers before anything else so a queue run never // pays for module registration it will not use. The parameter is checked // here rather than inside the handler because registering the hook at all // would load the endpoint class on every ordinary request just to be told // this is not a worker call. if ( Context::is( Context::WORKER ) ) { add_action( 'plugins_loaded', [ Queue\WorkerEndpoint::class, 'maybeHandle' ], 1 ); } add_action( 'plugins_loaded', [ $this, 'registerModules' ], 5 ); // The retention job runs in background contexts only, so the handler is // registered there rather than on every page view. A queued job whose // handler was never hooked would be claimed, do nothing, and be marked // complete — the retention policy would look armed and silently do // nothing at all. if ( Context::is( Context::ANY_BACKGROUND ) ) { add_action( Support\Hooks::PURGE_LOGS, [ Support\LogPurger::class, 'runScheduled' ] ); } // Trigger one: any request may finish the work an earlier request queued. // Registered as a shutdown hook, and the hook itself does one indexed // lookup before deciding to do anything at all. add_action( 'shutdown', [ $this, 'drainQueue' ], 0 ); if ( Context::is( Context::ADMIN ) ) { add_action( 'plugins_loaded', static fn() => self::make( Admin\Menu::class )->register(), 20 ); add_action( 'admin_init', [ Lifecycle::class, 'maybeUpgrade' ] ); } // Rebuilding on plugin/theme changes is what lets the manifest flags be // trusted at boot without re-detecting anything. add_action( 'activated_plugin', [ Manifest::class, 'rebuild' ] ); add_action( 'deactivated_plugin', [ Manifest::class, 'rebuild' ] ); add_action( 'switch_theme', [ Manifest::class, 'rebuild' ] ); add_action( 'upgrader_process_complete', [ Manifest::class, 'rebuild' ] ); if ( $profiling ) { Lifecycle::maybeProfile(); Profiler::mark( 'boot:hooks-registered' ); } } /** * Hand module registration to the manager. * * Deferred to `plugins_loaded` so WooCommerce and the active theme have * declared themselves before requirement checks run. * * @internal Hooked callback. */ public function registerModules(): void { $this->modules()->registerEnabled(); if ( defined( 'RAHKAR_WP_PROFILE' ) && RAHKAR_WP_PROFILE ) { Profiler::mark( 'boot:modules-registered' ); } } /** * Opportunistic queue drain on shutdown. * * The visitor's response is already closed by the time any job runs, so this * costs the page nothing. * * The watermark is read inline instead of through `Queue::maybeHasDue()`, * because calling that would autoload both the queue and the dispatcher on * every single request — three files loaded purely to discover there is * nothing to do. On a site with an empty queue, which is most sites most of * the time, this method now costs one array lookup against options WordPress * had already fetched. * * @internal Hooked callback. */ public function drainQueue(): void { if ( Context::is( Context::ANY_BACKGROUND ) ) { return; } $next = (int) get_option( self::QUEUE_WATERMARK_OPTION, 0 ); if ( 0 === $next || $next > time() ) { return; } Queue\Dispatcher::instance()->drainAfterResponse(); } /** * Module manager. */ public function modules(): ModuleManager { if ( null === $this->modules ) { $this->modules = new ModuleManager(); } return $this->modules; } /** * Resolve a shared instance. * * The lazy-handler pattern the modules use — `static fn() => * Plugin::make(Handler::class)->run()` — routes through here, which is what * keeps a handler class unloaded until its hook actually fires. * * @template T of object * * @param class-string $class Class to resolve. * * @return T */ public static function make( string $class ): object { $self = self::instance(); if ( isset( $self->bindings[ $class ] ) ) { // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- Inline type assertion for phpstan. /** @var T */ return $self->bindings[ $class ]; } if ( isset( $self->factories[ $class ] ) ) { $instance = ( $self->factories[ $class ] )(); } else { $instance = new $class(); } $self->bindings[ $class ] = $instance; // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- Inline type assertion for phpstan. /** @var T */ return $instance; } /** * Register a factory for a class that cannot be constructed with no arguments. * * @param class-string $class Class name. * @param callable():object $factory Factory closure. */ public function bind( string $class, callable $factory ): void { $this->factories[ $class ] = $factory; } /** * Replace a resolved instance. Test-suite affordance only. * * @param class-string $class Class name. * @param object $instance Instance to use. * * @internal */ public function swap( string $class, object $instance ): void { $this->bindings[ $class ] = $instance; } }