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

InlineKeyboardPagination::generateRange()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
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(array $items, string $command = 'pagination', int $selected_page = 1, int $items_per_page = 5)
285
	{
286
		$this->setCommand($command);
287
		$this->setItemsPerPage($items_per_page);
288
		$this->setItems($items);
289
		$this->setSelectedPage($selected_page);
290
	}
291
292
	/**
293
	 * @inheritdoc
294
	 * @throws InlineKeyboardPaginationException
295
	 */
296
	public function getPagination(int $selected_page = null): array
297
	{
298
		if ($selected_page !== null) {
299
			$this->setSelectedPage($selected_page);
300
		}
301
302
		return [
303
			'items'    => $this->getPreparedItems(),
304
			'keyboard' => $this->generateKeyboard(),
305
		];
306
	}
307
308
	/**
309
	 * Generate the keyboard with the correctly labelled buttons.
310
	 *
311
	 * @return array
312
	 */
313
	protected function generateKeyboard(): array
314
	{
315
		$buttons         = [];
316
		$number_of_pages = $this->getNumberOfPages();
317
318
		if ($number_of_pages === 1) {
319
			return $buttons;
320
		}
321
322
		if ($number_of_pages > $this->max_buttons) {
323
			if ($this->selected_page > 1) {
324
				// get previous page num
325
				$buttons[] = $this->generateButton($this->selected_page - 1, 'previous');
326
			}
327
			// for first pages
328
			if($this->selected_page > $this->range_offset + 1 && $number_of_pages >= $this->max_buttons){
329
				$buttons[] = $this->generateButton(1, 'first');
330
			}
331
332
			$range_offsets = $this->generateRange();
333
			// loop to show links to range of pages around current page
334 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...
335
				// if it's a valid page number...
336
				if ($i == $this->selected_page) {
337
					$buttons[] = $this->generateButton($this->selected_page, 'current');
338
				} elseif (($i > 0) && ($i <= $number_of_pages)) {
339
					$buttons[] = $this->generateButton($i, 'default');
340
				}
341
			}
342
343
			// if not on last page, show forward and last page links
344
			if($this->selected_page + $this->range_offset < $number_of_pages  && $number_of_pages >= $this->max_buttons){
345
				$buttons[] = $this->generateButton($number_of_pages, 'last');
346
			}
347
			if ($this->selected_page != $number_of_pages && $number_of_pages > 1) {
348
				$buttons[] = $this->generateButton($this->selected_page + 1, 'next');
349
			}
350
		} else {
351 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...
352
				// if it's a valid page number...
353
				if ($i == $this->selected_page) {
354
					$buttons[] = $this->generateButton($this->selected_page, 'current');
355
				} elseif (($i > 0) && ($i <= $number_of_pages)) {
356
					$buttons[] =  $this->generateButton($i, 'default');
357
				}
358
			}
359
		}
360
361
		// Set the correct labels.
362
		foreach ($buttons as $page => &$button) {
363
364
			$label_key = $button['label'];
365
366
			$label = $this->labels[$label_key] ?? '';
367
368
			if ($label === '') {
369
				$button = null;
370
				continue;
371
			}
372
373
			$button['text'] = sprintf($label, $button['text']);
374
		}
375
376
		return array_values(array_filter($buttons));
377
	}
378
379
	/**
380
	 * Get the range of intermediate buttons for the keyboard.
381
	 *
382
	 * @return array
383
	 */
384
	protected function generateRange(): array
385
	{
386
		$number_of_pages                = $this->getNumberOfPages();
387
388
		$from = $this->selected_page - $this->range_offset;
389
		$to = (($this->selected_page + $this->range_offset) + 1);
390
		$last = $number_of_pages - $this->selected_page;
391
		if($number_of_pages - $this->selected_page <= $this->range_offset)
392
			$from -= ($this->range_offset) - $last;
393
		if($this->selected_page < $this->range_offset + 1 )
394
			$to += ($this->range_offset + 1) - $this->selected_page;
395
396
		return compact('from', 'to');
397
	}
398
399
	/**
400
	 * Generate the button for the passed page.
401
	 *
402
	 * @param int $page
403
	 * @param string $label
404
	 *
405
	 * @return array
406
	 */
407
	protected function generateButton(int $page, string $label): array
408
	{
409
		return [
410
			'text'          => (string) $page,
411
			'callback_data' => $this->generateCallbackData($page),
412
			'label'         => $label,
413
		];
414
	}
415
416
	/**
417
	 * Generate the callback data for the passed page.
418
	 *
419
	 * @param int $page
420
	 *
421
	 * @return string
422
	 */
423
	protected function generateCallbackData(int $page): string
424
	{
425
		return str_replace(
426
			['{COMMAND}', '{OLD_PAGE}', '{NEW_PAGE}'],
427
			[$this->command, $this->selected_page, $page],
428
			$this->callback_data_format
429
		);
430
	}
431
432
	/**
433
	 * Get the prepared items for the selected page.
434
	 *
435
	 * @return array
436
	 */
437
	protected function getPreparedItems(): array
438
	{
439
		return array_slice($this->items, $this->getOffset(), $this->items_per_page);
440
	}
441
442
	/**
443
	 * Get the items offset for the selected page.
444
	 *
445
	 * @return int
446
	 */
447
	protected function getOffset(): int
448
	{
449
		return $this->items_per_page * ($this->selected_page - 1);
450
	}
451
452
	/**
453
	 * Get the parameters from the callback query.
454
	 *
455
	 * @todo Possibly make it work for custom formats too?
456
	 *
457
	 * @param string $data
458
	 *
459
	 * @return array
460
	 */
461
	public static function getParametersFromCallbackData($data): array
462
	{
463
		parse_str($data, $params);
464
465
		return $params;
466
	}
467
}
468