Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
PaginationRequest
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
3 / 3
6
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 fromRequest
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
4
 validateLimit
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * Pagination Request Parser
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
22use phpMyFAQ\Filter;
23use Symfony\Component\HttpFoundation\Request;
24
25/**
26 * Class PaginationRequest
27 *
28 * Parses and validates pagination query parameters from HTTP requests.
29 * Supports both page-based (page + per_page) and offset-based (limit + offset) pagination.
30 */
31class PaginationRequest
32{
33    public int $limit {
34        get {
35            return $this->limit;
36        }
37    }
38
39    public int $offset {
40        get {
41            return $this->offset;
42        }
43    }
44
45    public int $page {
46        get {
47            return $this->page;
48        }
49    }
50
51    public int $perPage {
52        get {
53            return $this->perPage;
54        }
55    }
56
57    public bool $isPageBased {
58        get {
59            return $this->isPageBased;
60        }
61    }
62
63    public bool $isOffsetBased {
64        get {
65            return $this->isOffsetBased;
66        }
67    }
68
69    /**
70     * Constructor
71     *
72     * @param int $limit Items per page
73     * @param int $offset Starting offset
74     * @param int $page Current page number
75     * @param int $perPage Items per page (alias for limit)
76     * @param bool $isPageBased Whether page-based pagination was used
77     * @param bool $isOffsetBased Whether offset-based pagination was used
78     */
79    private function __construct(
80        int $limit,
81        int $offset,
82        int $page,
83        int $perPage,
84        bool $isPageBased,
85        bool $isOffsetBased,
86    ) {
87        $this->limit = $limit;
88        $this->offset = $offset;
89        $this->page = $page;
90        $this->perPage = $perPage;
91        $this->isPageBased = $isPageBased;
92        $this->isOffsetBased = $isOffsetBased;
93    }
94
95    /**
96     * Creates a PaginationRequest from a Symfony Request object
97     *
98     * @param Request $request The HTTP request
99     * @param int $defaultPerPage Default items per page
100     * @param int $maxPerPage Maximum allowed items per page
101     * @return self
102     */
103    public static function fromRequest(Request $request, int $defaultPerPage = 25, int $maxPerPage = 100): self
104    {
105        // Parse query parameters
106        $page = Filter::filterVar($request->query->get('page'), FILTER_VALIDATE_INT, default: 1);
107        $perPage = Filter::filterVar($request->query->get('per_page'), FILTER_VALIDATE_INT, default: null);
108        $limit = Filter::filterVar($request->query->get('limit'), FILTER_VALIDATE_INT, default: null);
109        $offset = Filter::filterVar($request->query->get('offset'), FILTER_VALIDATE_INT, default: null);
110
111        // Validate page number
112        if ($page < 1) {
113            $page = 1;
114        }
115
116        // Determine pagination style and values
117        $isOffsetBased = false;
118        $isPageBased = false;
119
120        // Priority: explicit offset > page-based > defaults
121        if ($offset !== null) {
122            // Offset-based pagination
123            $isOffsetBased = true;
124            $offset = max(0, $offset); // Ensure non-negative
125            $limit ??= $perPage ?? $defaultPerPage;
126            $limit = self::validateLimit($limit, $maxPerPage);
127            $perPage = $limit;
128            // Calculate page from offset
129            $page = (int) floor($offset / $limit) + 1;
130        }
131
132        if ($offset === null) {
133            // Page-based pagination
134            $isPageBased = true;
135            $perPage ??= $limit ?? $defaultPerPage;
136            $perPage = self::validateLimit($perPage, $maxPerPage);
137            $limit = $perPage;
138            // Calculate offset from page
139            $offset = ($page - 1) * $perPage;
140        }
141
142        return new self((int) $limit, $offset, $page, (int) $perPage, $isPageBased, $isOffsetBased);
143    }
144
145    /**
146     * Validates and constrains the limit value
147     *
148     * @param int $limit The limit to validate
149     * @param int $maxPerPage Maximum allowed limit
150     * @return int Validated limit
151     */
152    private static function validateLimit(int $limit, int $maxPerPage): int
153    {
154        // Ensure the limit is at least 1
155        $limit = max(1, $limit);
156
157        // Ensure limit doesn't exceed maximum
158        return min($limit, $maxPerPage);
159    }
160}