diff --git a/src/Query/IteratorBuilder.php b/src/Query/IteratorBuilder.php index e016c92b86d..d888010b9e9 100644 --- a/src/Query/IteratorBuilder.php +++ b/src/Query/IteratorBuilder.php @@ -7,20 +7,33 @@ abstract class IteratorBuilder extends Builder { protected $randomize = false; + private $itemsBeingPaginated; public function count() { return $this->getFilteredAndLimitedItems()->count(); } + public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) + { + // The total and the page are both taken from the same items, so they only need working out once. + $this->itemsBeingPaginated = $this->getFilteredItems(); + + try { + return parent::paginate($perPage, $columns, $pageName, $page); + } finally { + $this->itemsBeingPaginated = null; + } + } + protected function getCountForPagination() { - return $this->getFilteredItems()->count(); + return ($this->itemsBeingPaginated ?? $this->getFilteredItems())->count(); } public function get($columns = ['*']) { - $items = $this->getFilteredItems(); + $items = $this->itemsBeingPaginated ?? $this->getFilteredItems(); if ($this->randomize) { $items = $items->shuffle(); @@ -108,9 +121,16 @@ protected function intersectFromWhereClause($entries, $filteredEntries, $where) private function intersectItems($entries, $filteredEntries) { - return $entries->filter(function ($entry) use ($filteredEntries) { - return $filteredEntries->contains($entry); - }); + if ($filteredEntries->contains(fn ($entry) => ! is_object($entry))) { + return $entries->filter(fn ($entry) => $filteredEntries->contains($entry)); + } + + // Both are taken from the same items, so an object can be looked up rather than compared with each of the others. + $filtered = $filteredEntries->mapWithKeys(fn ($entry) => [spl_object_id($entry) => true])->all(); + + return $entries->filter(fn ($entry) => is_object($entry) + ? isset($filtered[spl_object_id($entry)]) + : $filteredEntries->contains($entry)); } protected function filterWhereIn($entries, $where) diff --git a/tests/Search/QueryBuilderTest.php b/tests/Search/QueryBuilderTest.php index 0ed759281fb..5bd52d657e0 100644 --- a/tests/Search/QueryBuilderTest.php +++ b/tests/Search/QueryBuilderTest.php @@ -732,10 +732,37 @@ public function results_are_found_using_where_status() $this->assertCount(1, $results); $this->assertEquals(['b'], $results->map->reference->all()); } + + #[Test] + public function paginating_works_the_results_out_once() + { + $items = collect(range(1, 25))->map(fn ($i) => ['reference' => 'item'.$i, 'status' => $i % 5 ? 'published' : 'draft', 'site' => $i % 2 ? 'en' : 'fr']); + + $query = (new FakeQueryBuilder($items))->withoutData()->where('status', 'published')->where('site', 'en'); + + $paginator = $query->paginate(4, ['*'], 'page', 2); + + $this->assertSame(1, $query->searches); + $this->assertSame(10, $paginator->total()); + $this->assertEquals(['item11', 'item13', 'item17', 'item19'], collect($paginator->items())->map->reference->all()); + } + + #[Test] + public function a_query_can_be_used_again_after_paginating() + { + $items = collect(range(1, 6))->map(fn ($i) => ['reference' => 'item'.$i, 'site' => $i % 2 ? 'en' : 'fr']); + + $query = (new FakeQueryBuilder($items))->withoutData()->where('site', 'en'); + + $this->assertSame(3, $query->paginate(2)->total()); + $this->assertEquals(['item3'], $query->where('reference', 'item3')->get()->map->reference->all()); + $this->assertSame(1, $query->paginate(2)->total()); + } } class FakeQueryBuilder extends QueryBuilder { + public $searches = 0; protected $results; public function __construct($results) @@ -746,6 +773,8 @@ public function __construct($results) public function getSearchResults($query) { + $this->searches++; + return $this->results; } }