diff --git a/src/Database/Pagination.php b/src/Database/Pagination.php index 99f2c8f5..beca8b24 100644 --- a/src/Database/Pagination.php +++ b/src/Database/Pagination.php @@ -2,11 +2,36 @@ namespace Bow\Database; +use ArrayAccess; use Bow\Database\Collection as DatabaseCollection; use Bow\Support\Collection as SupportCollection; +use Countable; +use IteratorAggregate; +use Traversable; -class Pagination +class Pagination implements ArrayAccess, Countable, IteratorAggregate { + /** + * The base URL for pagination links. + * + * @var string|null + */ + private ?string $baseUrl = null; + + /** + * The query string parameters to append to pagination URLs. + * + * @var array + */ + private array $queryParams = []; + + /** + * The page query parameter name. + * + * @var string + */ + private string $pageParam = 'page'; + /** * Pagination constructor. * @@ -27,6 +52,88 @@ public function __construct( ) { } + /** + * Set the base URL for pagination links. + * + * @param string $url + * @return static + */ + public function setBaseUrl(string $url): static + { + $this->baseUrl = $url; + + return $this; + } + + /** + * Get the base URL for pagination links. + * + * @return string|null + */ + public function getBaseUrl(): ?string + { + return $this->baseUrl; + } + + /** + * Set the page query parameter name. + * + * @param string $name + * @return static + */ + public function setPageParam(string $name): static + { + $this->pageParam = $name; + + return $this; + } + + /** + * Get the page query parameter name. + * + * @return string + */ + public function getPageParam(): string + { + return $this->pageParam; + } + + /** + * Add query parameters to the pagination URLs. + * + * @param array $params + * @return static + */ + public function withQueryParams(array $params): static + { + $this->queryParams = array_merge($this->queryParams, $params); + + return $this; + } + + /** + * Set query parameters for the pagination URLs (replaces existing). + * + * @param array $params + * @return static + */ + public function setQueryParams(array $params): static + { + $this->queryParams = $params; + + return $this; + } + + /** + * Get the query parameters. + * + * @return array + */ + public function getQueryParams(): array + { + return $this->queryParams; + } + /** * Get the next page number. * @@ -106,4 +213,367 @@ public function total(): int { return $this->total; } + + /** + * Get the total number of pages. + * + * @return int + */ + public function totalPages(): int + { + return (int) ceil($this->total / $this->perPage); + } + + /** + * Check if there are multiple pages. + * + * @return bool + */ + public function hasPages(): bool + { + return $this->totalPages() > 1; + } + + /** + * Check if currently on the first page. + * + * @return bool + */ + public function onFirstPage(): bool + { + return $this->current === 1; + } + + /** + * Check if currently on the last page. + * + * @return bool + */ + public function onLastPage(): bool + { + return $this->current === $this->totalPages(); + } + + /** + * Check if the pagination has no items. + * + * @return bool + */ + public function isEmpty(): bool + { + return $this->data->isEmpty(); + } + + /** + * Check if the pagination has items. + * + * @return bool + */ + public function isNotEmpty(): bool + { + return !$this->isEmpty(); + } + + /** + * Get the number of items on the current page. + * + * @return int + */ + public function count(): int + { + return $this->data->count(); + } + + /** + * Get the "index" of the first item being paginated (1-indexed). + * + * @return int + */ + public function firstItem(): int + { + if ($this->total === 0) { + return 0; + } + + return ($this->current - 1) * $this->perPage + 1; + } + + /** + * Get the "index" of the last item being paginated (1-indexed). + * + * @return int + */ + public function lastItem(): int + { + if ($this->total === 0) { + return 0; + } + + return $this->firstItem() + $this->count() - 1; + } + + /** + * Get the pagination data as an array. + * + * @return array + */ + public function toArray(): array + { + return [ + 'current_page' => $this->current, + 'data' => $this->data->toArray(), + 'first_item' => $this->firstItem(), + 'last_item' => $this->lastItem(), + 'per_page' => $this->perPage, + 'total' => $this->total, + 'total_pages' => $this->totalPages(), + 'next_page' => $this->hasNext() ? $this->next : null, + 'previous_page' => $this->hasPrevious() ? $this->previous : null, + ]; + } + + /** + * Convert the pagination to JSON. + * + * @param int $options + * @return string + */ + public function toJson(int $options = 0): string + { + return json_encode($this->toArray(), $options); + } + + /** + * Build a URL for a specific page number. + * + * @param int $page + * @return string|null + */ + public function url(int $page): ?string + { + if ($this->baseUrl === null) { + return null; + } + + if ($page < 1 || $page > $this->totalPages()) { + return null; + } + + $params = array_merge($this->queryParams, [$this->pageParam => $page]); + + $query = http_build_query($params, '', '&', PHP_QUERY_RFC3986); + + $separator = str_contains($this->baseUrl, '?') ? '&' : '?'; + + return $this->baseUrl . $separator . $query; + } + + /** + * Get the URL for the next page. + * + * @return string|null + */ + public function nextPageUrl(): ?string + { + if (!$this->hasNext()) { + return null; + } + + return $this->url($this->next); + } + + /** + * Get the URL for the previous page. + * + * @return string|null + */ + public function previousPageUrl(): ?string + { + if (!$this->hasPrevious()) { + return null; + } + + return $this->url($this->previous); + } + + /** + * Get the URL for the first page. + * + * @return string|null + */ + public function firstPageUrl(): ?string + { + return $this->url(1); + } + + /** + * Get the URL for the last page. + * + * @return string|null + */ + public function lastPageUrl(): ?string + { + return $this->url($this->totalPages()); + } + + /** + * Get an array of URLs for a range of pages. + * + * @param int $onEachSide Number of links on each side of current page + * @return array + */ + public function getUrlRange(int $onEachSide = 3): array + { + $totalPages = $this->totalPages(); + $current = $this->current; + + $start = max(1, $current - $onEachSide); + $end = min($totalPages, $current + $onEachSide); + + $urls = []; + for ($page = $start; $page <= $end; $page++) { + $urls[$page] = $this->url($page); + } + + return $urls; + } + + /** + * Get pagination links data for rendering. + * + * @param int $onEachSide Number of links on each side of current page + * @return array + */ + public function links(int $onEachSide = 3): array + { + $totalPages = $this->totalPages(); + $current = $this->current; + + $links = []; + + // Previous link + $links[] = [ + 'url' => $this->previousPageUrl(), + 'label' => '« Previous', + 'active' => false, + 'disabled' => !$this->hasPrevious(), + ]; + + // Page number links + $start = max(1, $current - $onEachSide); + $end = min($totalPages, $current + $onEachSide); + + // Add first page and ellipsis if needed + if ($start > 1) { + $links[] = [ + 'url' => $this->url(1), + 'label' => '1', + 'active' => false, + 'disabled' => false, + ]; + + if ($start > 2) { + $links[] = [ + 'url' => null, + 'label' => '...', + 'active' => false, + 'disabled' => true, + ]; + } + } + + // Add page links + for ($page = $start; $page <= $end; $page++) { + $links[] = [ + 'url' => $this->url($page), + 'label' => (string) $page, + 'active' => $page === $current, + 'disabled' => false, + ]; + } + + // Add ellipsis and last page if needed + if ($end < $totalPages) { + if ($end < $totalPages - 1) { + $links[] = [ + 'url' => null, + 'label' => '...', + 'active' => false, + 'disabled' => true, + ]; + } + + $links[] = [ + 'url' => $this->url($totalPages), + 'label' => (string) $totalPages, + 'active' => false, + 'disabled' => false, + ]; + } + + // Next link + $links[] = [ + 'url' => $this->nextPageUrl(), + 'label' => 'Next »', + 'active' => false, + 'disabled' => !$this->hasNext(), + ]; + + return $links; + } + + /** + * Determine if an item exists at an offset. + * + * @param mixed $offset + * @return bool + */ + public function offsetExists(mixed $offset): bool + { + return $this->data->offsetExists($offset); + } + + /** + * Get an item at a given offset. + * + * @param mixed $offset + * @return mixed + */ + public function offsetGet(mixed $offset): mixed + { + return $this->data->offsetGet($offset); + } + + /** + * Set the item at a given offset. + * + * @param mixed $offset + * @param mixed $value + * @return void + */ + public function offsetSet(mixed $offset, mixed $value): void + { + $this->data->offsetSet($offset, $value); + } + + /** + * Unset the item at a given offset. + * + * @param mixed $offset + * @return void + */ + public function offsetUnset(mixed $offset): void + { + $this->data->offsetUnset($offset); + } + + /** + * Get an iterator for the items. + * + * @return Traversable + */ + public function getIterator(): Traversable + { + return $this->data->getIterator(); + } } diff --git a/tests/Database/PaginationTest.php b/tests/Database/PaginationTest.php index 524c2fee..874c1daa 100644 --- a/tests/Database/PaginationTest.php +++ b/tests/Database/PaginationTest.php @@ -364,4 +364,782 @@ public static function navigationHelpersProvider(): array 'no previous - previous is 0' => [false, 0], ]; } + + public function test_total_pages(): void + { + $pagination = $this->createPagination( + next: 2, + previous: 0, + total: 100, + perPage: 10, + current: 1 + ); + + $this->assertSame(10, $pagination->totalPages()); + } + + public function test_total_pages_with_remainder(): void + { + $pagination = $this->createPagination( + next: 2, + previous: 0, + total: 95, + perPage: 10, + current: 1 + ); + + $this->assertSame(10, $pagination->totalPages()); + } + + public function test_has_pages_returns_true_when_multiple_pages(): void + { + $pagination = $this->createPagination( + next: 2, + previous: 0, + total: 100, + perPage: 10, + current: 1 + ); + + $this->assertTrue($pagination->hasPages()); + } + + public function test_has_pages_returns_false_when_single_page(): void + { + $pagination = $this->createPagination( + next: 0, + previous: 0, + total: 5, + perPage: 10, + current: 1 + ); + + $this->assertFalse($pagination->hasPages()); + } + + public function test_on_first_page(): void + { + $pagination = $this->createPagination( + next: 2, + previous: 0, + total: 100, + perPage: 10, + current: 1 + ); + + $this->assertTrue($pagination->onFirstPage()); + } + + public function test_not_on_first_page(): void + { + $pagination = $this->createPagination( + next: 3, + previous: 1, + total: 100, + perPage: 10, + current: 2 + ); + + $this->assertFalse($pagination->onFirstPage()); + } + + public function test_on_last_page(): void + { + $pagination = $this->createPagination( + next: 0, + previous: 9, + total: 100, + perPage: 10, + current: 10 + ); + + $this->assertTrue($pagination->onLastPage()); + } + + public function test_not_on_last_page(): void + { + $pagination = $this->createPagination( + next: 2, + previous: 0, + total: 100, + perPage: 10, + current: 1 + ); + + $this->assertFalse($pagination->onLastPage()); + } + + public function test_is_empty(): void + { + $pagination = new Pagination( + next: 0, + previous: 0, + total: 0, + perPage: 10, + current: 1, + data: collect([]) + ); + + $this->assertTrue($pagination->isEmpty()); + } + + public function test_is_not_empty(): void + { + $pagination = $this->createPagination( + next: 2, + previous: 0, + total: 100, + perPage: 10, + current: 1 + ); + + $this->assertTrue($pagination->isNotEmpty()); + } + + public function test_count(): void + { + $pagination = $this->createPagination( + next: 2, + previous: 0, + total: 100, + perPage: 10, + current: 1, + itemCount: 10 + ); + + $this->assertSame(10, $pagination->count()); + } + + public function test_first_item(): void + { + $pagination = $this->createPagination( + next: 3, + previous: 1, + total: 100, + perPage: 10, + current: 2, + itemCount: 10 + ); + + $this->assertSame(11, $pagination->firstItem()); + } + + public function test_first_item_on_first_page(): void + { + $pagination = $this->createPagination( + next: 2, + previous: 0, + total: 100, + perPage: 10, + current: 1, + itemCount: 10 + ); + + $this->assertSame(1, $pagination->firstItem()); + } + + public function test_first_item_with_empty_results(): void + { + $pagination = new Pagination( + next: 0, + previous: 0, + total: 0, + perPage: 10, + current: 1, + data: collect([]) + ); + + $this->assertSame(0, $pagination->firstItem()); + } + + public function test_last_item(): void + { + $pagination = $this->createPagination( + next: 3, + previous: 1, + total: 100, + perPage: 10, + current: 2, + itemCount: 10 + ); + + $this->assertSame(20, $pagination->lastItem()); + } + + public function test_last_item_on_last_page_with_remainder(): void + { + $pagination = $this->createPagination( + next: 0, + previous: 9, + total: 95, + perPage: 10, + current: 10, + itemCount: 5 + ); + + $this->assertSame(95, $pagination->lastItem()); + } + + public function test_last_item_with_empty_results(): void + { + $pagination = new Pagination( + next: 0, + previous: 0, + total: 0, + perPage: 10, + current: 1, + data: collect([]) + ); + + $this->assertSame(0, $pagination->lastItem()); + } + + public function test_to_array(): void + { + $pagination = $this->createPagination( + next: 2, + previous: 0, + total: 30, + perPage: 10, + current: 1, + itemCount: 10 + ); + + $array = $pagination->toArray(); + + $this->assertIsArray($array); + $this->assertSame(1, $array['current_page']); + $this->assertSame(10, $array['per_page']); + $this->assertSame(30, $array['total']); + $this->assertSame(3, $array['total_pages']); + $this->assertSame(1, $array['first_item']); + $this->assertSame(10, $array['last_item']); + $this->assertSame(2, $array['next_page']); + $this->assertNull($array['previous_page']); + $this->assertIsArray($array['data']); + } + + public function test_to_json(): void + { + $pagination = $this->createPagination( + next: 2, + previous: 0, + total: 30, + perPage: 10, + current: 1, + itemCount: 3 + ); + + $json = $pagination->toJson(); + + $this->assertJson($json); + $decoded = json_decode($json, true); + $this->assertSame(1, $decoded['current_page']); + $this->assertSame(30, $decoded['total']); + } + + // ===== URL Support Tests ===== + + public function test_set_and_get_base_url(): void + { + $pagination = $this->createPagination(2, 0, 100, 10, 1); + + $pagination->setBaseUrl('https://example.com/items'); + + $this->assertSame('https://example.com/items', $pagination->getBaseUrl()); + } + + public function test_set_base_url_returns_self(): void + { + $pagination = $this->createPagination(2, 0, 100, 10, 1); + + $result = $pagination->setBaseUrl('https://example.com/items'); + + $this->assertSame($pagination, $result); + } + + public function test_set_and_get_page_param(): void + { + $pagination = $this->createPagination(2, 0, 100, 10, 1); + + $pagination->setPageParam('p'); + + $this->assertSame('p', $pagination->getPageParam()); + } + + public function test_default_page_param(): void + { + $pagination = $this->createPagination(2, 0, 100, 10, 1); + + $this->assertSame('page', $pagination->getPageParam()); + } + + public function test_with_query_params(): void + { + $pagination = $this->createPagination(2, 0, 100, 10, 1); + + $pagination->withQueryParams(['sort' => 'name']); + $pagination->withQueryParams(['order' => 'asc']); + + $params = $pagination->getQueryParams(); + $this->assertSame(['sort' => 'name', 'order' => 'asc'], $params); + } + + public function test_set_query_params_replaces_existing(): void + { + $pagination = $this->createPagination(2, 0, 100, 10, 1); + + $pagination->withQueryParams(['sort' => 'name']); + $pagination->setQueryParams(['filter' => 'active']); + + $params = $pagination->getQueryParams(); + $this->assertSame(['filter' => 'active'], $params); + } + + public function test_url_returns_null_without_base_url(): void + { + $pagination = $this->createPagination(2, 0, 100, 10, 1); + + $this->assertNull($pagination->url(1)); + } + + public function test_url_builds_correct_url(): void + { + $pagination = $this->createPagination(2, 0, 100, 10, 1); + $pagination->setBaseUrl('https://example.com/items'); + + $url = $pagination->url(2); + + $this->assertSame('https://example.com/items?page=2', $url); + } + + public function test_url_with_custom_page_param(): void + { + $pagination = $this->createPagination(2, 0, 100, 10, 1); + $pagination->setBaseUrl('https://example.com/items'); + $pagination->setPageParam('p'); + + $url = $pagination->url(2); + + $this->assertSame('https://example.com/items?p=2', $url); + } + + public function test_url_with_query_params(): void + { + $pagination = $this->createPagination(2, 0, 100, 10, 1); + $pagination->setBaseUrl('https://example.com/items'); + $pagination->withQueryParams(['sort' => 'name', 'order' => 'asc']); + + $url = $pagination->url(2); + + $this->assertStringContainsString('page=2', $url); + $this->assertStringContainsString('sort=name', $url); + $this->assertStringContainsString('order=asc', $url); + } + + public function test_url_with_existing_query_string(): void + { + $pagination = $this->createPagination(2, 0, 100, 10, 1); + $pagination->setBaseUrl('https://example.com/items?filter=active'); + + $url = $pagination->url(2); + + $this->assertSame('https://example.com/items?filter=active&page=2', $url); + } + + public function test_url_returns_null_for_invalid_page(): void + { + $pagination = $this->createPagination(2, 0, 100, 10, 1); + $pagination->setBaseUrl('https://example.com/items'); + + $this->assertNull($pagination->url(0)); + $this->assertNull($pagination->url(-1)); + $this->assertNull($pagination->url(11)); // total pages is 10 + } + + public function test_next_page_url(): void + { + $pagination = $this->createPagination(2, 0, 100, 10, 1); + $pagination->setBaseUrl('https://example.com/items'); + + $url = $pagination->nextPageUrl(); + + $this->assertSame('https://example.com/items?page=2', $url); + } + + public function test_next_page_url_returns_null_on_last_page(): void + { + $pagination = $this->createPagination(0, 9, 100, 10, 10); + $pagination->setBaseUrl('https://example.com/items'); + + $this->assertNull($pagination->nextPageUrl()); + } + + public function test_previous_page_url(): void + { + $pagination = $this->createPagination(3, 1, 100, 10, 2); + $pagination->setBaseUrl('https://example.com/items'); + + $url = $pagination->previousPageUrl(); + + $this->assertSame('https://example.com/items?page=1', $url); + } + + public function test_previous_page_url_returns_null_on_first_page(): void + { + $pagination = $this->createPagination(2, 0, 100, 10, 1); + $pagination->setBaseUrl('https://example.com/items'); + + $this->assertNull($pagination->previousPageUrl()); + } + + public function test_first_page_url(): void + { + $pagination = $this->createPagination(3, 1, 100, 10, 2); + $pagination->setBaseUrl('https://example.com/items'); + + $url = $pagination->firstPageUrl(); + + $this->assertSame('https://example.com/items?page=1', $url); + } + + public function test_last_page_url(): void + { + $pagination = $this->createPagination(2, 0, 100, 10, 1); + $pagination->setBaseUrl('https://example.com/items'); + + $url = $pagination->lastPageUrl(); + + $this->assertSame('https://example.com/items?page=10', $url); + } + + public function test_get_url_range(): void + { + $pagination = $this->createPagination(6, 4, 100, 10, 5); + $pagination->setBaseUrl('https://example.com/items'); + + $urls = $pagination->getUrlRange(2); + + $this->assertCount(5, $urls); + $this->assertArrayHasKey(3, $urls); + $this->assertArrayHasKey(4, $urls); + $this->assertArrayHasKey(5, $urls); + $this->assertArrayHasKey(6, $urls); + $this->assertArrayHasKey(7, $urls); + $this->assertSame('https://example.com/items?page=5', $urls[5]); + } + + public function test_get_url_range_at_start(): void + { + $pagination = $this->createPagination(2, 0, 100, 10, 1); + $pagination->setBaseUrl('https://example.com/items'); + + $urls = $pagination->getUrlRange(3); + + $this->assertArrayHasKey(1, $urls); + $this->assertArrayHasKey(2, $urls); + $this->assertArrayHasKey(3, $urls); + $this->assertArrayHasKey(4, $urls); + $this->assertArrayNotHasKey(0, $urls); + } + + public function test_get_url_range_at_end(): void + { + $pagination = $this->createPagination(0, 9, 100, 10, 10); + $pagination->setBaseUrl('https://example.com/items'); + + $urls = $pagination->getUrlRange(3); + + $this->assertArrayHasKey(7, $urls); + $this->assertArrayHasKey(8, $urls); + $this->assertArrayHasKey(9, $urls); + $this->assertArrayHasKey(10, $urls); + $this->assertArrayNotHasKey(11, $urls); + } + + public function test_links(): void + { + $pagination = $this->createPagination(6, 4, 100, 10, 5); + $pagination->setBaseUrl('https://example.com/items'); + + $links = $pagination->links(2); + + $this->assertIsArray($links); + + // First link should be "Previous" + $this->assertSame('« Previous', $links[0]['label']); + $this->assertFalse($links[0]['disabled']); + + // Last link should be "Next" + $lastIndex = count($links) - 1; + $this->assertSame('Next »', $links[$lastIndex]['label']); + $this->assertFalse($links[$lastIndex]['disabled']); + } + + public function test_links_with_current_page_marked_active(): void + { + $pagination = $this->createPagination(6, 4, 100, 10, 5); + $pagination->setBaseUrl('https://example.com/items'); + + $links = $pagination->links(2); + + $activePage = array_filter($links, fn($link) => $link['active'] === true); + $this->assertCount(1, $activePage); + $activeLink = array_values($activePage)[0]; + $this->assertSame('5', $activeLink['label']); + } + + public function test_links_on_first_page_has_disabled_previous(): void + { + $pagination = $this->createPagination(2, 0, 100, 10, 1); + $pagination->setBaseUrl('https://example.com/items'); + + $links = $pagination->links(2); + + $this->assertTrue($links[0]['disabled']); + $this->assertNull($links[0]['url']); + } + + public function test_links_on_last_page_has_disabled_next(): void + { + $pagination = $this->createPagination(0, 9, 100, 10, 10); + $pagination->setBaseUrl('https://example.com/items'); + + $links = $pagination->links(2); + + $lastIndex = count($links) - 1; + $this->assertTrue($links[$lastIndex]['disabled']); + $this->assertNull($links[$lastIndex]['url']); + } + + public function test_links_includes_ellipsis_when_needed(): void + { + $pagination = $this->createPagination(6, 4, 100, 10, 5); + $pagination->setBaseUrl('https://example.com/items'); + + $links = $pagination->links(1); + + $ellipsisLinks = array_filter($links, fn($link) => $link['label'] === '...'); + $this->assertGreaterThan(0, count($ellipsisLinks)); + } + + public function test_links_includes_first_and_last_page(): void + { + $pagination = $this->createPagination(6, 4, 100, 10, 5); + $pagination->setBaseUrl('https://example.com/items'); + + $links = $pagination->links(1); + + $labels = array_column($links, 'label'); + $this->assertContains('1', $labels); + $this->assertContains('10', $labels); + } + + // ===== ArrayAccess Tests ===== + + public function test_offset_exists_returns_true_for_existing_key(): void + { + $items = ['a' => 'apple', 'b' => 'banana']; + $pagination = new Pagination( + next: 0, + previous: 0, + total: 2, + perPage: 10, + current: 1, + data: collect($items) + ); + + $this->assertTrue(isset($pagination['a'])); + $this->assertTrue(isset($pagination['b'])); + } + + public function test_offset_exists_returns_false_for_non_existing_key(): void + { + $items = ['a' => 'apple']; + $pagination = new Pagination( + next: 0, + previous: 0, + total: 1, + perPage: 10, + current: 1, + data: collect($items) + ); + + $this->assertFalse(isset($pagination['z'])); + } + + public function test_offset_get_returns_value(): void + { + $items = ['first', 'second', 'third']; + $pagination = new Pagination( + next: 0, + previous: 0, + total: 3, + perPage: 10, + current: 1, + data: collect($items) + ); + + $this->assertSame('first', $pagination[0]); + $this->assertSame('second', $pagination[1]); + $this->assertSame('third', $pagination[2]); + } + + public function test_offset_get_with_associative_keys(): void + { + $items = ['name' => 'John', 'age' => 30]; + $pagination = new Pagination( + next: 0, + previous: 0, + total: 2, + perPage: 10, + current: 1, + data: collect($items) + ); + + $this->assertSame('John', $pagination['name']); + $this->assertSame(30, $pagination['age']); + } + + public function test_offset_set_modifies_value(): void + { + $items = ['a' => 'apple']; + $pagination = new Pagination( + next: 0, + previous: 0, + total: 1, + perPage: 10, + current: 1, + data: collect($items) + ); + + $pagination['a'] = 'avocado'; + + $this->assertSame('avocado', $pagination['a']); + } + + public function test_offset_set_adds_new_value(): void + { + $items = ['a' => 'apple']; + $pagination = new Pagination( + next: 0, + previous: 0, + total: 1, + perPage: 10, + current: 1, + data: collect($items) + ); + + $pagination['b'] = 'banana'; + + $this->assertSame('banana', $pagination['b']); + } + + public function test_offset_unset_removes_value(): void + { + $items = ['a' => 'apple', 'b' => 'banana']; + $pagination = new Pagination( + next: 0, + previous: 0, + total: 2, + perPage: 10, + current: 1, + data: collect($items) + ); + + unset($pagination['a']); + + $this->assertFalse(isset($pagination['a'])); + $this->assertTrue(isset($pagination['b'])); + } + + // ===== IteratorAggregate Tests ===== + + public function test_pagination_is_iterable(): void + { + $items = ['first', 'second', 'third']; + $pagination = new Pagination( + next: 0, + previous: 0, + total: 3, + perPage: 10, + current: 1, + data: collect($items) + ); + + $result = []; + foreach ($pagination as $key => $value) { + $result[$key] = $value; + } + + $this->assertSame([0 => 'first', 1 => 'second', 2 => 'third'], $result); + } + + public function test_pagination_iteration_with_associative_array(): void + { + $items = ['a' => 'apple', 'b' => 'banana', 'c' => 'cherry']; + $pagination = new Pagination( + next: 0, + previous: 0, + total: 3, + perPage: 10, + current: 1, + data: collect($items) + ); + + $result = []; + foreach ($pagination as $key => $value) { + $result[$key] = $value; + } + + $this->assertSame($items, $result); + } + + public function test_pagination_can_be_used_with_iterator_functions(): void + { + $items = [1, 2, 3, 4, 5]; + $pagination = new Pagination( + next: 0, + previous: 0, + total: 5, + perPage: 10, + current: 1, + data: collect($items) + ); + + $sum = 0; + foreach ($pagination as $item) { + $sum += $item; + } + + $this->assertSame(15, $sum); + } + + public function test_count_function_works_on_pagination(): void + { + $pagination = $this->createPagination( + next: 2, + previous: 0, + total: 100, + perPage: 10, + current: 1, + itemCount: 10 + ); + + $this->assertCount(10, $pagination); + } + + public function test_count_with_empty_pagination(): void + { + $pagination = new Pagination( + next: 0, + previous: 0, + total: 0, + perPage: 10, + current: 1, + data: collect([]) + ); + + $this->assertCount(0, $pagination); + } }