Completed
Push — develop ( 9c7ec6...e33a39 )
by Armando
01:30
created

InlineKeyboardPagination::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
c 0
b 0
f 0
rs 9.4285
cc 1
eloc 5
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
    /**
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
     * @return int
151
     */
152
    public function getItemsPerPage(): int
153
    {
154
        return $this->items_per_page;
155
    }
156
157
    /**
158
     * @param int $items_per_page
159
     *
160
     * @return InlineKeyboardPagination
161
     * @throws InlineKeyboardPaginationException
162
     */
163
    public function setItemsPerPage($items_per_page): InlineKeyboardPagination
164
    {
165
        if ($items_per_page <= 0) {
166
            throw new InlineKeyboardPaginationException('Invalid number of items per page, must be at least 1');
167
        }
168
        $this->items_per_page = $items_per_page;
169
170
        return $this;
171
    }
172
173
    /**
174
     * @param array $items
175
     *
176
     * @return InlineKeyboardPagination
177
     * @throws InlineKeyboardPaginationException
178
     */
179
    public function setItems(array $items): InlineKeyboardPagination
180
    {
181
        if (empty($items)) {
182
            throw new InlineKeyboardPaginationException('Items list empty.');
183
        }
184
        $this->items = $items;
185
186
        return $this;
187
    }
188
189
    /**
190
     * Calculate and return the number of pages.
191
     *
192
     * @return int
193
     */
194
    public function getNumberOfPages(): int
195
    {
196
        return (int) ceil(count($this->items) / $this->items_per_page);
197
    }
198
199
    /**
200
     * TelegramBotPagination constructor.
201
     *
202
     * @inheritdoc
203
     * @throws InlineKeyboardPaginationException
204
     */
205
    public function __construct(array $items, string $command = 'pagination', int $selected_page = 1, int $items_per_page = 5)
206
    {
207
        $this->setCommand($command);
208
        $this->setItemsPerPage($items_per_page);
209
        $this->setItems($items);
210
        $this->setSelectedPage($selected_page);
211
    }
212
213
    /**
214
     * @inheritdoc
215
     * @throws InlineKeyboardPaginationException
216
     */
217
    public function getPagination(int $selected_page = null): array
218
    {
219
        if ($selected_page !== null) {
220
            $this->setSelectedPage($selected_page);
221
        }
222
223
        return [
224
            'items'    => $this->getPreparedItems(),
225
            'keyboard' => $this->generateKeyboard(),
226
        ];
227
    }
228
229
    /**
230
     * Generate the keyboard with the correctly labelled buttons.
231
     *
232
     * @return array
233
     */
234
    protected function generateKeyboard(): array
235
    {
236
        $buttons         = [];
237
        $number_of_pages = $this->getNumberOfPages();
238
239
        if ($number_of_pages > $this->max_buttons) {
240
            $buttons[1] = $this->generateButton(1);
241
242
            $range = $this->generateRange();
243
            for ($i = $range['from']; $i < $range['to']; $i++) {
244
                $buttons[$i] = $this->generateButton($i);
245
            }
246
247
            $buttons[$number_of_pages] = $this->generateButton($number_of_pages);
248
        } else {
249
            for ($i = 1; $i <= $number_of_pages; $i++) {
250
                $buttons[$i] = $this->generateButton($i);
251
            }
252
        }
253
254
        // Set the correct labels.
255
        foreach ($buttons as $page => &$button) {
256
            $in_first_block = $this->selected_page <= 3 && $page <= 3;
257
            $in_last_block  = $this->selected_page >= $number_of_pages - 2 && $page >= $number_of_pages - 2;
258
259
            $label_key = 'next';
260
            if ($page === $this->selected_page) {
261
                $label_key = 'current';
262
            } elseif ($in_first_block || $in_last_block) {
263
                $label_key = 'default';
264
            } elseif ($page === 1) {
265
                $label_key = 'first';
266
            } elseif ($page === $number_of_pages) {
267
                $label_key = 'last';
268
            } elseif ($page < $this->selected_page) {
269
                $label_key = 'previous';
270
            }
271
272
            $label = $this->labels[$label_key] ?? '';
273
274
            if ($label === '') {
275
                $button = null;
276
                continue;
277
            }
278
279
            $button['text'] = sprintf($label, $page);
280
        }
281
282
        return array_values(array_filter($buttons));
283
    }
284
285
    /**
286
     * Get the range of intermediate buttons for the keyboard.
287
     *
288
     * @return array
289
     */
290
    protected function generateRange(): array
291
    {
292
        $number_of_intermediate_buttons = $this->max_buttons - 2;
293
        $number_of_pages                = $this->getNumberOfPages();
294
295
        if ($this->selected_page === 1) {
296
            $from = 2;
297
            $to   = $this->max_buttons;
298
        } elseif ($this->selected_page === $number_of_pages) {
299
            $from = $number_of_pages - $number_of_intermediate_buttons;
300
            $to   = $number_of_pages;
301
        } else {
302
            if ($this->selected_page < 3) {
303
                $from = $this->selected_page;
304
                $to   = $this->selected_page + $number_of_intermediate_buttons;
305
            } elseif (($number_of_pages - $this->selected_page) < 3) {
306
                $from = $number_of_pages - $number_of_intermediate_buttons;
307
                $to   = $number_of_pages;
308
            } else {
309
                // @todo: Find a nicer solution for page 3
310
                if ($this->force_button_count) {
311
                    $from = $this->selected_page - floor($number_of_intermediate_buttons / 2);
312
                    $to   = $this->selected_page + ceil($number_of_intermediate_buttons / 2) + ($this->selected_page === 3 && $this->max_buttons > 5);
313
                } else {
314
                    $from = $this->selected_page - 1;
315
                    $to   = $this->selected_page + ($this->selected_page === 3 ? $number_of_intermediate_buttons - 1 : 2);
316
                }
317
            }
318
        }
319
320
        return compact('from', 'to');
321
    }
322
323
    /**
324
     * Generate the button for the passed page.
325
     *
326
     * @param int $page
327
     *
328
     * @return array
329
     */
330
    protected function generateButton(int $page): array
331
    {
332
        return [
333
            'text'          => (string) $page,
334
            'callback_data' => $this->generateCallbackData($page),
335
        ];
336
    }
337
338
    /**
339
     * Generate the callback data for the passed page.
340
     *
341
     * @param int $page
342
     *
343
     * @return string
344
     */
345
    protected function generateCallbackData(int $page): string
346
    {
347
        return str_replace(
348
            ['{COMMAND}', '{OLD_PAGE}', '{NEW_PAGE}'],
349
            [$this->command, $this->selected_page, $page],
350
            $this->callback_data_format
351
        );
352
    }
353
354
    /**
355
     * Get the prepared items for the selected page.
356
     *
357
     * @return array
358
     */
359
    protected function getPreparedItems(): array
360
    {
361
        return array_slice($this->items, $this->getOffset(), $this->items_per_page);
362
    }
363
364
    /**
365
     * @return int
366
     */
367
    protected function getOffset(): int
368
    {
369
        return $this->items_per_page * ($this->selected_page - 1);
370
    }
371
372
    /**
373
     * Get the parameters from the callback query.
374
     *
375
     * @param string $data
376
     *
377
     * @return array
378
     */
379
    public static function getParametersFromCallbackData($data): array
380
    {
381
        parse_str($data, $params);
382
383
        return $params;
384
    }
385
}
386