Repository: laravel/framework (php)

Issue:
Cursor pagination fails if records have been deleted
<!-- DO NOT THROW THIS AWAY -->
<!-- Fill out the FULL versions with patch versions -->

- Laravel Version: 9.18.0
- PHP Version: 8.1.2
- Database Driver & Version: MariaDB 10.4.22

### Description:
When trying to follow a cursor returned by `cursorPaginate()`, if records have been deleted, a server error response (500) is returned.

An exception with the message "only arrays and objects are supported when cursor paginating items" is being thrown by Laravel.

This does not appear to be the expected behavior.

### Steps To Reproduce:
Simply use cursor pagination (with `cursorPaginate()`) in a query, retrieve part of the records, delete all records and try to follow the cursor.

<!-- If possible, please provide a GitHub repository to demonstrate your issue -->
<!-- laravel new bug-report --github="--public" -->


Pull request:
[8.x] Handle cursor paginator when no items are found
<!--
Please only send a pull request to branches that are currently supported: https://laravel.com/docs/releases#support-policy 

If you are unsure which branch your pull request should be sent to, please read: https://laravel.com/docs/contributions#which-branch

Pull requests without a descriptive title, thorough description, or tests will be closed.

In addition, please describe the benefit to end users; the reasons it does not break any existing features; how it makes building web applications easier, etc.
-->

Closes #42962 

As noted by issue #42962, if the user has a link to a next, or previous page from a pagination using cursor pagination, accessing this URL throws an exception when no items are found.

This PR:

- Modifies `AbstractCursorPaginator@previousCursor` and `AbstractCursorPaginator@nextCursor` methods to check if the `$items` collection is empty, and if so early returning `null` before trying to build their respective `Cursor` instances
- Add a test case that would fail without the this PR's proposed changes



Patch:
diff --git a/src/Illuminate/Pagination/AbstractCursorPaginator.php b/src/Illuminate/Pagination/AbstractCursorPaginator.php
index 1965588137..12344850b9 100644
--- a/src/Illuminate/Pagination/AbstractCursorPaginator.php
+++ b/src/Illuminate/Pagination/AbstractCursorPaginator.php
@@ -155,6 +155,10 @@ public function previousCursor()
             return null;
         }
 
+        if ($this->items->isEmpty()) {
+            return null;
+        }
+
         return $this->getCursorForItem($this->items->first(), false);
     }
 
@@ -170,6 +174,10 @@ public function nextCursor()
             return null;
         }
 
+        if ($this->items->isEmpty()) {
+            return null;
+        }
+
         return $this->getCursorForItem($this->items->last(), true);
     }
 


Test patch:
diff --git a/tests/Pagination/CursorPaginatorTest.php b/tests/Pagination/CursorPaginatorTest.php
index ad2def176f..535fb5b8aa 100644
--- a/tests/Pagination/CursorPaginatorTest.php
+++ b/tests/Pagination/CursorPaginatorTest.php
@@ -4,6 +4,7 @@
 
 use Illuminate\Pagination\Cursor;
 use Illuminate\Pagination\CursorPaginator;
+use Illuminate\Support\Collection;
 use PHPUnit\Framework\TestCase;
 
 class CursorPaginatorTest extends TestCase
@@ -76,6 +77,26 @@ public function testCanTransformPaginatorItems()
         $this->assertSame([['id' => 6], ['id' => 7]], $p->items());
     }
 
+    public function testReturnEmptyCursorWhenItemsAreEmpty()
+    {
+        $cursor = new Cursor(['id' => 25], true);
+
+        $p = new CursorPaginator(Collection::make(), 25, $cursor, [
+            'path' => 'http://website.com/test',
+            'cursorName' => 'cursor',
+            'parameters' => ['id'],
+        ]);
+
+        $this->assertInstanceOf(CursorPaginator::class, $p);
+        $this->assertSame([
+            'data' => [],
+            'path' => 'http://website.com/test',
+            'per_page' => 25,
+            'next_page_url' => null,
+            'prev_page_url' => null,
+        ], $p->toArray());
+    }
+
     protected function getCursor($params, $isNext = true)
     {
         return (new Cursor($params, $isNext))->encode();