├── .coveralls.yml ├── .editorconfig ├── .gitignore ├── .php_cs.dist ├── .scrutinizer.yml ├── .styleci.yml ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── docs ├── base_usage.md ├── custom_view.md ├── from_qb.md ├── from_request.md ├── from_request_and_qb.md ├── page_range.md ├── pagination_page_1.png ├── pagination_page_5.png ├── pagination_page_9.png └── parameter_name.md ├── phpstan.neon.dist ├── phpunit.xml.dist ├── src ├── DependencyInjection │ ├── Configuration.php │ └── GpsLabPaginationExtension.php ├── Entity │ └── Node.php ├── Exception │ ├── IncorrectPageNumberException.php │ └── OutOfRangeException.php ├── GpsLabPaginationBundle.php ├── ParamConverter │ └── PaginationParamConverter.php ├── Resources │ ├── config │ │ └── services.yml │ ├── translations │ │ ├── messages.en.yml │ │ └── messages.ru.yml │ └── views │ │ └── pagination.html.twig ├── Service │ ├── Builder.php │ ├── Configuration.php │ ├── NavigateRange.php │ └── View.php └── Twig │ └── Extension │ └── PaginationExtension.php └── tests ├── DependencyInjection └── GpsLabPaginationExtensionTest.php ├── Entity └── NodeTest.php ├── Exception ├── IncorrectPageNumberExceptionTest.php └── OutOfRangeExceptionTest.php ├── GpsLabPaginationBundleTest.php ├── ParamConverter └── PaginationParamConverterTest.php ├── Service ├── BuilderTest.php ├── ConfigurationTest.php ├── NavigateRangeTest.php └── ViewTest.php ├── TestCase.php ├── Twig └── Extension │ └── PaginationExtensionTest.php └── bootstrap.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | coverage_clover: build/coverage-clover.xml 3 | json_path: build/coveralls-upload.json 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; top-most EditorConfig file 2 | root = true 3 | 4 | ; Unix-style newlines 5 | [*] 6 | end_of_line = LF 7 | 8 | [*.php] 9 | indent_style = space 10 | indent_size = 4 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /build/ 3 | phpunit.xml 4 | composer.lock 5 | .php_cs 6 | .php_cs.cache 7 | phpstan.neon -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | 6 | @copyright Copyright (c) 2011, Peter Gribanov 7 | @license http://opensource.org/licenses/MIT 8 | EOF; 9 | 10 | return PhpCsFixer\Config::create() 11 | ->setRules([ 12 | '@Symfony' => true, 13 | 'header_comment' => [ 14 | 'comment_type' => 'PHPDoc', 15 | 'header' => $header, 16 | ], 17 | 'array_syntax' => ['syntax' => 'short'], 18 | 'no_superfluous_phpdoc_tags' => false, 19 | 'yoda_style' => false, 20 | 'ordered_imports' => [ 21 | 'sort_algorithm' => 'alpha', 22 | ], 23 | ]) 24 | ->setFinder( 25 | PhpCsFixer\Finder::create() 26 | ->in(__DIR__.'/src') 27 | ->in(__DIR__.'/tests') 28 | ->notPath('bootstrap.php') 29 | ) 30 | ; 31 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - php 3 | 4 | tools: 5 | external_code_coverage: 6 | timeout: 600 7 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: symfony 2 | 3 | disabled: 4 | - yoda_style 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | os: linux 4 | 5 | branches: 6 | except: 7 | - /^analysis-.*$/ 8 | 9 | cache: 10 | directories: 11 | - $HOME/.composer/cache 12 | 13 | before_install: 14 | - if [ -n "$GH_TOKEN" ]; then composer config github-oauth.github.com ${GH_TOKEN}; fi; 15 | - if [ -n "$SYMFONY_VERSION" ]; then composer require "symfony/symfony:${SYMFONY_VERSION}" --dev --no-update; fi; 16 | - if [ -n "$DOCTRINE_VERSION" ]; then composer require "doctrine/orm:${DOCTRINE_VERSION}" --dev --no-update; fi; 17 | - if [ -n "$TWIG_VERSION" ]; then composer require "twig/twig:${TWIG_VERSION}" --dev --no-update; fi; 18 | - if [ -n "$SENSIO_FRAMEWORK" ]; then composer require "sensio/framework-extra-bundle:${SENSIO_FRAMEWORK}" --dev --no-update; fi; 19 | - if [ -n "$PHPUNIT_VERSION" ]; then composer require "phpunit/phpunit:${PHPUNIT_VERSION}" --dev --no-update; fi; 20 | - if [ -n "$PHPSTAN_VERSION" ]; then composer require "phpstan/phpstan:${PHPSTAN_VERSION}" --dev --no-update; fi; 21 | 22 | install: COMPOSER_MEMORY_LIMIT=-1 composer install --prefer-dist --no-interaction --no-scripts --no-progress 23 | 24 | script: 25 | - vendor/bin/phpunit --coverage-clover build/coverage-clover.xml 26 | - wget https://scrutinizer-ci.com/ocular.phar 27 | - wget https://github.com/php-coveralls/php-coveralls/releases/download/v2.2.0/php-coveralls.phar 28 | - php ocular.phar code-coverage:upload --format=php-clover build/coverage-clover.xml 29 | - php php-coveralls.phar -v -c .coveralls.yml 30 | 31 | jobs: 32 | include: 33 | - stage: Test 34 | php: 5.5 35 | dist: trusty 36 | 37 | - stage: Test 38 | php: 5.6 39 | 40 | - stage: Test 41 | php: 7.0 42 | 43 | - stage: Test 44 | php: 7.1 45 | 46 | - stage: Test 47 | env: PHPUNIT_VERSION=6.5.* 48 | php: 7.2 49 | 50 | - stage: Test 51 | env: PHPUNIT_VERSION=6.5.* 52 | php: 7.3 53 | 54 | - stage: Test Symfony 55 | env: SYMFONY_VERSION=2.8.* 56 | php: 5.5 57 | dist: trusty 58 | 59 | - stage: Test Symfony 60 | env: SYMFONY_VERSION=3.4.* 61 | php: 5.5 62 | dist: trusty 63 | 64 | - stage: Test Symfony 65 | env: SYMFONY_VERSION=4.4.* PHPUNIT_VERSION=6.5.* 66 | php: 7.1 67 | 68 | - stage: Test Symfony 69 | env: SYMFONY_VERSION=5.0.* PHPUNIT_VERSION=6.5.* 70 | php: 7.2 71 | 72 | # Temporary not test Doctrine 2.4 because composer time out of execution 73 | # - stage: Test Doctrine 74 | # env: DOCTRINE_VERSION=2.4.* 75 | # php: 5.5 76 | # dist: trusty 77 | 78 | - stage: Test Doctrine 79 | env: DOCTRINE_VERSION=2.5.* 80 | php: 5.5 81 | dist: trusty 82 | 83 | - stage: Test Doctrine 84 | env: DOCTRINE_VERSION=2.6.* 85 | php: 7.1 86 | 87 | - stage: Test Twig 88 | env: TWIG_VERSION=1.* 89 | php: 5.5 90 | dist: trusty 91 | 92 | - stage: Test Twig 93 | env: TWIG_VERSION=2.* 94 | php: 7.0 95 | 96 | - stage: Test Twig 97 | env: TWIG_VERSION=3.* PHPUNIT_VERSION=6.5.* 98 | php: 7.2 99 | 100 | - stage: Test SensioFrameworkExtraBundle 101 | env: SENSIO_FRAMEWORK=3.* 102 | php: 5.5 103 | dist: trusty 104 | 105 | - stage: Test SensioFrameworkExtraBundle 106 | env: SENSIO_FRAMEWORK=4.* 107 | php: 5.5 108 | dist: trusty 109 | 110 | - stage: Test SensioFrameworkExtraBundle 111 | env: SENSIO_FRAMEWORK=5.0.* 112 | php: 5.5 113 | dist: trusty 114 | 115 | - stage: Test SensioFrameworkExtraBundle 116 | env: SENSIO_FRAMEWORK=5.1.* 117 | php: 5.5 118 | dist: trusty 119 | 120 | - stage: Test SensioFrameworkExtraBundle 121 | env: SENSIO_FRAMEWORK=5.2.* 122 | php: 5.5 123 | dist: trusty 124 | 125 | - stage: Test SensioFrameworkExtraBundle 126 | env: SENSIO_FRAMEWORK=5.3.* 127 | php: 7.1 128 | dist: trusty 129 | 130 | - stage: Code Quality 131 | name: PHP CS Fixer 132 | before_script: wget https://cs.symfony.com/download/php-cs-fixer-v2.phar -O php-cs-fixer 133 | script: php php-cs-fixer fix --diff --dry-run -v 134 | 135 | - stage: Code Quality 136 | name: PHPStan 137 | php: 7.2 138 | env: PHPSTAN_VERSION=0.11.* 139 | script: vendor/bin/phpstan analyse 140 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2011 GPS Lab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Latest Stable Version](https://img.shields.io/packagist/v/gpslab/pagination-bundle.svg?maxAge=3600&label=stable)](https://packagist.org/packages/gpslab/pagination-bundle) 2 | [![PHP from Travis config](https://img.shields.io/travis/php-v/gpslab/pagination-bundle.svg?maxAge=3600)](https://packagist.org/packages/gpslab/pagination-bundle) 3 | [![Build Status](https://img.shields.io/travis/gpslab/pagination-bundle.svg?maxAge=3600)](https://travis-ci.org/gpslab/pagination-bundle) 4 | [![Coverage Status](https://img.shields.io/coveralls/gpslab/pagination-bundle.svg?maxAge=3600)](https://coveralls.io/github/gpslab/pagination-bundle?branch=master) 5 | [![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/gpslab/pagination-bundle.svg?maxAge=3600)](https://scrutinizer-ci.com/g/gpslab/pagination-bundle/?branch=master) 6 | [![StyleCI](https://styleci.io/repos/86694387/shield?branch=master)](https://styleci.io/repos/86694387) 7 | [![License](https://img.shields.io/packagist/l/gpslab/pagination-bundle.svg?maxAge=3600)](https://github.com/gpslab/pagination-bundle) 8 | 9 | # PaginationBundle 10 | 11 | ![Pagination page 1](docs/pagination_page_1.png) 12 | 13 | ![Pagination page 4](docs/pagination_page_5.png) 14 | 15 | ![Pagination page 9](docs/pagination_page_9.png) 16 | 17 | ## Installation 18 | 19 | Pretty simple with [Composer](http://packagist.org), run: 20 | 21 | ```sh 22 | composer req gpslab/pagination-bundle 23 | ``` 24 | 25 | ## Configuration 26 | 27 | Default configuration 28 | 29 | ```yaml 30 | gpslab_pagination: 31 | # Page range used in pagination control 32 | max_navigate: 5 33 | 34 | # Name of URL parameter for page number 35 | parameter_name: 'page' 36 | 37 | # Sliding pagination controls template 38 | template: 'GpsLabPaginationBundle::pagination.html.twig' 39 | ``` 40 | 41 | ## Simple usage 42 | 43 | ```php 44 | use GpsLab\Bundle\PaginationBundle\Service\Configuration; 45 | 46 | class ArticleController extends Controller 47 | { 48 | private const ARTICLES_PER_PAGE = 30; 49 | 50 | public function index(ArticleRepository $rep, Configuration $pagination): Response 51 | { 52 | $pagination->setTotalPages(ceil($rep->getTotalPublished() / self::ARTICLES_PER_PAGE)); 53 | 54 | // get articles chunk 55 | $offset = ($pagination->getCurrentPage() - 1) * self::ARTICLES_PER_PAGE; 56 | $articles = $rep->getPublished(self::ARTICLES_PER_PAGE, $offset); 57 | 58 | return $this->render('AcmeDemoBundle:Article:index.html.twig', [ 59 | 'articles' => $articles, 60 | 'pagination' => $pagination 61 | ]); 62 | } 63 | } 64 | ``` 65 | 66 | Display pagination in template: 67 | 68 | ```twig 69 | 72 | ``` 73 | 74 | ## Documentation 75 | 76 | * [Base usage](docs/base_usage.md) 77 | * [From QueryBuilder](docs/from_qb.md) 78 | * [From HTTP request](docs/from_request.md) 79 | * [From HTTP request and QueryBuilder](docs/from_request_and_qb.md) 80 | * [Request parameter name](docs/parameter_name.md) 81 | * [Navigation pages range](docs/page_range.md) 82 | * [Custom view](docs/custom_view.md) 83 | 84 | ## License 85 | 86 | This bundle is under the [MIT license](http://opensource.org/licenses/MIT). See the complete license in the file: LICENSE 87 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gpslab/pagination-bundle", 3 | "license": "MIT", 4 | "type": "symfony-bundle", 5 | "description": "Pagination bundle", 6 | "keywords": ["php", "pagination", "symfony", "doctrine"], 7 | "homepage": "http://github.com/gpslab/pagination-bundle", 8 | "autoload": { 9 | "psr-4": { 10 | "GpsLab\\Bundle\\PaginationBundle\\": "src/" 11 | } 12 | }, 13 | "autoload-dev": { 14 | "psr-4": { 15 | "GpsLab\\Bundle\\PaginationBundle\\Tests\\": "tests/" 16 | } 17 | }, 18 | "require": { 19 | "php": ">=5.4.0", 20 | "symfony/http-kernel": "~2.3|~3.0|~4.0|~5.0", 21 | "symfony/dependency-injection": "~2.3|~3.0|~4.0|~5.0", 22 | "symfony/expression-language": "~2.3|~3.0|~4.0|~5.0", 23 | "symfony/config": "~2.3|~3.0|~4.0|~5.0", 24 | "symfony/routing": "~2.3|~3.0|~4.0|~5.0", 25 | "symfony/yaml": "~2.3|~3.0|~4.0|~5.0", 26 | "sensio/framework-extra-bundle": "~3.0|~4.0|~5.0", 27 | "doctrine/orm": "~2.4|~2.5|~2.6", 28 | "twig/twig": "^1.34|^2.0|^3.0" 29 | }, 30 | "require-dev": { 31 | "phpunit/phpunit": "^4.8.36" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /docs/base_usage.md: -------------------------------------------------------------------------------- 1 | Base usage 2 | ========== 3 | 4 | ```php 5 | namespace Acme\DemoBundle\Controller; 6 | 7 | use Acme\DemoBundle\Entity\Article; 8 | use Symfony\Bundle\FrameworkBundle\Controller\Controller; 9 | use Symfony\Component\HttpFoundation\Request; 10 | use Symfony\Component\HttpFoundation\Response; 11 | use Sensio\Bundle\FrameworkExtraBundle\Configuration; 12 | 13 | class ArticleController extends Controller 14 | { 15 | /** 16 | * Articles per page. 17 | */ 18 | private const PER_PAGE = 100; 19 | 20 | /** 21 | * @Configuration\Route("/article/", name="article_index") 22 | * @Configuration\Method({"GET"}) 23 | */ 24 | public function index(Request $request): Response 25 | { 26 | $router = $this->get('router'); 27 | 28 | // get total articles 29 | $total = (int) $this-> 30 | ->getDoctrine() 31 | ->createQueryBuilder() 32 | ->select('COUNT(*)') 33 | ->from(Article::class, 'a') 34 | ->getQuery() 35 | ->getSingleScalarResult() 36 | ; 37 | 38 | // build pagination 39 | $pagination = $this 40 | ->get('pagination') 41 | ->paginate( 42 | ceil($total / self::PER_PAGE), // total pages 43 | $request->query->get('page') // correct page 44 | ) 45 | // template of link to page 46 | // character "%d" is replaced by the page number 47 | // you don't need to customize the template, because default template is "?page=%d" 48 | ->setPageLink('/article/?page=%d') 49 | // link for first page 50 | // as a default used the page link template 51 | ->setFirstPageLink('/article/') 52 | ; 53 | 54 | // get articles chunk 55 | $articles = $this-> 56 | ->getDoctrine() 57 | ->createQueryBuilder() 58 | ->select('*') 59 | ->from(Article::class, 'a') 60 | ->setFirstResult(($pagination->getCurrentPage() - 1) * self::PER_PAGE) 61 | ->setMaxResults(self::PER_PAGE) 62 | ->getQuery() 63 | ->getResult() 64 | ; 65 | 66 | return $this->render('AcmeDemoBundle:Article:index.html.twig', [ 67 | 'total' => $total, 68 | 'articles' => $articles, 69 | 'pagination' => $pagination 70 | ]); 71 | } 72 | } 73 | ``` 74 | -------------------------------------------------------------------------------- /docs/custom_view.md: -------------------------------------------------------------------------------- 1 | Custom view 2 | =========== 3 | 4 | You can customize presents pagination. 5 | 6 | You can change template for all pagination on your project from config: 7 | 8 | ```yaml 9 | gpslab_pagination: 10 | # sliding pagination controls template 11 | template: 'custom_pagination.html.twig' 12 | ``` 13 | 14 | Or you can change template for concrete pagination: 15 | 16 | ```twig 17 | {# display navigation #} 18 | {{ pagination_render(pagination, 'custom_pagination.html.twig', {custom_var: 'foo'}) }} 19 | ``` 20 | 21 | Example [Material Design](https://material.io/guidelines/) template for pagination: 22 | 23 | ```twig 24 | {# custom_pagination.html.twig #} 25 | 26 | {# print 'foo' #} 27 | {{ custom_var }} 28 | 29 | {% if pagination.total > 1 %} 30 | {% spaceless %} 31 | 74 | {% endspaceless %} 75 | {% endif %} 76 | ``` 77 | -------------------------------------------------------------------------------- /docs/from_qb.md: -------------------------------------------------------------------------------- 1 | From QueryBuilder 2 | ================= 3 | 4 | ```php 5 | namespace Acme\DemoBundle\Controller; 6 | 7 | use Acme\DemoBundle\Entity\Article; 8 | use Symfony\Bundle\FrameworkBundle\Controller\Controller; 9 | use Symfony\Component\HttpFoundation\Request; 10 | use Symfony\Component\HttpFoundation\Response; 11 | use Sensio\Bundle\FrameworkExtraBundle\Configuration; 12 | use Acme\DemoBundle\Entity\Article; 13 | 14 | class ArticleController extends Controller 15 | { 16 | /** 17 | * Articles per page. 18 | */ 19 | private const PER_PAGE = 100; 20 | 21 | /** 22 | * @Configuration\Route("/article/", name="article_index") 23 | * @Configuration\Method({"GET"}) 24 | */ 25 | public function index(Request $request): Response 26 | { 27 | // create get articles query 28 | // would be better move this query to repository class 29 | $query = $this 30 | ->getDoctrine() 31 | ->getRepository(Article::calss) 32 | ->createQueryBuilder('a') 33 | ->where('a.enabled = :enabled') 34 | ->setParameter('enabled', true) 35 | ; 36 | 37 | // build pagination 38 | $pagination = $this 39 | ->get('pagination') 40 | ->paginateQuery( 41 | $query, 42 | self::PER_PAGE, 43 | $request->query->get('page') // correct page 44 | ) 45 | // register callback function as the page link builder 46 | ->setPageLink(function($page) { 47 | return $this->generateUrl('article_index', ['page' => $page]); 48 | }) 49 | // build link for first page 50 | ->setFirstPageLink($this->generateUrl('article_index')) 51 | ; 52 | 53 | return $this->render('AcmeDemoBundle:Article:index.html.twig', [ 54 | 'total' => $pagination->getTotalPages(), // total pages 55 | 'articles' => $query->getQuery()->getResult(), // get articles chunk 56 | 'pagination' => $pagination 57 | ]); 58 | } 59 | } 60 | ``` 61 | -------------------------------------------------------------------------------- /docs/from_request.md: -------------------------------------------------------------------------------- 1 | From HTTP request 2 | ================= 3 | 4 | ```php 5 | namespace Acme\DemoBundle\Controller; 6 | 7 | use Acme\DemoBundle\Entity\Article; 8 | use Symfony\Bundle\FrameworkBundle\Controller\Controller; 9 | use Symfony\Component\HttpFoundation\Request; 10 | use Symfony\Component\HttpFoundation\Response; 11 | use Symfony\Component\Routing\Generator\UrlGeneratorInterface; 12 | use Sensio\Bundle\FrameworkExtraBundle\Configuration; 13 | use Acme\DemoBundle\Entity\Article; 14 | 15 | class ArticleController extends Controller 16 | { 17 | /** 18 | * Articles per page. 19 | */ 20 | private const PER_PAGE = 100; 21 | 22 | /** 23 | * @Configuration\Route("/article/", name="article_index") 24 | * @Configuration\Method({"GET"}) 25 | */ 26 | public function index(Request $request): Response 27 | { 28 | $rep = $this->getDoctrine()->getRepository(Article::calss); 29 | 30 | $total = $rep->getTotalPublished(); 31 | $total_pages = ceil($total / self::PER_PAGE); 32 | 33 | // build pagination 34 | $pagination = $this->get('pagination')->paginateRequest( 35 | $request, 36 | $total_pages, 37 | 'p', // request parameter for page number 38 | UrlGeneratorInterface::ABSOLUTE_URL // build absolute url in pagination 39 | ); 40 | 41 | // get articles chunk 42 | $articles = $rep->getPublished( 43 | self::PER_PAGE, // limit 44 | ($pagination->getCurrentPage() - 1) * self::PER_PAGE // offset 45 | ); 46 | 47 | return $this->render('AcmeDemoBundle:Article:index.html.twig', [ 48 | 'total' => $total, 49 | 'articles' => $articles, 50 | 'pagination' => $pagination 51 | ]); 52 | } 53 | } 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/from_request_and_qb.md: -------------------------------------------------------------------------------- 1 | From HTTP request and QueryBuilder 2 | ================================== 3 | 4 | ```php 5 | namespace Acme\DemoBundle\Controller; 6 | 7 | use Acme\DemoBundle\Entity\Article; 8 | use Symfony\Bundle\FrameworkBundle\Controller\Controller; 9 | use Symfony\Component\HttpFoundation\Request; 10 | use Symfony\Component\HttpFoundation\Response; 11 | use Symfony\Component\Routing\Generator\UrlGeneratorInterface; 12 | use Sensio\Bundle\FrameworkExtraBundle\Configuration; 13 | use Acme\DemoBundle\Entity\Article; 14 | 15 | class ArticleController extends Controller 16 | { 17 | /** 18 | * Articles per page. 19 | */ 20 | private const PER_PAGE = 100; 21 | 22 | /** 23 | * @Configuration\Route("/article/", name="article_index") 24 | * @Configuration\Method({"GET"}) 25 | */ 26 | public function index(Request $request): Response 27 | { 28 | // create get articles query 29 | // would be better move this query to repository class 30 | $query = $this 31 | ->getDoctrine() 32 | ->getRepository(Article::calss) 33 | ->createQueryBuilder('a') 34 | ->where('a.enabled = :enabled') 35 | ->setParameter('enabled', true) 36 | ; 37 | 38 | // build pagination 39 | $pagination = $this->get('pagination')->paginateRequestQuery( 40 | $request, 41 | $query, 42 | self::PER_PAGE, // articles per page 43 | 'p', // request parameter for page number 44 | UrlGeneratorInterface::ABSOLUTE_URL // build absolute url in pagination 45 | ); 46 | 47 | return $this->render('AcmeDemoBundle:Article:index.html.twig', [ 48 | 'total' => $pagination->getTotalPages(), // total pages 49 | 'articles' => $query->getQuery()->getResult(), // get articles chunk 50 | 'pagination' => $pagination 51 | ]); 52 | } 53 | } 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/page_range.md: -------------------------------------------------------------------------------- 1 | Navigation pages range 2 | ====================== 3 | 4 | You can customize maximum pages in navigation menu. 5 | 6 | Configuration 7 | ------------- 8 | 9 | ```yaml 10 | gpslab_pagination: 11 | max_navigate: 10 12 | ``` 13 | 14 | Templates 15 | --------- 16 | 17 | ```twig 18 | 21 | ``` 22 | 23 | Controller 24 | ---------- 25 | 26 | ```php 27 | $pagination->setMaxNavigate(10); 28 | ``` 29 | 30 | Annotations 31 | ----------- 32 | 33 | ```php 34 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; 35 | 36 | /** 37 | * @ParamConverter("pagination", options={"max_navigate": 10}) 38 | */ 39 | public function index(Configuration $pagination): Response 40 | { 41 | // ... 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/pagination_page_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gpslab/pagination-bundle/071ab86322c37cc25c689962b3c324c18df4e080/docs/pagination_page_1.png -------------------------------------------------------------------------------- /docs/pagination_page_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gpslab/pagination-bundle/071ab86322c37cc25c689962b3c324c18df4e080/docs/pagination_page_5.png -------------------------------------------------------------------------------- /docs/pagination_page_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gpslab/pagination-bundle/071ab86322c37cc25c689962b3c324c18df4e080/docs/pagination_page_9.png -------------------------------------------------------------------------------- /docs/parameter_name.md: -------------------------------------------------------------------------------- 1 | Request parameter name 2 | ====================== 3 | 4 | As default used `page` as request parameter name. So for first page will be generated `/` link, for second `/?page=2`, 5 | for third `/?page=3` and etc. You can change this parameter name. 6 | 7 | Configuration 8 | ------------- 9 | 10 | ```yaml 11 | gpslab_pagination: 12 | parameter_name: 'p' 13 | ``` 14 | 15 | Controller 16 | ---------- 17 | 18 | ```php 19 | $pagination = new Configuration(); 20 | $pagination->setFirstPageLink('/'); 21 | $pagination->setPageLink('/?p=%d'); 22 | // or you can use callback function 23 | //$pagination->setPageLink(static function (int $number): string { 24 | // return sprintf('/?p=%d', $number); 25 | //}); 26 | ``` 27 | 28 | Annotations 29 | ----------- 30 | 31 | ```php 32 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; 33 | 34 | /** 35 | * @ParamConverter("pagination", options={"parameter_name": "p"}) 36 | */ 37 | public function index(Configuration $pagination): Response 38 | { 39 | // ... 40 | } 41 | ``` -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 7 3 | paths: 4 | - src 5 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | ./tests 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ./src 25 | 26 | ./src/Resources 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2011, Peter Gribanov 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace GpsLab\Bundle\PaginationBundle\DependencyInjection; 12 | 13 | use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; 14 | use Symfony\Component\Config\Definition\Builder\TreeBuilder; 15 | use Symfony\Component\Config\Definition\ConfigurationInterface; 16 | 17 | class Configuration implements ConfigurationInterface 18 | { 19 | /** 20 | * @return TreeBuilder 21 | */ 22 | public function getConfigTreeBuilder() 23 | { 24 | $tree_builder = new TreeBuilder('gpslab_pagination'); 25 | 26 | if (method_exists($tree_builder, 'getRootNode')) { 27 | // Symfony 4.2 + 28 | $root = $tree_builder->getRootNode(); 29 | } else { 30 | // Symfony 4.1 and below 31 | $root = $tree_builder->root('gpslab_pagination'); 32 | } 33 | 34 | // @codeCoverageIgnoreStart 35 | if (!$root instanceof ArrayNodeDefinition) { 36 | throw new \RuntimeException(sprintf('Config root node must be a "%s", given "%s".', 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition', get_class($root))); 37 | } 38 | // @codeCoverageIgnoreEnd 39 | 40 | $root->addDefaultsIfNotSet(); 41 | $root->children()->scalarNode('max_navigate')->defaultValue(5); 42 | $root->children()->scalarNode('parameter_name')->defaultValue('page'); 43 | $root->children()->scalarNode('template')->defaultValue('GpsLabPaginationBundle::pagination.html.twig'); 44 | 45 | return $tree_builder; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/DependencyInjection/GpsLabPaginationExtension.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2011, Peter Gribanov 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace GpsLab\Bundle\PaginationBundle\DependencyInjection; 12 | 13 | use Symfony\Component\Config\FileLocator; 14 | use Symfony\Component\DependencyInjection\ContainerBuilder; 15 | use Symfony\Component\DependencyInjection\Loader; 16 | use Symfony\Component\HttpKernel\DependencyInjection\Extension; 17 | 18 | class GpsLabPaginationExtension extends Extension 19 | { 20 | /** 21 | * @param array $configs 22 | * @param ContainerBuilder $container 23 | */ 24 | public function load(array $configs, ContainerBuilder $container) 25 | { 26 | $config = $this->processConfiguration(new Configuration(), $configs); 27 | 28 | $container->setParameter('pagination.max_navigate', $config['max_navigate']); 29 | $container->setParameter('pagination.parameter_name', $config['parameter_name']); 30 | $container->setParameter('pagination.template', $config['template']); 31 | 32 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 33 | $loader->load('services.yml'); 34 | } 35 | 36 | /** 37 | * @return string 38 | */ 39 | public function getAlias() 40 | { 41 | return 'gpslab_pagination'; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Entity/Node.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2011, Peter Gribanov 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace GpsLab\Bundle\PaginationBundle\Entity; 12 | 13 | class Node 14 | { 15 | /** 16 | * @var int 17 | */ 18 | private $page = 1; 19 | 20 | /** 21 | * @var string 22 | */ 23 | private $link = ''; 24 | 25 | /** 26 | * @var bool 27 | */ 28 | private $is_current = false; 29 | 30 | /** 31 | * @param int $page 32 | * @param string $link 33 | * @param bool $is_current 34 | */ 35 | public function __construct($page = 1, $link = '', $is_current = false) 36 | { 37 | $this->page = $page; 38 | $this->link = $link; 39 | $this->is_current = $is_current; 40 | } 41 | 42 | /** 43 | * @return bool 44 | */ 45 | public function isCurrent() 46 | { 47 | return $this->is_current; 48 | } 49 | 50 | /** 51 | * @return string 52 | */ 53 | public function getLink() 54 | { 55 | return $this->link; 56 | } 57 | 58 | /** 59 | * @return int 60 | */ 61 | public function getPage() 62 | { 63 | return $this->page; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Exception/IncorrectPageNumberException.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2011, Peter Gribanov 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace GpsLab\Bundle\PaginationBundle\Exception; 12 | 13 | use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; 14 | 15 | class IncorrectPageNumberException extends NotFoundHttpException 16 | { 17 | /** 18 | * @param mixed $current_page 19 | * 20 | * @return static 21 | */ 22 | public static function incorrect($current_page) 23 | { 24 | return new static(sprintf('Incorrect "%s" page number.', $current_page)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Exception/OutOfRangeException.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2011, Peter Gribanov 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace GpsLab\Bundle\PaginationBundle\Exception; 12 | 13 | use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; 14 | 15 | class OutOfRangeException extends NotFoundHttpException 16 | { 17 | /** 18 | * @param int $current_page 19 | * @param int $total_pages 20 | * 21 | * @return static 22 | */ 23 | public static function out($current_page, $total_pages) 24 | { 25 | return new static(sprintf('Select page "%s" is out of range "%s".', $current_page, $total_pages)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/GpsLabPaginationBundle.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2011, Peter Gribanov 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace GpsLab\Bundle\PaginationBundle; 12 | 13 | use GpsLab\Bundle\PaginationBundle\DependencyInjection\GpsLabPaginationExtension; 14 | use Symfony\Component\HttpKernel\Bundle\Bundle; 15 | 16 | class GpsLabPaginationBundle extends Bundle 17 | { 18 | /** 19 | * @return GpsLabPaginationExtension 20 | */ 21 | public function getContainerExtension() 22 | { 23 | if (!($this->extension instanceof GpsLabPaginationExtension)) { 24 | $this->extension = new GpsLabPaginationExtension(); 25 | } 26 | 27 | return $this->extension; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ParamConverter/PaginationParamConverter.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2011, Peter Gribanov 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace GpsLab\Bundle\PaginationBundle\ParamConverter; 12 | 13 | use GpsLab\Bundle\PaginationBundle\Service\Configuration; 14 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; 15 | use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface; 16 | use Symfony\Component\HttpFoundation\Request; 17 | use Symfony\Component\Routing\Generator\UrlGeneratorInterface; 18 | use Symfony\Component\Routing\RouterInterface; 19 | 20 | final class PaginationParamConverter implements ParamConverterInterface 21 | { 22 | /** 23 | * @var RouterInterface 24 | */ 25 | private $router; 26 | 27 | /** 28 | * @var int 29 | */ 30 | private $max_navigate; 31 | 32 | /** 33 | * @var string 34 | */ 35 | private $parameter_name; 36 | 37 | /** 38 | * @param RouterInterface $router 39 | * @param int $max_navigate 40 | * @param string $parameter_name 41 | */ 42 | public function __construct(RouterInterface $router, $max_navigate, $parameter_name) 43 | { 44 | $this->router = $router; 45 | $this->max_navigate = $max_navigate; 46 | $this->parameter_name = $parameter_name; 47 | } 48 | 49 | /** 50 | * @param ParamConverter $configuration 51 | * 52 | * @return bool 53 | */ 54 | public function supports(ParamConverter $configuration) 55 | { 56 | return 'GpsLab\Bundle\PaginationBundle\Service\Configuration' === $configuration->getClass(); 57 | } 58 | 59 | /** 60 | * @param Request $request 61 | * @param ParamConverter $converter 62 | * 63 | * @return bool 64 | */ 65 | public function apply(Request $request, ParamConverter $converter) 66 | { 67 | $options = $converter->getOptions(); 68 | $max_navigate = $this->max_navigate; 69 | $param_name = $this->parameter_name; 70 | $reference_type = UrlGeneratorInterface::ABSOLUTE_PATH; 71 | $reference_types = [ 72 | 'absolute_url' => UrlGeneratorInterface::ABSOLUTE_PATH, 73 | 'absolute_path' => UrlGeneratorInterface::ABSOLUTE_PATH, 74 | 'relative_path' => UrlGeneratorInterface::RELATIVE_PATH, 75 | 'network_path' => UrlGeneratorInterface::NETWORK_PATH, 76 | ]; 77 | 78 | if (isset($options['max_navigate'])) { 79 | $max_navigate = $options['max_navigate']; 80 | } 81 | 82 | if (isset($options['parameter_name'])) { 83 | $param_name = $options['parameter_name']; 84 | } 85 | 86 | if (isset($options['reference_type']) && 87 | array_key_exists($options['reference_type'], $reference_types) 88 | ) { 89 | $reference_type = $reference_types[$options['reference_type']]; 90 | } 91 | 92 | $current_page = (int) $request->get($param_name, 1); 93 | $current_page = $current_page > 1 ? $current_page : 1; 94 | 95 | // get routing params 96 | $route = $request->attributes->get('_route'); 97 | $route_params = array_merge($request->query->all(), $request->attributes->get('_route_params', [])); 98 | unset($route_params[$param_name]); 99 | 100 | // impossible resolve total pages here 101 | $total_pages = 0; 102 | 103 | $configuration = new Configuration($total_pages, $current_page); 104 | $configuration->setMaxNavigate($max_navigate); 105 | $configuration->setFirstPageLink($this->router->generate($route, $route_params, $reference_type)); 106 | $configuration->setPageLink(function ($number) use ($route, $route_params, $param_name, $reference_type) { 107 | return $this->router->generate($route, [$param_name => $number] + $route_params, $reference_type); 108 | }); 109 | 110 | $request->attributes->set($converter->getName(), $configuration); 111 | 112 | return true; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | pagination: 3 | class: GpsLab\Bundle\PaginationBundle\Service\Builder 4 | arguments: [ '@router', '%pagination.max_navigate%', '%pagination.parameter_name%' ] 5 | 6 | GpsLab\Bundle\PaginationBundle\ParamConverter\PaginationParamConverter: 7 | arguments: [ '@router', '%pagination.max_navigate%', '%pagination.parameter_name%' ] 8 | tags: 9 | - { name: request.param_converter, priority: -2, converter: pagination_converter } 10 | 11 | pagination.twig_extension: 12 | class: GpsLab\Bundle\PaginationBundle\Twig\Extension\PaginationExtension 13 | arguments: [ '%pagination.template%' ] 14 | tags: 15 | - { name: twig.extension } 16 | -------------------------------------------------------------------------------- /src/Resources/translations/messages.en.yml: -------------------------------------------------------------------------------- 1 | pagination: 2 | first_page: 3 | word: '«' 4 | title: 'Go to the first page' 5 | previous_page: 6 | word: '←' 7 | title: 'Go to the previous page' 8 | page_number: 9 | word: '%page%' 10 | title: 'Go to page number: %page%' 11 | next_page: 12 | word: '→' 13 | title: 'Go to the next page' 14 | last_page: 15 | word: '»' 16 | title: 'Go to the last page' 17 | current_page: 18 | word: '%page%' 19 | title: 'Current page' 20 | -------------------------------------------------------------------------------- /src/Resources/translations/messages.ru.yml: -------------------------------------------------------------------------------- 1 | pagination: 2 | first_page: 3 | word: '«' 4 | title: 'Перейти на первую страницу' 5 | previous_page: 6 | word: '←' 7 | title: 'Перейти на предыдущую страницу' 8 | page_number: 9 | word: '%page%' 10 | title: 'Перейти на страницу номер: %page%' 11 | next_page: 12 | word: '→' 13 | title: 'Перейти на следующую страницу' 14 | last_page: 15 | word: '»' 16 | title: 'Перейти на последнюю страницу' 17 | current_page: 18 | word: '%page%' 19 | title: 'Текущая страница' 20 | -------------------------------------------------------------------------------- /src/Resources/views/pagination.html.twig: -------------------------------------------------------------------------------- 1 | {% if pagination.total > 1 %} 2 | 37 | {% endif %} 38 | -------------------------------------------------------------------------------- /src/Service/Builder.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2011, Peter Gribanov 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace GpsLab\Bundle\PaginationBundle\Service; 12 | 13 | use Doctrine\ORM\QueryBuilder; 14 | use GpsLab\Bundle\PaginationBundle\Exception\IncorrectPageNumberException; 15 | use GpsLab\Bundle\PaginationBundle\Exception\OutOfRangeException; 16 | use Symfony\Bundle\FrameworkBundle\Routing\Router; 17 | use Symfony\Component\HttpFoundation\Request; 18 | use Symfony\Component\Routing\Generator\UrlGeneratorInterface; 19 | 20 | class Builder 21 | { 22 | /** 23 | * @var Router 24 | */ 25 | private $router; 26 | 27 | /** 28 | * The number of pages displayed in the navigation. 29 | * 30 | * @var int 31 | */ 32 | private $max_navigate; 33 | 34 | /** 35 | * Name of URL parameter for page number. 36 | * 37 | * @var string 38 | */ 39 | private $parameter_name; 40 | 41 | /** 42 | * @param Router $router Router service 43 | * @param int $max_navigate Maximum showing navigation links in pagination 44 | * @param string $parameter_name Name of URL parameter for page number 45 | */ 46 | public function __construct(Router $router, $max_navigate, $parameter_name) 47 | { 48 | $this->router = $router; 49 | $this->max_navigate = $max_navigate; 50 | $this->parameter_name = $parameter_name; 51 | } 52 | 53 | /** 54 | * @param int $total_pages Total available pages 55 | * @param int $current_page The current page number 56 | * 57 | * @return Configuration 58 | */ 59 | public function paginate($total_pages = 1, $current_page = 1) 60 | { 61 | return (new Configuration($total_pages, $current_page)) 62 | ->setMaxNavigate($this->max_navigate) 63 | ->setPageLink(sprintf('?%s=%%d', $this->parameter_name)) 64 | ; 65 | } 66 | 67 | /** 68 | * @param QueryBuilder $query Query for select entities 69 | * @param int $per_page Entities per page 70 | * @param int $current_page The current page number 71 | * 72 | * @return Configuration 73 | */ 74 | public function paginateQuery(QueryBuilder $query, $per_page, $current_page = 1) 75 | { 76 | $counter = clone $query; 77 | $total = $counter 78 | ->select(sprintf('COUNT(%s)', current($query->getRootAliases()))) 79 | ->getQuery() 80 | ->getSingleScalarResult() 81 | ; 82 | 83 | $total_pages = (int) ceil($total / $per_page); 84 | $current_page = $this->validateCurrentPage($current_page, $total_pages); 85 | 86 | $query 87 | ->setFirstResult(($current_page - 1) * $per_page) 88 | ->setMaxResults($per_page) 89 | ; 90 | 91 | return $this->paginate($total_pages, $current_page); 92 | } 93 | 94 | /** 95 | * @param Request $request Current HTTP request 96 | * @param int $total_pages Total available pages 97 | * @param string $parameter_name Name of URL parameter for page number 98 | * @param int $reference_type The type of reference (one of the constants in UrlGeneratorInterface) 99 | * 100 | * @return Configuration 101 | */ 102 | public function paginateRequest( 103 | Request $request, 104 | $total_pages, 105 | $parameter_name = '', 106 | $reference_type = UrlGeneratorInterface::ABSOLUTE_PATH 107 | ) { 108 | $parameter_name = $parameter_name ?: $this->parameter_name; 109 | $current_page = $this->validateCurrentPage($request->get($parameter_name), $total_pages); 110 | 111 | return $this->configureFromRequest( 112 | $request, 113 | $this->paginate($total_pages, $current_page), 114 | $parameter_name, 115 | $reference_type 116 | ); 117 | } 118 | 119 | /** 120 | * @param Request $request Current HTTP request 121 | * @param QueryBuilder $query Query for select entities 122 | * @param int $per_page Entities per page 123 | * @param string $parameter_name Name of URL parameter for page number 124 | * @param int $reference_type The type of reference (one of the constants in UrlGeneratorInterface) 125 | * 126 | * @return Configuration 127 | */ 128 | public function paginateRequestQuery( 129 | Request $request, 130 | QueryBuilder $query, 131 | $per_page, 132 | $parameter_name = 'page', 133 | $reference_type = UrlGeneratorInterface::ABSOLUTE_PATH 134 | ) { 135 | $parameter_name = $parameter_name ?: $this->parameter_name; 136 | 137 | return $this->configureFromRequest( 138 | $request, 139 | $this->paginateQuery($query, $per_page, $request->get($parameter_name)), 140 | $parameter_name, 141 | $reference_type 142 | ); 143 | } 144 | 145 | /** 146 | * @param mixed $current_page 147 | * @param int $total_pages 148 | * 149 | * @return int 150 | */ 151 | private function validateCurrentPage($current_page, $total_pages) 152 | { 153 | if ($current_page === null) { 154 | return 1; 155 | } 156 | 157 | if (!is_int($current_page) && (!is_string($current_page) || !ctype_digit($current_page))) { 158 | throw IncorrectPageNumberException::incorrect($current_page); 159 | } 160 | 161 | if ($current_page < 1 || $current_page > $total_pages) { 162 | throw OutOfRangeException::out((int) $current_page, $total_pages); 163 | } 164 | 165 | return (int) $current_page; 166 | } 167 | 168 | /** 169 | * @param Request $request 170 | * @param Configuration $configuration 171 | * @param string $parameter_name 172 | * @param int $reference_type 173 | * 174 | * @return Configuration 175 | */ 176 | private function configureFromRequest( 177 | Request $request, 178 | Configuration $configuration, 179 | $parameter_name, 180 | $reference_type 181 | ) { 182 | $route = $request->get('_route'); 183 | $route_params = array_merge($request->query->all(), $request->get('_route_params', [])); 184 | unset($route_params[$parameter_name]); 185 | 186 | return $configuration 187 | ->setPageLink(function ($number) use ($route, $route_params, $parameter_name, $reference_type) { 188 | $params = array_merge($route_params, [$parameter_name => $number]); 189 | 190 | return $this->router->generate($route, $params, $reference_type); 191 | }) 192 | ->setFirstPageLink($this->router->generate($route, $route_params, $reference_type)) 193 | ; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/Service/Configuration.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2011, Peter Gribanov 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace GpsLab\Bundle\PaginationBundle\Service; 12 | 13 | class Configuration 14 | { 15 | /** 16 | * Length of the list of pagination defaults. 17 | * 18 | * @var int 19 | */ 20 | const DEFAULT_LIST_LENGTH = 5; 21 | 22 | /** 23 | * @var int 24 | */ 25 | const DEFAULT_PAGE_LINK = '?page=%d'; 26 | 27 | /** 28 | * @var int 29 | */ 30 | private $total_pages = 0; 31 | 32 | /** 33 | * @var int 34 | */ 35 | private $current_page = 1; 36 | 37 | /** 38 | * @var View|null 39 | */ 40 | private $view; 41 | 42 | /** 43 | * The number of pages displayed in the navigation. 44 | * 45 | * @var int 46 | */ 47 | private $max_navigate = self::DEFAULT_LIST_LENGTH; 48 | 49 | /** 50 | * @var string|callable 51 | */ 52 | private $page_link = self::DEFAULT_PAGE_LINK; 53 | 54 | /** 55 | * @var string 56 | */ 57 | private $first_page_link = ''; 58 | 59 | /** 60 | * @param int $total_pages 61 | * @param int $current_page 62 | */ 63 | public function __construct($total_pages = 0, $current_page = 1) 64 | { 65 | $this->setCurrentPage($current_page); 66 | $this->setTotalPages($total_pages); 67 | } 68 | 69 | /** 70 | * @param int $total_pages 71 | * @param int $current_page 72 | * 73 | * @return self 74 | */ 75 | public static function create($total_pages = 0, $current_page = 1) 76 | { 77 | return new static($total_pages, $current_page); 78 | } 79 | 80 | /** 81 | * @return int 82 | */ 83 | public function getTotalPages() 84 | { 85 | return $this->total_pages; 86 | } 87 | 88 | /** 89 | * @param int $total_pages 90 | * 91 | * @return self 92 | */ 93 | public function setTotalPages($total_pages) 94 | { 95 | $this->total_pages = $total_pages; 96 | 97 | return $this; 98 | } 99 | 100 | /** 101 | * @return int 102 | */ 103 | public function getCurrentPage() 104 | { 105 | return $this->current_page; 106 | } 107 | 108 | /** 109 | * @param int $current_page 110 | * 111 | * @return self 112 | */ 113 | public function setCurrentPage($current_page) 114 | { 115 | $this->current_page = $current_page; 116 | 117 | return $this; 118 | } 119 | 120 | /** 121 | * @return int 122 | */ 123 | public function getMaxNavigate() 124 | { 125 | return $this->max_navigate; 126 | } 127 | 128 | /** 129 | * @param int $max_navigate 130 | * 131 | * @return self 132 | */ 133 | public function setMaxNavigate($max_navigate) 134 | { 135 | $this->max_navigate = $max_navigate; 136 | 137 | return $this; 138 | } 139 | 140 | /** 141 | * @return string|callable 142 | */ 143 | public function getPageLink() 144 | { 145 | return $this->page_link; 146 | } 147 | 148 | /** 149 | * Set page link. 150 | * 151 | * Basic reference, for example `page_%s.html` where %s page number, or 152 | * callback function which takes one parameter - the number of the page. 153 | * 154 | * 155 | * function ($number) { 156 | * return 'page_'.$number.'.html'; 157 | * } 158 | * 159 | * 160 | * @param string|callable $page_link 161 | * 162 | * @return self 163 | */ 164 | public function setPageLink($page_link) 165 | { 166 | $this->page_link = $page_link; 167 | 168 | return $this; 169 | } 170 | 171 | /** 172 | * @return string 173 | */ 174 | public function getFirstPageLink() 175 | { 176 | return $this->first_page_link; 177 | } 178 | 179 | /** 180 | * @param string $first_page_link 181 | * 182 | * @return self 183 | */ 184 | public function setFirstPageLink($first_page_link) 185 | { 186 | $this->first_page_link = $first_page_link; 187 | 188 | return $this; 189 | } 190 | 191 | /** 192 | * @return View 193 | */ 194 | public function getView() 195 | { 196 | if (!$this->view) { 197 | $this->view = new View($this, new NavigateRange($this)); 198 | } 199 | 200 | return $this->view; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/Service/NavigateRange.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2011, Peter Gribanov 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace GpsLab\Bundle\PaginationBundle\Service; 12 | 13 | class NavigateRange 14 | { 15 | /** 16 | * @var Configuration 17 | */ 18 | private $config; 19 | 20 | /** 21 | * @var int 22 | */ 23 | private $left_offset = -1; 24 | 25 | /** 26 | * @var int 27 | */ 28 | private $right_offset = -1; 29 | 30 | /** 31 | * @param Configuration $config 32 | */ 33 | public function __construct(Configuration $config) 34 | { 35 | $this->config = $config; 36 | } 37 | 38 | /** 39 | * @return int 40 | */ 41 | public function getLeftOffset() 42 | { 43 | return $this->buildOffset()->left_offset; 44 | } 45 | 46 | /** 47 | * @return int 48 | */ 49 | public function getRightOffset() 50 | { 51 | return $this->buildOffset()->right_offset; 52 | } 53 | 54 | /** 55 | * @return self 56 | */ 57 | private function buildOffset() 58 | { 59 | if ($this->left_offset < 0) { 60 | $this->definitionOffset(); 61 | $this->adjustmentLargeLeftOffset(); 62 | $this->adjustmentLargeRightOffset(); 63 | $this->adjustmentLowerLeftOffset(); 64 | } 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * Definition of offset to the left and to the right of the selected page. 71 | */ 72 | private function definitionOffset() 73 | { 74 | $this->left_offset = (int) floor(($this->config->getMaxNavigate() - 1) / 2); 75 | $this->right_offset = (int) ceil(($this->config->getMaxNavigate() - 1) / 2); 76 | } 77 | 78 | /** 79 | * Adjustment, if the offset is too large left. 80 | */ 81 | private function adjustmentLargeLeftOffset() 82 | { 83 | if ($this->config->getCurrentPage() - $this->left_offset < 1) { 84 | $offset = abs($this->config->getCurrentPage() - 1 - $this->left_offset); 85 | $this->left_offset -= $offset; 86 | $this->right_offset += $offset; 87 | } 88 | } 89 | 90 | /** 91 | * Adjustment, if the offset is too large right. 92 | */ 93 | private function adjustmentLargeRightOffset() 94 | { 95 | if ($this->config->getCurrentPage() + $this->right_offset > $this->config->getTotalPages()) { 96 | $offset = abs( 97 | $this->config->getTotalPages() - 98 | $this->config->getCurrentPage() - 99 | $this->right_offset 100 | ); 101 | $this->left_offset += $offset; 102 | $this->right_offset -= $offset; 103 | } 104 | } 105 | 106 | /** 107 | * Left offset should point not lower of the first page. 108 | */ 109 | private function adjustmentLowerLeftOffset() 110 | { 111 | if ($this->left_offset >= $this->config->getCurrentPage()) { 112 | $this->left_offset = $this->config->getCurrentPage() - 1; 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Service/View.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2011, Peter Gribanov 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace GpsLab\Bundle\PaginationBundle\Service; 12 | 13 | use Doctrine\Common\Collections\ArrayCollection; 14 | use GpsLab\Bundle\PaginationBundle\Entity\Node; 15 | 16 | class View implements \IteratorAggregate 17 | { 18 | /** 19 | * @var Configuration 20 | */ 21 | private $config; 22 | 23 | /** 24 | * @var NavigateRange 25 | */ 26 | private $range; 27 | 28 | /** 29 | * @var Node|null 30 | */ 31 | private $first; 32 | 33 | /** 34 | * @var Node|null 35 | */ 36 | private $prev; 37 | 38 | /** 39 | * @var Node|null 40 | */ 41 | private $current; 42 | 43 | /** 44 | * @var Node|null 45 | */ 46 | private $next; 47 | 48 | /** 49 | * @var Node|null 50 | */ 51 | private $last; 52 | 53 | /** 54 | * @var ArrayCollection|null 55 | */ 56 | private $list; 57 | 58 | /** 59 | * @param Configuration $config 60 | * @param NavigateRange $range 61 | */ 62 | public function __construct(Configuration $config, NavigateRange $range) 63 | { 64 | $this->config = $config; 65 | $this->range = $range; 66 | } 67 | 68 | /** 69 | * @return int 70 | */ 71 | public function getTotal() 72 | { 73 | return $this->config->getTotalPages(); 74 | } 75 | 76 | /** 77 | * @return Node|null 78 | */ 79 | public function getFirst() 80 | { 81 | if (!$this->first && $this->config->getCurrentPage() > 1) { 82 | $this->first = new Node(1, $this->buildLink(1)); 83 | } 84 | 85 | return $this->first; 86 | } 87 | 88 | /** 89 | * @return Node|null 90 | */ 91 | public function getPrev() 92 | { 93 | if (!$this->prev && $this->config->getCurrentPage() > 1) { 94 | $this->prev = new Node( 95 | $this->config->getCurrentPage() - 1, 96 | $this->buildLink($this->config->getCurrentPage() - 1) 97 | ); 98 | } 99 | 100 | return $this->prev; 101 | } 102 | 103 | /** 104 | * @return Node 105 | */ 106 | public function getCurrent() 107 | { 108 | if (!$this->current) { 109 | $this->current = new Node( 110 | $this->config->getCurrentPage(), 111 | $this->buildLink($this->config->getCurrentPage()), 112 | true 113 | ); 114 | } 115 | 116 | return $this->current; 117 | } 118 | 119 | /** 120 | * @return Node|null 121 | */ 122 | public function getNext() 123 | { 124 | if (!$this->next && $this->config->getCurrentPage() < $this->getTotal()) { 125 | $this->next = new Node( 126 | $this->config->getCurrentPage() + 1, 127 | $this->buildLink($this->config->getCurrentPage() + 1) 128 | ); 129 | } 130 | 131 | return $this->next; 132 | } 133 | 134 | /** 135 | * @return Node|null 136 | */ 137 | public function getLast() 138 | { 139 | if (!$this->last && $this->config->getCurrentPage() < $this->getTotal()) { 140 | $this->last = new Node($this->getTotal(), $this->buildLink($this->getTotal())); 141 | } 142 | 143 | return $this->last; 144 | } 145 | 146 | /** 147 | * @return ArrayCollection|Node[] 148 | */ 149 | public function getIterator() 150 | { 151 | if (!($this->list instanceof ArrayCollection)) { 152 | $this->list = new ArrayCollection(); 153 | 154 | if ($this->getTotal() > 1) { 155 | // determining the first and last pages in paging based on the current page and offset 156 | $page = $this->config->getCurrentPage() - $this->range->getLeftOffset(); 157 | $page_to = $this->config->getCurrentPage() + $this->range->getRightOffset(); 158 | 159 | while ($page <= $page_to) { 160 | $this->list->add(new Node( 161 | $page, 162 | $this->buildLink($page), 163 | $page === $this->config->getCurrentPage() 164 | )); 165 | ++$page; 166 | } 167 | } 168 | } 169 | 170 | return $this->list; 171 | } 172 | 173 | /** 174 | * @param int $page 175 | * 176 | * @return string 177 | */ 178 | private function buildLink($page) 179 | { 180 | if ($page === 1 && $this->config->getFirstPageLink()) { 181 | return $this->config->getFirstPageLink(); 182 | } 183 | 184 | if (is_callable($this->config->getPageLink())) { 185 | return call_user_func($this->config->getPageLink(), $page); 186 | } 187 | 188 | return sprintf($this->config->getPageLink(), $page); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/Twig/Extension/PaginationExtension.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2011, Peter Gribanov 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace GpsLab\Bundle\PaginationBundle\Twig\Extension; 12 | 13 | use GpsLab\Bundle\PaginationBundle\Service\Configuration; 14 | use Twig\Environment; 15 | use Twig\Extension\AbstractExtension; 16 | use Twig\TwigFunction; 17 | 18 | class PaginationExtension extends AbstractExtension 19 | { 20 | /** 21 | * @var string 22 | */ 23 | private $template; 24 | 25 | /** 26 | * @param string $template 27 | */ 28 | public function __construct($template) 29 | { 30 | $this->template = $template; 31 | } 32 | 33 | /** 34 | * @return array 35 | */ 36 | public function getFunctions() 37 | { 38 | return [ 39 | new TwigFunction( 40 | 'pagination_render', 41 | [$this, 'renderPagination'], 42 | ['is_safe' => ['html'], 'needs_environment' => true] 43 | ), 44 | ]; 45 | } 46 | 47 | /** 48 | * @param \Twig\Environment $env 49 | * @param Configuration $pagination 50 | * @param string $template 51 | * @param array $view_params 52 | * @param int $max_navigate 53 | * 54 | * @throws \Twig\Error\LoaderError 55 | * @throws \Twig\Error\RuntimeError 56 | * @throws \Twig\Error\SyntaxError 57 | * 58 | * @return string 59 | */ 60 | public function renderPagination( 61 | Environment $env, 62 | Configuration $pagination, 63 | $template = null, 64 | array $view_params = [], 65 | $max_navigate = 0 66 | ) { 67 | if ($max_navigate > 0) { 68 | // not change original object 69 | $new_pagination = clone $pagination; 70 | $new_pagination->setMaxNavigate($max_navigate); 71 | 72 | $pagination_view = $new_pagination->getView(); 73 | } else { 74 | $pagination_view = $pagination->getView(); 75 | } 76 | 77 | return $env->render( 78 | $template ?: $this->template, 79 | array_merge($view_params, ['pagination' => $pagination_view]) 80 | ); 81 | } 82 | 83 | /** 84 | * @return string 85 | */ 86 | public function getName() 87 | { 88 | return 'gpslab_pagination_extension'; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/DependencyInjection/GpsLabPaginationExtensionTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2011, Peter Gribanov 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace GpsLab\Bundle\PaginationBundle\Tests\DependencyInjection; 12 | 13 | use GpsLab\Bundle\PaginationBundle\DependencyInjection\GpsLabPaginationExtension; 14 | use GpsLab\Bundle\PaginationBundle\Tests\TestCase; 15 | use Symfony\Component\DependencyInjection\ContainerBuilder; 16 | 17 | class GpsLabPaginationExtensionTest extends TestCase 18 | { 19 | /** 20 | * @var GpsLabPaginationExtension 21 | */ 22 | private $extension; 23 | 24 | protected function setUp() 25 | { 26 | $this->extension = new GpsLabPaginationExtension(); 27 | } 28 | 29 | public function testLoad() 30 | { 31 | $container = new ContainerBuilder(); 32 | $this->extension->load([], $container); 33 | 34 | self::assertEquals(5, $container->getParameter('pagination.max_navigate')); 35 | self::assertEquals('page', $container->getParameter('pagination.parameter_name')); 36 | self::assertEquals( 37 | 'GpsLabPaginationBundle::pagination.html.twig', 38 | $container->getParameter('pagination.template') 39 | ); 40 | } 41 | 42 | public function testGetAlias() 43 | { 44 | self::assertEquals('gpslab_pagination', $this->extension->getAlias()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Entity/NodeTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2011, Peter Gribanov 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace GpsLab\Bundle\PaginationBundle\Tests\Entity; 12 | 13 | use GpsLab\Bundle\PaginationBundle\Entity\Node; 14 | use GpsLab\Bundle\PaginationBundle\Tests\TestCase; 15 | 16 | class NodeTest extends TestCase 17 | { 18 | /** 19 | * @return array 20 | */ 21 | public function getNodes() 22 | { 23 | return [ 24 | [1, '', false], 25 | [4, 'http://example.com/?p=4', true], 26 | ]; 27 | } 28 | 29 | /** 30 | * @dataProvider getNodes 31 | * 32 | * @param int $page 33 | * @param string $link 34 | * @param bool $is_current 35 | */ 36 | public function test($page, $link, $is_current) 37 | { 38 | $node = new Node($page, $link, $is_current); 39 | self::assertEquals($page, $node->getPage()); 40 | self::assertEquals($link, $node->getLink()); 41 | self::assertEquals($is_current, $node->isCurrent()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Exception/IncorrectPageNumberExceptionTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2011, Peter Gribanov 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace GpsLab\Bundle\PaginationBundle\Tests\Exception; 12 | 13 | use GpsLab\Bundle\PaginationBundle\Exception\IncorrectPageNumberException; 14 | use GpsLab\Bundle\PaginationBundle\Tests\TestCase; 15 | 16 | class IncorrectPageNumberExceptionTest extends TestCase 17 | { 18 | public function testIncorrect() 19 | { 20 | $current_page = -5; 21 | $message = sprintf('Incorrect "%s" page number.', $current_page); 22 | 23 | $exception = IncorrectPageNumberException::incorrect($current_page); 24 | 25 | self::assertInstanceOf('GpsLab\Bundle\PaginationBundle\Exception\IncorrectPageNumberException', $exception); 26 | self::assertInstanceOf('Symfony\Component\HttpKernel\Exception\NotFoundHttpException', $exception); 27 | self::assertEquals($message, $exception->getMessage()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Exception/OutOfRangeExceptionTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2011, Peter Gribanov 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace GpsLab\Bundle\PaginationBundle\Tests\Exception; 12 | 13 | use GpsLab\Bundle\PaginationBundle\Exception\OutOfRangeException; 14 | use GpsLab\Bundle\PaginationBundle\Tests\TestCase; 15 | 16 | class OutOfRangeExceptionTest extends TestCase 17 | { 18 | public function testIncorrect() 19 | { 20 | $current_page = -5; 21 | $total_pages = 10; 22 | $message = sprintf('Select page "%s" is out of range "%s".', $current_page, $total_pages); 23 | 24 | $exception = OutOfRangeException::out($current_page, $total_pages); 25 | 26 | self::assertInstanceOf('GpsLab\Bundle\PaginationBundle\Exception\OutOfRangeException', $exception); 27 | self::assertInstanceOf('Symfony\Component\HttpKernel\Exception\NotFoundHttpException', $exception); 28 | self::assertEquals($message, $exception->getMessage()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/GpsLabPaginationBundleTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2011, Peter Gribanov 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace GpsLab\Bundle\PaginationBundle\Tests; 12 | 13 | use GpsLab\Bundle\PaginationBundle\GpsLabPaginationBundle; 14 | 15 | class GpsLabPaginationBundleTest extends TestCase 16 | { 17 | public function testGetContainerExtension() 18 | { 19 | $bundle = new GpsLabPaginationBundle(); 20 | $extension = $bundle->getContainerExtension(); 21 | 22 | self::assertInstanceOf( 23 | 'GpsLab\Bundle\PaginationBundle\DependencyInjection\GpsLabPaginationExtension', 24 | $extension 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/ParamConverter/PaginationParamConverterTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2011, Peter Gribanov 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace GpsLab\Bundle\PaginationBundle\Tests\ParamConverter; 12 | 13 | use GpsLab\Bundle\PaginationBundle\ParamConverter\PaginationParamConverter; 14 | use GpsLab\Bundle\PaginationBundle\Service\Configuration; 15 | use GpsLab\Bundle\PaginationBundle\Tests\TestCase; 16 | use PHPUnit\Framework\MockObject\MockObject; 17 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; 18 | use Symfony\Component\HttpFoundation\Request; 19 | use Symfony\Component\Routing\Generator\UrlGeneratorInterface; 20 | use Symfony\Component\Routing\RouterInterface; 21 | 22 | class PaginationParamConverterTest extends TestCase 23 | { 24 | const MAX_NAVIGATE = 10; 25 | 26 | const PARAMETER_NAME = 'p'; 27 | 28 | /** 29 | * @var RouterInterface|\PHPUnit_Framework_MockObject_MockObject|MockObject 30 | */ 31 | private $router; 32 | 33 | /** 34 | * @var ParamConverter|\PHPUnit_Framework_MockObject_MockObject|MockObject 35 | */ 36 | private $configuration; 37 | 38 | /** 39 | * @var PaginationParamConverter 40 | */ 41 | private $converter; 42 | 43 | protected function setUp() 44 | { 45 | $this->router = $this->getMockNoConstructor('Symfony\Component\Routing\RouterInterface'); 46 | $this->configuration = $this->getMockNoConstructor('Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter'); 47 | 48 | $this->converter = new PaginationParamConverter($this->router, self::MAX_NAVIGATE, self::PARAMETER_NAME); 49 | } 50 | 51 | public function testSupports() 52 | { 53 | $this->configuration 54 | ->expects(self::once()) 55 | ->method('getClass') 56 | ->willReturn('GpsLab\Bundle\PaginationBundle\Service\Configuration'); 57 | 58 | self::assertTrue($this->converter->supports($this->configuration)); 59 | } 60 | 61 | public function testNotSupports() 62 | { 63 | $this->configuration 64 | ->expects(self::once()) 65 | ->method('getClass') 66 | ->willReturn('stdClass'); 67 | 68 | self::assertFalse($this->converter->supports($this->configuration)); 69 | } 70 | 71 | /** 72 | * @return array 73 | */ 74 | public function getOptions() 75 | { 76 | return [ 77 | [ 78 | [], 79 | [], 80 | [], 81 | self::MAX_NAVIGATE, 82 | self::PARAMETER_NAME, 83 | UrlGeneratorInterface::ABSOLUTE_PATH, 84 | ], 85 | [ 86 | [ 87 | 'max_navigate' => 5, 88 | ], 89 | [], 90 | [], 91 | 5, 92 | self::PARAMETER_NAME, 93 | UrlGeneratorInterface::ABSOLUTE_PATH, 94 | ], 95 | [ 96 | [ 97 | 'parameter_name' => 'page', 98 | ], 99 | [], 100 | [], 101 | self::MAX_NAVIGATE, 102 | 'page', 103 | UrlGeneratorInterface::ABSOLUTE_PATH, 104 | ], 105 | [ 106 | [ 107 | 'reference_type' => 'absolute_url', 108 | ], 109 | [], 110 | [], 111 | self::MAX_NAVIGATE, 112 | self::PARAMETER_NAME, 113 | UrlGeneratorInterface::ABSOLUTE_PATH, 114 | ], 115 | [ 116 | [ 117 | 'reference_type' => 'absolute_path', 118 | ], 119 | [], 120 | [], 121 | self::MAX_NAVIGATE, 122 | self::PARAMETER_NAME, 123 | UrlGeneratorInterface::ABSOLUTE_PATH, 124 | ], 125 | [ 126 | [ 127 | 'reference_type' => 'relative_path', 128 | ], 129 | [], 130 | [], 131 | self::MAX_NAVIGATE, 132 | self::PARAMETER_NAME, 133 | UrlGeneratorInterface::RELATIVE_PATH, 134 | ], 135 | [ 136 | [ 137 | 'reference_type' => 'network_path', 138 | ], 139 | [], 140 | [], 141 | self::MAX_NAVIGATE, 142 | self::PARAMETER_NAME, 143 | UrlGeneratorInterface::NETWORK_PATH, 144 | ], 145 | [ 146 | [], 147 | [ 148 | 'foo' => 'bar', 149 | ], 150 | [], 151 | self::MAX_NAVIGATE, 152 | self::PARAMETER_NAME, 153 | UrlGeneratorInterface::ABSOLUTE_PATH, 154 | ], 155 | [ 156 | [], 157 | [], 158 | [ 159 | 'foo' => 'bar', 160 | ], 161 | self::MAX_NAVIGATE, 162 | self::PARAMETER_NAME, 163 | UrlGeneratorInterface::ABSOLUTE_PATH, 164 | ], 165 | [ 166 | [], 167 | [ 168 | 'foo' => 'bar', 169 | ], 170 | [ 171 | 'foo' => 'baz', 172 | ], 173 | self::MAX_NAVIGATE, 174 | self::PARAMETER_NAME, 175 | UrlGeneratorInterface::ABSOLUTE_PATH, 176 | ], 177 | [ 178 | [], 179 | [ 180 | self::PARAMETER_NAME => 'bar', 181 | ], 182 | [ 183 | self::PARAMETER_NAME => 'baz', 184 | ], 185 | self::MAX_NAVIGATE, 186 | self::PARAMETER_NAME, 187 | UrlGeneratorInterface::ABSOLUTE_PATH, 188 | ], 189 | ]; 190 | } 191 | 192 | /** 193 | * @dataProvider getOptions 194 | * 195 | * @param array $options 196 | * @param array $query 197 | * @param array $route_params 198 | * @param int $max_navigate 199 | * @param string $parameter_name 200 | * @param string $reference_type 201 | */ 202 | public function testApply( 203 | array $options, 204 | array $query, 205 | array $route_params, 206 | $max_navigate, 207 | $parameter_name, 208 | $reference_type 209 | ) { 210 | $route = 'my_route'; 211 | $prop_name = 'pagination'; 212 | $first_page_link = 'first_page_link'; 213 | $page_link = 'page_link'; 214 | $page_number = 1; 215 | 216 | $expected_route_params = array_merge($query, $route_params); 217 | unset($expected_route_params[$parameter_name]); 218 | 219 | $this->configuration 220 | ->expects(self::once()) 221 | ->method('getOptions') 222 | ->willReturn($options); 223 | $this->configuration 224 | ->expects(self::once()) 225 | ->method('getName') 226 | ->willReturn($prop_name); 227 | 228 | $this->router 229 | ->expects(self::at(0)) 230 | ->method('generate') 231 | ->with($route, $expected_route_params, $reference_type) 232 | ->willReturn($first_page_link); 233 | $this->router 234 | ->expects(self::at(1)) 235 | ->method('generate') 236 | ->with($route, [$parameter_name => $page_number] + $expected_route_params, $reference_type) 237 | ->willReturn($page_link); 238 | 239 | $request = new Request($query, [], [ 240 | '_route' => $route, 241 | '_route_params' => $route_params, 242 | ]); 243 | 244 | self::assertTrue($this->converter->apply($request, $this->configuration)); 245 | 246 | self::assertTrue($request->attributes->has($prop_name)); 247 | /* @var $configuration Configuration */ 248 | $configuration = $request->attributes->get($prop_name); 249 | self::assertInstanceOf('GpsLab\Bundle\PaginationBundle\Service\Configuration', $configuration); 250 | self::assertSame($max_navigate, $configuration->getMaxNavigate()); 251 | self::assertSame($first_page_link, $configuration->getFirstPageLink()); 252 | $callable_page_link = $configuration->getPageLink(); 253 | self::assertInternalType('callable', $callable_page_link); 254 | self::assertSame($page_link, $callable_page_link($page_number)); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /tests/Service/BuilderTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2011, Peter Gribanov 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace GpsLab\Bundle\PaginationBundle\Tests\Service; 12 | 13 | use Doctrine\ORM\AbstractQuery; 14 | use Doctrine\ORM\QueryBuilder; 15 | use GpsLab\Bundle\PaginationBundle\Service\Builder; 16 | use GpsLab\Bundle\PaginationBundle\Tests\TestCase; 17 | use Symfony\Bundle\FrameworkBundle\Routing\Router; 18 | use Symfony\Component\HttpFoundation\Request; 19 | use Symfony\Component\Routing\Generator\UrlGeneratorInterface; 20 | 21 | class BuilderTest extends TestCase 22 | { 23 | /** 24 | * @var \PHPUnit_Framework_MockObject_MockObject|Router 25 | */ 26 | private $router; 27 | 28 | /** 29 | * @var \PHPUnit_Framework_MockObject_MockObject|AbstractQuery 30 | */ 31 | private $query; 32 | 33 | /** 34 | * @var \PHPUnit_Framework_MockObject_MockObject|QueryBuilder 35 | */ 36 | private $query_builder; 37 | 38 | /** 39 | * @var Request 40 | */ 41 | private $request; 42 | 43 | /** 44 | * @var array 45 | */ 46 | private $query_params = [ 47 | 'foo' => 'bar', 48 | 'p' => 2, 49 | ]; 50 | 51 | protected function setUp() 52 | { 53 | $this->router = $this->getMockNoConstructor('Symfony\Bundle\FrameworkBundle\Routing\Router'); 54 | $this->query = $this->getMockAbstract('Doctrine\ORM\AbstractQuery', ['getSingleScalarResult']); 55 | $this->query_builder = $this->getMockNoConstructor('Doctrine\ORM\QueryBuilder'); 56 | 57 | $this->request = new Request($this->query_params); 58 | } 59 | 60 | public function testDefaultPageLink() 61 | { 62 | $builder = new Builder($this->router, 5, 'p'); 63 | 64 | self::assertEquals('?p=%d', $builder->paginate(10, 3)->getPageLink()); 65 | } 66 | 67 | /** 68 | * @return array 69 | */ 70 | public function getPaginateData() 71 | { 72 | return [ 73 | [5, 10, 1], 74 | [10, 150, 33], 75 | ]; 76 | } 77 | 78 | /** 79 | * @dataProvider getPaginateData 80 | * 81 | * @param int $max_navigate 82 | * @param int $total_pages 83 | * @param int $current_page 84 | */ 85 | public function testPaginate($max_navigate, $total_pages, $current_page) 86 | { 87 | $builder = new Builder($this->router, $max_navigate, 'page'); 88 | $config = $builder->paginate($total_pages, $current_page); 89 | 90 | self::assertEquals($max_navigate, $config->getMaxNavigate()); 91 | self::assertEquals($total_pages, $config->getTotalPages()); 92 | self::assertEquals($current_page, $config->getCurrentPage()); 93 | } 94 | 95 | /** 96 | * @return array 97 | */ 98 | public function getPaginateQueryData() 99 | { 100 | return [ 101 | [5, 5, 10, 1], 102 | [10, 10, 150, 7], 103 | ]; 104 | } 105 | 106 | /** 107 | * @dataProvider getPaginateQueryData 108 | * 109 | * @param int $max_navigate 110 | * @param int $per_page 111 | * @param int $total 112 | * @param int $current_page 113 | */ 114 | public function testPaginateQuery($max_navigate, $per_page, $total, $current_page) 115 | { 116 | $this->countQuery($total); 117 | 118 | $this->query_builder 119 | ->expects($this->once()) 120 | ->method('setFirstResult') 121 | ->with(($current_page - 1) * $per_page) 122 | ->willReturnSelf() 123 | ; 124 | $this->query_builder 125 | ->expects($this->once()) 126 | ->method('setMaxResults') 127 | ->with($per_page) 128 | ->willReturnSelf() 129 | ; 130 | 131 | $builder = new Builder($this->router, $max_navigate, 'page'); 132 | $config = $builder->paginateQuery($this->query_builder, $per_page, $current_page); 133 | 134 | self::assertEquals($max_navigate, $config->getMaxNavigate()); 135 | self::assertEquals(ceil($total / $per_page), $config->getTotalPages()); 136 | self::assertEquals($current_page, $config->getCurrentPage()); 137 | } 138 | 139 | /** 140 | * @expectedException \GpsLab\Bundle\PaginationBundle\Exception\OutOfRangeException 141 | */ 142 | public function testPaginateQueryOutOfRange() 143 | { 144 | $total = 10; 145 | $per_page = 5; 146 | $current_page = 150; 147 | 148 | $this->countQuery($total); 149 | 150 | $builder = new Builder($this->router, 5, 'page'); 151 | $builder->paginateQuery($this->query_builder, $per_page, $current_page); 152 | } 153 | 154 | /** 155 | * @expectedException \GpsLab\Bundle\PaginationBundle\Exception\IncorrectPageNumberException 156 | */ 157 | public function testPaginateRequestIncorrectPage() 158 | { 159 | $this->request = new Request(array_merge($this->query_params, [ 160 | 'page' => 'foo', 161 | ])); 162 | 163 | $builder = new Builder($this->router, 5, 'page'); 164 | $builder->paginateRequest($this->request, 10); 165 | } 166 | 167 | /** 168 | * @expectedException \GpsLab\Bundle\PaginationBundle\Exception\OutOfRangeException 169 | */ 170 | public function testPaginateRequestLowPageNumber() 171 | { 172 | $this->request = new Request(array_merge($this->query_params, [ 173 | 'p' => 0, 174 | ])); 175 | 176 | $builder = new Builder($this->router, 5, 'page'); 177 | $builder->paginateRequest($this->request, 10, 'p'); 178 | } 179 | 180 | /** 181 | * @expectedException \GpsLab\Bundle\PaginationBundle\Exception\OutOfRangeException 182 | */ 183 | public function testPaginateRequestOutOfRange() 184 | { 185 | $this->request = new Request(array_merge($this->query_params, [ 186 | 'p' => 150, 187 | ])); 188 | 189 | $builder = new Builder($this->router, 5, 'page'); 190 | $builder->paginateRequest($this->request, 10, 'p'); 191 | } 192 | 193 | public function testPaginateRequest() 194 | { 195 | $max_navigate = 6; 196 | $total_pages = 10; 197 | $parameter_name = 'p'; 198 | $route = '_route'; 199 | $route_params = ['foo' => 'baz', '_route_params' => 123]; 200 | $all_params = array_merge($this->query_params, $route_params); 201 | $reference_type = UrlGeneratorInterface::ABSOLUTE_URL; 202 | $this->request = new Request(array_merge($this->query_params, [ 203 | 'p' => null, 204 | ]), [], [ 205 | '_route' => $route, 206 | '_route_params' => $route_params, 207 | ]); 208 | 209 | $that = $this; 210 | $this->router 211 | ->expects($this->atLeastOnce()) 212 | ->method('generate') 213 | ->willReturnCallback(function ($_route, $_route_params, $_reference_type) use ( 214 | $that, 215 | $route, 216 | $reference_type 217 | ) { 218 | $that->assertEquals($reference_type, $_reference_type); 219 | $that->assertEquals($route, $_route); 220 | 221 | return $_route.http_build_query($_route_params); 222 | }) 223 | ; 224 | 225 | $builder = new Builder($this->router, $max_navigate, 'page'); 226 | $config = $builder->paginateRequest($this->request, $total_pages, $parameter_name, $reference_type); 227 | 228 | self::assertEquals($max_navigate, $config->getMaxNavigate()); 229 | self::assertEquals($total_pages, $config->getTotalPages()); 230 | self::assertEquals(1, $config->getCurrentPage()); 231 | unset($all_params['p']); 232 | self::assertEquals($route.http_build_query($all_params), $config->getFirstPageLink()); 233 | self::assertInstanceOf('Closure', $config->getPageLink()); 234 | $page_number = 3; 235 | self::assertEquals( 236 | $route.http_build_query($all_params + [$parameter_name => $page_number]), 237 | call_user_func($config->getPageLink(), $page_number) 238 | ); 239 | } 240 | 241 | /** 242 | * @expectedException \GpsLab\Bundle\PaginationBundle\Exception\IncorrectPageNumberException 243 | */ 244 | public function testPaginateRequesQuerytIncorrectPage() 245 | { 246 | $this->request = new Request(array_merge($this->query_params, [ 247 | 'page' => 'foo', 248 | ])); 249 | $this->countQuery(10); 250 | 251 | $builder = new Builder($this->router, 5, 'page'); 252 | $builder->paginateRequestQuery($this->request, $this->query_builder, 5); 253 | } 254 | 255 | /** 256 | * @expectedException \GpsLab\Bundle\PaginationBundle\Exception\OutOfRangeException 257 | */ 258 | public function testPaginateRequestQueryLowPageNumber() 259 | { 260 | $this->request = new Request(array_merge($this->query_params, [ 261 | 'p' => 0, 262 | ])); 263 | $this->countQuery(10); 264 | 265 | $builder = new Builder($this->router, 5, 'page'); 266 | $builder->paginateRequestQuery($this->request, $this->query_builder, 5, 'p'); 267 | } 268 | 269 | /** 270 | * @expectedException \GpsLab\Bundle\PaginationBundle\Exception\OutOfRangeException 271 | */ 272 | public function testPaginateRequestQueryOutOfRange() 273 | { 274 | $this->request = new Request(array_merge($this->query_params, [ 275 | 'p' => 150, 276 | ])); 277 | $this->countQuery(10); 278 | 279 | $builder = new Builder($this->router, 5, 'page'); 280 | $builder->paginateRequestQuery($this->request, $this->query_builder, 5, 'p'); 281 | } 282 | 283 | public function testPaginateRequestQuery() 284 | { 285 | $per_page = 10; 286 | $current_page = 7; 287 | $max_navigate = 6; 288 | $total = 150; 289 | $parameter_name = 'p'; 290 | $route = '_route'; 291 | $route_params = ['foo' => 'baz', '_route_params']; 292 | $all_params = array_merge($this->query_params, $route_params); 293 | $reference_type = UrlGeneratorInterface::ABSOLUTE_URL; 294 | $this->request = new Request(array_merge($this->query_params, [ 295 | 'p' => $current_page, 296 | ]), [], [ 297 | '_route' => $route, 298 | '_route_params' => $route_params, 299 | ]); 300 | 301 | $this->countQuery($total); 302 | $this->query_builder 303 | ->expects($this->once()) 304 | ->method('setFirstResult') 305 | ->with(($current_page - 1) * $per_page) 306 | ->willReturnSelf() 307 | ; 308 | $this->query_builder 309 | ->expects($this->once()) 310 | ->method('setMaxResults') 311 | ->with($per_page) 312 | ->willReturnSelf() 313 | ; 314 | 315 | $that = $this; 316 | $this->router 317 | ->expects($this->atLeastOnce()) 318 | ->method('generate') 319 | ->willReturnCallback(function ($_route, $_route_params, $_reference_type) use ( 320 | $that, 321 | $route, 322 | $reference_type 323 | ) { 324 | $that->assertEquals($reference_type, $_reference_type); 325 | $that->assertEquals($route, $_route); 326 | 327 | return $_route.http_build_query($_route_params); 328 | }) 329 | ; 330 | 331 | $builder = new Builder($this->router, $max_navigate, 'page'); 332 | $config = $builder->paginateRequestQuery( 333 | $this->request, 334 | $this->query_builder, 335 | $per_page, 336 | $parameter_name, 337 | $reference_type 338 | ); 339 | 340 | self::assertEquals($max_navigate, $config->getMaxNavigate()); 341 | self::assertEquals(ceil($total / $per_page), $config->getTotalPages()); 342 | self::assertEquals($current_page, $config->getCurrentPage()); 343 | unset($all_params['p']); 344 | self::assertEquals($route.http_build_query($all_params), $config->getFirstPageLink()); 345 | self::assertInstanceOf('Closure', $config->getPageLink()); 346 | $page_number = 3; 347 | self::assertEquals( 348 | $route.http_build_query($all_params + [$parameter_name => $page_number]), 349 | call_user_func($config->getPageLink(), $page_number) 350 | ); 351 | } 352 | 353 | /** 354 | * @param int $total 355 | */ 356 | private function countQuery($total) 357 | { 358 | $this->query 359 | ->expects($this->once()) 360 | ->method('getSingleScalarResult') 361 | ->willReturn($total); 362 | 363 | $this->query_builder 364 | ->expects($this->once()) 365 | ->method('getRootAliases') 366 | ->willReturn(['a', 'b']); 367 | $this->query_builder 368 | ->expects($this->once()) 369 | ->method('select') 370 | ->with('COUNT(a)') 371 | ->willReturnSelf(); 372 | $this->query_builder 373 | ->expects($this->once()) 374 | ->method('getQuery') 375 | ->willReturn($this->query); 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /tests/Service/ConfigurationTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2011, Peter Gribanov 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace GpsLab\Bundle\PaginationBundle\Tests\Service; 12 | 13 | use GpsLab\Bundle\PaginationBundle\Service\Configuration; 14 | use GpsLab\Bundle\PaginationBundle\Tests\TestCase; 15 | 16 | class ConfigurationTest extends TestCase 17 | { 18 | /** 19 | * @var Configuration 20 | */ 21 | private $config; 22 | 23 | protected function setUp() 24 | { 25 | $this->config = new Configuration(150, 33); 26 | } 27 | 28 | public function testDefaultPageLink() 29 | { 30 | self::assertEquals('?page=%d', $this->config->getPageLink()); 31 | } 32 | 33 | /** 34 | * @return array 35 | */ 36 | public function getConfigs() 37 | { 38 | return [ 39 | [10, 1], 40 | [150, 33], 41 | ]; 42 | } 43 | 44 | /** 45 | * @dataProvider getConfigs 46 | * 47 | * @param int $total_pages 48 | * @param int $current_page 49 | */ 50 | public function testConstruct($total_pages, $current_page) 51 | { 52 | $config = new Configuration($total_pages, $current_page); 53 | self::assertEquals($total_pages, $config->getTotalPages()); 54 | self::assertEquals($current_page, $config->getCurrentPage()); 55 | } 56 | 57 | /** 58 | * @dataProvider getConfigs 59 | * 60 | * @param int $total_pages 61 | * @param int $current_page 62 | */ 63 | public function testCreate($total_pages, $current_page) 64 | { 65 | $config = Configuration::create($total_pages, $current_page); 66 | self::assertEquals($total_pages, $config->getTotalPages()); 67 | self::assertEquals($current_page, $config->getCurrentPage()); 68 | } 69 | 70 | /** 71 | * @return array 72 | */ 73 | public function getMethods() 74 | { 75 | return [ 76 | [ 77 | 150, 78 | 10, 79 | 'getTotalPages', 80 | 'setTotalPages', 81 | ], 82 | [ 83 | 33, 84 | 1, 85 | 'getCurrentPage', 86 | 'setCurrentPage', 87 | ], 88 | [ 89 | Configuration::DEFAULT_LIST_LENGTH, 90 | Configuration::DEFAULT_LIST_LENGTH + 5, 91 | 'getMaxNavigate', 92 | 'setMaxNavigate', 93 | ], 94 | [ 95 | Configuration::DEFAULT_PAGE_LINK, 96 | 'page_%s.html', 97 | 'getPageLink', 98 | 'setPageLink', 99 | ], 100 | [ 101 | Configuration::DEFAULT_PAGE_LINK, 102 | function ($number) { 103 | return 'page_'.$number.'.html'; 104 | }, 105 | 'getPageLink', 106 | 'setPageLink', 107 | ], 108 | [ 109 | '', 110 | '/index.html', 111 | 'getFirstPageLink', 112 | 'setFirstPageLink', 113 | ], 114 | ]; 115 | } 116 | 117 | /** 118 | * @dataProvider getMethods 119 | * 120 | * @param mixed $default 121 | * @param mixed $new 122 | * @param string $getter 123 | * @param string $setter 124 | */ 125 | public function testSetGet($default, $new, $getter, $setter) 126 | { 127 | self::assertEquals($default, call_user_func([$this->config, $getter])); 128 | self::assertEquals($this->config, call_user_func([$this->config, $setter], $new)); 129 | self::assertEquals($new, call_user_func([$this->config, $getter])); 130 | } 131 | 132 | public function testGetView() 133 | { 134 | $view = $this->config->getView(); 135 | self::assertInstanceOf('GpsLab\Bundle\PaginationBundle\Service\View', $view); 136 | 137 | // test lazy load 138 | $this->config->setPageLink('?p=%s'); 139 | self::assertEquals($view, $this->config->getView()); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /tests/Service/NavigateRangeTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2011, Peter Gribanov 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace GpsLab\Bundle\PaginationBundle\Tests\Service; 12 | 13 | use GpsLab\Bundle\PaginationBundle\Service\Configuration; 14 | use GpsLab\Bundle\PaginationBundle\Service\NavigateRange; 15 | use GpsLab\Bundle\PaginationBundle\Tests\TestCase; 16 | 17 | class NavigateRangeTest extends TestCase 18 | { 19 | /** 20 | * @var \PHPUnit_Framework_MockObject_MockObject|Configuration 21 | */ 22 | private $config; 23 | 24 | /** 25 | * @var NavigateRange 26 | */ 27 | private $range; 28 | 29 | protected function setUp() 30 | { 31 | $this->config = $this->getMockNoConstructor('GpsLab\Bundle\PaginationBundle\Service\Configuration'); 32 | 33 | $this->range = new NavigateRange($this->config); 34 | } 35 | 36 | /** 37 | * @return array 38 | */ 39 | public function getOffsets() 40 | { 41 | return [ 42 | [5, 1, 2, 0, 1], 43 | [5, 2, 2, 1, 0], 44 | [5, 1, 10, 0, 4], 45 | [5, 2, 10, 1, 3], 46 | [5, 3, 10, 2, 2], 47 | [5, 4, 10, 2, 2], 48 | [5, 8, 10, 2, 2], 49 | [5, 9, 10, 3, 1], 50 | [5, 10, 10, 4, 0], 51 | [5, 1, 1, 0, 0], // list pages is empty 52 | ]; 53 | } 54 | 55 | /** 56 | * @dataProvider getOffsets 57 | * 58 | * @param int $max_navigate 59 | * @param int $current_page 60 | * @param int $total_pages 61 | * @param int $left_offset 62 | * @param int $right_offset 63 | */ 64 | public function testBuildOffset($max_navigate, $current_page, $total_pages, $left_offset, $right_offset) 65 | { 66 | $this->config 67 | ->expects($this->exactly(2)) // test cache build result 68 | ->method('getMaxNavigate') 69 | ->willReturn($max_navigate); 70 | $this->config 71 | ->expects($this->atLeastOnce()) 72 | ->method('getCurrentPage') 73 | ->willReturn($current_page); 74 | $this->config 75 | ->expects($this->atLeastOnce()) 76 | ->method('getTotalPages') 77 | ->willReturn($total_pages); 78 | 79 | self::assertEquals($left_offset, $this->range->getLeftOffset()); 80 | self::assertEquals($right_offset, $this->range->getRightOffset()); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tests/Service/ViewTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2011, Peter Gribanov 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace GpsLab\Bundle\PaginationBundle\Tests\Service; 12 | 13 | use Doctrine\Common\Collections\ArrayCollection; 14 | use GpsLab\Bundle\PaginationBundle\Entity\Node; 15 | use GpsLab\Bundle\PaginationBundle\Service\Configuration; 16 | use GpsLab\Bundle\PaginationBundle\Service\NavigateRange; 17 | use GpsLab\Bundle\PaginationBundle\Service\View; 18 | use GpsLab\Bundle\PaginationBundle\Tests\TestCase; 19 | 20 | class ViewTest extends TestCase 21 | { 22 | /** 23 | * @var \PHPUnit_Framework_MockObject_MockObject|Configuration 24 | */ 25 | private $config; 26 | 27 | /** 28 | * @var \PHPUnit_Framework_MockObject_MockObject|NavigateRange 29 | */ 30 | private $range; 31 | 32 | /** 33 | * @var View 34 | */ 35 | private $view; 36 | 37 | protected function setUp() 38 | { 39 | $this->config = $this->getMockNoConstructor('GpsLab\Bundle\PaginationBundle\Service\Configuration'); 40 | $this->range = $this->getMockNoConstructor('GpsLab\Bundle\PaginationBundle\Service\NavigateRange'); 41 | 42 | $this->view = new View($this->config, $this->range); 43 | } 44 | 45 | public function testGetTotal() 46 | { 47 | $this->config 48 | ->expects($this->once()) 49 | ->method('getTotalPages') 50 | ->willReturn('110') 51 | ; 52 | 53 | self::assertEquals(110, $this->view->getTotal()); 54 | } 55 | 56 | /** 57 | * @return array 58 | */ 59 | public function getFailNodes() 60 | { 61 | return [ 62 | ['getFirst', 1], 63 | ['getPrev', 1], 64 | ['getNext', 110], 65 | ['getLast', 110], 66 | ]; 67 | } 68 | 69 | /** 70 | * @dataProvider getFailNodes 71 | * 72 | * @param string $method 73 | * @param int $current_page 74 | */ 75 | public function testGetNodeFail($method, $current_page) 76 | { 77 | $this->config 78 | ->expects($this->any()) 79 | ->method('getTotalPages') 80 | ->willReturn(110) 81 | ; 82 | $this->config 83 | ->expects($this->any()) 84 | ->method('getCurrentPage') 85 | ->willReturn($current_page) 86 | ; 87 | 88 | self::assertNull(call_user_func([$this->view, $method])); 89 | } 90 | 91 | /** 92 | * @return array 93 | */ 94 | public function getPageLinks() 95 | { 96 | return [ 97 | ['page_%s.html'], 98 | [function ($number) { 99 | return 'page_'.$number.'.html'; 100 | }], 101 | ]; 102 | } 103 | 104 | /** 105 | * @return array 106 | */ 107 | public function getFirstPageLinks() 108 | { 109 | return [ 110 | ['page_%s.html', ''], 111 | ['page_%s.html', '/index.html'], 112 | [function ($number) { 113 | return 'page_'.$number.'.html'; 114 | }, ''], 115 | [function ($number) { 116 | return 'page_'.$number.'.html'; 117 | }, '/index.html'], 118 | ]; 119 | } 120 | 121 | /** 122 | * @param string|callable $page_link 123 | * @param int $number 124 | * 125 | * @return string 126 | */ 127 | protected function getLink($page_link, $number) 128 | { 129 | return is_callable($page_link) ? call_user_func($page_link, $number) : sprintf($page_link, $number); 130 | } 131 | 132 | /** 133 | * @dataProvider getFirstPageLinks 134 | * 135 | * @param string|callable $page_link 136 | * @param string $first_page_link 137 | */ 138 | public function testGetFirst($page_link, $first_page_link) 139 | { 140 | $this->config 141 | ->expects($this->once()) 142 | ->method('getCurrentPage') 143 | ->willReturn(10); 144 | $this->config 145 | ->expects($first_page_link ? $this->atLeastOnce() : $this->once()) 146 | ->method('getFirstPageLink') 147 | ->willReturn($first_page_link); 148 | $this->config 149 | ->expects($first_page_link ? $this->never() : $this->atLeastOnce()) 150 | ->method('getPageLink') 151 | ->willReturn($page_link); 152 | 153 | $node = $this->view->getFirst(); 154 | self::assertInstanceOf('GpsLab\Bundle\PaginationBundle\Entity\Node', $node); 155 | self::assertEquals(1, $node->getPage()); 156 | if ($first_page_link) { 157 | self::assertEquals($first_page_link, $node->getLink()); 158 | } else { 159 | self::assertEquals($this->getLink($page_link, 1), $node->getLink()); 160 | } 161 | } 162 | 163 | /** 164 | * @dataProvider getPageLinks 165 | * 166 | * @param string|callable $page_link 167 | */ 168 | public function testGetPrev($page_link) 169 | { 170 | $this->config 171 | ->expects($this->atLeastOnce()) 172 | ->method('getCurrentPage') 173 | ->willReturn(5); 174 | $this->config 175 | ->expects($this->never()) 176 | ->method('getFirstPageLink') 177 | ->willReturn(''); 178 | $this->config 179 | ->expects($this->atLeastOnce()) 180 | ->method('getPageLink') 181 | ->willReturn($page_link); 182 | 183 | $node = $this->view->getPrev(); 184 | self::assertInstanceOf('GpsLab\Bundle\PaginationBundle\Entity\Node', $node); 185 | self::assertEquals(4, $node->getPage()); 186 | self::assertEquals($this->getLink($page_link, 4), $node->getLink()); 187 | } 188 | 189 | /** 190 | * @dataProvider getFirstPageLinks 191 | * 192 | * @param string|callable $page_link 193 | * @param string $first_page_link 194 | */ 195 | public function testGetCurrent($page_link, $first_page_link) 196 | { 197 | $this->config 198 | ->expects($this->atLeastOnce()) 199 | ->method('getCurrentPage') 200 | ->willReturn(1); 201 | $this->config 202 | ->expects($first_page_link ? $this->atLeastOnce() : $this->once()) 203 | ->method('getFirstPageLink') 204 | ->willReturn($first_page_link); 205 | $this->config 206 | ->expects($first_page_link ? $this->never() : $this->atLeastOnce()) 207 | ->method('getPageLink') 208 | ->willReturn($page_link); 209 | 210 | $node = $this->view->getCurrent(); 211 | self::assertInstanceOf('GpsLab\Bundle\PaginationBundle\Entity\Node', $node); 212 | self::assertEquals(1, $node->getPage()); 213 | if ($first_page_link) { 214 | self::assertEquals($first_page_link, $node->getLink()); 215 | } else { 216 | self::assertEquals($this->getLink($page_link, 1), $node->getLink()); 217 | } 218 | } 219 | 220 | /** 221 | * @dataProvider getPageLinks 222 | * 223 | * @param string|callable $page_link 224 | */ 225 | public function testGetNext($page_link) 226 | { 227 | $this->config 228 | ->expects($this->atLeastOnce()) 229 | ->method('getCurrentPage') 230 | ->willReturn(5); 231 | $this->config 232 | ->expects($this->atLeastOnce()) 233 | ->method('getTotalPages') 234 | ->willReturn(10); 235 | $this->config 236 | ->expects($this->never()) 237 | ->method('getFirstPageLink') 238 | ->willReturn(''); 239 | $this->config 240 | ->expects($this->atLeastOnce()) 241 | ->method('getPageLink') 242 | ->willReturn($page_link); 243 | 244 | $node = $this->view->getNext(); 245 | self::assertInstanceOf('GpsLab\Bundle\PaginationBundle\Entity\Node', $node); 246 | self::assertEquals(6, $node->getPage()); 247 | self::assertEquals($this->getLink($page_link, 6), $node->getLink()); 248 | } 249 | 250 | /** 251 | * @dataProvider getPageLinks 252 | * 253 | * @param string|callable $page_link 254 | */ 255 | public function testGetLast($page_link) 256 | { 257 | $this->config 258 | ->expects($this->atLeastOnce()) 259 | ->method('getCurrentPage') 260 | ->willReturn(5); 261 | $this->config 262 | ->expects($this->atLeastOnce()) 263 | ->method('getTotalPages') 264 | ->willReturn(10); 265 | $this->config 266 | ->expects($this->never()) 267 | ->method('getFirstPageLink') 268 | ->willReturn(''); 269 | $this->config 270 | ->expects($this->atLeastOnce()) 271 | ->method('getPageLink') 272 | ->willReturn($page_link); 273 | 274 | $node = $this->view->getLast(); 275 | self::assertInstanceOf('GpsLab\Bundle\PaginationBundle\Entity\Node', $node); 276 | self::assertEquals(10, $node->getPage()); 277 | self::assertEquals($this->getLink($page_link, 10), $node->getLink()); 278 | } 279 | 280 | /** 281 | * @return array 282 | */ 283 | public function getNodes() 284 | { 285 | return [ 286 | [ 287 | 2, 288 | '/?page=%s', 289 | null, 290 | new ArrayCollection([ 291 | new Node(1, '/?page=1', true), 292 | new Node(2, '/?page=2'), 293 | ]), 294 | ], 295 | [ 296 | 2, 297 | '/?page=%s', 298 | null, 299 | new ArrayCollection([ 300 | new Node(1, '/?page=1'), 301 | new Node(2, '/?page=2', true), 302 | ]), 303 | ], 304 | [ 305 | 10, 306 | '/?page=%s', 307 | null, 308 | new ArrayCollection([ 309 | new Node(1, '/?page=1', true), 310 | new Node(2, '/?page=2'), 311 | new Node(3, '/?page=3'), 312 | new Node(4, '/?page=4'), 313 | new Node(5, '/?page=5'), 314 | ]), 315 | ], 316 | [ 317 | 10, 318 | '/?page=%s', 319 | null, 320 | new ArrayCollection([ 321 | new Node(6, '/?page=6'), 322 | new Node(7, '/?page=7'), 323 | new Node(8, '/?page=8'), 324 | new Node(9, '/?page=9'), 325 | new Node(10, '/?page=10', true), 326 | ]), 327 | ], 328 | [ 329 | 10, 330 | '/?page=%s', 331 | null, 332 | new ArrayCollection([ 333 | new Node(3, '/?page=3'), 334 | new Node(4, '/?page=4'), 335 | new Node(5, '/?page=5', true), 336 | new Node(6, '/?page=6'), 337 | new Node(7, '/?page=7'), 338 | ]), 339 | ], 340 | [ 341 | 10, 342 | function ($number) { 343 | return sprintf('/?page=%s', $number); 344 | }, 345 | '/', 346 | new ArrayCollection([ 347 | new Node(4, '/?page=4'), 348 | new Node(5, '/?page=5', true), 349 | new Node(6, '/?page=6'), 350 | new Node(7, '/?page=7'), 351 | ]), 352 | ], 353 | ]; 354 | } 355 | 356 | /** 357 | * @dataProvider getNodes 358 | * 359 | * @param int $total_pages 360 | * @param string|\Closure $page_link 361 | * @param string $first_page_link 362 | * @param ArrayCollection $list 363 | */ 364 | public function testGetIterator($total_pages, $page_link, $first_page_link, $list) 365 | { 366 | $current_page = 1; 367 | foreach ($list as $node) { 368 | /** @var $node Node */ 369 | if ($node->isCurrent()) { 370 | $current_page = $node->getPage(); 371 | } 372 | } 373 | 374 | $left_offset = $current_page - $list->first()->getPage(); 375 | $right_offset = $list->last()->getPage() - $current_page; 376 | 377 | if ($list->first()->getPage() === 1) { 378 | $this->config 379 | ->expects($this->once()) 380 | ->method('getFirstPageLink') 381 | ->willReturn($first_page_link); 382 | } else { 383 | $this->config 384 | ->expects($this->never()) 385 | ->method('getFirstPageLink'); 386 | } 387 | 388 | $this->config 389 | ->expects($this->once()) 390 | ->method('getTotalPages') 391 | ->willReturn($total_pages); 392 | $this->config 393 | ->expects($this->atLeastOnce()) 394 | ->method('getCurrentPage') 395 | ->willReturn($current_page); 396 | $this->config 397 | ->expects($this->atLeastOnce()) 398 | ->method('getPageLink') 399 | ->willReturn($page_link); 400 | 401 | $this->range 402 | ->expects($this->once()) 403 | ->method('getLeftOffset') 404 | ->willReturn($left_offset); 405 | $this->range 406 | ->expects($this->once()) 407 | ->method('getRightOffset') 408 | ->willReturn($right_offset); 409 | 410 | self::assertEquals($list, $this->view->getIterator()); 411 | } 412 | 413 | public function testGetIteratorEmpty() 414 | { 415 | $this->config 416 | ->expects($this->once()) 417 | ->method('getTotalPages') 418 | ->willReturn(1); 419 | $this->config 420 | ->expects($this->never()) 421 | ->method('getCurrentPage'); 422 | $this->config 423 | ->expects($this->never()) 424 | ->method('getPageLink'); 425 | $this->config 426 | ->expects($this->never()) 427 | ->method('getFirstPageLink'); 428 | 429 | $this->range 430 | ->expects($this->never()) 431 | ->method('getLeftOffset'); 432 | $this->range 433 | ->expects($this->never()) 434 | ->method('getRightOffset'); 435 | 436 | self::assertEquals(new ArrayCollection(), $this->view->getIterator()); 437 | } 438 | } 439 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2011, Peter Gribanov 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace GpsLab\Bundle\PaginationBundle\Tests; 12 | 13 | use PHPUnit\Framework\MockObject\MockObject; 14 | use PHPUnit\Framework\TestCase as BaseTestCase; 15 | 16 | class TestCase extends BaseTestCase 17 | { 18 | /** 19 | * @param string $class_name 20 | * 21 | * @return \PHPUnit_Framework_MockObject_MockObject|MockObject 22 | */ 23 | protected function getMockNoConstructor($class_name) 24 | { 25 | return $this 26 | ->getMockBuilder($class_name) 27 | ->disableOriginalConstructor() 28 | ->disableOriginalClone() 29 | ->getMock(); 30 | } 31 | 32 | /** 33 | * @param string $class_name 34 | * 35 | * @return \PHPUnit_Framework_MockObject_MockObject|MockObject 36 | */ 37 | protected function getMockAbstract($class_name, array $methods) 38 | { 39 | return $this 40 | ->getMockBuilder($class_name) 41 | ->disableOriginalConstructor() 42 | ->setMethods($methods) 43 | ->getMockForAbstractClass(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Twig/Extension/PaginationExtensionTest.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2011, Peter Gribanov 8 | * @license http://opensource.org/licenses/MIT 9 | */ 10 | 11 | namespace GpsLab\Bundle\PaginationBundle\Tests\Twig\Extension; 12 | 13 | use GpsLab\Bundle\PaginationBundle\Service\Configuration; 14 | use GpsLab\Bundle\PaginationBundle\Tests\TestCase; 15 | use GpsLab\Bundle\PaginationBundle\Twig\Extension\PaginationExtension; 16 | 17 | class PaginationExtensionTest extends TestCase 18 | { 19 | /** 20 | * @var PaginationExtension 21 | */ 22 | private $extension; 23 | 24 | /** 25 | * @var string 26 | */ 27 | private $template = 'foo'; 28 | 29 | protected function setUp() 30 | { 31 | $this->extension = new PaginationExtension($this->template); 32 | } 33 | 34 | public function testGetFunctions() 35 | { 36 | $functions = $this->extension->getFunctions(); 37 | 38 | self::assertInternalType('array', $functions); 39 | self::assertCount(1, $functions); 40 | self::assertInstanceOf('Twig\TwigFunction', $functions[0]); 41 | } 42 | 43 | public function testRender() 44 | { 45 | $expected = 'bar'; 46 | $view = 'baz'; 47 | /* @var $env \PHPUnit_Framework_MockObject_MockObject|\Twig\Environment */ 48 | $env = $this->getMockNoConstructor('Twig\Environment'); 49 | $env 50 | ->expects($this->once()) 51 | ->method('render') 52 | ->with($this->template, ['pagination' => $view]) 53 | ->willReturn($expected); 54 | 55 | /* @var $configuration \PHPUnit_Framework_MockObject_MockObject|Configuration */ 56 | $configuration = $this->getMockNoConstructor('GpsLab\Bundle\PaginationBundle\Service\Configuration'); 57 | $configuration 58 | ->expects($this->once()) 59 | ->method('getView') 60 | ->willReturn($view); 61 | 62 | self::assertEquals($expected, $this->extension->renderPagination( 63 | $env, 64 | $configuration 65 | )); 66 | } 67 | 68 | public function testRenderChangeTemplate() 69 | { 70 | $expected = 'bar'; 71 | $view = 'baz'; 72 | $template = 'my_template'; 73 | /* @var $env \PHPUnit_Framework_MockObject_MockObject|\Twig\Environment */ 74 | $env = $this->getMockNoConstructor('Twig\Environment'); 75 | $env 76 | ->expects($this->once()) 77 | ->method('render') 78 | ->with($template, ['pagination' => $view, 'my_params' => 12345]) 79 | ->willReturn($expected); 80 | 81 | /* @var $configuration \PHPUnit_Framework_MockObject_MockObject|Configuration */ 82 | $configuration = $this->getMockNoConstructor('GpsLab\Bundle\PaginationBundle\Service\Configuration'); 83 | $configuration 84 | ->expects($this->once()) 85 | ->method('getView') 86 | ->willReturn($view); 87 | 88 | self::assertEquals($expected, $this->extension->renderPagination( 89 | $env, 90 | $configuration, 91 | $template, 92 | ['my_params' => 12345] 93 | )); 94 | } 95 | 96 | public function testRenderNoOverrideTemplateParams() 97 | { 98 | $expected = 'bar'; 99 | $view = 'baz'; 100 | /* @var $env \PHPUnit_Framework_MockObject_MockObject|\Twig\Environment */ 101 | $env = $this->getMockNoConstructor('Twig\Environment'); 102 | $env 103 | ->expects($this->once()) 104 | ->method('render') 105 | ->with($this->template, ['pagination' => $view]) 106 | ->willReturn($expected); 107 | 108 | /* @var $configuration \PHPUnit_Framework_MockObject_MockObject|Configuration */ 109 | $configuration = $this->getMockNoConstructor('GpsLab\Bundle\PaginationBundle\Service\Configuration'); 110 | $configuration 111 | ->expects($this->once()) 112 | ->method('getView') 113 | ->willReturn($view); 114 | 115 | self::assertEquals($expected, $this->extension->renderPagination( 116 | $env, 117 | $configuration, 118 | null, 119 | ['pagination' => 12345] 120 | )); 121 | } 122 | 123 | public function testRenderWithCustomMaxNavigate() 124 | { 125 | $old_max_navigate = 5; 126 | $new_max_navigate = 10; 127 | $expected = 'bar'; 128 | 129 | $configuration = new Configuration(); 130 | $configuration->setTotalPages(100); 131 | $configuration->setMaxNavigate($old_max_navigate); 132 | 133 | /* @var $env \PHPUnit_Framework_MockObject_MockObject|\Twig\Environment */ 134 | $env = $this->getMockNoConstructor('Twig\Environment'); 135 | $env 136 | ->expects($this->once()) 137 | ->method('render') 138 | ->willReturnCallback(function ($template, array $context) use ($expected, $configuration, $new_max_navigate) { 139 | self::assertSame($this->template, $template); 140 | self::assertArrayHasKey('pagination', $context); 141 | self::assertSame(['pagination'], array_keys($context), 'Context has only "pagination" key.'); 142 | $view = $context['pagination']; 143 | self::assertInstanceOf('GpsLab\Bundle\PaginationBundle\Service\View', $view); 144 | self::assertNotEquals($configuration->getView(), $view); 145 | self::assertCount($new_max_navigate, iterator_to_array($view)); 146 | 147 | return $expected; 148 | }); 149 | 150 | self::assertSame($expected, $this->extension->renderPagination( 151 | $env, 152 | $configuration, 153 | null, 154 | [], 155 | $new_max_navigate 156 | )); 157 | self::assertSame($old_max_navigate, $configuration->getMaxNavigate(), 'The max navigate is not changed in original object.'); 158 | } 159 | 160 | public function testGetName() 161 | { 162 | self::assertEquals('gpslab_pagination_extension', $this->extension->getName()); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |