resolveOptionStore( $registry ); $products = $this->resolveProductRepository( $registry ); $productMeta = $this->resolveProductMetaRepository( $registry ); $statusStore = new ProductLinkHealthStatusStore( $productMeta ); $collector = new SavedProductLinkHealthUrlCollector( $products, $statusStore ); $runner = new ScheduledLinkHealthBatchRunner( new OptionStoreSettingsRepository( $options ), $options, $collector, $statusStore, new CloudLinkHealthClient(), new SystemClock(), RetryPolicy::forJobType( RetryPolicy::JOB_LINK_HEALTH_BATCH ) ); $this->runner = $runner; $registry->bind( self::BINDING_LINK_HEALTH_BATCH_RUNNER, $runner ); } /** * Wire WordPress hooks. * * @param HookRegistrar $hooks Shared hook registrar. */ public function boot( HookRegistrar $hooks ): void { $hooks->addFilter( 'cron_schedules', static function ( $schedules ): array { if ( ! is_array( $schedules ) ) { $schedules = array(); } if ( ! array_key_exists( self::INTERVAL_LINK_HEALTH_DAILY, $schedules ) ) { $schedules[ self::INTERVAL_LINK_HEALTH_DAILY ] = array( 'interval' => 86400, 'display' => 'affilun: daily link-health batch', ); } return $schedules; } ); $hooks->addAction( 'affilun_activate', function (): void { $this->onActivate(); } ); $hooks->addAction( 'init', function (): void { $this->ensureScheduled(); } ); $runner = $this->runner; $hooks->addAction( self::HOOK_LINK_HEALTH_BATCH, static function () use ( $runner ): void { if ( $runner === null ) { return; } try { $runner->tick(); } catch ( Throwable $e ) { if ( function_exists( 'error_log' ) ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Cron failure surface; intentional. error_log( 'AFFILUN ScheduledLinkHealthBatchRunner tick failed: ' . $e->getMessage() ); } } } ); } /** * Activation entry point: schedule daily link-health batches. */ public function onActivate(): void { if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_schedule_event' ) ) { return; } $this->ensureScheduled(); } /** * Ensure existing installs also get the link-health cron event. */ public function ensureScheduled(): void { if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_schedule_event' ) ) { return; } $timestamp = wp_next_scheduled( self::HOOK_LINK_HEALTH_BATCH ); if ( ! $timestamp ) { $this->scheduleNextBatch(); return; } if ( function_exists( 'wp_get_schedule' ) && function_exists( 'wp_unschedule_event' ) && wp_get_schedule( self::HOOK_LINK_HEALTH_BATCH ) !== self::INTERVAL_LINK_HEALTH_DAILY ) { $this->unscheduleBatches(); $this->scheduleNextBatch(); } } /** * Schedule the next daily link-health batch. */ private function scheduleNextBatch(): void { wp_schedule_event( time() + 300, self::INTERVAL_LINK_HEALTH_DAILY, self::HOOK_LINK_HEALTH_BATCH ); } /** * Deactivation entry point: unschedule link-health batches. */ public function onDeactivate(): void { if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_unschedule_event' ) ) { return; } $this->unscheduleBatches(); } /** * Unschedule all pending link-health batches. */ private function unscheduleBatches(): void { $timestamp = wp_next_scheduled( self::HOOK_LINK_HEALTH_BATCH ); while ( false !== $timestamp && null !== $timestamp ) { wp_unschedule_event( (int) $timestamp, self::HOOK_LINK_HEALTH_BATCH ); $timestamp = wp_next_scheduled( self::HOOK_LINK_HEALTH_BATCH ); } } /** * Runner bound after register(), or null before register(). */ public function runner(): ?ScheduledLinkHealthBatchRunner { return $this->runner; } /** * Resolve option store from the registry. * * @param ServiceProviderRegistry $registry Registry. */ private function resolveOptionStore( ServiceProviderRegistry $registry ): OptionStore { $key = PersistenceServiceProvider::BINDING_OPTION_STORE; if ( ! $registry->has( $key ) ) { throw new LogicException( 'LinkHealthCronServiceProvider requires OptionStore binding.' ); } $value = $registry->get( $key ); if ( ! $value instanceof OptionStore ) { $message = sprintf( 'LinkHealthCronServiceProvider expected OptionStore under "%s"; got %s.', $key, is_object( $value ) ? get_class( $value ) : gettype( $value ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new LogicException( $message ); } return $value; } /** * Resolve product repository from the registry. * * @param ServiceProviderRegistry $registry Registry. */ private function resolveProductRepository( ServiceProviderRegistry $registry ): ProductRepository { $key = PersistenceServiceProvider::BINDING_PRODUCT_REPOSITORY; if ( ! $registry->has( $key ) ) { throw new LogicException( 'LinkHealthCronServiceProvider requires ProductRepository binding.' ); } $value = $registry->get( $key ); if ( ! $value instanceof ProductRepository ) { $message = sprintf( 'LinkHealthCronServiceProvider expected ProductRepository under "%s"; got %s.', $key, is_object( $value ) ? get_class( $value ) : gettype( $value ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new LogicException( $message ); } return $value; } /** * Resolve product-meta repository from the registry. * * @param ServiceProviderRegistry $registry Registry. */ private function resolveProductMetaRepository( ServiceProviderRegistry $registry ): ProductMetaRepository { $key = PersistenceServiceProvider::BINDING_PRODUCT_META_REPOSITORY; if ( ! $registry->has( $key ) ) { throw new LogicException( 'LinkHealthCronServiceProvider requires ProductMetaRepository binding.' ); } $value = $registry->get( $key ); if ( ! $value instanceof ProductMetaRepository ) { $message = sprintf( 'LinkHealthCronServiceProvider expected ProductMetaRepository under "%s"; got %s.', $key, is_object( $value ) ? get_class( $value ) : gettype( $value ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new LogicException( $message ); } return $value; } }