Completed
Pull Request — develop (#3)
by
unknown
01:18
created

InlineKeyboardPagination::generateRange()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 16
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 10
nc 4
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
    protected $items_per_page;
18
19
    /**
20
     * @var integer
21
     */
22
    protected $max_buttons = 5;
23
24
    /**
25
     * @var bool
26
     */
27
    protected $force_button_count = false;
28
29
    /**
30
     * @var integer
31
     */
32
    protected $selected_page;
33
34
    /**
35
     * @var array
36
     */
37
    protected $items;
38
39
    /**
40
     * @var integer
41
     */
42
    protected $range_offset = 1;
43
44
    /**
45
     * @var string
46
     */
47
    protected $command;
48
49
    /**
50
     * @var string
51
     */
52
    protected $callback_data_format = 'command={COMMAND}&oldPage={OLD_PAGE}&newPage={NEW_PAGE}';
53
54
    /**
55
     * @var array
56
     */
57
    protected $labels = [
58
        'default'  => '%d',
59
        'first'    => '« %d',
60
        'previous' => '‹ %d',
61
        'current'  => '· %d ·',
62
        'next'     => '%d ›',
63
        'last'     => '%d »',
64
    ];
65
66
    /**
67
     * @inheritdoc
68
     * @throws InlineKeyboardPaginationException
69
     */
70
    public function setMaxButtons(int $max_buttons = 5, bool $force_button_count = false): InlineKeyboardPagination
71
    {
72
        if ($max_buttons < 3 || $max_buttons > 8) {
73
            throw new InlineKeyboardPaginationException('Invalid max buttons, must be between 3 and 8.');
74
        }
75
        $this->max_buttons = $max_buttons;
76
        $this->force_button_count = $force_button_count;
77
78
        return $this;
79
    }
80
81
    /**
82
     * Get the current callback format.
83
     *
84
     * @return string
85
     */
86
    public function getCallbackDataFormat(): string
87
    {
88
        return $this->callback_data_format;
89
    }
90
91
    /**
92
     * Set the callback_data format.
93
     *
94
     * @param string $callback_data_format
95
     *
96
     * @return InlineKeyboardPagination
97
     */
98
    public function setCallbackDataFormat(string $callback_data_format): InlineKeyboardPagination
99
    {
100
        $this->callback_data_format = $callback_data_format;
101
102
        return $this;
103
    }
104
105
    /**
106
     * Return list of keyboard button labels.
107
     *
108
     * @return array
109
     */
110
    public function getLabels(): array
111
    {
112
        return $this->labels;
113
    }
114
115
    /**
116
     * Set the keyboard button labels.
117
     *
118
     * @param array $labels
119
     *
120
     * @return InlineKeyboardPagination
121
     */
122
    public function setLabels($labels): InlineKeyboardPagination
123
    {
124
        $this->labels = $labels;
125
126
        return $this;
127
    }
128
129
    /**
130
     * @inheritdoc
131
     */
132
    public function setCommand(string $command = 'pagination'): InlineKeyboardPagination
133
    {
134
        $this->command = $command;
135
136
        return $this;
137
    }
138
139
    /**
140
     * @inheritdoc
141
     * @throws InlineKeyboardPaginationException
142
     */
143
    public function setSelectedPage(int $selected_page): InlineKeyboardPagination
144
    {
145
        $number_of_pages = $this->getNumberOfPages();
146
        /*if ($selected_page < 1 || $selected_page > $number_of_pages) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
49% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
147
            throw new CustomInlineKeyboardPaginationException('Invalid selected page, must be between 1 and ' . $number_of_pages);
148
        }*/
149
150
        // if current page is greater than total pages...
151
        if ($selected_page > $number_of_pages) {
152
            // set current page to last page
153
            $selected_page = $number_of_pages;
154
        }
155
        // if current page is less than first page...
156
        if ($selected_page < 1) {
157
            // set current page to first page
158
            $selected_page = 1;
159
        }
160
        $this->selected_page = $selected_page;
161
162
        return $this;
163
    }
164
165
    /**
166
     * Get the number of items shown per page.
167
     *
168
     * @return int
169
     */
170
    public function getItemsPerPage(): int
171
    {
172
        return $this->items_per_page;
173
    }
174
175
    /**
176
     * Set how many items should be shown per page.
177
     *
178
     * @param int $items_per_page
179
     *
180
     * @return InlineKeyboardPagination
181
     * @throws InlineKeyboardPaginationException
182
     */
183
    public function setItemsPerPage($items_per_page): InlineKeyboardPagination
184
    {
185
        if ($items_per_page <= 0) {
186
            throw new InlineKeyboardPaginationException('Invalid number of items per page, must be at least 1');
187
        }
188
        $this->items_per_page = $items_per_page;
189
190
        return $this;
191
    }
192
193
    /**
194
     * Set the items for the pagination.
195
     *
196
     * @param array $items
197
     *
198
     * @return InlineKeyboardPagination
199
     * @throws InlineKeyboardPaginationException
200
     */
201
    public function setItems(array $items): InlineKeyboardPagination
202
    {
203
        if (empty($items)) {
204
            throw new InlineKeyboardPaginationException('Items list empty.');
205
        }
206
        $this->items = $items;
207
208
        return $this;
209
    }
210
211
    /**
212
     * Set max number of pages based on labels which user defined
213
     *
214
     * @return InlineKeyboardPagination
215
     * @throws InlineKeyboardPaginationException
216
     */
217
    public function setMaxPageBasedOnLabels(): InlineKeyboardPagination
218
    {
219
        $max_buttons = 0;
220
        $count = count($this->labels);
221
        if ($count < 2) {
222
            throw new InlineKeyboardPaginationException('Invalid number of labels was passed to paginator');
223
        }
224
225
        if (isset($this->labels['current'])) {
226
            $max_buttons++;
227
        }
228
229
        if (isset($this->labels['first'])) {
230
            $max_buttons++;
231
        }
232
233
        if (isset($this->labels['last'])) {
234
            $max_buttons++;
235
        }
236
237
        if (isset($this->labels['previous'])) {
238
            $max_buttons++;
239
        }
240
241
        if (isset($this->labels['next'])) {
242
            $max_buttons++;
243
        }
244
        $max_buttons += $this->range_offset * 2;
245
246
        $this->max_buttons = $max_buttons;
247
248
        return $this;
249
    }
250
251
    /**
252
     * Set offset of range
253
     *
254
     * @return InlineKeyboardPagination
255
     * @throws InlineKeyboardPaginationException
256
     */
257
    public function setRangeOffset($offset): InlineKeyboardPagination
258
    {
259
        if ($offset < 0 || !is_numeric($offset)) {
260
            throw new InlineKeyboardPaginationException('Invalid offset for range');
261
        }
262
263
        $this->range_offset = $offset;
0 ignored issues
show
Documentation Bug introduced by
It seems like $offset can also be of type double or string. However, the property $range_offset is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
264
265
        return $this;
266
    }
267
268
    /**
269
     * Calculate and return the number of pages.
270
     *
271
     * @return int
272
     */
273
    public function getNumberOfPages(): int
274
    {
275
        return (int)ceil(count($this->items) / $this->items_per_page);
276
    }
277
278
    /**
279
     * TelegramBotPagination constructor.
280
     *
281
     * @inheritdoc
282
     * @throws InlineKeyboardPaginationException
283
     */
284
    public function __construct(
285
        array $items,
286
        string $command = 'pagination',
287
        int $selected_page = 1,
288
        int $items_per_page = 5
289
    ) {
290
        $this->setCommand($command);
291
        $this->setItemsPerPage($items_per_page);
292
        $this->setItems($items);
293
        $this->setSelectedPage($selected_page);
294
    }
295
296
    /**
297
     * @inheritdoc
298
     * @throws InlineKeyboardPaginationException
299
     */
300
    public function getPagination(int $selected_page = null): array
301
    {
302
        if ($selected_page !== null) {
303
            $this->setSelectedPage($selected_page);
304
        }
305
306
        return [
307
            'items'    => $this->getPreparedItems(),
308
            'keyboard' => $this->generateKeyboard(),
309
        ];
310
    }
311
312
    /**
313
     * Generate the keyboard with the correctly labelled buttons.
314
     *
315
     * @return array
316
     */
317
    protected function generateKeyboard(): array
318
    {
319
        $buttons = [];
320
        $number_of_pages = $this->getNumberOfPages();
321
322
        if ($number_of_pages === 1) {
323
            return $buttons;
324
        }
325
326
        if ($number_of_pages > $this->max_buttons) {
327
            if ($this->selected_page > 1) {
328
                // get previous page num
329
                $buttons[] = $this->generateButton($this->selected_page - 1, 'previous');
330
            }
331
            // for first pages
332
            if ($this->selected_page > $this->range_offset + 1 && $number_of_pages >= $this->max_buttons) {
333
                $buttons[] = $this->generateButton(1, 'first');
334
            }
335
336
            $range_offsets = $this->generateRange();
337
            // loop to show links to range of pages around current page
338 View Code Duplication
            for ($i = $range_offsets['from']; $i < $range_offsets['to']; $i++) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
339
                // if it's a valid page number...
340
                if ($i == $this->selected_page) {
341
                    $buttons[] = $this->generateButton($this->selected_page, 'current');
342
                } elseif (($i > 0) && ($i <= $number_of_pages)) {
343
                    $buttons[] = $this->generateButton($i, 'default');
344
                }
345
            }
346
347
            // if not on last page, show forward and last page links
348
            if ($this->selected_page + $this->range_offset < $number_of_pages && $number_of_pages >= $this->max_buttons) {
349
                $buttons[] = $this->generateButton($number_of_pages, 'last');
350
            }
351
            if ($this->selected_page != $number_of_pages && $number_of_pages > 1) {
352
                $buttons[] = $this->generateButton($this->selected_page + 1, 'next');
353
            }
354
        } else {
355 View Code Duplication
            for ($i = 1; $i <= $number_of_pages; $i++) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
356
                // if it's a valid page number...
357
                if ($i == $this->selected_page) {
358
                    $buttons[] = $this->generateButton($this->selected_page, 'current');
359
                } elseif (($i > 0) && ($i <= $number_of_pages)) {
360
                    $buttons[] = $this->generateButton($i, 'default');
361
                }
362
            }
363
        }
