Completed
Pull Request — master (#8)
by Eldar
01:13
created

InlineKeyboardPagination::setSelectedPage()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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