From f103c2d05a3738f5c6227d3e0f9e4f7fd8212137 Mon Sep 17 00:00:00 2001 From: Mehrdad Date: Sun, 15 Oct 2017 23:14:21 +0330 Subject: [PATCH 1/3] Redesign Paginator - Reformat generateButton to get label - define Offset for range - Refactor generateRange to have better algorithm and consider Offsets - Refactor generateKeyboard to work with this new solution - Add optional setMaxPageBasedOnLabels to generate max_page according to labels --- README.md | 7 + src/InlineKeyboardPagination.php | 834 +++++++++++++++++-------------- 2 files changed, 460 insertions(+), 381 deletions(-) diff --git a/README.md b/README.md index ab00909..dd8ba0a 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,14 @@ $callback_data_format = 'command={COMMAND}&oldPage={OLD_PAGE}&newPage={NEW_PAGE} // Define inline keyboard pagination. $ikp = new InlineKeyboardPagination($items, $command); $ikp->setMaxButtons(7, true); // Second parameter set to always show 7 buttons if possible. +$ikp->setRangeOffset(1); //optional: if you change offsets of selected page, you can use this method. e.g if selected page is 6 and Range offset set as 1, you will have 5 & 7 in pagination $ikp->setLabels($labels); + +/* + *optional: But recommended. if you want that max_page will set according to labels you defined, + * please call this method. if you remove $label elements and then call this method, max_page will be defined according to labels + */ +$ikp->setMaxPageBasedOnLabels(); $ikp->setCallbackDataFormat($callback_data_format); // Get pagination. diff --git a/src/InlineKeyboardPagination.php b/src/InlineKeyboardPagination.php index 926d695..7e23209 100644 --- a/src/InlineKeyboardPagination.php +++ b/src/InlineKeyboardPagination.php @@ -11,385 +11,457 @@ */ class InlineKeyboardPagination implements InlineKeyboardPaginator { - /** - * @var integer - */ - private $items_per_page; - - /** - * @var integer - */ - private $max_buttons = 5; - - /** - * @var bool - */ - private $force_button_count = false; - - /** - * @var integer - */ - private $selected_page; - - /** - * @var array - */ - private $items; - - /** - * @var string - */ - private $command; - - /** - * @var string - */ - private $callback_data_format = 'command={COMMAND}&oldPage={OLD_PAGE}&newPage={NEW_PAGE}'; - - /** - * @var array - */ - private $labels = [ - 'default' => '%d', - 'first' => '« %d', - 'previous' => '‹ %d', - 'current' => '· %d ·', - 'next' => '%d ›', - 'last' => '%d »', - ]; - - /** - * @inheritdoc - * @throws InlineKeyboardPaginationException - */ - public function setMaxButtons(int $max_buttons = 5, bool $force_button_count = false): InlineKeyboardPagination - { - if ($max_buttons < 5 || $max_buttons > 8) { - throw new InlineKeyboardPaginationException('Invalid max buttons, must be between 5 and 8.'); - } - $this->max_buttons = $max_buttons; - $this->force_button_count = $force_button_count; - - return $this; - } - - /** - * Get the current callback format. - * - * @return string - */ - public function getCallbackDataFormat(): string - { - return $this->callback_data_format; - } - - /** - * Set the callback_data format. - * - * @param string $callback_data_format - * - * @return InlineKeyboardPagination - */ - public function setCallbackDataFormat(string $callback_data_format): InlineKeyboardPagination - { - $this->callback_data_format = $callback_data_format; - - return $this; - } - - /** - * Return list of keyboard button labels. - * - * @return array - */ - public function getLabels(): array - { - return $this->labels; - } - - /** - * Set the keyboard button labels. - * - * @param array $labels - * - * @return InlineKeyboardPagination - */ - public function setLabels($labels): InlineKeyboardPagination - { - $this->labels = $labels; - - return $this; - } - - /** - * @inheritdoc - */ - public function setCommand(string $command = 'pagination'): InlineKeyboardPagination - { - $this->command = $command; - - return $this; - } - - /** - * @inheritdoc - * @throws InlineKeyboardPaginationException - */ - public function setSelectedPage(int $selected_page): InlineKeyboardPagination - { - $number_of_pages = $this->getNumberOfPages(); - if ($selected_page < 1 || $selected_page > $number_of_pages) { - throw new InlineKeyboardPaginationException('Invalid selected page, must be between 1 and ' . $number_of_pages); - } - $this->selected_page = $selected_page; - - return $this; - } - - /** - * Get the number of items shown per page. - * - * @return int - */ - public function getItemsPerPage(): int - { - return $this->items_per_page; - } - - /** - * Set how many items should be shown per page. - * - * @param int $items_per_page - * - * @return InlineKeyboardPagination - * @throws InlineKeyboardPaginationException - */ - public function setItemsPerPage($items_per_page): InlineKeyboardPagination - { - if ($items_per_page <= 0) { - throw new InlineKeyboardPaginationException('Invalid number of items per page, must be at least 1'); - } - $this->items_per_page = $items_per_page; - - return $this; - } - - /** - * Set the items for the pagination. - * - * @param array $items - * - * @return InlineKeyboardPagination - * @throws InlineKeyboardPaginationException - */ - public function setItems(array $items): InlineKeyboardPagination - { - if (empty($items)) { - throw new InlineKeyboardPaginationException('Items list empty.'); - } - $this->items = $items; - - return $this; - } - - /** - * Calculate and return the number of pages. - * - * @return int - */ - public function getNumberOfPages(): int - { - return (int) ceil(count($this->items) / $this->items_per_page); - } - - /** - * TelegramBotPagination constructor. - * - * @inheritdoc - * @throws InlineKeyboardPaginationException - */ - public function __construct(array $items, string $command = 'pagination', int $selected_page = 1, int $items_per_page = 5) - { - $this->setCommand($command); - $this->setItemsPerPage($items_per_page); - $this->setItems($items); - $this->setSelectedPage($selected_page); - } - - /** - * @inheritdoc - * @throws InlineKeyboardPaginationException - */ - public function getPagination(int $selected_page = null): array - { - if ($selected_page !== null) { - $this->setSelectedPage($selected_page); - } - - return [ - 'items' => $this->getPreparedItems(), - 'keyboard' => $this->generateKeyboard(), - ]; - } - - /** - * Generate the keyboard with the correctly labelled buttons. - * - * @return array - */ - protected function generateKeyboard(): array - { - $buttons = []; - $number_of_pages = $this->getNumberOfPages(); - - if ($number_of_pages > $this->max_buttons) { - $buttons[1] = $this->generateButton(1); - - $range = $this->generateRange(); - for ($i = $range['from']; $i < $range['to']; $i++) { - $buttons[$i] = $this->generateButton($i); - } - - $buttons[$number_of_pages] = $this->generateButton($number_of_pages); - } else { - for ($i = 1; $i <= $number_of_pages; $i++) { - $buttons[$i] = $this->generateButton($i); - } - } - - // Set the correct labels. - foreach ($buttons as $page => &$button) { - $in_first_block = $this->selected_page <= 3 && $page <= 3; - $in_last_block = $this->selected_page >= $number_of_pages - 2 && $page >= $number_of_pages - 2; - - $label_key = 'next'; - if ($page === $this->selected_page) { - $label_key = 'current'; - } elseif ($in_first_block || $in_last_block) { - $label_key = 'default'; - } elseif ($page === 1) { - $label_key = 'first'; - } elseif ($page === $number_of_pages) { - $label_key = 'last'; - } elseif ($page < $this->selected_page) { - $label_key = 'previous'; - } - - $label = $this->labels[$label_key] ?? ''; - - if ($label === '') { - $button = null; - continue; - } - - $button['text'] = sprintf($label, $page); - } - - return array_values(array_filter($buttons)); - } - - /** - * Get the range of intermediate buttons for the keyboard. - * - * @return array - */ - protected function generateRange(): array - { - $number_of_intermediate_buttons = $this->max_buttons - 2; - $number_of_pages = $this->getNumberOfPages(); - - if ($this->selected_page === 1) { - $from = 2; - $to = $this->max_buttons; - } elseif ($this->selected_page === $number_of_pages) { - $from = $number_of_pages - $number_of_intermediate_buttons; - $to = $number_of_pages; - } else { - if ($this->selected_page < 3) { - $from = $this->selected_page; - $to = $this->selected_page + $number_of_intermediate_buttons; - } elseif (($number_of_pages - $this->selected_page) < 3) { - $from = $number_of_pages - $number_of_intermediate_buttons; - $to = $number_of_pages; - } else { - // @todo: Find a nicer solution for page 3 - if ($this->force_button_count) { - $from = $this->selected_page - floor($number_of_intermediate_buttons / 2); - $to = $this->selected_page + ceil($number_of_intermediate_buttons / 2) + ($this->selected_page === 3 && $this->max_buttons > 5); - } else { - $from = $this->selected_page - 1; - $to = $this->selected_page + ($this->selected_page === 3 ? $number_of_intermediate_buttons - 1 : 2); - } - } - } - - return compact('from', 'to'); - } - - /** - * Generate the button for the passed page. - * - * @param int $page - * - * @return array - */ - protected function generateButton(int $page): array - { - return [ - 'text' => (string) $page, - 'callback_data' => $this->generateCallbackData($page), - ]; - } - - /** - * Generate the callback data for the passed page. - * - * @param int $page - * - * @return string - */ - protected function generateCallbackData(int $page): string - { - return str_replace( - ['{COMMAND}', '{OLD_PAGE}', '{NEW_PAGE}'], - [$this->command, $this->selected_page, $page], - $this->callback_data_format - ); - } - - /** - * Get the prepared items for the selected page. - * - * @return array - */ - protected function getPreparedItems(): array - { - return array_slice($this->items, $this->getOffset(), $this->items_per_page); - } - - /** - * Get the items offset for the selected page. - * - * @return int - */ - protected function getOffset(): int - { - return $this->items_per_page * ($this->selected_page - 1); - } - - /** - * Get the parameters from the callback query. - * - * @todo Possibly make it work for custon formats too? - * - * @param string $data - * - * @return array - */ - public static function getParametersFromCallbackData($data): array - { - parse_str($data, $params); - - return $params; - } + /** + * @var integer + */ + protected $items_per_page; + + /** + * @var integer + */ + protected $max_buttons = 5; + + /** + * @var bool + */ + protected $force_button_count = false; + + /** + * @var integer + */ + protected $selected_page; + + /** + * @var array + */ + protected $items; + + /** + * @var integer + */ + protected $range_offset = 1; + + /** + * @var string + */ + protected $command; + + /** + * @var string + */ + protected $callback_data_format = 'command={COMMAND}&oldPage={OLD_PAGE}&newPage={NEW_PAGE}'; + + /** + * @var array + */ + protected $labels = [ + 'default' => '%d', + 'first' => '« %d', + 'previous' => '‹ %d', + 'current' => '· %d ·', + 'next' => '%d ›', + 'last' => '%d »', + ]; + + /** + * @inheritdoc + * @throws InlineKeyboardPaginationException + */ + public function setMaxButtons(int $max_buttons = 5, bool $force_button_count = false): InlineKeyboardPagination + { + if ($max_buttons < 3 || $max_buttons > 8) { + throw new InlineKeyboardPaginationException('Invalid max buttons, must be between 3 and 8.'); + } + $this->max_buttons = $max_buttons; + $this->force_button_count = $force_button_count; + + return $this; + } + + /** + * Get the current callback format. + * + * @return string + */ + public function getCallbackDataFormat(): string + { + return $this->callback_data_format; + } + + /** + * Set the callback_data format. + * + * @param string $callback_data_format + * + * @return InlineKeyboardPagination + */ + public function setCallbackDataFormat(string $callback_data_format): InlineKeyboardPagination + { + $this->callback_data_format = $callback_data_format; + + return $this; + } + + /** + * Return list of keyboard button labels. + * + * @return array + */ + public function getLabels(): array + { + return $this->labels; + } + + /** + * Set the keyboard button labels. + * + * @param array $labels + * + * @return InlineKeyboardPagination + */ + public function setLabels($labels): InlineKeyboardPagination + { + $this->labels = $labels; + + return $this; + } + + /** + * @inheritdoc + */ + public function setCommand(string $command = 'pagination'): InlineKeyboardPagination + { + $this->command = $command; + + return $this; + } + + /** + * @inheritdoc + * @throws InlineKeyboardPaginationException + */ + public function setSelectedPage(int $selected_page): InlineKeyboardPagination + { + $number_of_pages = $this->getNumberOfPages(); + /*if ($selected_page < 1 || $selected_page > $number_of_pages) { + throw new CustomInlineKeyboardPaginationException('Invalid selected page, must be between 1 and ' . $number_of_pages); + }*/ + + // if current page is greater than total pages... + if ($selected_page > $number_of_pages) { + // set current page to last page + $selected_page = $number_of_pages; + } + // if current page is less than first page... + if ($selected_page < 1) { + // set current page to first page + $selected_page = 1; + } + $this->selected_page = $selected_page; + + return $this; + } + + /** + * Get the number of items shown per page. + * + * @return int + */ + public function getItemsPerPage(): int + { + return $this->items_per_page; + } + + /** + * Set how many items should be shown per page. + * + * @param int $items_per_page + * + * @return InlineKeyboardPagination + * @throws InlineKeyboardPaginationException + */ + public function setItemsPerPage($items_per_page): InlineKeyboardPagination + { + if ($items_per_page <= 0) { + throw new InlineKeyboardPaginationException('Invalid number of items per page, must be at least 1'); + } + $this->items_per_page = $items_per_page; + + return $this; + } + + /** + * Set the items for the pagination. + * + * @param array $items + * + * @return InlineKeyboardPagination + * @throws InlineKeyboardPaginationException + */ + public function setItems(array $items): InlineKeyboardPagination + { + if (empty($items)) { + throw new InlineKeyboardPaginationException('Items list empty.'); + } + $this->items = $items; + + return $this; + } + + /** + * Set max number of pages based on labels which user defined + * + * @return InlineKeyboardPagination + * @throws InlineKeyboardPaginationException + */ + public function setMaxPageBasedOnLabels(): InlineKeyboardPagination + { + $max_buttons = 0; + $count = count($this->labels); + if ($count < 2) { + throw new InlineKeyboardPaginationException('Invalid number of labels was passed to paginator'); + } + + if (isset($this->labels['current'])) { + $max_buttons++; + } + + if (isset($this->labels['first'])) { + $max_buttons++; + } + + if (isset($this->labels['last'])) { + $max_buttons++; + } + + if (isset($this->labels['previous'])) { + $max_buttons++; + } + + if (isset($this->labels['next'])) { + $max_buttons++; + } + $max_buttons += $this->range_offset*2; + + $this->max_buttons = $max_buttons; + + return $this; + } + + /** + * Set offset of range + * + * @return InlineKeyboardPagination + * @throws InlineKeyboardPaginationException + */ + public function setRangeOffset($offset): InlineKeyboardPagination + { + if ($offset < 0 || !is_numeric($offset)) { + throw new InlineKeyboardPaginationException('Invalid offset for range'); + } + + $this->range_offset = $offset; + + return $this; + } + + /** + * Calculate and return the number of pages. + * + * @return int + */ + public function getNumberOfPages(): int + { + return (int) ceil(count($this->items) / $this->items_per_page); + } + + /** + * TelegramBotPagination constructor. + * + * @inheritdoc + * @throws InlineKeyboardPaginationException + */ + public function __construct(array $items, string $command = 'pagination', int $selected_page = 1, int $items_per_page = 5) + { + $this->setCommand($command); + $this->setItemsPerPage($items_per_page); + $this->setItems($items); + $this->setSelectedPage($selected_page); + } + + /** + * @inheritdoc + * @throws InlineKeyboardPaginationException + */ + public function getPagination(int $selected_page = null): array + { + if ($selected_page !== null) { + $this->setSelectedPage($selected_page); + } + + return [ + 'items' => $this->getPreparedItems(), + 'keyboard' => $this->generateKeyboard(), + ]; + } + + /** + * Generate the keyboard with the correctly labelled buttons. + * + * @return array + */ + protected function generateKeyboard(): array + { + $buttons = []; + $number_of_pages = $this->getNumberOfPages(); + + if ($number_of_pages === 1) { + return $buttons; + } + + if ($number_of_pages > $this->max_buttons) { + if ($this->selected_page > 1) { + // get previous page num + $buttons[] = $this->generateButton($this->selected_page - 1, 'previous'); + } + // for first pages + if($this->selected_page > $this->range_offset + 1 && $number_of_pages >= $this->max_buttons){ + $buttons[] = $this->generateButton(1, 'first'); + } + + $range_offsets = $this->generateRange(); + // loop to show links to range of pages around current page + for ($i = $range_offsets['from'] ; $i < $range_offsets['to'] ; $i++) { + // if it's a valid page number... + if ($i == $this->selected_page) { + $buttons[] = $this->generateButton($this->selected_page, 'current'); + } elseif (($i > 0) && ($i <= $number_of_pages)) { + $buttons[] = $this->generateButton($i, 'default'); + } + } + + // if not on last page, show forward and last page links + if($this->selected_page + $this->range_offset < $number_of_pages && $number_of_pages >= $this->max_buttons){ + $buttons[] = $this->generateButton($number_of_pages, 'last'); + } + if ($this->selected_page != $number_of_pages && $number_of_pages > 1) { + $buttons[] = $this->generateButton($this->selected_page + 1, 'next'); + } + } else { + for ($i = 1; $i <= $number_of_pages; $i++) { + // if it's a valid page number... + if ($i == $this->selected_page) { + $buttons[] = $this->generateButton($this->selected_page, 'current'); + } elseif (($i > 0) && ($i <= $number_of_pages)) { + $buttons[] = $this->generateButton($i, 'default'); + } + } + } + + // Set the correct labels. + foreach ($buttons as $page => &$button) { + + $label_key = $button['label']; + + $label = $this->labels[$label_key] ?? ''; + + if ($label === '') { + $button = null; + continue; + } + + $button['text'] = sprintf($label, $button['text']); + } + + return array_values(array_filter($buttons)); + } + + /** + * Get the range of intermediate buttons for the keyboard. + * + * @return array + */ + protected function generateRange(): array + { + $number_of_pages = $this->getNumberOfPages(); + + $from = $this->selected_page - $this->range_offset; + $to = (($this->selected_page + $this->range_offset) + 1); + $last = $number_of_pages - $this->selected_page; + if($number_of_pages - $this->selected_page <= $this->range_offset) + $from -= ($this->range_offset) - $last; + if($this->selected_page < $this->range_offset + 1 ) + $to += ($this->range_offset + 1) - $this->selected_page; + + return compact('from', 'to'); + } + + /** + * Generate the button for the passed page. + * + * @param int $page + * @param string $label + * + * @return array + */ + protected function generateButton(int $page, string $label): array + { + return [ + 'text' => (string) $page, + 'callback_data' => $this->generateCallbackData($page), + 'label' => $label, + ]; + } + + /** + * Generate the callback data for the passed page. + * + * @param int $page + * + * @return string + */ + protected function generateCallbackData(int $page): string + { + return str_replace( + ['{COMMAND}', '{OLD_PAGE}', '{NEW_PAGE}'], + [$this->command, $this->selected_page, $page], + $this->callback_data_format + ); + } + + /** + * Get the prepared items for the selected page. + * + * @return array + */ + protected function getPreparedItems(): array + { + return array_slice($this->items, $this->getOffset(), $this->items_per_page); + } + + /** + * Get the items offset for the selected page. + * + * @return int + */ + protected function getOffset(): int + { + return $this->items_per_page * ($this->selected_page - 1); + } + + /** + * Get the parameters from the callback query. + * + * @todo Possibly make it work for custom formats too? + * + * @param string $data + * + * @return array + */ + public static function getParametersFromCallbackData($data): array + { + parse_str($data, $params); + + return $params; + } } From d98f050adbb883924088dfe037700231737a5f7a Mon Sep 17 00:00:00 2001 From: Mehrdad Date: Mon, 16 Oct 2017 00:23:44 +0330 Subject: [PATCH 2/3] fix PSR-2 format issue --- src/InlineKeyboardPagination.php | 912 ++++++++++++++++--------------- 1 file changed, 459 insertions(+), 453 deletions(-) diff --git a/src/InlineKeyboardPagination.php b/src/InlineKeyboardPagination.php index 7e23209..350ebaf 100644 --- a/src/InlineKeyboardPagination.php +++ b/src/InlineKeyboardPagination.php @@ -11,457 +11,463 @@ */ class InlineKeyboardPagination implements InlineKeyboardPaginator { - /** - * @var integer - */ - protected $items_per_page; - - /** - * @var integer - */ - protected $max_buttons = 5; - - /** - * @var bool - */ - protected $force_button_count = false; - - /** - * @var integer - */ - protected $selected_page; - - /** - * @var array - */ - protected $items; - - /** - * @var integer - */ - protected $range_offset = 1; - - /** - * @var string - */ - protected $command; - - /** - * @var string - */ - protected $callback_data_format = 'command={COMMAND}&oldPage={OLD_PAGE}&newPage={NEW_PAGE}'; - - /** - * @var array - */ - protected $labels = [ - 'default' => '%d', - 'first' => '« %d', - 'previous' => '‹ %d', - 'current' => '· %d ·', - 'next' => '%d ›', - 'last' => '%d »', - ]; - - /** - * @inheritdoc - * @throws InlineKeyboardPaginationException - */ - public function setMaxButtons(int $max_buttons = 5, bool $force_button_count = false): InlineKeyboardPagination - { - if ($max_buttons < 3 || $max_buttons > 8) { - throw new InlineKeyboardPaginationException('Invalid max buttons, must be between 3 and 8.'); - } - $this->max_buttons = $max_buttons; - $this->force_button_count = $force_button_count; - - return $this; - } - - /** - * Get the current callback format. - * - * @return string - */ - public function getCallbackDataFormat(): string - { - return $this->callback_data_format; - } - - /** - * Set the callback_data format. - * - * @param string $callback_data_format - * - * @return InlineKeyboardPagination - */ - public function setCallbackDataFormat(string $callback_data_format): InlineKeyboardPagination - { - $this->callback_data_format = $callback_data_format; - - return $this; - } - - /** - * Return list of keyboard button labels. - * - * @return array - */ - public function getLabels(): array - { - return $this->labels; - } - - /** - * Set the keyboard button labels. - * - * @param array $labels - * - * @return InlineKeyboardPagination - */ - public function setLabels($labels): InlineKeyboardPagination - { - $this->labels = $labels; - - return $this; - } - - /** - * @inheritdoc - */ - public function setCommand(string $command = 'pagination'): InlineKeyboardPagination - { - $this->command = $command; - - return $this; - } - - /** - * @inheritdoc - * @throws InlineKeyboardPaginationException - */ - public function setSelectedPage(int $selected_page): InlineKeyboardPagination - { - $number_of_pages = $this->getNumberOfPages(); - /*if ($selected_page < 1 || $selected_page > $number_of_pages) { - throw new CustomInlineKeyboardPaginationException('Invalid selected page, must be between 1 and ' . $number_of_pages); - }*/ - - // if current page is greater than total pages... - if ($selected_page > $number_of_pages) { - // set current page to last page - $selected_page = $number_of_pages; - } - // if current page is less than first page... - if ($selected_page < 1) { - // set current page to first page - $selected_page = 1; - } - $this->selected_page = $selected_page; - - return $this; - } - - /** - * Get the number of items shown per page. - * - * @return int - */ - public function getItemsPerPage(): int - { - return $this->items_per_page; - } - - /** - * Set how many items should be shown per page. - * - * @param int $items_per_page - * - * @return InlineKeyboardPagination - * @throws InlineKeyboardPaginationException - */ - public function setItemsPerPage($items_per_page): InlineKeyboardPagination - { - if ($items_per_page <= 0) { - throw new InlineKeyboardPaginationException('Invalid number of items per page, must be at least 1'); - } - $this->items_per_page = $items_per_page; - - return $this; - } - - /** - * Set the items for the pagination. - * - * @param array $items - * - * @return InlineKeyboardPagination - * @throws InlineKeyboardPaginationException - */ - public function setItems(array $items): InlineKeyboardPagination - { - if (empty($items)) { - throw new InlineKeyboardPaginationException('Items list empty.'); - } - $this->items = $items; - - return $this; - } - - /** - * Set max number of pages based on labels which user defined - * - * @return InlineKeyboardPagination - * @throws InlineKeyboardPaginationException - */ - public function setMaxPageBasedOnLabels(): InlineKeyboardPagination - { - $max_buttons = 0; - $count = count($this->labels); - if ($count < 2) { - throw new InlineKeyboardPaginationException('Invalid number of labels was passed to paginator'); - } - - if (isset($this->labels['current'])) { - $max_buttons++; - } - - if (isset($this->labels['first'])) { - $max_buttons++; - } - - if (isset($this->labels['last'])) { - $max_buttons++; - } - - if (isset($this->labels['previous'])) { - $max_buttons++; - } - - if (isset($this->labels['next'])) { - $max_buttons++; - } - $max_buttons += $this->range_offset*2; - - $this->max_buttons = $max_buttons; - - return $this; - } - - /** - * Set offset of range - * - * @return InlineKeyboardPagination - * @throws InlineKeyboardPaginationException - */ - public function setRangeOffset($offset): InlineKeyboardPagination - { - if ($offset < 0 || !is_numeric($offset)) { - throw new InlineKeyboardPaginationException('Invalid offset for range'); - } - - $this->range_offset = $offset; - - return $this; - } - - /** - * Calculate and return the number of pages. - * - * @return int - */ - public function getNumberOfPages(): int - { - return (int) ceil(count($this->items) / $this->items_per_page); - } - - /** - * TelegramBotPagination constructor. - * - * @inheritdoc - * @throws InlineKeyboardPaginationException - */ - public function __construct(array $items, string $command = 'pagination', int $selected_page = 1, int $items_per_page = 5) - { - $this->setCommand($command); - $this->setItemsPerPage($items_per_page); - $this->setItems($items); - $this->setSelectedPage($selected_page); - } - - /** - * @inheritdoc - * @throws InlineKeyboardPaginationException - */ - public function getPagination(int $selected_page = null): array - { - if ($selected_page !== null) { - $this->setSelectedPage($selected_page); - } - - return [ - 'items' => $this->getPreparedItems(), - 'keyboard' => $this->generateKeyboard(), - ]; - } - - /** - * Generate the keyboard with the correctly labelled buttons. - * - * @return array - */ - protected function generateKeyboard(): array - { - $buttons = []; - $number_of_pages = $this->getNumberOfPages(); - - if ($number_of_pages === 1) { - return $buttons; - } - - if ($number_of_pages > $this->max_buttons) { - if ($this->selected_page > 1) { - // get previous page num - $buttons[] = $this->generateButton($this->selected_page - 1, 'previous'); - } - // for first pages - if($this->selected_page > $this->range_offset + 1 && $number_of_pages >= $this->max_buttons){ - $buttons[] = $this->generateButton(1, 'first'); - } - - $range_offsets = $this->generateRange(); - // loop to show links to range of pages around current page - for ($i = $range_offsets['from'] ; $i < $range_offsets['to'] ; $i++) { - // if it's a valid page number... - if ($i == $this->selected_page) { - $buttons[] = $this->generateButton($this->selected_page, 'current'); - } elseif (($i > 0) && ($i <= $number_of_pages)) { - $buttons[] = $this->generateButton($i, 'default'); - } - } - - // if not on last page, show forward and last page links - if($this->selected_page + $this->range_offset < $number_of_pages && $number_of_pages >= $this->max_buttons){ - $buttons[] = $this->generateButton($number_of_pages, 'last'); - } - if ($this->selected_page != $number_of_pages && $number_of_pages > 1) { - $buttons[] = $this->generateButton($this->selected_page + 1, 'next'); - } - } else { - for ($i = 1; $i <= $number_of_pages; $i++) { - // if it's a valid page number... - if ($i == $this->selected_page) { - $buttons[] = $this->generateButton($this->selected_page, 'current'); - } elseif (($i > 0) && ($i <= $number_of_pages)) { - $buttons[] = $this->generateButton($i, 'default'); - } - } - } - - // Set the correct labels. - foreach ($buttons as $page => &$button) { - - $label_key = $button['label']; - - $label = $this->labels[$label_key] ?? ''; - - if ($label === '') { - $button = null; - continue; - } - - $button['text'] = sprintf($label, $button['text']); - } - - return array_values(array_filter($buttons)); - } - - /** - * Get the range of intermediate buttons for the keyboard. - * - * @return array - */ - protected function generateRange(): array - { - $number_of_pages = $this->getNumberOfPages(); - - $from = $this->selected_page - $this->range_offset; - $to = (($this->selected_page + $this->range_offset) + 1); - $last = $number_of_pages - $this->selected_page; - if($number_of_pages - $this->selected_page <= $this->range_offset) - $from -= ($this->range_offset) - $last; - if($this->selected_page < $this->range_offset + 1 ) - $to += ($this->range_offset + 1) - $this->selected_page; - - return compact('from', 'to'); - } - - /** - * Generate the button for the passed page. - * - * @param int $page - * @param string $label - * - * @return array - */ - protected function generateButton(int $page, string $label): array - { - return [ - 'text' => (string) $page, - 'callback_data' => $this->generateCallbackData($page), - 'label' => $label, - ]; - } - - /** - * Generate the callback data for the passed page. - * - * @param int $page - * - * @return string - */ - protected function generateCallbackData(int $page): string - { - return str_replace( - ['{COMMAND}', '{OLD_PAGE}', '{NEW_PAGE}'], - [$this->command, $this->selected_page, $page], - $this->callback_data_format - ); - } - - /** - * Get the prepared items for the selected page. - * - * @return array - */ - protected function getPreparedItems(): array - { - return array_slice($this->items, $this->getOffset(), $this->items_per_page); - } - - /** - * Get the items offset for the selected page. - * - * @return int - */ - protected function getOffset(): int - { - return $this->items_per_page * ($this->selected_page - 1); - } - - /** - * Get the parameters from the callback query. - * - * @todo Possibly make it work for custom formats too? - * - * @param string $data - * - * @return array - */ - public static function getParametersFromCallbackData($data): array - { - parse_str($data, $params); - - return $params; - } + /** + * @var integer + */ + protected $items_per_page; + + /** + * @var integer + */ + protected $max_buttons = 5; + + /** + * @var bool + */ + protected $force_button_count = false; + + /** + * @var integer + */ + protected $selected_page; + + /** + * @var array + */ + protected $items; + + /** + * @var integer + */ + protected $range_offset = 1; + + /** + * @var string + */ + protected $command; + + /** + * @var string + */ + protected $callback_data_format = 'command={COMMAND}&oldPage={OLD_PAGE}&newPage={NEW_PAGE}'; + + /** + * @var array + */ + protected $labels = [ + 'default' => '%d', + 'first' => '« %d', + 'previous' => '‹ %d', + 'current' => '· %d ·', + 'next' => '%d ›', + 'last' => '%d »', + ]; + + /** + * @inheritdoc + * @throws InlineKeyboardPaginationException + */ + public function setMaxButtons(int $max_buttons = 5, bool $force_button_count = false): InlineKeyboardPagination + { + if ($max_buttons < 3 || $max_buttons > 8) { + throw new InlineKeyboardPaginationException('Invalid max buttons, must be between 3 and 8.'); + } + $this->max_buttons = $max_buttons; + $this->force_button_count = $force_button_count; + + return $this; + } + + /** + * Get the current callback format. + * + * @return string + */ + public function getCallbackDataFormat(): string + { + return $this->callback_data_format; + } + + /** + * Set the callback_data format. + * + * @param string $callback_data_format + * + * @return InlineKeyboardPagination + */ + public function setCallbackDataFormat(string $callback_data_format): InlineKeyboardPagination + { + $this->callback_data_format = $callback_data_format; + + return $this; + } + + /** + * Return list of keyboard button labels. + * + * @return array + */ + public function getLabels(): array + { + return $this->labels; + } + + /** + * Set the keyboard button labels. + * + * @param array $labels + * + * @return InlineKeyboardPagination + */ + public function setLabels($labels): InlineKeyboardPagination + { + $this->labels = $labels; + + return $this; + } + + /** + * @inheritdoc + */ + public function setCommand(string $command = 'pagination'): InlineKeyboardPagination + { + $this->command = $command; + + return $this; + } + + /** + * @inheritdoc + * @throws InlineKeyboardPaginationException + */ + public function setSelectedPage(int $selected_page): InlineKeyboardPagination + { + $number_of_pages = $this->getNumberOfPages(); + /*if ($selected_page < 1 || $selected_page > $number_of_pages) { + throw new CustomInlineKeyboardPaginationException('Invalid selected page, must be between 1 and ' . $number_of_pages); + }*/ + + // if current page is greater than total pages... + if ($selected_page > $number_of_pages) { + // set current page to last page + $selected_page = $number_of_pages; + } + // if current page is less than first page... + if ($selected_page < 1) { + // set current page to first page + $selected_page = 1; + } + $this->selected_page = $selected_page; + + return $this; + } + + /** + * Get the number of items shown per page. + * + * @return int + */ + public function getItemsPerPage(): int + { + return $this->items_per_page; + } + + /** + * Set how many items should be shown per page. + * + * @param int $items_per_page + * + * @return InlineKeyboardPagination + * @throws InlineKeyboardPaginationException + */ + public function setItemsPerPage($items_per_page): InlineKeyboardPagination + { + if ($items_per_page <= 0) { + throw new InlineKeyboardPaginationException('Invalid number of items per page, must be at least 1'); + } + $this->items_per_page = $items_per_page; + + return $this; + } + + /** + * Set the items for the pagination. + * + * @param array $items + * + * @return InlineKeyboardPagination + * @throws InlineKeyboardPaginationException + */ + public function setItems(array $items): InlineKeyboardPagination + { + if (empty($items)) { + throw new InlineKeyboardPaginationException('Items list empty.'); + } + $this->items = $items; + + return $this; + } + + /** + * Set max number of pages based on labels which user defined + * + * @return InlineKeyboardPagination + * @throws InlineKeyboardPaginationException + */ + public function setMaxPageBasedOnLabels(): InlineKeyboardPagination + { + $max_buttons = 0; + $count = count($this->labels); + if ($count < 2) { + throw new InlineKeyboardPaginationException('Invalid number of labels was passed to paginator'); + } + + if (isset($this->labels['current'])) { + $max_buttons++; + } + + if (isset($this->labels['first'])) { + $max_buttons++; + } + + if (isset($this->labels['last'])) { + $max_buttons++; + } + + if (isset($this->labels['previous'])) { + $max_buttons++; + } + + if (isset($this->labels['next'])) { + $max_buttons++; + } + $max_buttons += $this->range_offset * 2; + + $this->max_buttons = $max_buttons; + + return $this; + } + + /** + * Set offset of range + * + * @return InlineKeyboardPagination + * @throws InlineKeyboardPaginationException + */ + public function setRangeOffset($offset): InlineKeyboardPagination + { + if ($offset < 0 || !is_numeric($offset)) { + throw new InlineKeyboardPaginationException('Invalid offset for range'); + } + + $this->range_offset = $offset; + + return $this; + } + + /** + * Calculate and return the number of pages. + * + * @return int + */ + public function getNumberOfPages(): int + { + return (int)ceil(count($this->items) / $this->items_per_page); + } + + /** + * TelegramBotPagination constructor. + * + * @inheritdoc + * @throws InlineKeyboardPaginationException + */ + public function __construct( + array $items, + string $command = 'pagination', + int $selected_page = 1, + int $items_per_page = 5 + ) { + $this->setCommand($command); + $this->setItemsPerPage($items_per_page); + $this->setItems($items); + $this->setSelectedPage($selected_page); + } + + /** + * @inheritdoc + * @throws InlineKeyboardPaginationException + */ + public function getPagination(int $selected_page = null): array + { + if ($selected_page !== null) { + $this->setSelectedPage($selected_page); + } + + return [ + 'items' => $this->getPreparedItems(), + 'keyboard' => $this->generateKeyboard(), + ]; + } + + /** + * Generate the keyboard with the correctly labelled buttons. + * + * @return array + */ + protected function generateKeyboard(): array + { + $buttons = []; + $number_of_pages = $this->getNumberOfPages(); + + if ($number_of_pages === 1) { + return $buttons; + } + + if ($number_of_pages > $this->max_buttons) { + if ($this->selected_page > 1) { + // get previous page num + $buttons[] = $this->generateButton($this->selected_page - 1, 'previous'); + } + // for first pages + if ($this->selected_page > $this->range_offset + 1 && $number_of_pages >= $this->max_buttons) { + $buttons[] = $this->generateButton(1, 'first'); + } + + $range_offsets = $this->generateRange(); + // loop to show links to range of pages around current page + for ($i = $range_offsets['from']; $i < $range_offsets['to']; $i++) { + // if it's a valid page number... + if ($i == $this->selected_page) { + $buttons[] = $this->generateButton($this->selected_page, 'current'); + } elseif (($i > 0) && ($i <= $number_of_pages)) { + $buttons[] = $this->generateButton($i, 'default'); + } + } + + // if not on last page, show forward and last page links + if ($this->selected_page + $this->range_offset < $number_of_pages && $number_of_pages >= $this->max_buttons) { + $buttons[] = $this->generateButton($number_of_pages, 'last'); + } + if ($this->selected_page != $number_of_pages && $number_of_pages > 1) { + $buttons[] = $this->generateButton($this->selected_page + 1, 'next'); + } + } else { + for ($i = 1; $i <= $number_of_pages; $i++) { + // if it's a valid page number... + if ($i == $this->selected_page) { + $buttons[] = $this->generateButton($this->selected_page, 'current'); + } elseif (($i > 0) && ($i <= $number_of_pages)) { + $buttons[] = $this->generateButton($i, 'default'); + } + } + } + + // Set the correct labels. + foreach ($buttons as $page => &$button) { + + $label_key = $button['label']; + + $label = $this->labels[$label_key] ?? ''; + + if ($label === '') { + $button = null; + continue; + } + + $button['text'] = sprintf($label, $button['text']); + } + + return array_values(array_filter($buttons)); + } + + /** + * Get the range of intermediate buttons for the keyboard. + * + * @return array + */ + protected function generateRange(): array + { + $number_of_pages = $this->getNumberOfPages(); + + $from = $this->selected_page - $this->range_offset; + $to = (($this->selected_page + $this->range_offset) + 1); + $last = $number_of_pages - $this->selected_page; + if ($number_of_pages - $this->selected_page <= $this->range_offset) { + $from -= ($this->range_offset) - $last; + } + if ($this->selected_page < $this->range_offset + 1) { + $to += ($this->range_offset + 1) - $this->selected_page; + } + + return compact('from', 'to'); + } + + /** + * Generate the button for the passed page. + * + * @param int $page + * @param string $label + * + * @return array + */ + protected function generateButton(int $page, string $label): array + { + return [ + 'text' => (string)$page, + 'callback_data' => $this->generateCallbackData($page), + 'label' => $label, + ]; + } + + /** + * Generate the callback data for the passed page. + * + * @param int $page + * + * @return string + */ + protected function generateCallbackData(int $page): string + { + return str_replace( + ['{COMMAND}', '{OLD_PAGE}', '{NEW_PAGE}'], + [$this->command, $this->selected_page, $page], + $this->callback_data_format + ); + } + + /** + * Get the prepared items for the selected page. + * + * @return array + */ + protected function getPreparedItems(): array + { + return array_slice($this->items, $this->getOffset(), $this->items_per_page); + } + + /** + * Get the items offset for the selected page. + * + * @return int + */ + protected function getOffset(): int + { + return $this->items_per_page * ($this->selected_page - 1); + } + + /** + * Get the parameters from the callback query. + * + * @todo Possibly make it work for custom formats too? + * + * @param string $data + * + * @return array + */ + public static function getParametersFromCallbackData($data): array + { + parse_str($data, $params); + + return $params; + } } From f10d558a710cfa900749d6b25efd5bd1e4e09f38 Mon Sep 17 00:00:00 2001 From: Mehrdad Date: Tue, 17 Oct 2017 11:08:07 +0330 Subject: [PATCH 3/3] Change name of setMaxPageBasedOnLabels() to setMaxButtonsBasedOnLabels() --- README.md | 2 +- tests/InlineKeyboardPaginationTest.php | 603 +++++++++++++++---------- 2 files changed, 370 insertions(+), 235 deletions(-) diff --git a/README.md b/README.md index dd8ba0a..e8e381e 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ $ikp->setLabels($labels); *optional: But recommended. if you want that max_page will set according to labels you defined, * please call this method. if you remove $label elements and then call this method, max_page will be defined according to labels */ -$ikp->setMaxPageBasedOnLabels(); +$ikp->setMaxButtonsBasedOnLabels(); $ikp->setCallbackDataFormat($callback_data_format); // Get pagination. diff --git a/tests/InlineKeyboardPaginationTest.php b/tests/InlineKeyboardPaginationTest.php index 18f576d..2feb4cf 100644 --- a/tests/InlineKeyboardPaginationTest.php +++ b/tests/InlineKeyboardPaginationTest.php @@ -7,334 +7,469 @@ /** * Class InlineKeyboardPaginationTest */ -final class InlineKeyboardPaginationTest extends \PHPUnit\Framework\TestCase + +/** + * Class InlineKeyboardPagination + * Based on https://github.com/php-telegram-bot/inline-keyboard-pagination + * + * @package MehrdadKhoddami\TelegramBot\InlineKeyboardPagination + */ +class InlineKeyboardPagination implements InlineKeyboardPaginator { /** - * @var int + * @var integer + */ + protected $items_per_page; + + /** + * @var integer + */ + protected $max_buttons = 5; + + /** + * @var bool + */ + protected $force_button_count = false; + + /** + * @var integer + */ + protected $selected_page; + + /** + * @var array */ - private $items_per_page = 5; + protected $items; /** - * @var int + * @var integer + */ + protected $range_offset = 1; + + /** + * @var string */ - private $selected_page; + protected $command; /** * @var string */ - private $command; + protected $callback_data_format = 'command={COMMAND}&oldPage={OLD_PAGE}&newPage={NEW_PAGE}'; /** * @var array */ - private $items; + protected $labels = [ + 'default' => '%d', + 'first' => '« %d', + 'previous' => '‹ %d', + 'current' => '· %d ·', + 'next' => '%d ›', + 'last' => '%d »', + ]; /** - * InlineKeyboardPaginationTest constructor. + * @inheritdoc + * @throws InlineKeyboardPaginationException */ - public function __construct() + public function setMaxButtons(int $max_buttons = 5, bool $force_button_count = false): InlineKeyboardPagination { - parent::__construct(); + if ($max_buttons < 2 || $max_buttons > 8) { + throw new InlineKeyboardPaginationException('Invalid max buttons, must be between 2 and 8.'); + } + $this->max_buttons = $max_buttons; + $this->force_button_count = $force_button_count; - $this->items = range(1, 100); - $this->command = 'testCommand'; - $this->selected_page = random_int(1, 15); + return $this; } - public function testValidConstructor() + /** + * Set max number of buttons based on labels which user defined & range of selected page + * + * @return InlineKeyboardPagination + * @throws InlineKeyboardPaginationException + */ + public function setMaxButtonsBasedOnLabels(): InlineKeyboardPagination { - $ikp = new InlineKeyboardPagination($this->items, $this->command, $this->selected_page, $this->items_per_page); + $max_buttons = 0; + $count = count($this->labels); + if ($count < 2) { + throw new InlineKeyboardPaginationException('Invalid number of labels was passed to paginator'); + } + + if (isset($this->labels['current'])) { + $max_buttons++; + } - $data = $ikp->getPagination(); + if (isset($this->labels['first'])) { + $max_buttons++; + } + + if (isset($this->labels['last'])) { + $max_buttons++; + } + + if (isset($this->labels['previous'])) { + $max_buttons++; + } + + if (isset($this->labels['next'])) { + $max_buttons++; + } + $max_buttons += $this->range_offset * 2; - $this->assertArrayHasKey('items', $data); - $this->assertCount($this->items_per_page, $data['items']); - $this->assertArrayHasKey('keyboard', $data); - $this->assertArrayHasKey(0, $data['keyboard']); - $this->assertArrayHasKey('text', $data['keyboard'][0]); - $this->assertStringStartsWith("command={$this->command}", $data['keyboard'][0]['callback_data']); + $this->max_buttons = $max_buttons; + + return $this; } /** - * @expectedException \TelegramBot\InlineKeyboardPagination\Exceptions\InlineKeyboardPaginationException - * @expectedExceptionMessage Invalid selected page, must be between 1 and 20 + * Get the current callback format. + * + * @return string */ - public function testInvalidConstructor() + public function getCallbackDataFormat(): string { - $ikp = new InlineKeyboardPagination($this->items, $this->command, 10000, $this->items_per_page); - $ikp->getPagination(); + return $this->callback_data_format; } /** - * @expectedException \TelegramBot\InlineKeyboardPagination\Exceptions\InlineKeyboardPaginationException - * @expectedExceptionMessage Items list empty. + * Set the callback_data format. + * + * @param string $callback_data_format + * + * @return InlineKeyboardPagination */ - public function testEmptyItemsConstructor() + public function setCallbackDataFormat(string $callback_data_format): InlineKeyboardPagination { - $ikp = new InlineKeyboardPagination([]); - $ikp->getPagination(); + $this->callback_data_format = $callback_data_format; + + return $this; } - public function testCallbackDataFormat() + /** + * Return list of keyboard button labels. + * + * @return array + */ + public function getLabels(): array { - $ikp = new InlineKeyboardPagination(range(1, 10), 'cmd', 2, 5); - - self::assertAllButtonPropertiesEqual([ - [ - 'command=cmd&oldPage=2&newPage=1', - 'command=cmd&oldPage=2&newPage=2', - ], - ], 'callback_data', [$ikp->getPagination()['keyboard']]); - - $ikp->setCallbackDataFormat('{COMMAND};{OLD_PAGE};{NEW_PAGE}'); - - self::assertAllButtonPropertiesEqual([ - [ - 'cmd;2;1', - 'cmd;2;2', - ], - ], 'callback_data', [$ikp->getPagination()['keyboard']]); + return $this->labels; } - public function testCallbackDataParser() + /** + * Set the keyboard button labels. + * + * @param array $labels + * + * @return InlineKeyboardPagination + */ + public function setLabels($labels): InlineKeyboardPagination { - $ikp = new InlineKeyboardPagination($this->items, $this->command, $this->selected_page, $this->items_per_page); - $data = $ikp->getPagination(); + $this->labels = $labels; - $callback_data = $ikp::getParametersFromCallbackData($data['keyboard'][0]['callback_data']); + return $this; + } - self::assertSame([ - 'command' => $this->command, - 'oldPage' => "$this->selected_page", - 'newPage' => '1', // because we're getting the button at position 0, which is page 1 - ], $callback_data); + /** + * @inheritdoc + */ + public function setCommand(string $command = 'pagination'): InlineKeyboardPagination + { + $this->command = $command; + + return $this; } - public function testValidPagination() + /** + * @inheritdoc + * @throws InlineKeyboardPaginationException + */ + public function setSelectedPage(int $selected_page): InlineKeyboardPagination { - $ikp = new InlineKeyboardPagination($this->items, $this->command, $this->selected_page, $this->items_per_page); + $number_of_pages = $this->getNumberOfPages(); - $length = (int) ceil(count($this->items) / $this->items_per_page); + // if current page is greater than total pages... + if ($selected_page > $number_of_pages) { + // set current page to last page + $selected_page = $number_of_pages; + } + // if current page is less than first page... + if ($selected_page < 1) { + // set current page to first page + $selected_page = 1; + } + $this->selected_page = $selected_page; + + return $this; + } + + /** + * Get the number of items shown per page. + * + * @return int + */ + public function getItemsPerPage(): int + { + return $this->items_per_page; + } - for ($i = 1; $i < $length; $i++) { - $ikp->getPagination($i); + /** + * Set how many items should be shown per page. + * + * @param int $items_per_page + * + * @return InlineKeyboardPagination + * @throws InlineKeyboardPaginationException + */ + public function setItemsPerPage($items_per_page): InlineKeyboardPagination + { + if ($items_per_page <= 0) { + throw new InlineKeyboardPaginationException('Invalid number of items per page, must be at least 1'); } + $this->items_per_page = $items_per_page; - $this->assertTrue(true); + return $this; } /** - * @expectedException \TelegramBot\InlineKeyboardPagination\Exceptions\InlineKeyboardPaginationException - * @expectedExceptionMessage Invalid selected page, must be between 1 and 20 + * Set the items for the pagination. + * + * @param array $items + * + * @return InlineKeyboardPagination + * @throws InlineKeyboardPaginationException */ - public function testInvalidPagination() + public function setItems(array $items): InlineKeyboardPagination { - $ikp = new InlineKeyboardPagination($this->items, $this->command, $this->selected_page, $this->items_per_page); - $ikp->getPagination($ikp->getNumberOfPages() + 1); + if (empty($items)) { + throw new InlineKeyboardPaginationException('Items list empty.'); + } + $this->items = $items; + + return $this; } - public function testSetMaxButtons() + /** + * Set offset of range + * + * @return InlineKeyboardPagination + * @throws InlineKeyboardPaginationException + */ + public function setRangeOffset($offset): InlineKeyboardPagination { - $ikp = new InlineKeyboardPagination($this->items, $this->command, $this->selected_page, $this->items_per_page); - $ikp->setMaxButtons(6); - self::assertTrue(true); + if ($offset < 0 || !is_numeric($offset)) { + throw new InlineKeyboardPaginationException('Invalid offset for range'); + } + + $this->range_offset = $offset; + + return $this; } - public function testForceButtonsCount() + /** + * Calculate and return the number of pages. + * + * @return int + */ + public function getNumberOfPages(): int { - $ikp = new InlineKeyboardPagination(range(1, 10), 'cbdata', 1, 1); + return (int)ceil(count($this->items) / $this->items_per_page); + } - // testing with 8 flexible buttons - $ikp->setMaxButtons(8, false); + /** + * TelegramBotPagination constructor. + * + * @inheritdoc + * @throws InlineKeyboardPaginationException + */ + public function __construct( + array $items, + string $command = 'pagination', + int $selected_page = 1, + int $items_per_page = 5 + ) { + $this->setCommand($command); + $this->setItemsPerPage($items_per_page); + $this->setItems($items); + $this->setSelectedPage($selected_page); + } - self::assertAllButtonPropertiesEqual([ - ['· 1 ·', '2', '3', '4 ›', '5 ›', '6 ›', '7 ›', '10 »'], - ], 'text', [$ikp->getPagination(1)['keyboard']]); + /** + * @inheritdoc + * @throws InlineKeyboardPaginationException + */ + public function getPagination(int $selected_page = null): array + { + if ($selected_page !== null) { + $this->setSelectedPage($selected_page); + } + + return [ + 'items' => $this->getPreparedItems(), + 'keyboard' => $this->generateKeyboard(), + ]; + } - self::assertAllButtonPropertiesEqual([ - ['« 1', '‹ 4', '· 5 ·', '6 ›', '10 »'], - ], 'text', [$ikp->getPagination(5)['keyboard']]); + /** + * Generate the keyboard with the correctly labelled buttons. + * + * @return array + */ + protected function generateKeyboard(): array + { + $buttons = []; + $number_of_pages = $this->getNumberOfPages(); - // testing with 8 fixed buttons - $ikp->setMaxButtons(8, true); + if ($number_of_pages === 1) { + return $buttons; + } - self::assertAllButtonPropertiesEqual([ - ['· 1 ·', '2', '3', '4 ›', '5 ›', '6 ›', '7 ›', '10 »'], - ], 'text', [$ikp->getPagination(1)['keyboard']]); + if ($number_of_pages > $this->max_buttons) { + if ($this->selected_page > 1) { + // get previous page num + $buttons[] = $this->generateButton($this->selected_page - 1, 'previous'); + } + // for first pages + if ($this->selected_page > $this->range_offset + 1 && $number_of_pages >= $this->max_buttons) { + $buttons[] = $this->generateButton(1, 'first'); + } + + $range_offsets = $this->generateRange(); + // loop to show links to range of pages around current page + for ($i = $range_offsets['from']; $i < $range_offsets['to']; $i++) { + // if it's a valid page number... + if ($i == $this->selected_page) { + $buttons[] = $this->generateButton($this->selected_page, 'current'); + } elseif (($i > 0) && ($i <= $number_of_pages)) { + $buttons[] = $this->generateButton($i, 'default'); + } + } + + // if not on last page, show forward and last page links + if ($this->selected_page + $this->range_offset < $number_of_pages && $number_of_pages >= $this->max_buttons) { + $buttons[] = $this->generateButton($number_of_pages, 'last'); + } + if ($this->selected_page != $number_of_pages && $number_of_pages > 1) { + $buttons[] = $this->generateButton($this->selected_page + 1, 'next'); + } + } else { + for ($i = 1; $i <= $number_of_pages; $i++) { + // if it's a valid page number... + if ($i == $this->selected_page) { + $buttons[] = $this->generateButton($this->selected_page, 'current'); + } elseif (($i > 0) && ($i <= $number_of_pages)) { + $buttons[] = $this->generateButton($i, 'default'); + } + } + } - self::assertAllButtonPropertiesEqual([ - ['· 1 ·', '2', '3', '4 ›', '5 ›', '6 ›', '7 ›', '10 »'], - ], 'text', [$ikp->getPagination(1)['keyboard']]); + // Set the correct labels. + foreach ($buttons as $page => &$button) { - self::assertAllButtonPropertiesEqual([ - ['« 1', '‹ 2', '‹ 3', '‹ 4', '· 5 ·', '6 ›', '7 ›', '10 »'], - ], 'text', [$ikp->getPagination(5)['keyboard']]); + $label_key = $button['label']; - // testing with 7 fixed buttons - $ikp->setMaxButtons(7, true); + $label = $this->labels[$label_key] ?? ''; - self::assertAllButtonPropertiesEqual([ - ['· 1 ·', '2', '3', '4 ›', '5 ›', '6 ›', '10 »'], - ], 'text', [$ikp->getPagination(1)['keyboard']]); + if ($label === '') { + $button = null; + continue; + } - self::assertAllButtonPropertiesEqual([ - ['« 1', '‹ 3', '‹ 4', '· 5 ·', '6 ›', '7 ›', '10 »'], - ], 'text', [$ikp->getPagination(5)['keyboard']]); + $button['text'] = sprintf($label, $button['text']); + } - self::assertAllButtonPropertiesEqual([ - ['« 1', '‹ 5', '‹ 6', '‹ 7', '8', '9', '· 10 ·'], - ], 'text', [$ikp->getPagination(10)['keyboard']]); + return array_values(array_filter($buttons)); } /** - * @expectedException \TelegramBot\InlineKeyboardPagination\Exceptions\InlineKeyboardPaginationException - * @expectedExceptionMessage Invalid max buttons, must be between 5 and 8. + * Get the range of intermediate buttons for the keyboard. + * + * @return array */ - public function testInvalidMaxButtons() + protected function generateRange(): array { - $ikp = new InlineKeyboardPagination($this->items, $this->command, $this->selected_page, $this->items_per_page); - $ikp->setMaxButtons(2); - $ikp->getPagination(); + $number_of_pages = $this->getNumberOfPages(); + + $from = $this->selected_page - $this->range_offset; + $to = (($this->selected_page + $this->range_offset) + 1); + $last = $number_of_pages - $this->selected_page; + if ($number_of_pages - $this->selected_page <= $this->range_offset) { + $from -= ($this->range_offset) - $last; + } + if ($this->selected_page < $this->range_offset + 1) { + $to += ($this->range_offset + 1) - $this->selected_page; + } + + return compact('from', 'to'); } /** - * @expectedException \TelegramBot\InlineKeyboardPagination\Exceptions\InlineKeyboardPaginationException - * @expectedExceptionMessage Invalid selected page, must be between 1 and 20 + * Generate the button for the passed page. + * + * @param int $page + * @param string $label + * + * @return array */ - public function testInvalidSelectedPage() - { - $ikp = new InlineKeyboardPagination($this->items, $this->command, $this->selected_page, $this->items_per_page); - $ikp->setSelectedPage(-5); - $ikp->getPagination(); - } - - public function testGetItemsPerPage() + protected function generateButton(int $page, string $label): array { - $ikp = new InlineKeyboardPagination($this->items, $this->command, $this->selected_page, 4); - self::assertEquals(4, $ikp->getItemsPerPage()); + return [ + 'text' => (string)$page, + 'callback_data' => $this->generateCallbackData($page), + 'label' => $label, + ]; } /** - * @expectedException \TelegramBot\InlineKeyboardPagination\Exceptions\InlineKeyboardPaginationException - * @expectedExceptionMessage Invalid number of items per page, must be at least 1 + * Generate the callback data for the passed page. + * + * @param int $page + * + * @return string */ - public function testInvalidItemsPerPage() + protected function generateCallbackData(int $page): string { - $ikp = new InlineKeyboardPagination($this->items, $this->command, $this->selected_page, 0); - $ikp->getPagination(); + return str_replace( + ['{COMMAND}', '{OLD_PAGE}', '{NEW_PAGE}'], + [$this->command, $this->selected_page, $page], + $this->callback_data_format + ); } - public function testButtonLabels() + /** + * Get the prepared items for the selected page. + * + * @return array + */ + protected function getPreparedItems(): array { - $cbdata = 'command=%s&oldPage=%d&newPage=%d'; - $command = 'cbdata'; - $ikp1 = new InlineKeyboardPagination(range(1, 1), $command, 1, $this->items_per_page); - $ikp10 = new InlineKeyboardPagination(range(1, $this->items_per_page * 10), $command, 1, $this->items_per_page); - - // current - $keyboard = [$ikp1->getPagination(1)['keyboard']]; - self::assertAllButtonPropertiesEqual([ - ['· 1 ·'], - ], 'text', $keyboard); - self::assertAllButtonPropertiesEqual([ - [ - sprintf($cbdata, $command, 1, 1), - ], - ], 'callback_data', $keyboard); - - // first, previous, current, next, last - $keyboard = [$ikp10->getPagination(5)['keyboard']]; - self::assertAllButtonPropertiesEqual([ - ['« 1', '‹ 4', '· 5 ·', '6 ›', '10 »'], - ], 'text', $keyboard); - self::assertAllButtonPropertiesEqual([ - [ - sprintf($cbdata, $command, 5, 1), - sprintf($cbdata, $command, 5, 4), - sprintf($cbdata, $command, 5, 5), - sprintf($cbdata, $command, 5, 6), - sprintf($cbdata, $command, 5, 10), - ], - ], 'callback_data', $keyboard); - - // first, previous, current, last - $keyboard = [$ikp10->getPagination(9)['keyboard']]; - self::assertAllButtonPropertiesEqual([ - ['« 1', '‹ 7', '8', '· 9 ·', '10'], - ], 'text', $keyboard); - self::assertAllButtonPropertiesEqual([ - [ - sprintf($cbdata, $command, 9, 1), - sprintf($cbdata, $command, 9, 7), - sprintf($cbdata, $command, 9, 8), - sprintf($cbdata, $command, 9, 9), - sprintf($cbdata, $command, 9, 10), - ], - ], 'callback_data', $keyboard); - - // first, previous, current - $keyboard = [$ikp10->getPagination(10)['keyboard']]; - self::assertAllButtonPropertiesEqual([ - ['« 1', '‹ 7', '8', '9', '· 10 ·'], - ], 'text', $keyboard); - self::assertAllButtonPropertiesEqual([ - [ - sprintf($cbdata, $command, 10, 1), - sprintf($cbdata, $command, 10, 7), - sprintf($cbdata, $command, 10, 8), - sprintf($cbdata, $command, 10, 9), - sprintf($cbdata, $command, 10, 10), - ], - ], 'callback_data', $keyboard); - - // custom labels, skipping some buttons - // first, previous, current, next, last - $labels = [ - 'first' => '', - 'previous' => 'previous %d', - 'current' => null, - 'next' => '%d next', - 'last' => 'last', - ]; - $ikp10->setLabels($labels); - self::assertEquals($labels, $ikp10->getLabels()); - - $keyboard = [$ikp10->getPagination(5)['keyboard']]; - self::assertAllButtonPropertiesEqual([ - ['previous 4', '6 next', 'last'], - ], 'text', $keyboard); - self::assertAllButtonPropertiesEqual([ - [ - sprintf($cbdata, $command, 5, 4), - sprintf($cbdata, $command, 5, 6), - sprintf($cbdata, $command, 5, 10), - ], - ], 'callback_data', $keyboard); + return array_slice($this->items, $this->getOffset(), $this->items_per_page); } - public static function assertButtonPropertiesEqual($value, $property, $keyboard, $row, $column, $message = '') + /** + * Get the items offset for the selected page. + * + * @return int + */ + protected function getOffset(): int { - $row_raw = array_values($keyboard)[$row]; - $column_raw = array_values($row_raw)[$column]; - - self::assertSame($value, $column_raw[$property], $message); + return $this->items_per_page * ($this->selected_page - 1); } - public static function assertRowButtonPropertiesEqual(array $values, $property, $keyboard, $row, $message = '') + /** + * Get the parameters from the callback query. + * + * @todo Possibly make it work for custom formats too? + * + * @param string $data + * + * @return array + */ + public static function getParametersFromCallbackData($data): array { - $column = 0; - foreach ($values as $value) { - self::assertButtonPropertiesEqual($value, $property, $keyboard, $row, $column++, $message); - } - self::assertCount(count(array_values($keyboard)[$row]), $values); - } + parse_str($data, $params); - public static function assertAllButtonPropertiesEqual(array $all_values, $property, $keyboard, $message = '') - { - $row = 0; - foreach ($all_values as $values) { - self::assertRowButtonPropertiesEqual($values, $property, $keyboard, $row++, $message); - } - self::assertCount(count($keyboard), $all_values); + return $params; } -} +} \ No newline at end of file