InlineKeyboardPagination::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 4
c 1
b 0
f 0
dl 0
loc 10
rs 10
cc 1
nc 1
nop 4
1
<?php
2
3
namespace TelegramBot\InlineKeyboardPagination;
4
5
use TelegramBot\InlineKeyboardPagination\Exceptions\InlineKeyboardPaginationException;
6
7
/**
8
 * Class InlineKeyboardPagination
9
 *
10
 * @package TelegramBot\InlineKeyboardPagination
11
 */
12
class InlineKeyboardPagination implements InlineKeyboardPaginator
13
{
14
    protected array $items;
15
    protected int $itemsPerPage;
16
    protected int $selectedPage;
17
    protected int $maxButtons = 5;
18
    protected bool $forceButtonCount = false;
19
    protected string $command;
20
    protected string $callbackDataFormat = 'command={COMMAND}&oldPage={OLD_PAGE}&newPage={NEW_PAGE}';
21
    protected array $labels = [
22
        'default'  => '%d',
23
        'first'    => '« %d',
24
        'previous' => '‹ %d',
25
        'current'  => '· %d ·',
26
        'next'     => '%d ›',
27
        'last'     => '%d »',
28
    ];
29
30
    /**
31
     * @param int  $maxButtons
32
     * @param bool $forceButtonCount
33
     *
34
     * @return self
35
     * @throws InlineKeyboardPaginationException
36
     */
37
    public function setMaxButtons(int $maxButtons = 5, bool $forceButtonCount = false): self
38
    {
39
        if ($maxButtons < 5 || $maxButtons > 8) {
40
            throw InlineKeyboardPaginationException::invalidMaxButtons();
41
        }
42
43
        $this->maxButtons       = $maxButtons;
44
        $this->forceButtonCount = $forceButtonCount;
45
46
        return $this;
47
    }
48
49
    /**
50
     * Get the current callback format.
51
     *
52
     * @return string
53
     */
54
    public function getCallbackDataFormat(): string
55
    {
56
        return $this->callbackDataFormat;
57
    }
58
59
    /**
60
     * Set the callback_data format.
61
     *
62
     * @param string $callbackDataFormat
63
     *
64
     * @return self
65
     */
66
    public function setCallbackDataFormat(string $callbackDataFormat): self
67
    {
68
        $this->callbackDataFormat = $callbackDataFormat;
69
70
        return $this;
71
    }
72
73
    /**
74
     * Return list of keyboard button labels.
75
     *
76
     * @return array
77
     */
78
    public function getLabels(): array
79
    {
80
        return $this->labels;
81
    }
82
83
    /**
84
     * Set the keyboard button labels.
85
     *
86
     * @param array $labels
87
     *
88
     * @return self
89
     */
90
    public function setLabels(array $labels): self
91
    {
92
        $this->labels = $labels;
93
94
        return $this;
95
    }
96
97
    /**
98
     * @inheritdoc
99
     */
100
    public function setCommand(string $command = 'pagination'): self
101
    {
102
        $this->command = $command;
103
104
        return $this;
105
    }
106
107
    /**
108
     * @inheritdoc
109
     * @throws InlineKeyboardPaginationException
110
     */
111
    public function setSelectedPage(int $selectedPage): self
112
    {
113
        $numberOfPages = $this->getNumberOfPages();
114
        if ($selectedPage < 1 || $selectedPage > $numberOfPages) {
115
            throw InlineKeyboardPaginationException::pageMustBeBetween(1, $numberOfPages);
116
        }
117
118
        $this->selectedPage = $selectedPage;
119
120
        return $this;
121
    }
122
123
    /**
124
     * Get the number of items shown per page.
125
     *
126
     * @return int
127
     */
128
    public function getItemsPerPage(): int
129
    {
130
        return $this->itemsPerPage;
131
    }
132
133
    /**
134
     * Set how many items should be shown per page.
135
     *
136
     * @param int $itemsPerPage
137
     *
138
     * @return self
139
     * @throws InlineKeyboardPaginationException
140
     */
141
    public function setItemsPerPage(int $itemsPerPage): self
142
    {
143
        if ($itemsPerPage <= 0) {
144
            throw InlineKeyboardPaginationException::invalidItemsPerPage();
145
        }
146
147
        $this->itemsPerPage = $itemsPerPage;
148
149
        return $this;
150
    }
151
152
    /**
153
     * Set the items for the pagination.
154
     *
155
     * @param array $items
156
     *
157
     * @return self
158
     * @throws InlineKeyboardPaginationException
159
     */
160
    public function setItems(array $items): self
161
    {
162
        if (empty($items)) {
163
            throw InlineKeyboardPaginationException::noItems();
164
        }
165
166
        $this->items = $items;
167
168
        return $this;
169
    }
170
171
    /**
172
     * Calculate and return the number of pages.
173
     *
174
     * @return int
175
     */
176
    public function getNumberOfPages(): int
177
    {
178
        return (int) ceil(count($this->items) / $this->itemsPerPage);
179
    }
180
181
    /**
182
     * TelegramBotPagination constructor.
183
     *
184
     * @inheritdoc
185
     * @throws InlineKeyboardPaginationException
186
     */
187
    public function __construct(
188
        array $items,
189
        string $command = 'pagination',
190
        int $selectedPage = 1,
191
        int $itemsPerPage = 5
192
    ) {
193
        $this->setCommand($command);
194
        $this->setItemsPerPage($itemsPerPage);
195
        $this->setItems($items);
196
        $this->setSelectedPage($selectedPage);
197
    }
198
199
    /**
200
     * @inheritdoc
201
     * @throws InlineKeyboardPaginationException
202
     */
203
    public function getPagination(int $selectedPage = null): array
204
    {
205
        if ($selectedPage !== null) {
206
            $this->setSelectedPage($selectedPage);
207
        }
208
209
        return [
210
            'items'    => $this->getPreparedItems(),
211
            'keyboard' => $this->generateKeyboard(),
212
        ];
213
    }
214
215
    /**
216
     * Generate the keyboard with the correctly labelled buttons.
217
     *
218
     * @return array
219
     */
220
    protected function generateKeyboard(): array
221
    {
222
        $buttons = $this->generateButtons();
223
        $buttons = $this->applyButtonLabels($buttons);
224
225
        return array_values(array_filter($buttons));
226
    }
227
228
    /**
229
     * Generate all buttons for this inline keyboard.
230
     *
231
     * @return array
232
     */
233
    protected function generateButtons(): array
234
    {
235
        $numberOfPages = $this->getNumberOfPages();
236
237
        $range = ['from' => 2, 'to' => $numberOfPages - 1];
238
239
        if ($numberOfPages > $this->maxButtons) {
240
            $range = $this->generateRange();
241
        }
242
243
        $buttons[1] = $this->generateButton(1);
0 ignored issues
show
Comprehensibility Best Practice introduced by
$buttons was never initialized. Although not strictly required by PHP, it is generally a good practice to add $buttons = array(); before regardless.
Loading history...
244
        for ($i = $range['from']; $i <= $range['to']; $i++) {
245
            $buttons[$i] = $this->generateButton($i);
246
        }
247
        $buttons[$numberOfPages] = $this->generateButton($numberOfPages);
248
249
        return $buttons;
250
    }
251
252
    /**
253
     * Apply correct text labels to the keyboard buttons.
254
     *
255
     * @param array $buttons
256
     *
257
     * @return array
258
     */
259
    protected function applyButtonLabels(array $buttons): array
260
    {
261
        $numberOfPages = $this->getNumberOfPages();
262
263
        foreach ($buttons as $page => &$button) {
264
            $inFirstBlock = max($this->selectedPage, $page) <= 3;
265
            $inLastBlock  = min($this->selectedPage, $page) >= $numberOfPages - 2;
266
267
            $labelKey = 'next';
268
            if ($page === $this->selectedPage) {
269
                $labelKey = 'current';
270
            } elseif ($inFirstBlock || $inLastBlock) {
271
                $labelKey = 'default';
272
            } elseif ($page === 1) {
273
                $labelKey = 'first';
274
            } elseif ($page === $numberOfPages) {
275
                $labelKey = 'last';
276
            } elseif ($page < $this->selectedPage) {
277
                $labelKey = 'previous';
278
            }
279
280
            $label = $this->labels[$labelKey] ?? '';
281
282
            // Remove button for undefined labels.
283
            if ($label === '') {
284
                $button = null;
285
                continue;
286
            }
287
288
            $button['text'] = sprintf($label, $page);
289
        }
290
291
        return $buttons;
292
    }
293
294
    /**
295
     * Get the range of intermediate buttons for the keyboard.
296
     *
297
     * @return array
298
     */
299
    protected function generateRange(): array
300
    {
301
        $numberOfIntermediateButtons = $this->maxButtons - 2; // Minus first and last buttons.
302
        $numberOfPages               = $this->getNumberOfPages();
303
304
        $from = $this->selectedPage - 1;
305
        $to   = $this->selectedPage + 1;
306
307
        if ($this->selectedPage === 1) {
308
            $from = 2;
309
            $to   = $this->maxButtons - 1;
310
        } elseif ($this->selectedPage === $numberOfPages) {
311
            $from = $numberOfPages - $numberOfIntermediateButtons;
312
            $to   = $numberOfPages - 1;
313
        } elseif ($this->selectedPage === 3) {
314
            // Special case because this button is in the center of a flexible pagination.
315
            $to += $numberOfIntermediateButtons - 3;
316
        } elseif ($this->selectedPage < 3) {
317
            // First half.
318
            $from = $this->selectedPage;
319
            $to   = $this->selectedPage + $numberOfIntermediateButtons - 1;
320
        } elseif (($numberOfPages - $this->selectedPage) < 3) {
321
            // Last half.
322
            $from = $numberOfPages - $numberOfIntermediateButtons;
323
            $to   = $numberOfPages - 1;
324
        } elseif ($this->forceButtonCount) {
325
            $from = (int) max(2, $this->selectedPage - floor($numberOfIntermediateButtons / 2));
326
            $to   = $from + $numberOfIntermediateButtons - 1;
327
        }
328
329
        return compact('from', 'to');
330
    }
331
332
    /**
333
     * Generate the button for the passed page.
334
     *
335
     * @param int $page
336
     *
337
     * @return array
338
     */
339
    protected function generateButton(int $page): array
340
    {
341
        return [
342
            'text'          => $page,
343
            'callback_data' => $this->generateCallbackData($page),
344
        ];
345
    }
346
347
    /**
348
     * Generate the callback data for the passed page.
349
     *
350
     * @param int $page
351
     *
352
     * @return string
353
     */
354
    protected function generateCallbackData(int $page): string
355
    {
356
        return str_replace(
357
            ['{COMMAND}', '{OLD_PAGE}', '{NEW_PAGE}'],
358
            [$this->command, $this->selectedPage, $page],
359
            $this->callbackDataFormat
360
        );
361
    }
362
363
    /**
364
     * Get the prepared items for the selected page.
365
     *
366
     * @return array
367
     */
368
    protected function getPreparedItems(): array
369
    {
370
        return array_slice($this->items, $this->getOffset(), $this->itemsPerPage);
371
    }
372
373
    /**
374
     * Get the items offset for the selected page.
375
     *
376
     * @return int
377
     */
378
    protected function getOffset(): int
379
    {
380
        return $this->itemsPerPage * ($this->selectedPage - 1);
381
    }
382
383
    /**
384
     * Get the parameters from the callback query.
385
     *
386
     * @todo Possibly make it work for custom formats too?
387
     *
388
     * @param string $data
389
     *
390
     * @return array
391
     */
392
    public static function getParametersFromCallbackData(string $data): array
393
    {
394
        parse_str($data, $params);
395
396
        return $params;
397
    }
398
}
399