0). * * @throws InvalidArgumentException When `$maxScanRows` is * non-positive. */ public function __construct( ProductRepository $products, SyncPolicy $policy, int $maxScanRows = self::DEFAULT_MAX_SCAN_ROWS ) { if ( $maxScanRows <= 0 ) { $message = sprintf( 'StaleProductFinder: maxScanRows must be > 0, got %d.', $maxScanRows ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } $this->products = $products; $this->policy = $policy; $this->maxScanRows = $maxScanRows; } /** * Records currently in `stale` or `expired` state, ordered * by priority desc / `lastSyncedAt` asc (nulls first). * * @param int $limit Maximum number of records to return * (must be > 0). * * @return list * * @throws InvalidArgumentException When `$limit` is * non-positive. */ public function findStale( int $limit ): array { return $this->collect( $limit, static function ( SyncState $state ): bool { return $state->isStale() || $state->isExpired(); } ); } /** * Records currently in `expired` state, ordered as in * {@see self::findStale()}. * * @param int $limit Maximum number of records to return * (must be > 0). * * @return list * * @throws InvalidArgumentException When `$limit` is * non-positive. */ public function findExpired( int $limit ): array { return $this->collect( $limit, static function ( SyncState $state ): bool { return $state->isExpired(); } ); } /** * Records currently in `broken` state, ordered as in * {@see self::findStale()}. * * @param int $limit Maximum number of records to return * (must be > 0). * * @return list * * @throws InvalidArgumentException When `$limit` is * non-positive. */ public function findBroken( int $limit ): array { return $this->collect( $limit, static function ( SyncState $state ): bool { return $state->isBroken(); } ); } /** * Cap on the candidate rows the finder pulls per call. */ public function maxScanRows(): int { return $this->maxScanRows; } /** * Walk the repository up to {@see self::$maxScanRows}, keep * the records whose state passes `$accept`, then sort and * truncate to `$limit`. * * @param int $limit Maximum records to return. * @param callable(SyncState): bool $accept State predicate. * * @return list * * @throws InvalidArgumentException When `$limit` is non-positive. */ private function collect( int $limit, callable $accept ): array { if ( $limit <= 0 ) { $message = sprintf( 'StaleProductFinder: limit must be > 0, got %d.', $limit ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } $matches = array(); $page = 1; $scanned = 0; while ( $scanned < $this->maxScanRows ) { $remaining = $this->maxScanRows - $scanned; $perPage = $remaining < self::SCAN_PAGE_SIZE ? $remaining : self::SCAN_PAGE_SIZE; $query = new ProductListQuery( $page, $perPage, null, null, null, null, null, ProductListQuery::SORT_KEY_LAST_SYNCED_AT, ProductListQuery::SORT_ORDER_ASC ); $result = $this->products->findPaginated( $query ); $items = $result->items(); foreach ( $items as $record ) { ++$scanned; $state = $this->policy->evaluateState( $record ); if ( $accept( $state ) ) { $matches[] = $record; } } if ( count( $items ) < $perPage ) { break; } ++$page; } usort( $matches, function ( ProductRecord $a, ProductRecord $b ): int { $priorityA = $this->policy->priorityFor( $a )->weight(); $priorityB = $this->policy->priorityFor( $b )->weight(); if ( $priorityA !== $priorityB ) { return $priorityB <=> $priorityA; } $lastA = $a->lastSyncedAt(); $lastB = $b->lastSyncedAt(); if ( $lastA === null && $lastB !== null ) { return -1; } if ( $lastA !== null && $lastB === null ) { return 1; } if ( $lastA !== null && $lastB !== null ) { $cmp = $lastA->getTimestamp() <=> $lastB->getTimestamp(); if ( $cmp !== 0 ) { return $cmp; } } $idA = $a->id(); $idB = $b->id(); if ( $idA === null && $idB === null ) { return 0; } if ( $idA === null ) { return -1; } if ( $idB === null ) { return 1; } return $idA <=> $idB; } ); return array_slice( $matches, 0, $limit ); } }