diff --git a/composer.json b/composer.json index 2e36170..6a86090 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,6 @@ "php": "^8.1", "symfony/mime": ">=6.0", "nesbot/carbon": ">=2.0", - "illuminate/pagination": ">=9.0", "illuminate/collections": ">=9.0", "zbateson/mail-mime-parser": "^3.0", "egulias/email-validator": "^4.0" diff --git a/src/Collections/PaginatedCollection.php b/src/Collections/PaginatedCollection.php index 8a23a3f..695dc7b 100644 --- a/src/Collections/PaginatedCollection.php +++ b/src/Collections/PaginatedCollection.php @@ -2,14 +2,13 @@ namespace DirectoryTree\ImapEngine\Collections; -use Illuminate\Pagination\LengthAwarePaginator; -use Illuminate\Pagination\Paginator; +use DirectoryTree\ImapEngine\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; class PaginatedCollection extends Collection { /** - * Number of total entries. + * The total number of items. */ protected int $total = 0; @@ -18,24 +17,19 @@ class PaginatedCollection extends Collection */ public function paginate(int $perPage = 15, ?int $page = null, string $pageName = 'page', bool $prepaginated = false): LengthAwarePaginator { - $page = $page ?: Paginator::resolveCurrentPage($pageName); - $total = $this->total ?: $this->count(); - $results = ! $prepaginated && $total ? $this->forPage($page, $perPage)->toArray() : $this->all(); + $results = ! $prepaginated && $total ? $this->forPage($page, $perPage) : $this; - return $this->paginator($results, $total, $perPage, $page, [ - 'path' => Paginator::resolveCurrentPath(), - 'pageName' => $pageName, - ]); + return $this->paginator($results, $total, $perPage, $page, $pageName); } /** * Create a new length-aware paginator instance. */ - protected function paginator(array $items, int $total, int $perPage, ?int $currentPage, array $options): LengthAwarePaginator + protected function paginator(Collection $items, int $total, int $perPage, ?int $currentPage, string $pageName): LengthAwarePaginator { - return new LengthAwarePaginator($items, $total, $perPage, $currentPage, $options); + return new LengthAwarePaginator($items, $total, $perPage, $currentPage, $pageName); } /** diff --git a/src/Folder.php b/src/Folder.php index 13a2e88..a4d25e8 100644 --- a/src/Folder.php +++ b/src/Folder.php @@ -82,7 +82,10 @@ public function is(Folder $folder): bool */ public function messages(): MessageQuery { - return new MessageQuery(tap($this)->select(true), new ImapQueryBuilder); + // Ensure the folder is selected. + $this->select(true); + + return new MessageQuery($this, new ImapQueryBuilder); } /** diff --git a/src/Mailbox.php b/src/Mailbox.php index 94e524d..8b60767 100644 --- a/src/Mailbox.php +++ b/src/Mailbox.php @@ -185,9 +185,10 @@ public function inbox(): Folder */ public function folders(): FolderRepository { - return new FolderRepository( - tap($this)->connection() - ); + // Ensure the connection is established. + $this->connection(); + + return new FolderRepository($this); } /** diff --git a/src/MessageQuery.php b/src/MessageQuery.php index a9f0b7b..b001677 100644 --- a/src/MessageQuery.php +++ b/src/MessageQuery.php @@ -8,11 +8,11 @@ use DirectoryTree\ImapEngine\Connection\Responses\UntaggedResponse; use DirectoryTree\ImapEngine\Connection\Tokens\Atom; use DirectoryTree\ImapEngine\Enums\ImapFetchIdentifier; +use DirectoryTree\ImapEngine\Pagination\LengthAwarePaginator; +use DirectoryTree\ImapEngine\Support\ForwardsCalls; use DirectoryTree\ImapEngine\Support\Str; -use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; use Illuminate\Support\Traits\Conditionable; -use Illuminate\Support\Traits\ForwardsCalls; /** * @mixin \DirectoryTree\ImapEngine\Connection\ImapQueryBuilder diff --git a/src/Pagination/LengthAwarePaginator.php b/src/Pagination/LengthAwarePaginator.php new file mode 100644 index 0000000..968944f --- /dev/null +++ b/src/Pagination/LengthAwarePaginator.php @@ -0,0 +1,173 @@ +currentPage = max($currentPage, 1); + + $this->path = rtrim($path, '/'); + } + + /** + * Handle dynamic method calls on the paginator. + */ + public function __call(string $method, array $parameters): mixed + { + return $this->forwardCallTo($this->items, $method, $parameters); + } + + /** + * Get the items being paginated. + */ + public function items(): Collection + { + return $this->items; + } + + /** + * Get the total number of items. + */ + public function total(): int + { + return $this->total; + } + + /** + * Get the number of items per page. + */ + public function perPage(): int + { + return $this->perPage; + } + + /** + * Get the current page number. + */ + public function currentPage(): int + { + return $this->currentPage; + } + + /** + * Get the last page (total pages). + */ + public function lastPage(): int + { + return (int) ceil($this->total / $this->perPage); + } + + /** + * Determine if there are enough items to split into multiple pages. + */ + public function hasPages(): bool + { + return $this->total() > $this->perPage(); + } + + /** + * Determine if there is a next page. + */ + public function hasMorePages(): bool + { + return $this->currentPage() < $this->lastPage(); + } + + /** + * Generate the URL for a given page. + */ + public function url(int $page): string + { + $params = array_merge($this->query, [$this->pageName => $page]); + + $queryString = http_build_query($params); + + return $this->path.($queryString ? '?'.$queryString : ''); + } + + /** + * Get the URL for the next page, or null if none. + */ + public function nextPageUrl(): ?string + { + if ($this->hasMorePages()) { + return $this->url($this->currentPage() + 1); + } + + return null; + } + + /** + * Get the URL for the previous page, or null if none. + */ + public function previousPageUrl(): ?string + { + if ($this->currentPage() > 1) { + return $this->url($this->currentPage() - 1); + } + + return null; + } + + /** + * Convert the pagination data to an array. + */ + public function toArray(): array + { + return [ + 'path' => $this->path, + 'total' => $this->total(), + 'to' => $this->calculateTo(), + 'per_page' => $this->perPage(), + 'last_page' => $this->lastPage(), + 'first_page_url' => $this->url(1), + 'data' => $this->items()->toArray(), + 'current_page' => $this->currentPage(), + 'next_page_url' => $this->nextPageUrl(), + 'prev_page_url' => $this->previousPageUrl(), + 'last_page_url' => $this->url($this->lastPage()), + 'from' => $this->total() ? ($this->currentPage() - 1) * $this->perPage() + 1 : null, + ]; + } + + /** + * Calculate the "to" index for the current page. + */ + protected function calculateTo(): ?int + { + if (! $this->total()) { + return null; + } + + $to = $this->currentPage() * $this->perPage(); + + return min($to, $this->total()); + } + + /** + * Convert the instance to JSON. + */ + public function jsonSerialize(): array + { + return $this->toArray(); + } +} diff --git a/src/Support/ForwardsCalls.php b/src/Support/ForwardsCalls.php new file mode 100644 index 0000000..377eeec --- /dev/null +++ b/src/Support/ForwardsCalls.php @@ -0,0 +1,42 @@ +{$method}(...$parameters); + } catch (Error|BadMethodCallException $e) { + $pattern = '~^Call to undefined method (?P[^:]+)::(?P[^\(]+)\(\)$~'; + + if (! preg_match($pattern, $e->getMessage(), $matches)) { + throw $e; + } + + if ($matches['class'] != get_class($object) || + $matches['method'] != $method) { + throw $e; + } + + static::throwBadMethodCallException($method); + } + } + + /** + * Throw a bad method call exception for the given method. + */ + protected static function throwBadMethodCallException(string $method) + { + throw new BadMethodCallException(sprintf( + 'Call to undefined method %s::%s()', static::class, $method + )); + } +} diff --git a/tests/Unit/Collections/LengthAwarePaginator.php b/tests/Unit/Collections/LengthAwarePaginator.php new file mode 100644 index 0000000..bf8a9ec --- /dev/null +++ b/tests/Unit/Collections/LengthAwarePaginator.php @@ -0,0 +1,106 @@ +items())->toBe($items); +}); + +test('it calculates the total correctly', function () { + $paginator = new LengthAwarePaginator(new Collection, total: 25, perPage: 5, currentPage: 1); + + expect($paginator->total())->toBe(25); +}); + +test('it calculates the per page correctly', function () { + $paginator = new LengthAwarePaginator(new Collection, total: 30, perPage: 5, currentPage: 1); + + expect($paginator->perPage())->toBe(5); +}); + +test('it can determine the current page', function () { + $paginator = new LengthAwarePaginator(new Collection, total: 30, perPage: 5, currentPage: 2); + + expect($paginator->currentPage())->toBe(2); +}); + +test('it calculates the last page', function () { + $paginator = new LengthAwarePaginator(new Collection, total: 30, perPage: 5, currentPage: 1); + + expect($paginator->lastPage())->toBe(6); +}); + +test('it can tell if there are enough items for multiple pages', function () { + $paginator = new LengthAwarePaginator(new Collection, total: 6, perPage: 5, currentPage: 1); + + expect($paginator->hasPages())->toBeTrue(); +}); + +test('it can detect if there are more pages', function () { + $paginator = new LengthAwarePaginator(new Collection, total: 10, perPage: 5, currentPage: 1); + + expect($paginator->hasMorePages())->toBeTrue(); +}); + +test('it provides a correct next page url', function () { + $paginator = new LengthAwarePaginator(new Collection, total: 10, perPage: 5, currentPage: 1, path: 'http://example.com/users'); + + expect($paginator->nextPageUrl())->toBe('http://example.com/users?page=2'); +}); + +test('it provides a correct previous page url', function () { + $paginator = new LengthAwarePaginator(new Collection, total: 10, perPage: 5, currentPage: 2, path: 'http://example.com/users'); + + expect($paginator->previousPageUrl())->toBe('http://example.com/users?page=1'); +}); + +test('it returns null for next page url when on last page', function () { + $paginator = new LengthAwarePaginator(new Collection, total: 10, perPage: 5, currentPage: 2, path: 'http://example.com/users'); + + expect($paginator->nextPageUrl())->toBeNull(); +}); + +test('it returns null for previous page url when on first page', function () { + $paginator = new LengthAwarePaginator(new Collection, total: 10, perPage: 5, currentPage: 1, path: 'http://example.com/users'); + + expect($paginator->previousPageUrl())->toBeNull(); +}); + +test('it returns an array representation', function () { + $paginator = new LengthAwarePaginator(new Collection(['Item 1', 'Item 2']), total: 2, perPage: 2, currentPage: 1, path: 'http://example.com/users'); + + $array = $paginator->toArray(); + + expect($array)->toMatchArray([ + 'path' => 'http://example.com/users', + 'data' => ['Item 1', 'Item 2'], + 'total' => 2, + 'per_page' => 2, + 'last_page' => 1, + 'current_page' => 1, + 'from' => 1, + 'to' => 2, + 'first_page_url' => 'http://example.com/users?page=1', + 'last_page_url' => 'http://example.com/users?page=1', + 'next_page_url' => null, + 'prev_page_url' => null, + ]); +}); + +test('it preserves existing query parameters in the next page url', function () { + $paginator = new LengthAwarePaginator( + new Collection, + total: 10, + perPage: 5, + currentPage: 1, + path: 'http://example.com/users', + query: ['foo' => 'bar'] + ); + + expect($paginator->nextPageUrl())->toBe('http://example.com/users?foo=bar&page=2'); +});