*/ private $postProvider; /** * Opportunity scanner. */ private ExistingPostOpportunityScanner $scanner; /** * @param OptionStore $options Option storage. * @param callable $postProvider Batch post provider. * @param ExistingPostOpportunityScanner $scanner Opportunity scanner. * * @phpstan-param callable(int,int): list $postProvider */ public function __construct( OptionStore $options, callable $postProvider, ?ExistingPostOpportunityScanner $scanner = null ) { $this->options = $options; $this->postProvider = $postProvider; $this->scanner = $scanner ?? new ExistingPostOpportunityScanner(); } /** * Start a new scan job. * * @return array{id:string,status:string,offset:int,processed:int,opportunities:list>} */ public function start(): array { $job = array( 'id' => 'scan_' . gmdate( 'YmdHis' ) . '_' . bin2hex( random_bytes( 4 ) ), 'status' => 'queued', 'offset' => 0, 'processed' => 0, 'opportunities' => array(), 'created_at' => gmdate( 'c' ), 'updated_at' => gmdate( 'c' ), ); $this->storeJob( $job ); return $job; } /** * Run one bounded batch. * * @param string $jobId Job id. * @param int $limit Batch size. * * @return array{id:string,status:string,offset:int,processed:int,opportunities:list>}|null */ public function runBatch( string $jobId, int $limit = 10 ): ?array { $job = $this->findJob( $jobId ); if ( $job === null || $job['status'] === 'complete' ) { return $job; } $limit = max( 1, min( 25, $limit ) ); $provider = $this->postProvider; $posts = $provider( (int) $job['offset'], $limit ); $batch = $this->scanner->scanBatch( $posts ); $job['status'] = count( $posts ) < $limit ? 'complete' : 'running'; $job['offset'] = (int) $job['offset'] + count( $posts ); $job['processed'] = (int) $job['processed'] + count( $posts ); $job['opportunities'] = array_merge( $job['opportunities'], $batch['opportunities'] ); $job['updated_at'] = gmdate( 'c' ); $this->storeJob( $job ); return $job; } /** * Return a job by id. * * @param string $jobId Job id. * * @return array{id:string,status:string,offset:int,processed:int,opportunities:list>}|null */ public function findJob( string $jobId ): ?array { foreach ( $this->readJobs() as $job ) { if ( isset( $job['id'] ) && $job['id'] === $jobId ) { return $this->normalizeJob( $job ); } } return null; } /** * Return most recent job. * * @return array{id:string,status:string,offset:int,processed:int,opportunities:list>}|null */ public function latest(): ?array { $jobs = $this->readJobs(); if ( $jobs === array() ) { return null; } $last = end( $jobs ); return is_array( $last ) ? $this->normalizeJob( $last ) : null; } /** * Store/replace a job. * * @param array $job Job. */ private function storeJob( array $job ): void { $jobs = $this->readJobs(); $replaced = false; foreach ( $jobs as $index => $existing ) { if ( isset( $existing['id'] ) && $existing['id'] === $job['id'] ) { $jobs[ $index ] = $job; $replaced = true; } } if ( ! $replaced ) { $jobs[] = $job; } if ( count( $jobs ) > self::MAX_RETAINED_JOBS ) { $jobs = array_slice( $jobs, - self::MAX_RETAINED_JOBS ); } $this->options->update( self::OPTION_JOBS, array_values( $jobs ) ); } /** * Read raw jobs. * * @return list> */ private function readJobs(): array { $stored = $this->options->get( self::OPTION_JOBS, array() ); if ( ! is_array( $stored ) ) { return array(); } $jobs = array(); foreach ( $stored as $job ) { if ( is_array( $job ) ) { $jobs[] = $job; } } return $jobs; } /** * Normalize a persisted job. * * @param array $job Job. * * @return array{id:string,status:string,offset:int,processed:int,opportunities:list>} */ private function normalizeJob( array $job ): array { $opportunities = array(); if ( isset( $job['opportunities'] ) && is_array( $job['opportunities'] ) ) { foreach ( $job['opportunities'] as $opportunity ) { if ( is_array( $opportunity ) ) { $opportunities[] = $opportunity; } } } return array( 'id' => isset( $job['id'] ) && is_string( $job['id'] ) ? $job['id'] : '', 'status' => isset( $job['status'] ) && is_string( $job['status'] ) ? $job['status'] : 'queued', 'offset' => isset( $job['offset'] ) && is_int( $job['offset'] ) ? $job['offset'] : 0, 'processed' => isset( $job['processed'] ) && is_int( $job['processed'] ) ? $job['processed'] : 0, 'opportunities' => $opportunities, ); } }