364
365
        // Set the correct labels.
366
        foreach ($buttons as $page => &$button) {
367
368
            $label_key = $button['label'];
369
370
            $label = $this->labels[$label_key] ?? '';
371
372
            if ($label === '') {
373
                $button = null;
374
                continue;
375
            }
376
377
            $button['text'] = sprintf($label, $button['text']);
378
        }
379
380
        return array_values(array_filter($buttons));
381
    }
382
383
    /**
384
     * Get the range of intermediate buttons for the keyboard.
385
     *
386
     * @return array
387
     */
388
    protected function generateRange(): array
389
    {
390
        $number_of_pages = $this->getNumberOfPages();
391
392
        $from = $this->selected_page - $this->range_offset;
393
        $to = (($this->selected_page + $this->range_offset) + 1);
394
        $last = $number_of_pages - $this->selected_page;
395
        if ($number_of_pages - $this->selected_page <= $this->range_offset) {
396
            $from -= ($this->range_offset) - $last;
397
        }
398
        if ($this->selected_page < $this->range_offset + 1) {
399
            $to += ($this->range_offset + 1) - $this->selected_page;
400
        }
401
402
        return compact('from', 'to');
403
    }
404
405
    /**
406
     * Generate the button for the passed page.
407
     *
408
     * @param int $page
409
     * @param string $label
410
     *
411
     * @return array
412
     */
