diff --git a/.gitignore b/.gitignore index 988be09b1948..485512fa4a7b 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,9 @@ $RECYCLE.BIN/ .env .vagrant Vagrantfile +user_guide_src/venv/ +.python-version +user_guide_src/.python-version #------------------------- # Temporary Files diff --git a/deptrac.yaml b/deptrac.yaml index 7f5687af4bca..c70f4d7126cc 100644 --- a/deptrac.yaml +++ b/deptrac.yaml @@ -164,6 +164,10 @@ deptrac: API: - Format - HTTP + - Database + - Model + - Pager + - URI Cache: - I18n Controller: diff --git a/system/API/ResponseTrait.php b/system/API/ResponseTrait.php index f64cc671a514..cc04fa509fd9 100644 --- a/system/API/ResponseTrait.php +++ b/system/API/ResponseTrait.php @@ -13,11 +13,16 @@ namespace CodeIgniter\API; +use CodeIgniter\Database\BaseBuilder; +use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Format\Format; use CodeIgniter\Format\FormatterInterface; use CodeIgniter\HTTP\CLIRequest; use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\ResponseInterface; +use CodeIgniter\HTTP\URI; +use CodeIgniter\Model; +use Throwable; /** * Provides common, more readable, methods to provide @@ -321,7 +326,7 @@ protected function format($data = null) // if we don't have a formatter, make one $this->formatter ??= $format->getFormatter($mime); - $asHtml = $this->stringAsHtml ?? false; + $asHtml = property_exists($this, 'stringAsHtml') ? $this->stringAsHtml : false; if ( ($mime === 'application/json' && $asHtml && is_string($data)) @@ -360,4 +365,148 @@ protected function setResponseFormat(?string $format = null) return $this; } + + // -------------------------------------------------------------------- + // Pagination Methods + // -------------------------------------------------------------------- + + /** + * Paginates the given model or query builder and returns + * an array containing the paginated results along with + * metadata such as total items, total pages, current page, + * and items per page. + * + * The result would be in the following format: + * [ + * 'data' => [...], + * 'meta' => [ + * 'page' => 1, + * 'perPage' => 20, + * 'total' => 100, + * 'totalPages' => 5, + * ], + * 'links' => [ + * 'self' => '/api/items?page=1&perPage=20', + * 'first' => '/api/items?page=1&perPage=20', + * 'last' => '/api/items?page=5&perPage=20', + * 'prev' => null, + * 'next' => '/api/items?page=2&perPage=20', + * ] + * ] + */ + protected function paginate(BaseBuilder|Model $resource, int $perPage = 20): ResponseInterface + { + try { + assert($this->request instanceof IncomingRequest); + + $page = max(1, (int) ($this->request->getGet('page') ?? 1)); + + // If using a Model we can use its built-in paginate method + if ($resource instanceof Model) { + $data = $resource->paginate($perPage, 'default', $page); + $pager = $resource->pager; + + $meta = [ + 'page' => $pager->getCurrentPage(), + 'perPage' => $pager->getPerPage(), + 'total' => $pager->getTotal(), + 'totalPages' => $pager->getPageCount(), + ]; + } else { + // Query Builder, we need to handle pagination manually + $offset = ($page - 1) * $perPage; + $total = (clone $resource)->countAllResults(); + $data = $resource->limit($perPage, $offset)->get()->getResultArray(); + + $meta = [ + 'page' => $page, + 'perPage' => $perPage, + 'total' => $total, + 'totalPages' => (int) ceil($total / $perPage), + ]; + } + + $links = $this->buildLinks($meta); + + $this->response->setHeader('Link', $this->linkHeader($links)); + $this->response->setHeader('X-Total-Count', (string) $meta['total']); + + return $this->respond([ + 'data' => $data, + 'meta' => $meta, + 'links' => $links, + ]); + } catch (DatabaseException $e) { + log_message('error', lang('RESTful.cannotPaginate') . ' ' . $e->getMessage()); + + return $this->failServerError(lang('RESTful.cannotPaginate')); + } catch (Throwable $e) { + log_message('error', lang('RESTful.paginateError') . ' ' . $e->getMessage()); + + return $this->failServerError(lang('RESTful.paginateError')); + } + } + + /** + * Builds pagination links based on the current request URI and pagination metadata. + * + * @param array $meta Pagination metadata (page, perPage, total, totalPages) + * + * @return array Array of pagination links with relations as keys + */ + private function buildLinks(array $meta): array + { + assert($this->request instanceof IncomingRequest); + + /** @var URI $uri */ + $uri = current_url(true); + $query = $this->request->getGet(); + + $set = static function ($page) use ($uri, $query, $meta): string { + $params = $query; + $params['page'] = $page; + + // Ensure perPage is in the links if it's not default + if (! isset($params['perPage']) && $meta['perPage'] !== 20) { + $params['perPage'] = $meta['perPage']; + } + + return (string) (new URI((string) $uri))->setQuery(http_build_query($params)); + }; + + $totalPages = max(1, (int) $meta['totalPages']); + $page = (int) $meta['page']; + + return [ + 'self' => $set($page), + 'first' => $set(1), + 'last' => $set($totalPages), + 'prev' => $page > 1 ? $set($page - 1) : null, + 'next' => $page < $totalPages ? $set($page + 1) : null, + ]; + } + + /** + * Formats the pagination links into a single Link header string + * for middleware/machine use. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link + * @see https://datatracker.ietf.org/doc/html/rfc8288 + * + * @param array $links Pagination links with relations as keys + * + * @return string Formatted Link header value + */ + private function linkHeader(array $links): string + { + $parts = []; + + foreach (['self', 'first', 'prev', 'next', 'last'] as $rel) { + if ($links[$rel] !== null && $links[$rel] !== '') { + $parts[] = "<{$links[$rel]}>; rel=\"{$rel}\""; + } + } + + return implode(', ', $parts); + } } diff --git a/system/Language/en/RESTful.php b/system/Language/en/RESTful.php index 59e014b72f7e..e1aaa10f12d5 100644 --- a/system/Language/en/RESTful.php +++ b/system/Language/en/RESTful.php @@ -14,4 +14,6 @@ // RESTful language settings return [ 'notImplemented' => '"{0}" action not implemented.', + 'cannotPaginate' => 'Unable to retrieve paginated data.', + 'paginateError' => 'An error occurred while paginating results.', ]; diff --git a/tests/system/API/ResponseTraitTest.php b/tests/system/API/ResponseTraitTest.php index 0443d90dc391..cc8748ce1b4a 100644 --- a/tests/system/API/ResponseTraitTest.php +++ b/tests/system/API/ResponseTraitTest.php @@ -14,6 +14,10 @@ namespace CodeIgniter\API; use CodeIgniter\Config\Factories; +use CodeIgniter\Database\BaseBuilder; +use CodeIgniter\Database\BaseConnection; +use CodeIgniter\Database\BaseResult; +use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Format\FormatterInterface; use CodeIgniter\Format\JSONFormatter; use CodeIgniter\Format\XMLFormatter; @@ -21,12 +25,15 @@ use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\HTTP\SiteURI; use CodeIgniter\HTTP\UserAgent; +use CodeIgniter\Model; +use CodeIgniter\Pager\Pager; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockIncomingRequest; use CodeIgniter\Test\Mock\MockResponse; use Config\App; use Config\Cookie; use Config\Services; +use Exception; use PHPUnit\Framework\Attributes\Group; use stdClass; @@ -682,4 +689,368 @@ private function invoke(object $controller, string $method, array $args = []): o return $method(...$args); } + + /** + * Helper method to create a mock Model with a mock Pager + * + * @param array> $data + */ + private function createMockModelWithPager(array $data, int $page, int $perPage, int $total, int $totalPages): Model + { + // Create a mock Pager + $pager = $this->createMock(Pager::class); + $pager->method('getCurrentPage')->willReturn($page); + $pager->method('getPerPage')->willReturn($perPage); + $pager->method('getTotal')->willReturn($total); + $pager->method('getPageCount')->willReturn($totalPages); + + // Create a mock Model with a public pager property + $model = $this->createMock(Model::class); + + $model->method('paginate')->willReturn($data); + $model->pager = $pager; + + return $model; + } + + public function testPaginateWithModel(): void + { + // Mock data + $data = [ + ['id' => 1, 'name' => 'Item 1'], + ['id' => 2, 'name' => 'Item 2'], + ]; + + $model = $this->createMockModelWithPager($data, 1, 20, 50, 3); + + $controller = $this->makeController('/api/items'); + + $this->invoke($controller, 'paginate', [$model, 20]); + + // Check response structure + $responseBody = json_decode($this->response->getBody(), true); + + $this->assertArrayHasKey('data', $responseBody); + $this->assertArrayHasKey('meta', $responseBody); + $this->assertArrayHasKey('links', $responseBody); + + // Check meta + $this->assertSame(1, $responseBody['meta']['page']); + $this->assertSame(20, $responseBody['meta']['perPage']); + $this->assertSame(50, $responseBody['meta']['total']); + $this->assertSame(3, $responseBody['meta']['totalPages']); + + // Check data + $this->assertSame($data, $responseBody['data']); + + // Check headers + $this->assertSame('50', $this->response->getHeaderLine('X-Total-Count')); + $this->assertNotEmpty($this->response->getHeaderLine('Link')); + } + + public function testPaginateWithQueryBuilder(): void + { + // Mock the database and builder + $db = $this->createMock(BaseConnection::class); + + $builder = $this->getMockBuilder(BaseBuilder::class) + ->setConstructorArgs(['test_table', $db]) + ->onlyMethods(['countAllResults', 'limit', 'get']) + ->getMock(); + + $result = $this->createMock(BaseResult::class); + + $data = [ + ['id' => 1, 'name' => 'Item 1'], + ['id' => 2, 'name' => 'Item 2'], + ]; + + // Mock the query builder chain + $builder->method('countAllResults')->willReturn(50); + $builder->method('limit')->willReturnSelf(); + $builder->method('get')->willReturn($result); + $result->method('getResultArray')->willReturn($data); + + $controller = $this->makeController('/api/items'); + + $this->invoke($controller, 'paginate', [$builder, 20]); + + // Check response structure + $responseBody = json_decode($this->response->getBody(), true); + + $this->assertArrayHasKey('data', $responseBody); + $this->assertArrayHasKey('meta', $responseBody); + $this->assertArrayHasKey('links', $responseBody); + + // Check meta + $this->assertSame(1, $responseBody['meta']['page']); + $this->assertSame(20, $responseBody['meta']['perPage']); + $this->assertSame(50, $responseBody['meta']['total']); + $this->assertSame(3, $responseBody['meta']['totalPages']); + } + + public function testPaginateWithCustomPerPage(): void + { + $data = [ + ['id' => 1, 'name' => 'Item 1'], + ['id' => 2, 'name' => 'Item 2'], + ['id' => 3, 'name' => 'Item 3'], + ['id' => 4, 'name' => 'Item 4'], + ['id' => 5, 'name' => 'Item 5'], + ]; + + $model = $this->createMockModelWithPager($data, 1, 5, 25, 5); + + $controller = $this->makeController('/api/items'); + + $this->invoke($controller, 'paginate', [$model, 5]); + + $responseBody = json_decode($this->response->getBody(), true); + + // Check meta with custom perPage + $this->assertSame(5, $responseBody['meta']['perPage']); + $this->assertSame(25, $responseBody['meta']['total']); + $this->assertSame(5, $responseBody['meta']['totalPages']); + } + + public function testPaginateWithPageParameter(): void + { + $data = [ + ['id' => 21, 'name' => 'Item 21'], + ['id' => 22, 'name' => 'Item 22'], + ]; + + $model = $this->createMockModelWithPager($data, 2, 20, 50, 3); + + // Create controller with page=2 in query string + $controller = $this->makeController('/api/items?page=2'); + Services::superglobals()->setGet('page', '2'); + + $this->invoke($controller, 'paginate', [$model, 20]); + + $responseBody = json_decode($this->response->getBody(), true); + + // Check that we're on page 2 + $this->assertSame(2, $responseBody['meta']['page']); + + // Check links + $this->assertStringContainsString('page=2', (string) $responseBody['links']['self']); + $this->assertStringContainsString('page=1', (string) $responseBody['links']['prev']); + $this->assertStringContainsString('page=3', (string) $responseBody['links']['next']); + } + + public function testPaginateLinksStructure(): void + { + $data = [['id' => 1, 'name' => 'Item 1']]; + + $model = $this->createMockModelWithPager($data, 2, 20, 100, 5); + + Services::superglobals()->setGet('page', '2'); + $controller = $this->makeController('/api/items?page=2'); + + $this->invoke($controller, 'paginate', [$model, 20]); + + $responseBody = json_decode($this->response->getBody(), true); + + // Check all link types exist + $this->assertArrayHasKey('self', $responseBody['links']); + $this->assertArrayHasKey('first', $responseBody['links']); + $this->assertArrayHasKey('last', $responseBody['links']); + $this->assertArrayHasKey('prev', $responseBody['links']); + $this->assertArrayHasKey('next', $responseBody['links']); + + // Check link values + $this->assertStringContainsString('page=2', (string) $responseBody['links']['self']); + $this->assertStringContainsString('page=1', (string) $responseBody['links']['first']); + $this->assertStringContainsString('page=5', (string) $responseBody['links']['last']); + $this->assertStringContainsString('page=1', (string) $responseBody['links']['prev']); + $this->assertStringContainsString('page=3', (string) $responseBody['links']['next']); + } + + public function testPaginateFirstPageNoPrevLink(): void + { + $data = [['id' => 1, 'name' => 'Item 1']]; + + $model = $this->createMockModelWithPager($data, 1, 20, 50, 3); + + $controller = $this->makeController('/api/items'); + + $this->invoke($controller, 'paginate', [$model, 20]); + + $responseBody = json_decode($this->response->getBody(), true); + + // First page should not have a prev link + $this->assertNull($responseBody['links']['prev']); + // But should have a next link + $this->assertNotNull($responseBody['links']['next']); + } + + public function testPaginateLastPageNoNextLink(): void + { + $data = [['id' => 41, 'name' => 'Item 41']]; + + $model = $this->createMockModelWithPager($data, 3, 20, 50, 3); + + Services::superglobals()->setGet('page', '3'); + $controller = $this->makeController('/api/items?page=3'); + + $this->invoke($controller, 'paginate', [$model, 20]); + + $responseBody = json_decode($this->response->getBody(), true); + + // Last page should not have a next link + $this->assertNull($responseBody['links']['next']); + // But should have a prev link + $this->assertNotNull($responseBody['links']['prev']); + } + + public function testPaginateLinkHeader(): void + { + $data = [['id' => 1, 'name' => 'Item 1']]; + + $model = $this->createMockModelWithPager($data, 2, 20, 100, 5); + + Services::superglobals()->setGet('page', '2'); + $controller = $this->makeController('/api/items?page=2'); + + $this->invoke($controller, 'paginate', [$model, 20]); + + $linkHeader = $this->response->getHeaderLine('Link'); + + // Check that Link header is properly formatted + $this->assertStringContainsString('rel="self"', $linkHeader); + $this->assertStringContainsString('rel="first"', $linkHeader); + $this->assertStringContainsString('rel="last"', $linkHeader); + $this->assertStringContainsString('rel="prev"', $linkHeader); + $this->assertStringContainsString('rel="next"', $linkHeader); + + // Check format ; rel="relation" + $this->assertMatchesRegularExpression('/<[^>]+>;\s*rel="self"/', $linkHeader); + $this->assertMatchesRegularExpression('/<[^>]+>;\s*rel="first"/', $linkHeader); + } + + public function testPaginateXTotalCountHeader(): void + { + $data = [['id' => 1, 'name' => 'Item 1']]; + + $model = $this->createMockModelWithPager($data, 1, 20, 150, 8); + + $controller = $this->makeController('/api/items'); + + $this->invoke($controller, 'paginate', [$model, 20]); + + // Check X-Total-Count header + $this->assertSame('150', $this->response->getHeaderLine('X-Total-Count')); + } + + public function testPaginateWithDatabaseException(): void + { + $model = $this->createMock(Model::class); + + // Make the model throw a DatabaseException + $model->method('paginate')->willThrowException( + new DatabaseException('Database error'), + ); + + $controller = $this->makeController('/api/items'); + + $this->invoke($controller, 'paginate', [$model, 20]); + + // Should return a 500 error + $this->assertSame(500, $this->response->getStatusCode()); + + $responseBody = json_decode($this->response->getBody(), true); + + // Check error response structure + $this->assertArrayHasKey('status', $responseBody); + $this->assertArrayHasKey('error', $responseBody); + $this->assertArrayHasKey('messages', $responseBody); + $this->assertSame(500, $responseBody['status']); + } + + public function testPaginateWithGenericException(): void + { + $model = $this->createMock(Model::class); + + // Make the model throw a generic exception + $model->method('paginate')->willThrowException( + new Exception('Generic error'), + ); + + $controller = $this->makeController('/api/items'); + + $this->invoke($controller, 'paginate', [$model, 20]); + + // Should return a 500 error + $this->assertSame(500, $this->response->getStatusCode()); + + $responseBody = json_decode($this->response->getBody(), true); + + // Check error response structure + $this->assertSame(500, $responseBody['status']); + $this->assertArrayHasKey('error', $responseBody); + } + + public function testPaginateWithNonDefaultPerPageInLinks(): void + { + $data = [['id' => 1, 'name' => 'Item 1']]; + + $model = $this->createMockModelWithPager($data, 1, 10, 50, 5); + + $controller = $this->makeController('/api/items'); + + $this->invoke($controller, 'paginate', [$model, 10]); + + $responseBody = json_decode($this->response->getBody(), true); + + // Check that perPage is included in links when it's not the default (20) + $this->assertStringContainsString('perPage=10', (string) $responseBody['links']['self']); + $this->assertStringContainsString('perPage=10', (string) $responseBody['links']['first']); + $this->assertStringContainsString('perPage=10', (string) $responseBody['links']['last']); + $this->assertStringContainsString('perPage=10', (string) $responseBody['links']['next']); + } + + public function testPaginatePreservesOtherQueryParameters(): void + { + $data = [['id' => 1, 'name' => 'Item 1']]; + + $model = $this->createMockModelWithPager($data, 1, 20, 50, 3); + + Services::superglobals()->setGet('filter', 'active'); + Services::superglobals()->setGet('sort', 'name'); + $controller = $this->makeController('/api/items?filter=active&sort=name'); + + $this->invoke($controller, 'paginate', [$model, 20]); + + $responseBody = json_decode($this->response->getBody(), true); + + // Check that other query parameters are preserved in links + $this->assertStringContainsString('filter=active', (string) $responseBody['links']['self']); + $this->assertStringContainsString('sort=name', (string) $responseBody['links']['self']); + $this->assertStringContainsString('filter=active', (string) $responseBody['links']['next']); + $this->assertStringContainsString('sort=name', (string) $responseBody['links']['next']); + } + + public function testPaginateSinglePage(): void + { + $data = [ + ['id' => 1, 'name' => 'Item 1'], + ['id' => 2, 'name' => 'Item 2'], + ]; + + $model = $this->createMockModelWithPager($data, 1, 20, 2, 1); + + $controller = $this->makeController('/api/items'); + + $this->invoke($controller, 'paginate', [$model, 20]); + + $responseBody = json_decode($this->response->getBody(), true); + + // For a single page, prev and next should be null + $this->assertNull($responseBody['links']['prev']); + $this->assertNull($responseBody['links']['next']); + // First and last should point to page 1 + $this->assertStringContainsString('page=1', (string) $responseBody['links']['first']); + $this->assertStringContainsString('page=1', (string) $responseBody['links']['last']); + } } diff --git a/user_guide_src/source/changelogs/v4.7.0.rst b/user_guide_src/source/changelogs/v4.7.0.rst index d91e9e2fd604..bd93171be60e 100644 --- a/user_guide_src/source/changelogs/v4.7.0.rst +++ b/user_guide_src/source/changelogs/v4.7.0.rst @@ -81,6 +81,7 @@ Libraries - **Email:** Added support for choosing the SMTP authorization method. You can change it via ``Config\Email::$SMTPAuthMethod`` option. - **Image:** The ``ImageMagickHandler`` has been rewritten to rely solely on the PHP ``imagick`` extension. - **Image:** Added ``ImageMagickHandler::clearMetadata()`` method to remove image metadata for privacy protection. +- **ResponseTrait:** Added ``paginate``` method to simplify paginated API responses. See :ref:`ResponseTrait::paginate() ` for details. - **Time:** added methods ``Time::addCalendarMonths()`` and ``Time::subCalendarMonths()`` Commands diff --git a/user_guide_src/source/outgoing/api_responses.rst b/user_guide_src/source/outgoing/api_responses.rst index eee219591f8f..645951cf4347 100644 --- a/user_guide_src/source/outgoing/api_responses.rst +++ b/user_guide_src/source/outgoing/api_responses.rst @@ -1,9 +1,9 @@ -################## -API Response Trait -################## +############# +API Responses +############# Much of modern PHP development requires building APIs, whether simply to provide data for a javascript-heavy -single page application, or as a standalone product. CodeIgniter provides an API Response trait that can be +single page application, or as a standalone product. CodeIgniter provides a couple of traits that can be used with any controller to make common response types simple, with no need to remember which HTTP status code should be returned for which response types. @@ -11,9 +11,9 @@ should be returned for which response types. :local: :depth: 2 -************* -Example Usage -************* +***************** +Response Examples +***************** The following example shows a common usage pattern within your controllers. @@ -250,3 +250,74 @@ Class Reference Sets the appropriate status code to use when there is a server error. .. literalinclude:: api_responses/017.php + +.. _api_response_trait_paginate: + +******************** +Pagination Responses +******************** + +When returning paginated results from an API endpoint, you can use the ``paginate()`` method to return the +results along with the pagination information. This helps to keep consistent responses across your API, while +providing all of the information that clients will need to properly page through the results. + +------------- +Example Usage +------------- + +.. literalinclude:: api_responses/018.php + +A typical response might look like: + +.. code-block:: json + + { + "data": [ + { + "id": 1, + "username": "admin", + "email": "admin@example.com" + }, + { + "id": 2, + "username": "user", + "email": "user@example.com" + } + ], + "meta": { + "page": 1, + "perPage": 20, + "total": 2, + "totalPages": 1 + }, + "links": { + "self": "http://example.com/users?page=1", + "first": "http://example.com/users?page=1", + "last": "http://example.com/users?page=1", + "next": null, + "previous": null + } + } + +The ``paginate()`` method will always wrap the results in a ``data`` element, and will also include ``meta`` +and ``links`` elements to help the client page through the results. If there are no results, the ``data`` element will +be an empty array, and the ``meta`` and ``links`` elements will still be present, but with values that indicate no results. + +You can also pass it a Builder instance instead of a Model, as long as the Builder is properly configured with the table +name and any necessary joins or where clauses. + +.. literalinclude:: api_responses/019.php + +*************** +Class Reference +*************** + +.. php:method:: paginate(Model|BaseBuilder $resource, int $perPage = 20) + + :param Model|BaseBuilder $resource: The resource to paginate, either a Model or a Builder instance. + :param int $perPage: The number of items to return per page. + + Generates a paginated response from the given resource. The resource can be either a Model or a Builder + instance. The method will automatically determine the current page from the request's query parameters. + The response will include the paginated data, along with metadata about the pagination state and links + to navigate through the pages. diff --git a/user_guide_src/source/outgoing/api_responses/018.php b/user_guide_src/source/outgoing/api_responses/018.php new file mode 100644 index 000000000000..efacd81ac89b --- /dev/null +++ b/user_guide_src/source/outgoing/api_responses/018.php @@ -0,0 +1,18 @@ +where('active', 1); + + return $this->paginate($model, 20); + } +} diff --git a/user_guide_src/source/outgoing/api_responses/019.php b/user_guide_src/source/outgoing/api_responses/019.php new file mode 100644 index 000000000000..63f249085c8a --- /dev/null +++ b/user_guide_src/source/outgoing/api_responses/019.php @@ -0,0 +1,18 @@ +table('users') + ->where('active', 1); + + return $this->paginate(resource: $builder, perPage: 20); + } +} diff --git a/utils/phpstan-baseline/function.alreadyNarrowedType.neon b/utils/phpstan-baseline/function.alreadyNarrowedType.neon new file mode 100644 index 000000000000..7f6d5a1b1fdd --- /dev/null +++ b/utils/phpstan-baseline/function.alreadyNarrowedType.neon @@ -0,0 +1,13 @@ +# total 2 errors + +parameters: + ignoreErrors: + - + message: '#^Call to function property_exists\(\) with \$this\(class@anonymous/tests/system/API/ResponseTraitTest\.php\:188\) and ''stringAsHtml'' will always evaluate to true\.$#' + count: 1 + path: ../../tests/system/API/ResponseTraitTest.php + + - + message: '#^Call to function property_exists\(\) with \$this\(class@anonymous/tests/system/API/ResponseTraitTest\.php\:308\) and ''stringAsHtml'' will always evaluate to true\.$#' + count: 1 + path: ../../tests/system/API/ResponseTraitTest.php diff --git a/utils/phpstan-baseline/function.impossibleType.neon b/utils/phpstan-baseline/function.impossibleType.neon new file mode 100644 index 000000000000..431a49bc5aed --- /dev/null +++ b/utils/phpstan-baseline/function.impossibleType.neon @@ -0,0 +1,18 @@ +# total 3 errors + +parameters: + ignoreErrors: + - + message: '#^Call to function property_exists\(\) with \$this\(CodeIgniter\\Debug\\ExceptionHandler\) and ''stringAsHtml'' will always evaluate to false\.$#' + count: 1 + path: ../../system/Debug/ExceptionHandler.php + + - + message: '#^Call to function property_exists\(\) with \$this\(class@anonymous/tests/system/API/ResponseTraitTest\.php\:130\) and ''stringAsHtml'' will always evaluate to false\.$#' + count: 1 + path: ../../tests/system/API/ResponseTraitTest.php + + - + message: '#^Call to function property_exists\(\) with \$this\(class@anonymous/tests/system/API/ResponseTraitTest\.php\:628\) and ''stringAsHtml'' will always evaluate to false\.$#' + count: 1 + path: ../../tests/system/API/ResponseTraitTest.php diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index c1fa33e7414b..07dad94ede7f 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 2767 errors +# total 2766 errors includes: - argument.type.neon @@ -10,6 +10,8 @@ includes: - codeigniter.superglobalAccessAssign.neon - deadCode.unreachable.neon - empty.notAllowed.neon + - function.alreadyNarrowedType.neon + - function.impossibleType.neon - method.alreadyNarrowedType.neon - method.childParameterType.neon - method.childReturnType.neon diff --git a/utils/phpstan-baseline/property.notFound.neon b/utils/phpstan-baseline/property.notFound.neon index 3fab0404eb87..3d66f73b98bc 100644 --- a/utils/phpstan-baseline/property.notFound.neon +++ b/utils/phpstan-baseline/property.notFound.neon @@ -1,4 +1,4 @@ -# total 57 errors +# total 51 errors parameters: ignoreErrors: @@ -17,21 +17,6 @@ parameters: count: 14 path: ../../system/Database/SQLSRV/Forge.php - - - message: '#^Access to an undefined property CodeIgniter\\Debug\\ExceptionHandler\:\:\$stringAsHtml\.$#' - count: 1 - path: ../../system/Debug/ExceptionHandler.php - - - - message: '#^Access to an undefined property CodeIgniter\\Debug\\Exceptions\:\:\$stringAsHtml\.$#' - count: 1 - path: ../../system/Debug/Exceptions.php - - - - message: '#^Access to an undefined property CodeIgniter\\RESTful\\ResourceController\:\:\$stringAsHtml\.$#' - count: 1 - path: ../../system/RESTful/ResourceController.php - - message: '#^Access to an undefined property Config\\Session\:\:\$lockAttempts\.$#' count: 1 @@ -42,21 +27,6 @@ parameters: count: 1 path: ../../system/Session/Handlers/RedisHandler.php - - - message: '#^Access to an undefined property CodeIgniter\\Test\\Mock\\MockResourcePresenter\:\:\$stringAsHtml\.$#' - count: 1 - path: ../../system/Test/Mock/MockResourcePresenter.php - - - - message: '#^Access to an undefined property class@anonymous/tests/system/API/ResponseTraitTest\.php\:123\:\:\$stringAsHtml\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Access to an undefined property class@anonymous/tests/system/API/ResponseTraitTest\.php\:621\:\:\$stringAsHtml\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - message: '#^Access to an undefined property CodeIgniter\\Database\\BaseConnection\:\:\$mysqli\.$#' count: 1