Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.89% covered (success)
88.89%
64 / 72
90.91% covered (success)
90.91%
10 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
Pagination
91.43% covered (success)
91.43%
64 / 70
90.91% covered (success)
90.91%
10 / 11
25.39
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getCurrentPageFromUrl
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 render
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 renderPageNumbers
53.85% covered (warning)
53.85%
7 / 13
0.00% covered (danger)
0.00%
0 / 1
5.57
 shouldSkipBeforeCurrentPage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 shouldSkipAfterCurrentPage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 renderPageLink
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addNavigationButtons
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
3
 renderUrl
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 renderLink
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 renderLayout
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * Pagination handler class.
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    Anatoliy Belsky <ab@php.net>
12 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
13 * @copyright 2009-2026 phpMyFAQ Team
14 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
15 * @link      https://www.phpmyfaq.de
16 * @since     2009-09-27
17 */
18
19declare(strict_types=1);
20
21namespace phpMyFAQ;
22
23use phpMyFAQ\Pagination\PaginationTemplates;
24use phpMyFAQ\Pagination\UrlConfig;
25
26/**
27 * Class Pagination
28 */
29class Pagination
30{
31    private const string TPL_VAR_LINK_URL = '{LINK_URL}';
32    private const string TPL_VAR_LINK_TEXT = '{LINK_TEXT}';
33    private const string TPL_VAR_LAYOUT_CONTENT = '{LAYOUT_CONTENT}';
34
35    private int $currentPage;
36
37    public string $baseUrl {
38        set {
39            $this->baseUrl = $value;
40            $this->currentPage = $this->getCurrentPageFromUrl($value);
41        }
42    }
43
44    public int $total {
45        set => $this->total = max(0, $value);
46    }
47
48    public int $perPage {
49        set => $this->perPage = max(1, $value);
50    }
51
52    public int $adjacent {
53        set => $this->adjacent = max(1, $value);
54    }
55
56    public function __construct(
57        string $baseUrl = '',
58        int $total = 0,
59        int $perPage = 10,
60        public readonly PaginationTemplates $templates = new PaginationTemplates(),
61        public readonly UrlConfig $urlConfig = new UrlConfig(),
62        int $adjacent = 4,
63    ) {
64        $this->adjacent = $adjacent;
65        $this->perPage = $perPage;
66        $this->total = $total;
67        $this->baseUrl = $baseUrl;
68    }
69
70    protected function getCurrentPageFromUrl(string $url): int
71    {
72        $page = 1;
73
74        if ($url !== '' && $url !== '0') {
75            $match = [];
76            if (Strings::preg_match('/[?&]' . $this->urlConfig->pageParamName . '=(\d+)/', $url, $match) !== 0) {
77                $page = $match[1] ?? $page;
78            }
79        }
80
81        return (int) $page;
82    }
83
84    public function render(): string
85    {
86        $pages = (int) ceil($this->total / $this->perPage);
87        $content = $this->renderPageNumbers($pages);
88        $content = $this->addNavigationButtons($content, $pages);
89
90        return $this->renderLayout(implode(separator: '&nbsp;&nbsp;', array: $content));
91    }
92
93    /**
94     * @return string[]
95     */
96    protected function renderPageNumbers(int $pages): array
97    {
98        $content = [];
99        $adjacent = (int) max(floor($this->adjacent / 2), 1);
100
101        for ($page = 1; $page <= $pages; ++$page) {
102            if ($this->shouldSkipBeforeCurrentPage($page, $adjacent)) {
103                $content[] = '<li class="disabled"><a>&hellip;</a></li>';
104                $page = $this->currentPage - $adjacent - 1;
105                continue;
106            }
107
108            if ($this->shouldSkipAfterCurrentPage($page, $adjacent, $pages)) {
109                $content[] = '<li class="disabled"><a>&hellip;</a></li>';
110                $page = $pages - $this->adjacent;
111                continue;
112            }
113
114            $content[] = $this->renderPageLink($page);
115        }
116
117        return $content;
118    }
119
120    protected function shouldSkipBeforeCurrentPage(int $page, int $adjacent): bool
121    {
122        return $page > $this->adjacent && $page < ($this->currentPage - $adjacent);
123    }
124
125    protected function shouldSkipAfterCurrentPage(int $page, int $adjacent, int $totalPages): bool
126    {
127        return $page > ($this->currentPage + $adjacent) && $page <= ($totalPages - $this->adjacent);
128    }
129
130    protected function renderPageLink(int $page): string
131    {
132        $link = $this->renderUrl($this->baseUrl, $page);
133        $template = $page === $this->currentPage ? $this->templates->currentPage : $this->templates->link;
134
135        return $this->renderLink($template, $link, $page);
136    }
137
138    /* @mago-expect lint:halstead - renders the full pagination control in one method */
139    /**
140     * @param string[] $content
141     * @return string[]
142     */
143    protected function addNavigationButtons(array $content, int $pages): array
144    {
145        if (1 < $this->currentPage) {
146            array_unshift($content, $this->renderLink(
147                $this->templates->prevPage,
148                $this->renderUrl($this->baseUrl, $this->currentPage - 1),
149                $this->currentPage - 1,
150            ));
151            array_unshift($content, $this->renderLink(
152                $this->templates->firstPage,
153                $this->renderUrl($this->baseUrl, page: 1),
154                linkText: 1,
155            ));
156        }
157
158        if ($pages > $this->currentPage) {
159            $content[] = $this->renderLink(
160                $this->templates->nextPage,
161                $this->renderUrl($this->baseUrl, $this->currentPage + 1),
162                $this->currentPage + 1,
163            );
164            $content[] = $this->renderLink(
165                $this->templates->lastPage,
166                $this->renderUrl($this->baseUrl, $pages),
167                $pages,
168            );
169        }
170
171        return $content;
172    }
173
174    protected function renderUrl(string $url = '', int $page = 1): string
175    {
176        if ($url === '') {
177            return sprintf($this->urlConfig->rewriteUrl, $page);
178        }
179
180        $urlToParse = '%s%s%s=%d';
181        $replacedUrl = Strings::preg_replace(
182            ['/[?&](amp;|)' . $this->urlConfig->pageParamName . '=(\d+)/'],
183            replacement: '',
184            subject: $url,
185        );
186        $cleanedUrl = is_string($replacedUrl) ? $replacedUrl : $url;
187        $separator = str_contains($cleanedUrl, needle: '?') ? '&' : '?';
188        return sprintf($urlToParse, $cleanedUrl, $separator, $this->urlConfig->pageParamName, $page);
189    }
190
191    protected function renderLink(string $tpl, string $url, string|int|float $linkText): string
192    {
193        $search = [self::TPL_VAR_LINK_URL, self::TPL_VAR_LINK_TEXT];
194        $replace = [$url, (string) $linkText];
195
196        return str_replace($search, $replace, $tpl);
197    }
198
199    protected function renderLayout(string $content): string
200    {
201        return str_replace(self::TPL_VAR_LAYOUT_CONTENT, $content, $this->templates->layout);
202    }
203}