413
    protected function generateButton(int $page, string $label): array
414
    {
415
        return [
416
            'text'          => (string)$page,
417
            'callback_data' => $this->generateCallbackData($page),
418
            'label'         => $label,
419
        ];
420
    }
421
422
    /**
423
     * Generate the callback data for the passed page.
424
     *
425
     * @param int $page
426
     *
427
     * @return string
428
     */
429
    protected function generateCallbackData(int $page): string
430
    {
431
        return str_replace(
432
            ['{COMMAND}', '{OLD_PAGE}', '{NEW_PAGE}'],
433
            [$this->command, $this->selected_page, $page],
434
            $this->callback_data_format
435
        );
436
    }
437
438
    /**
439
     * Get the prepared items for the selected page.
440
     *
441
     * @return array
442
     */
443
    protected function getPreparedItems(): array
444
    {
445
        return array_slice($this->items, $this->getOffset(), $this->items_per_page);
446
    }
447
448
    /**
449
     * Get the items offset for the selected page.
450
     *
451
     * @return int
452
     */
453
    protected function getOffset(): int
454
    {
455
        return $this->items_per_page * ($this->selected_page - 1);
456
    }
457
458
    /**
459
     * Get the parameters from the callback query.
460
     *
461
     * @todo Possibly make it work for custom formats too?
462
     *
463
     * @param string $data
464
     *
465
     * @return array
466
     */
467
    public static function getParametersFromCallbackData($data): array
468
    {
469
        parse_str($data, $params);
470
471
        return $params;
472
    }
473
}
474