Completed
Push — master ( 274637...164c34 )
by Armando
01:56 queued 48s
created

InlineKeyboardPagination::generateRange()   C

Complexity

Conditions 8
Paths 8

Size

Total Lines 32
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

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