$factories */ public function page(array $factories, int $requestedPage, int $size = 5, ?callable $score = null): array { $sources = []; $unavailable = []; foreach ($factories as $label => $factory) { try { $sources[$label] = $factory(); } catch (QueryException $exception) { report($exception); $unavailable[] = $label; } } // If a source fails after its count, remove it and recompute the page offsets. do { $failed = false; $total = array_sum(array_column($sources, 'count')); $last = max(1, (int) ceil($total / $size)); $page = min(max(1, $requestedPage), $last); $first = ($page - 1) * $size; $items = []; $positions = array_fill_keys(array_keys($sources), 0); $buffers = array_fill_keys(array_keys($sources), []); for ($position = 0; $position < min($total, $first + $size); $position++) { $best = null; $bestScore = PHP_INT_MIN; foreach ($sources as $label => $source) { if ($buffers[$label] === [] && $positions[$label] < $source['count']) { try { $batch = iterator_to_array((function () use ($source, $positions, $label, $size) { yield from ($source['fetch'])($positions[$label], $size); })(), false); $positions[$label] += count($batch); $buffers[$label] = $batch; if ($batch === []) { $positions[$label] = $source['count']; } } catch (QueryException $exception) { report($exception); $unavailable[] = $label; unset($sources[$label]); $failed = true; break; } } if ($buffers[$label] === []) { continue; } $candidateScore = $score ? $score($buffers[$label][0]) : 0; if ($best === null || $candidateScore > $bestScore) { $best = $label; $bestScore = $candidateScore; } } if ($failed || $best === null) { break; } $item = array_shift($buffers[$best]); if ($position >= $first) { $items[] = [...$item, 'category' => $best]; } } } while ($failed); return ['data' => $items, 'total' => $total, 'current_page' => $page, 'last_page' => $last, 'per_page' => $size, 'unavailable' => $unavailable]; } }