Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
64 / 64
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
PaginationMetadata
100.00% covered (success)
100.00%
64 / 64
100.00% covered (success)
100.00%
3 / 3
17
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 generateLinks
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
1 / 1
13
 toArray
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * Pagination Metadata Generator
5 *
6 * This Source Code Form is subject to the terms of the Mozilla Public License,
7 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 * obtain one at https://mozilla.org/MPL/2.0/.
9 *
10 * @package   phpMyFAQ
11 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
12 * @copyright 2026 phpMyFAQ Team
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @since     2026-01-11
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Api\Pagination;
21
22/**
23 * Class PaginationMetadata
24 *
25 * Generates pagination metadata for API responses including page counts, links, and status flags.
26 */
27class PaginationMetadata
28{
29    private int $total;
30
31    private int $count;
32
33    private int $perPage;
34
35    private int $currentPage;
36
37    private int $totalPages;
38
39    private int $offset;
40
41    private bool $hasMore;
42
43    private bool $hasPrevious;
44
45    private array $links;
46
47    /**
48     * Constructor
49     *
50     * @param int $total Total number of items across all pages
51     * @param PaginationRequest $request The pagination request
52     * @param string $baseUrl Base URL for generating pagination links
53     * @param int $actualCount Actual count of items in the current response
54     */
55    public function __construct(int $total, PaginationRequest $request, string $baseUrl, int $actualCount = 0)
56    {
57        $this->total = max(0, $total);
58        $this->perPage = $request->perPage;
59        $this->currentPage = $request->page;
60        $this->offset = $request->offset;
61        $this->count = $actualCount > 0 ? $actualCount : min($this->perPage, max(0, $this->total - $this->offset));
62
63        // Calculate total pages (minimum 1 even for empty results)
64        $this->totalPages = $this->perPage > 0 ? max(1, (int) ceil($this->total / $this->perPage)) : 1;
65
66        // Calculate navigation flags
67        $this->hasMore = $this->currentPage < $this->totalPages;
68        $this->hasPrevious = $this->currentPage > 1;
69
70        // Generate pagination links
71        $this->links = $this->generateLinks($baseUrl, $request);
72    }
73
74    /**
75     * Generates pagination links (first, last, prev, next)
76     *
77     * @param string $baseUrl Base URL for links
78     * @param PaginationRequest $request The pagination request
79     * @return array Links array
80     */
81    private function generateLinks(string $baseUrl, PaginationRequest $request): array
82    {
83        // Parse existing query parameters from base URL
84        $urlParts = parse_url($baseUrl);
85        $basePath = $urlParts['path'] ?? '';
86        $queryParams = [];
87
88        if (is_array($urlParts) && array_key_exists('query', $urlParts)) {
89            parse_str($urlParts['query'], $queryParams);
90        }
91
92        // Remove pagination parameters to rebuild them
93        unset($queryParams['page'], $queryParams['per_page'], $queryParams['limit'], $queryParams['offset']);
94
95        $links = [
96            'first' => null,
97            'last' => null,
98            'prev' => null,
99            'next' => null,
100        ];
101
102        // Determine pagination style for links
103        if ($request->isPageBased) {
104            // Page-based links
105            $queryParams['per_page'] = $this->perPage;
106
107            // First page link
108            if ($this->totalPages > 0) {
109                $firstParams = array_merge($queryParams, ['page' => 1]);
110                $links['first'] = $basePath . '?' . http_build_query($firstParams);
111            }
112
113            // Last page link
114            if ($this->totalPages > 0) {
115                $lastParams = array_merge($queryParams, ['page' => $this->totalPages]);
116                $links['last'] = $basePath . '?' . http_build_query($lastParams);
117            }
118
119            // Previous page link
120            if ($this->hasPrevious) {
121                $prevParams = array_merge($queryParams, ['page' => $this->currentPage - 1]);
122                $links['prev'] = $basePath . '?' . http_build_query($prevParams);
123            }
124
125            // Next page link
126            if ($this->hasMore) {
127                $nextParams = array_merge($queryParams, ['page' => $this->currentPage + 1]);
128                $links['next'] = $basePath . '?' . http_build_query($nextParams);
129            }
130        }
131
132        if (!$request->isPageBased) {
133            // Offset-based links
134            $queryParams['limit'] = $this->perPage;
135
136            // First page link
137            if ($this->totalPages > 0) {
138                $firstParams = array_merge($queryParams, ['offset' => 0]);
139                $links['first'] = $basePath . '?' . http_build_query($firstParams);
140            }
141
142            // Last page link
143            if ($this->totalPages > 0) {
144                $lastOffset = max(0, ($this->totalPages - 1) * $this->perPage);
145                $lastParams = array_merge($queryParams, ['offset' => $lastOffset]);
146                $links['last'] = $basePath . '?' . http_build_query($lastParams);
147            }
148
149            // Previous page link
150            if ($this->hasPrevious) {
151                $prevOffset = max(0, $this->offset - $this->perPage);
152                $prevParams = array_merge($queryParams, ['offset' => $prevOffset]);
153                $links['prev'] = $basePath . '?' . http_build_query($prevParams);
154            }
155
156            // Next page link
157            if ($this->hasMore) {
158                $nextOffset = $this->offset + $this->perPage;
159                $nextParams = array_merge($queryParams, ['offset' => $nextOffset]);
160                $links['next'] = $basePath . '?' . http_build_query($nextParams);
161            }
162        }
163
164        return $links;
165    }
166
167    /**
168     * Converts metadata to array format for API response
169     *
170     * @return array
171     */
172    public function toArray(): array
173    {
174        return [
175            'total' => $this->total,
176            'count' => $this->count,
177            'per_page' => $this->perPage,
178            'current_page' => $this->currentPage,
179            'total_pages' => $this->totalPages,
180            'offset' => $this->offset,
181            'has_more' => $this->hasMore,
182            'has_previous' => $this->hasPrevious,
183            'links' => $this->links,
184        ];
185    }
186}