diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc05456..58ece9e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: strategy: fail-fast: false matrix: - php-versions: ['7.4', '8.0', '8.1', '8.2', '8.3'] + php-versions: ['8.1', '8.2', '8.3'] db-type: [mysql] prefer-lowest: ['', 'prefer-lowest'] @@ -31,7 +31,9 @@ jobs: - name: Setup MySQL 8 if: matrix.db-type == 'mysql' - run: docker run --rm --name=mysqld -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=cakephp -p 3306:3306 -d mysql:8 --default-authentication-plugin=mysql_native_password --disable-log-bin + run: | + sudo service mysql start + mysql -h 127.0.0.1 -u root -proot -e 'CREATE DATABASE pagination;' - name: Validate composer.json and composer.lock run: composer validate --strict @@ -66,15 +68,11 @@ jobs: composer update fi - - name: Wait for MySQL - if: matrix.db-type == 'mysql' - run: while ! `mysqladmin ping -h 127.0.0.1 --silent`; do printf 'Waiting for MySQL...\n'; sleep 2; done; - - name: Run PHPUnit testsuite run: | if [[ ${{ matrix.db-type }} == 'mysql' ]]; then - export DB_URL='mysql://root:root@127.0.0.1/cakephp'; - mysql -h 127.0.0.1 -u root -proot cakephp < ./tests/Schema/articles.sql + export DB_URL='mysql://root:root@127.0.0.1/pagination'; + mysql -h 127.0.0.1 -u root -proot pagination < ./tests/Schema/articles.sql fi vendor/bin/phpunit --stderr; @@ -87,7 +85,7 @@ jobs: strategy: matrix: - php-versions: ['7.4'] + php-versions: ['8.1'] steps: - name: Checkout diff --git a/composer.json b/composer.json index f492cf4..6c683b1 100644 --- a/composer.json +++ b/composer.json @@ -1,9 +1,9 @@ { "name": "bcrowe/cakephp-api-pagination", - "description": "CakePHP 4 plugin that injects pagination information into API responses.", + "description": "CakePHP 5 plugin that injects pagination information into API responses.", "type": "cakephp-plugin", "keywords": [ - "cakephp", "api", "pagination", "cakephp3", "cakephp4" + "cakephp", "api", "pagination", "cakephp3", "cakephp4", "cakephp5" ], "homepage": "https://github.com/bcrowe/cakephp-api-pagination", "license": "MIT", @@ -16,13 +16,13 @@ } ], "require": { - "php": ">=7.2", - "cakephp/cakephp": "^4.2" + "php": ">=8.1", + "cakephp/cakephp": "^5.0" }, "require-dev": { - "phpunit/phpunit" : "^8.5.23", + "phpunit/phpunit" : "^10.5.5", "scrutinizer/ocular": "1.7", - "cakephp/cakephp-codesniffer": "^4.7" + "cakephp/cakephp-codesniffer": "^5.0" }, "autoload": { "psr-4": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 5ce9bef..b4bae58 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,12 +1,11 @@ @@ -14,24 +13,7 @@ tests - - - - - - - - - - src/ - - - - - - - - - + + + diff --git a/src/Controller/Component/ApiPaginationComponent.php b/src/Controller/Component/ApiPaginationComponent.php index bdf4168..22b3dee 100644 --- a/src/Controller/Component/ApiPaginationComponent.php +++ b/src/Controller/Component/ApiPaginationComponent.php @@ -4,6 +4,7 @@ namespace BryanCrowe\ApiPagination\Controller\Component; use Cake\Controller\Component; +use Cake\Datasource\Paging\PaginatedInterface; use Cake\Event\Event; /** @@ -18,18 +19,25 @@ class ApiPaginationComponent extends Component * * @var array */ - protected $_defaultConfig = [ + protected array $_defaultConfig = [ 'key' => 'pagination', 'aliases' => [], 'visible' => [], ]; + /** + * Paging params of paginated result set (if any). + * + * @var array + */ + protected array $pagingParams = []; + /** * Holds the paging information array from the request. * * @var array */ - protected $pagingInfo = []; + protected array $pagingInfo = []; /** * Injects the pagination info into the response if the current request is a @@ -38,7 +46,7 @@ class ApiPaginationComponent extends Component * @param \Cake\Event\Event $event The Controller.beforeRender event. * @return void */ - public function beforeRender(Event $event) + public function beforeRender(Event $event): void { if (!$this->isPaginatedApiRequest()) { return; @@ -46,8 +54,8 @@ public function beforeRender(Event $event) $subject = $event->getSubject(); $modelName = ucfirst($this->getConfig('model', $subject->getName())); - if (isset($this->getController()->getRequest()->getAttribute('paging')[$modelName])) { - $this->pagingInfo = $this->getController()->getRequest()->getAttribute('paging')[$modelName]; + if (isset($this->pagingParams[$modelName])) { + $this->pagingInfo = $this->pagingParams[$modelName]; } $config = $this->getConfig(); @@ -75,7 +83,7 @@ public function beforeRender(Event $event) * * @return void */ - protected function setAliases() + protected function setAliases(): void { foreach ($this->getConfig('aliases') as $key => $value) { $this->pagingInfo[$value] = $this->pagingInfo[$key]; @@ -89,7 +97,7 @@ protected function setAliases() * * @return void */ - protected function setVisibility() + protected function setVisibility(): void { $visible = $this->getConfig('visible'); foreach ($this->pagingInfo as $key => $value) { @@ -105,15 +113,29 @@ protected function setVisibility() * * @return bool True if JSON or XML with paging, otherwise false. */ - protected function isPaginatedApiRequest() + protected function isPaginatedApiRequest(): bool { - if ( - $this->getController()->getRequest()->getAttribute('paging') - && $this->getController()->getRequest()->is(['json', 'xml']) - ) { - return true; + if (!$this->getController()->getRequest()->is(['json', 'xml'])) { + return false; + } + + // Cake 4 way for the people who want to keep embracing paging attribute pattern + if ($this->getController()->getRequest()->getAttribute('paging')) { + $this->pagingParams = $this->getController()->getRequest()->getAttribute('paging'); + + return !empty($this->pagingParams); + } + + // Since cake 5, paging params are no longer part of the request attribute. + // Hence, we check for all the view vars and if paginated interface found then we pick the first one and use it. + // @see https://github.com/cakephp/cakephp/pull/16317#issuecomment-1045873277 + foreach ($this->getController()->viewBuilder()->getVars() as $value) { + if ($value instanceof PaginatedInterface) { + $this->pagingParams[$value->pagingParam('alias')] = $value->pagingParams(); + break; + } } - return false; + return !empty($this->pagingParams); } } diff --git a/tests/Fixture/ArticlesFixture.php b/tests/Fixture/ArticlesFixture.php index dca49ce..bee0a97 100644 --- a/tests/Fixture/ArticlesFixture.php +++ b/tests/Fixture/ArticlesFixture.php @@ -5,9 +5,9 @@ class ArticlesFixture extends TestFixture { - public $table = 'bryancrowe_articles'; + public string $table = 'bryancrowe_articles'; - public $records = [ + public array $records = [ ['title' => 'Post #1', 'body' => 'This is the article body.'], ['title' => 'Post #2', 'body' => 'This is the article body.'], ['title' => 'Post #3', 'body' => 'This is the article body.'], diff --git a/tests/TestCase/Controller/Component/ApiPaginationComponentOnNonConventionalControllerNameTest.php b/tests/TestCase/Controller/Component/ApiPaginationComponentOnNonConventionalControllerNameTest.php index d158cfc..db63234 100644 --- a/tests/TestCase/Controller/Component/ApiPaginationComponentOnNonConventionalControllerNameTest.php +++ b/tests/TestCase/Controller/Component/ApiPaginationComponentOnNonConventionalControllerNameTest.php @@ -1,12 +1,15 @@ request = new Request(['url' => '/articles']); $this->response = $this->createMock('Cake\Http\Response'); - $this->controller = new ArticlesIndexController($this->request, $this->response); + $this->controller = new ArticlesIndexController($this->request); + $this->controller = $this->controller->setResponse($this->response); $this->Articles = TableRegistry::getTableLocator()->get('BryanCrowe/ApiPagination.Articles', ['table' => 'bryancrowe_articles']); parent::setUp(); } @@ -71,12 +83,12 @@ public function testVariousModelValueOnNonConventionalController(array $config, * * @return array[] */ - public function dataForTestVariousModelValueOnNonConventionalController(): array + public static function dataForTestVariousModelValueOnNonConventionalController(): array { return [ [[], []], - [['model' => 'Articles'], $this->getDefaultPagination()], - [['model' => 'articles'], $this->getDefaultPagination()], + [['model' => 'Articles'], self::getDefaultPagination()], + [['model' => 'articles'], self::getDefaultPagination()], [['model' => 'NonExistingModel'], []], ]; } @@ -86,27 +98,27 @@ public function dataForTestVariousModelValueOnNonConventionalController(): array * * @return array */ - private function getDefaultPagination(): array + private static function getDefaultPagination(): array { return [ - 'count' => 23, - 'current' => 20, - 'perPage' => 20, - 'page' => 1, - 'requestedPage' => 1, - 'pageCount' => 2, - 'start' => 1, - 'end' => 20, - 'prevPage' => false, - 'nextPage' => true, 'sort' => null, 'direction' => null, 'sortDefault' => false, 'directionDefault' => false, 'completeSort' => [], - 'limit' => null, + 'perPage' => 20, + 'requestedPage' => 1, + 'alias' => 'Articles', 'scope' => null, - 'finder' => 'all', + 'limit' => null, + 'count' => 20, + 'totalCount' => 23, + 'pageCount' => 2, + 'currentPage' => 1, + 'start' => 1, + 'end' => 20, + 'hasPrevPage' => false, + 'hasNextPage' => true, ]; } } diff --git a/tests/TestCase/Controller/Component/ApiPaginationComponentTest.php b/tests/TestCase/Controller/Component/ApiPaginationComponentTest.php index 2d13563..cf86c5e 100644 --- a/tests/TestCase/Controller/Component/ApiPaginationComponentTest.php +++ b/tests/TestCase/Controller/Component/ApiPaginationComponentTest.php @@ -1,12 +1,15 @@ request = new Request(['url' => '/articles']); $this->response = $this->createMock('Cake\Http\Response'); - $this->controller = new ArticlesController($this->request, $this->response); + $this->controller = new ArticlesController($this->request); + $this->controller->setResponse($this->response); $this->Articles = TableRegistry::getTableLocator()->get('BryanCrowe/ApiPagination.Articles', ['table' => 'bryancrowe_articles']); parent::setUp(); } @@ -74,24 +86,24 @@ public function testDefaultPaginationSettings() $result = $apiPaginationComponent->getController()->viewBuilder()->getVar('pagination'); $expected = [ - 'count' => 23, - 'current' => 20, - 'perPage' => 20, - 'page' => 1, - 'requestedPage' => 1, - 'pageCount' => 2, - 'start' => 1, - 'end' => 20, - 'prevPage' => false, - 'nextPage' => true, 'sort' => null, 'direction' => null, 'sortDefault' => false, 'directionDefault' => false, 'completeSort' => [], - 'limit' => null, + 'perPage' => 20, + 'requestedPage' => 1, + 'alias' => 'Articles', 'scope' => null, - 'finder' => 'all', + 'limit' => null, + 'count' => 20, + 'totalCount' => 23, + 'pageCount' => 2, + 'currentPage' => 1, + 'start' => 1, + 'end' => 20, + 'hasPrevPage' => false, + 'hasNextPage' => true, ]; $this->assertSame($expected, $result); @@ -111,14 +123,14 @@ public function testVisibilitySettings() $apiPaginationComponent = new ApiPaginationComponent( $this->controller->components(), [ - 'visible' => [ - 'page', - 'current', - 'count', - 'prevPage', - 'nextPage', - 'pageCount', - ], + 'visible' => [ + 'requestedPage', + 'count', + 'totalCount', + 'hasPrevPage', + 'hasNextPage', + 'pageCount', + ], ] ); $event = new Event('Controller.beforeRender', $this->controller); @@ -126,12 +138,12 @@ public function testVisibilitySettings() $result = $apiPaginationComponent->getController()->viewBuilder()->getVar('pagination'); $expected = [ - 'count' => 23, - 'current' => 20, - 'page' => 1, + 'requestedPage' => 1, + 'count' => 20, + 'totalCount' => 23, 'pageCount' => 2, - 'prevPage' => false, - 'nextPage' => true, + 'hasPrevPage' => false, + 'hasNextPage' => true, ]; $this->assertSame($expected, $result); @@ -151,11 +163,11 @@ public function testAliasSettings() $apiPaginationComponent = new ApiPaginationComponent( $this->controller->components(), [ - 'aliases' => [ - 'page' => 'curPage', - 'current' => 'currentCount', - 'count' => 'totalCount', - ], + 'aliases' => [ + 'currentPage' => 'curPage', + 'perPage' => 'currentCount', + 'totalCount' => 'noOfResults', + ], ] ); $event = new Event('Controller.beforeRender', $this->controller); @@ -163,24 +175,24 @@ public function testAliasSettings() $result = $apiPaginationComponent->getController()->viewBuilder()->getVar('pagination'); $expected = [ - 'perPage' => 20, - 'requestedPage' => 1, - 'pageCount' => 2, - 'start' => 1, - 'end' => 20, - 'prevPage' => false, - 'nextPage' => true, 'sort' => null, 'direction' => null, 'sortDefault' => false, 'directionDefault' => false, 'completeSort' => [], - 'limit' => null, + 'requestedPage' => 1, + 'alias' => 'Articles', 'scope' => null, - 'finder' => 'all', + 'limit' => null, + 'count' => 20, + 'pageCount' => 2, + 'start' => 1, + 'end' => 20, + 'hasPrevPage' => false, + 'hasNextPage' => true, 'curPage' => 1, 'currentCount' => 20, - 'totalCount' => 23, + 'noOfResults' => 23, ]; $this->assertSame($expected, $result); @@ -199,33 +211,31 @@ public function testKeySetting() $this->controller->set('data', $this->controller->paginate($this->Articles)); $apiPaginationComponent = new ApiPaginationComponent( $this->controller->components(), - [ - 'key' => 'paging', - ] + ['key' => 'paging'] ); $event = new Event('Controller.beforeRender', $this->controller); $apiPaginationComponent->beforeRender($event); $result = $apiPaginationComponent->getController()->viewBuilder()->getVar('paging'); $expected = [ - 'count' => 23, - 'current' => 20, - 'perPage' => 20, - 'page' => 1, - 'requestedPage' => 1, - 'pageCount' => 2, - 'start' => 1, - 'end' => 20, - 'prevPage' => false, - 'nextPage' => true, 'sort' => null, 'direction' => null, 'sortDefault' => false, 'directionDefault' => false, 'completeSort' => [], - 'limit' => null, + 'perPage' => 20, + 'requestedPage' => 1, + 'alias' => 'Articles', 'scope' => null, - 'finder' => 'all', + 'limit' => null, + 'count' => 20, + 'totalCount' => 23, + 'pageCount' => 2, + 'currentPage' => 1, + 'start' => 1, + 'end' => 20, + 'hasPrevPage' => false, + 'hasNextPage' => true, ]; $this->assertSame($expected, $result); @@ -245,19 +255,19 @@ public function testAllSettings() $apiPaginationComponent = new ApiPaginationComponent( $this->controller->components(), [ - 'key' => 'fun', - 'aliases' => [ - 'page' => 'currentPage', - 'count' => 'totalCount', - 'limit' => 'unusedAlias', - ], - 'visible' => [ - 'currentPage', - 'totalCount', - 'limit', - 'prevPage', - 'nextPage', - ], + 'key' => 'fun', + 'aliases' => [ + 'currentPage' => 'page', + 'totalCount' => 'noOfResults', + 'limit' => 'unusedAlias', + ], + 'visible' => [ + 'page', + 'noOfResults', + 'limit', + 'hasPrevPage', + 'hasNextPage', + ], ] ); $event = new Event('Controller.beforeRender', $this->controller); @@ -265,10 +275,10 @@ public function testAllSettings() $result = $apiPaginationComponent->getController()->viewBuilder()->getVar('fun'); $expected = [ - 'prevPage' => false, - 'nextPage' => true, - 'currentPage' => 1, - 'totalCount' => 23, + 'hasPrevPage' => false, + 'hasNextPage' => true, + 'page' => 1, + 'noOfResults' => 23, ]; $this->assertSame($expected, $result); diff --git a/tests/test_app/TestApp/Controller/ArticlesController.php b/tests/test_app/TestApp/Controller/ArticlesController.php index bc2f5cd..8990b0c 100644 --- a/tests/test_app/TestApp/Controller/ArticlesController.php +++ b/tests/test_app/TestApp/Controller/ArticlesController.php @@ -7,8 +7,4 @@ class ArticlesController extends Controller { - public function initialize(): void - { - parent::initialize(); - } } diff --git a/tests/test_app/TestApp/Controller/ArticlesIndexController.php b/tests/test_app/TestApp/Controller/ArticlesIndexController.php index 45822d9..26471ed 100644 --- a/tests/test_app/TestApp/Controller/ArticlesIndexController.php +++ b/tests/test_app/TestApp/Controller/ArticlesIndexController.php @@ -7,8 +7,4 @@ class ArticlesIndexController extends Controller { - public function initialize(): void - { - parent::initialize(); - } }