From 45073a8db3bb20e48c26f2d11362341411ab7f94 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Sat, 3 May 2025 16:26:29 +0800 Subject: [PATCH 01/59] docs: add changelog and upgrade for v4.6.2 (#9547) --- user_guide_src/source/changelogs/index.rst | 1 + user_guide_src/source/changelogs/v4.6.2.rst | 35 ++++++++++++ .../source/installation/upgrade_462.rst | 55 +++++++++++++++++++ .../source/installation/upgrading.rst | 1 + 4 files changed, 92 insertions(+) create mode 100644 user_guide_src/source/changelogs/v4.6.2.rst create mode 100644 user_guide_src/source/installation/upgrade_462.rst diff --git a/user_guide_src/source/changelogs/index.rst b/user_guide_src/source/changelogs/index.rst index ee417164aa87..bd881c8b59fe 100644 --- a/user_guide_src/source/changelogs/index.rst +++ b/user_guide_src/source/changelogs/index.rst @@ -12,6 +12,7 @@ See all the changes. .. toctree:: :titlesonly: + v4.6.2 v4.6.1 v4.6.0 v4.5.8 diff --git a/user_guide_src/source/changelogs/v4.6.2.rst b/user_guide_src/source/changelogs/v4.6.2.rst new file mode 100644 index 000000000000..dd988c4778b5 --- /dev/null +++ b/user_guide_src/source/changelogs/v4.6.2.rst @@ -0,0 +1,35 @@ +############# +Version 4.6.2 +############# + +Release Date: Unreleased + +**4.6.2 release of CodeIgniter4** + +.. contents:: + :local: + :depth: 3 + +******** +BREAKING +******** + +*************** +Message Changes +*************** + +******* +Changes +******* + +************ +Deprecations +************ + +********** +Bugs Fixed +********** + +See the repo's +`CHANGELOG.md `_ +for a complete list of bugs fixed. diff --git a/user_guide_src/source/installation/upgrade_462.rst b/user_guide_src/source/installation/upgrade_462.rst new file mode 100644 index 000000000000..6679e48e2364 --- /dev/null +++ b/user_guide_src/source/installation/upgrade_462.rst @@ -0,0 +1,55 @@ +############################# +Upgrading from 4.6.1 to 4.6.2 +############################# + +Please refer to the upgrade instructions corresponding to your installation method. + +- :ref:`Composer Installation App Starter Upgrading ` +- :ref:`Composer Installation Adding CodeIgniter4 to an Existing Project Upgrading ` +- :ref:`Manual Installation Upgrading ` + +.. contents:: + :local: + :depth: 2 + +********************** +Mandatory File Changes +********************** + +**************** +Breaking Changes +**************** + +********************* +Breaking Enhancements +********************* + +************* +Project Files +************* + +Some files in the **project space** (root, app, public, writable) received updates. Due to +these files being outside of the **system** scope they will not be changed without your intervention. + +.. note:: There are some third-party CodeIgniter modules available to assist + with merging changes to the project space: + `Explore on Packagist `_. + +Content Changes +=============== + +The following files received significant changes (including deprecations or visual adjustments) +and it is recommended that you merge the updated versions with your application: + +Config +------ + +- @TODO + +All Changes +=========== + +This is a list of all files in the **project space** that received changes; +many will be simple comments or formatting that have no effect on the runtime: + +- @TODO diff --git a/user_guide_src/source/installation/upgrading.rst b/user_guide_src/source/installation/upgrading.rst index a2125cba81a3..0b9f2dd806d0 100644 --- a/user_guide_src/source/installation/upgrading.rst +++ b/user_guide_src/source/installation/upgrading.rst @@ -16,6 +16,7 @@ See also :doc:`./backward_compatibility_notes`. backward_compatibility_notes + upgrade_462 upgrade_461 upgrade_460 upgrade_458 From d8db40350ad78cd473dae49ab7ab1e9d0cf54a95 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 15:57:37 +0000 Subject: [PATCH 02/59] chore(deps-dev): update rector/rector requirement from 2.0.14 to 2.0.15 Updates the requirements on [rector/rector](https://github.com/rectorphp/rector) to permit the latest version. - [Release notes](https://github.com/rectorphp/rector/releases) - [Commits](https://github.com/rectorphp/rector/compare/2.0.14...2.0.15) --- updated-dependencies: - dependency-name: rector/rector dependency-version: 2.0.15 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 1f07772400f4..d3e5615ec211 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "phpunit/phpcov": "^9.0.2 || ^10.0", "phpunit/phpunit": "^10.5.16 || ^11.2", "predis/predis": "^1.1 || ^2.3", - "rector/rector": "2.0.14", + "rector/rector": "2.0.15", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { From 9cb6a083606be837d715bcd641fc35cbe7a1fac8 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Tue, 6 May 2025 20:48:29 +0200 Subject: [PATCH 03/59] chore: update predis/predis to version 3 (#9550) --- admin/framework/composer.json | 2 +- composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/framework/composer.json b/admin/framework/composer.json index 82d06cd9d05c..0550502fd22c 100644 --- a/admin/framework/composer.json +++ b/admin/framework/composer.json @@ -24,7 +24,7 @@ "mikey179/vfsstream": "^1.6.12", "nexusphp/cs-config": "^3.6", "phpunit/phpunit": "^10.5.16 || ^11.2", - "predis/predis": "^1.1 || ^2.3" + "predis/predis": "^3.0" }, "suggest": { "ext-curl": "If you use CURLRequest class", diff --git a/composer.json b/composer.json index d3e5615ec211..158d1508848c 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpcov": "^9.0.2 || ^10.0", "phpunit/phpunit": "^10.5.16 || ^11.2", - "predis/predis": "^1.1 || ^2.3", + "predis/predis": "^3.0", "rector/rector": "2.0.15", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, From 10d39156c5ba2fce4c44adfaa6e842ed8ac72ebf Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Wed, 7 May 2025 21:11:16 +0800 Subject: [PATCH 04/59] chore: bump to laminas-escaper v2.17 (#9552) --- system/ThirdParty/Escaper/Escaper.php | 44 ++------------ .../ThirdParty/Escaper/EscaperInterface.php | 58 +++++++++++++++++++ 2 files changed, 64 insertions(+), 38 deletions(-) create mode 100644 system/ThirdParty/Escaper/EscaperInterface.php diff --git a/system/ThirdParty/Escaper/Escaper.php b/system/ThirdParty/Escaper/Escaper.php index 4fce36bd0c6c..39d9b0b1cdac 100644 --- a/system/ThirdParty/Escaper/Escaper.php +++ b/system/ThirdParty/Escaper/Escaper.php @@ -30,7 +30,7 @@ * * @final */ -class Escaper +class Escaper implements EscaperInterface { /** * Entity Map mapping Unicode codepoints to any available named HTML entities. @@ -183,24 +183,13 @@ public function getEncoding() return $this->encoding; } - /** - * Escape a string for the HTML Body context where there are very few characters - * of special meaning. Internally this will use htmlspecialchars(). - * - * @return ($string is non-empty-string ? non-empty-string : string) - */ + /** @inheritDoc */ public function escapeHtml(string $string) { return htmlspecialchars($string, $this->htmlSpecialCharsFlags, $this->encoding); } - /** - * Escape a string for the HTML Attribute context. We use an extended set of characters - * to escape that are not covered by htmlspecialchars() to cover cases where an attribute - * might be unquoted or quoted illegally (e.g. backticks are valid quotes for IE). - * - * @return ($string is non-empty-string ? non-empty-string : string) - */ + /** @inheritDoc */ public function escapeHtmlAttr(string $string) { $string = $this->toUtf8($string); @@ -214,17 +203,7 @@ public function escapeHtmlAttr(string $string) return $this->fromUtf8($result); } - /** - * Escape a string for the Javascript context. This does not use json_encode(). An extended - * set of characters are escaped beyond ECMAScript's rules for Javascript literal string - * escaping in order to prevent misinterpretation of Javascript as HTML leading to the - * injection of special characters and entities. The escaping used should be tolerant - * of cases where HTML escaping was not applied on top of Javascript escaping correctly. - * Backslash escaping is not used as it still leaves the escaped character as-is and so - * is not useful in a HTML context. - * - * @return ($string is non-empty-string ? non-empty-string : string) - */ + /** @inheritDoc */ public function escapeJs(string $string) { $string = $this->toUtf8($string); @@ -238,24 +217,13 @@ public function escapeJs(string $string) return $this->fromUtf8($result); } - /** - * Escape a string for the URI or Parameter contexts. This should not be used to escape - * an entire URI - only a subcomponent being inserted. The function is a simple proxy - * to rawurlencode() which now implements RFC 3986 since PHP 5.3 completely. - * - * @return ($string is non-empty-string ? non-empty-string : string) - */ + /** @inheritDoc */ public function escapeUrl(string $string) { return rawurlencode($string); } - /** - * Escape a string for the CSS context. CSS escaping can be applied to any string being - * inserted into CSS and escapes everything except alphanumerics. - * - * @return ($string is non-empty-string ? non-empty-string : string) - */ + /** @inheritDoc */ public function escapeCss(string $string) { $string = $this->toUtf8($string); diff --git a/system/ThirdParty/Escaper/EscaperInterface.php b/system/ThirdParty/Escaper/EscaperInterface.php new file mode 100644 index 000000000000..3930db88ac01 --- /dev/null +++ b/system/ThirdParty/Escaper/EscaperInterface.php @@ -0,0 +1,58 @@ + Date: Tue, 13 May 2025 15:29:43 +0000 Subject: [PATCH 05/59] chore(deps-dev): update rector/rector requirement from 2.0.15 to 2.0.16 Updates the requirements on [rector/rector](https://github.com/rectorphp/rector) to permit the latest version. - [Release notes](https://github.com/rectorphp/rector/releases) - [Commits](https://github.com/rectorphp/rector/compare/2.0.15...2.0.16) --- updated-dependencies: - dependency-name: rector/rector dependency-version: 2.0.16 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 158d1508848c..4bc099e2b219 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "phpunit/phpcov": "^9.0.2 || ^10.0", "phpunit/phpunit": "^10.5.16 || ^11.2", "predis/predis": "^3.0", - "rector/rector": "2.0.15", + "rector/rector": "2.0.16", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { From 551c56ddddd5d040bc3793fc019a9b800da1595c Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Thu, 15 May 2025 20:48:39 +0200 Subject: [PATCH 06/59] chore: add missing EscaperInterface to the AutoloadConfig (#9561) * chore: add missing EscaperInterface to the AutoloadConfig * update laminas/laminas-escaper to ^2.17 * update laminas/laminas-escaper to ^2.17 --- admin/framework/composer.json | 2 +- composer.json | 2 +- system/Config/AutoloadConfig.php | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/admin/framework/composer.json b/admin/framework/composer.json index 0550502fd22c..499a97eae337 100644 --- a/admin/framework/composer.json +++ b/admin/framework/composer.json @@ -13,7 +13,7 @@ "php": "^8.1", "ext-intl": "*", "ext-mbstring": "*", - "laminas/laminas-escaper": "^2.14", + "laminas/laminas-escaper": "^2.17", "psr/log": "^3.0" }, "require-dev": { diff --git a/composer.json b/composer.json index 4bc099e2b219..d91d2df06b27 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "php": "^8.1", "ext-intl": "*", "ext-mbstring": "*", - "laminas/laminas-escaper": "^2.14", + "laminas/laminas-escaper": "^2.17", "psr/log": "^3.0" }, "require-dev": { diff --git a/system/Config/AutoloadConfig.php b/system/Config/AutoloadConfig.php index 0a99cdb0bb28..b6d7f72bdd54 100644 --- a/system/Config/AutoloadConfig.php +++ b/system/Config/AutoloadConfig.php @@ -14,6 +14,7 @@ namespace CodeIgniter\Config; use Laminas\Escaper\Escaper; +use Laminas\Escaper\EscaperInterface; use Laminas\Escaper\Exception\ExceptionInterface; use Laminas\Escaper\Exception\InvalidArgumentException as EscaperInvalidArgumentException; use Laminas\Escaper\Exception\RuntimeException; @@ -119,6 +120,7 @@ class AutoloadConfig ExceptionInterface::class => SYSTEMPATH . 'ThirdParty/Escaper/Exception/ExceptionInterface.php', EscaperInvalidArgumentException::class => SYSTEMPATH . 'ThirdParty/Escaper/Exception/InvalidArgumentException.php', RuntimeException::class => SYSTEMPATH . 'ThirdParty/Escaper/Exception/RuntimeException.php', + EscaperInterface::class => SYSTEMPATH . 'ThirdParty/Escaper/EscaperInterface.php', Escaper::class => SYSTEMPATH . 'ThirdParty/Escaper/Escaper.php', ]; From 153c7380f68beebb6467e0c1258aeceb5ec3a7ba Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Sun, 18 May 2025 09:29:10 +0200 Subject: [PATCH 07/59] fix: remove service dependency from sanitize_filename() helper function (#9560) * fix: remove service dependency from sanitize_filename() helper function * deprecate Security::sanitizeFilename and SecurityInterface::sanitizeFilename --- system/Helpers/security_helper.php | 64 ++++++++++++++++++- system/Security/Security.php | 51 ++------------- system/Security/SecurityInterface.php | 2 + user_guide_src/source/changelogs/v4.6.2.rst | 7 ++ .../source/helpers/security_helper.rst | 4 +- user_guide_src/source/libraries/security.rst | 2 + 6 files changed, 78 insertions(+), 52 deletions(-) diff --git a/system/Helpers/security_helper.php b/system/Helpers/security_helper.php index d42ac0dda777..d73fd6219b12 100644 --- a/system/Helpers/security_helper.php +++ b/system/Helpers/security_helper.php @@ -15,11 +15,69 @@ if (! function_exists('sanitize_filename')) { /** - * Sanitize a filename to use in a URI. + * Sanitize Filename + * + * Tries to sanitize filenames in order to prevent directory traversal attempts + * and other security threats, which is particularly useful for files that + * were supplied via user input. + * + * If it is acceptable for the user input to include relative paths, + * e.g. file/in/some/approved/folder.txt, you can set the second optional + * parameter, $relativePath to TRUE. + * + * @param string $filename Input file name + * @param bool $relativePath Whether to preserve paths */ - function sanitize_filename(string $filename): string + function sanitize_filename(string $filename, bool $relativePath = false): string { - return service('security')->sanitizeFilename($filename); + // List of sanitized filename strings + $bad = [ + '../', + '', + '<', + '>', + "'", + '"', + '&', + '$', + '#', + '{', + '}', + '[', + ']', + '=', + ';', + '?', + '%20', + '%22', + '%3c', + '%253c', + '%3e', + '%0e', + '%28', + '%29', + '%2528', + '%26', + '%24', + '%3f', + '%3b', + '%3d', + ]; + + if (! $relativePath) { + $bad[] = './'; + $bad[] = '/'; + } + + $filename = remove_invisible_characters($filename, false); + + do { + $old = $filename; + $filename = str_replace($bad, '', $filename); + } while ($old !== $filename); + + return stripslashes($filename); } } diff --git a/system/Security/Security.php b/system/Security/Security.php index 9e87a2177cda..aa744d9ed343 100644 --- a/system/Security/Security.php +++ b/system/Security/Security.php @@ -427,59 +427,16 @@ public function shouldRedirect(): bool * e.g. file/in/some/approved/folder.txt, you can set the second optional * parameter, $relativePath to TRUE. * + * @deprecated 4.6.2 Use `sanitize_filename()` instead + * * @param string $str Input file name * @param bool $relativePath Whether to preserve paths */ public function sanitizeFilename(string $str, bool $relativePath = false): string { - // List of sanitize filename strings - $bad = [ - '../', - '', - '<', - '>', - "'", - '"', - '&', - '$', - '#', - '{', - '}', - '[', - ']', - '=', - ';', - '?', - '%20', - '%22', - '%3c', - '%253c', - '%3e', - '%0e', - '%28', - '%29', - '%2528', - '%26', - '%24', - '%3f', - '%3b', - '%3d', - ]; - - if (! $relativePath) { - $bad[] = './'; - $bad[] = '/'; - } - - $str = remove_invisible_characters($str, false); - - do { - $old = $str; - $str = str_replace($bad, '', $str); - } while ($old !== $str); + helper('security'); - return stripslashes($str); + return sanitize_filename($str, $relativePath); } /** diff --git a/system/Security/SecurityInterface.php b/system/Security/SecurityInterface.php index 03a5ba2321d0..ebd8919cd109 100644 --- a/system/Security/SecurityInterface.php +++ b/system/Security/SecurityInterface.php @@ -66,6 +66,8 @@ public function shouldRedirect(): bool; * e.g. file/in/some/approved/folder.txt, you can set the second optional * parameter, $relativePath to TRUE. * + * @deprecated 4.6.2 Use `sanitize_filename()` instead + * * @param string $str Input file name * @param bool $relativePath Whether to preserve paths */ diff --git a/user_guide_src/source/changelogs/v4.6.2.rst b/user_guide_src/source/changelogs/v4.6.2.rst index dd988c4778b5..089fb01e1514 100644 --- a/user_guide_src/source/changelogs/v4.6.2.rst +++ b/user_guide_src/source/changelogs/v4.6.2.rst @@ -22,14 +22,21 @@ Message Changes Changes ******* +- **Security:** The ``sanitize_filename()`` function from the Security helper now supports a second parameter to control whether relative paths are allowed. + ************ Deprecations ************ +- **Security:** The ``Security::sanitizeFilename()`` method is deprecated. Use ``sanitize_filename()`` instead. +- **Security:** The ``SecurityInterface::sanitizeFilename()`` method is deprecated. + ********** Bugs Fixed ********** +- **Security:** Fixed a bug where the ``sanitize_filename()`` function from the Security helper would throw an error when used in CLI requests. + See the repo's `CHANGELOG.md `_ for a complete list of bugs fixed. diff --git a/user_guide_src/source/helpers/security_helper.rst b/user_guide_src/source/helpers/security_helper.rst index 500b7fd85b11..405a6e489051 100644 --- a/user_guide_src/source/helpers/security_helper.rst +++ b/user_guide_src/source/helpers/security_helper.rst @@ -20,15 +20,15 @@ Available Functions The following functions are available: -.. php:function:: sanitize_filename($filename) +.. php:function:: sanitize_filename($filename[, $relativePath = false]) :param string $filename: Filename + :param bool $relativePath: Whether the relative path is acceptable (available since v4.6.2) :returns: Sanitized file name :rtype: string Provides protection against directory traversal. - This function is an alias for ``\CodeIgniter\Security::sanitizeFilename()``. For more info, please see the :doc:`Security Library <../libraries/security>` documentation. diff --git a/user_guide_src/source/libraries/security.rst b/user_guide_src/source/libraries/security.rst index 07e2e8756d18..8fc49d646b1a 100644 --- a/user_guide_src/source/libraries/security.rst +++ b/user_guide_src/source/libraries/security.rst @@ -241,3 +241,5 @@ If it is acceptable for the user input to include relative paths, e.g., **file/i the second optional parameter, ``$relativePath`` to ``true``. .. literalinclude:: security/010.php + +This method is an alias for the ``sanitize_filename()`` function from the Security helper. From e7898a0e717faa07e2bd5bf2210d28afa024e2c0 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Sun, 18 May 2025 17:55:28 +0800 Subject: [PATCH 08/59] test: ensure all test methods have native return types (#9565) --- .php-cs-fixer.tests.php | 3 ++- tests/_support/Autoloader/FatalLocator.php | 2 +- tests/_support/Config/Services.php | 4 +--- tests/_support/Entity/Cast/CastPassParameters.php | 4 +--- tests/system/Files/FileTest.php | 2 +- tests/system/Models/TimestampModelTest.php | 4 ++-- tests/system/Test/BootstrapFCPATHTest.php | 5 +---- 7 files changed, 9 insertions(+), 15 deletions(-) diff --git a/.php-cs-fixer.tests.php b/.php-cs-fixer.tests.php index bf37256da58a..412400aebaee 100644 --- a/.php-cs-fixer.tests.php +++ b/.php-cs-fixer.tests.php @@ -31,7 +31,8 @@ ->notName('#Foobar.php$#'); $overrides = [ - 'void_return' => true, + 'phpdoc_to_return_type' => true, + 'void_return' => true, ]; return $config diff --git a/tests/_support/Autoloader/FatalLocator.php b/tests/_support/Autoloader/FatalLocator.php index 0b98fddc6eca..438c41480f01 100644 --- a/tests/_support/Autoloader/FatalLocator.php +++ b/tests/_support/Autoloader/FatalLocator.php @@ -34,7 +34,7 @@ class FatalLocator extends FileLocator * * @return false|string The path to the file, or false if not found. */ - public function locateFile(string $file, ?string $folder = null, string $ext = 'php') + public function locateFile(string $file, ?string $folder = null, string $ext = 'php'): false|string { $folder ??= 'null'; diff --git a/tests/_support/Config/Services.php b/tests/_support/Config/Services.php index 712c79004a0b..3cb7c53388c3 100644 --- a/tests/_support/Config/Services.php +++ b/tests/_support/Config/Services.php @@ -31,10 +31,8 @@ class Services extends BaseServices * The URI class provides a way to model and manipulate URIs. * * @param string|null $uri The URI string - * - * @return URI */ - public static function uri(?string $uri = null, bool $getShared = true) + public static function uri(?string $uri = null, bool $getShared = true): URI { // Intercept our test case if ($uri === 'testCanReplaceFrameworkServices') { diff --git a/tests/_support/Entity/Cast/CastPassParameters.php b/tests/_support/Entity/Cast/CastPassParameters.php index 2cbfce0f324b..3f304e3622a0 100644 --- a/tests/_support/Entity/Cast/CastPassParameters.php +++ b/tests/_support/Entity/Cast/CastPassParameters.php @@ -22,10 +22,8 @@ class CastPassParameters extends BaseCast * * @param mixed $value Data * @param array $params Additional param - * - * @return mixed */ - public static function set($value, array $params = []) + public static function set($value, array $params = []): string { return $value . ':' . json_encode($params); } diff --git a/tests/system/Files/FileTest.php b/tests/system/Files/FileTest.php index b8e82edd02fd..c74c6b63ffeb 100644 --- a/tests/system/Files/FileTest.php +++ b/tests/system/Files/FileTest.php @@ -172,7 +172,7 @@ public function testGetDestination(): void /** * @return array> */ - public static function provideGetSizeData() + public static function provideGetSizeData(): array { return [ 'returns KB binary' => [ diff --git a/tests/system/Models/TimestampModelTest.php b/tests/system/Models/TimestampModelTest.php index ce2675a2d89d..9af7cd3b9547 100644 --- a/tests/system/Models/TimestampModelTest.php +++ b/tests/system/Models/TimestampModelTest.php @@ -40,7 +40,7 @@ protected function tearDown(): void /** * @return int|string Insert ID */ - private function allowDatesPrepareOneRecord(array $data) + private function allowDatesPrepareOneRecord(array $data): int|string { $this->createModel(UserTimestampModel::class); $this->db->table('user')->truncate(); @@ -60,7 +60,7 @@ private function allowDatesPrepareOneRecord(array $data) /** * @return int|string Insert ID */ - private function doNotAllowDatesPrepareOneRecord(array $data) + private function doNotAllowDatesPrepareOneRecord(array $data): int|string { $this->createModel(UserTimestampModel::class); $this->db->table('user')->truncate(); diff --git a/tests/system/Test/BootstrapFCPATHTest.php b/tests/system/Test/BootstrapFCPATHTest.php index cc03ef99b1b7..2af9b87b560d 100644 --- a/tests/system/Test/BootstrapFCPATHTest.php +++ b/tests/system/Test/BootstrapFCPATHTest.php @@ -100,10 +100,7 @@ private function fileContents(): string return $fileContents . 'echo FCPATH;' . PHP_EOL; } - /** - * @return false|string - */ - private function readOutput(string $file) + private function readOutput(string $file): false|string { ob_start(); system('php -f ' . $file); From c8f8ff7fe7fa11bde38304c1e2ce04af0b24ea54 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Sun, 18 May 2025 17:55:54 +0800 Subject: [PATCH 09/59] refactor: add `system/util_bootstrap.php` to curb overreliance to `system/Test/bootstrap.php` (#9562) * Add `Boot::bootConsole()` method * Add `system/util_bootstrap.php` * Refactor `utils/check_permission_x.php` --- system/Boot.php | 29 +++++++++++++ system/util_bootstrap.php | 80 ++++++++++++++++++++++++++++++++++++ utils/check_permission_x.php | 42 ++++++++++++------- 3 files changed, 137 insertions(+), 14 deletions(-) create mode 100644 system/util_bootstrap.php diff --git a/system/Boot.php b/system/Boot.php index 502692249b06..720c65facb43 100644 --- a/system/Boot.php +++ b/system/Boot.php @@ -16,6 +16,7 @@ use CodeIgniter\Cache\FactoriesCache; use CodeIgniter\CLI\Console; use CodeIgniter\Config\DotEnv; +use Config\App; use Config\Autoload; use Config\Modules; use Config\Optimize; @@ -75,6 +76,34 @@ public static function bootWeb(Paths $paths): int return EXIT_SUCCESS; } + /** + * Used by command line scripts other than + * * `spark` + * * `php-cli` + * * `phpunit` + * + * @used-by `system/util_bootstrap.php` + */ + public static function bootConsole(Paths $paths): void + { + static::definePathConstants($paths); + static::loadConstants(); + static::checkMissingExtensions(); + + static::loadDotEnv($paths); + static::loadEnvironmentBootstrap($paths); + + static::loadCommonFunctions(); + static::loadAutoloader(); + static::setExceptionHandler(); + static::initializeKint(); + static::autoloadHelpers(); + + // We need to force the request to be a CLIRequest since we're in console + Services::createRequest(new App(), true); + service('routes')->loadRoutes(); + } + /** * Used by `spark` * diff --git a/system/util_bootstrap.php b/system/util_bootstrap.php new file mode 100644 index 000000000000..3152cc8d1839 --- /dev/null +++ b/system/util_bootstrap.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +use CodeIgniter\Boot; +use Config\Paths; + +error_reporting(E_ALL); +ini_set('display_errors', '1'); +ini_set('display_startup_errors', '1'); + +/* + * --------------------------------------------------------------- + * DEFINE ENVIRONMENT + * --------------------------------------------------------------- + * + * As this bootstrap file is primarily used by internal scripts + * across the framework and other CodeIgniter projects, we need + * to make sure it recognizes that we're in development. + */ + +$_SERVER['CI_ENVIRONMENT'] = 'development'; +define('ENVIRONMENT', 'development'); +defined('CI_DEBUG') || define('CI_DEBUG', true); + +/* + * --------------------------------------------------------------- + * SET UP OUR PATH CONSTANTS + * --------------------------------------------------------------- + * + * The path constants provide convenient access to the folders + * throughout the application. We have to set them up here + * so they are available in the config files that are loaded. + */ + +defined('HOMEPATH') || define('HOMEPATH', realpath(rtrim(getcwd(), '\\/ ')) . DIRECTORY_SEPARATOR); + +$source = match (true) { + is_dir(HOMEPATH . 'app/') => HOMEPATH, + is_dir('vendor/codeigniter4/framework/') => 'vendor/codeigniter4/framework/', + is_dir('vendor/codeigniter4/codeigniter4/') => 'vendor/codeigniter4/codeigniter4/', + default => throw new RuntimeException('Unable to determine the source directory.'), +}; + +defined('CONFIGPATH') || define('CONFIGPATH', realpath($source . 'app/Config') . DIRECTORY_SEPARATOR); +defined('PUBLICPATH') || define('PUBLICPATH', realpath($source . 'public') . DIRECTORY_SEPARATOR); +unset($source); + +require CONFIGPATH . 'Paths.php'; +$paths = new Paths(); + +defined('CIPATH') || define('CIPATH', realpath($paths->systemDirectory . '/../') . DIRECTORY_SEPARATOR); +defined('FCPATH') || define('FCPATH', PUBLICPATH); + +if (is_dir(HOMEPATH . 'vendor/')) { + define('VENDORPATH', realpath(HOMEPATH . 'vendor/') . DIRECTORY_SEPARATOR); + define('COMPOSER_PATH', (string) realpath(HOMEPATH . 'vendor/autoload.php')); +} + +/* + *--------------------------------------------------------------- + * BOOTSTRAP THE APPLICATION + *--------------------------------------------------------------- + * + * This process sets up the path constants, loads and registers + * our autoloader, along with Composer's, loads our constants + * and fires up an environment-specific bootstrapping. + */ + +require $paths->systemDirectory . '/Boot.php'; +Boot::bootConsole($paths); diff --git a/utils/check_permission_x.php b/utils/check_permission_x.php index 84f4b00a1b1a..9922174b152d 100644 --- a/utils/check_permission_x.php +++ b/utils/check_permission_x.php @@ -13,16 +13,24 @@ namespace Utils; -require __DIR__ . '/../system/Test/bootstrap.php'; +require __DIR__ . '/../system/util_bootstrap.php'; use CodeIgniter\CLI\CLI; +use FilesystemIterator; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use RuntimeException; +use SplFileInfo; -function findExecutableFiles($dir) +/** + * @param list $excludeDirs + * + * @return list + */ +function findExecutableFiles(string $dir, array $excludeDirs = []): array { - $execFileList = [ + static $execFileList = [ + '.github/scripts/deploy-userguide', 'admin/release-userguide', 'admin/release-deploy', 'admin/apibot', @@ -37,23 +45,28 @@ function findExecutableFiles($dir) $executableFiles = []; - // Check if the directory exists if (! is_dir($dir)) { throw new RuntimeException('No such directory: ' . $dir); } - // Create a Recursive Directory Iterator $iterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($dir), + new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS | FilesystemIterator::CURRENT_AS_FILEINFO), + RecursiveIteratorIterator::CHILD_FIRST | RecursiveIteratorIterator::LEAVES_ONLY, ); - // Iterate over each item in the directory + /** @var SplFileInfo $fileinfo */ foreach ($iterator as $fileinfo) { - // Check if the item is a file and is executable - if ($fileinfo->isFile() && is_executable($fileinfo->getPathname())) { - $filePath = $fileinfo->getPathname(); + $filePath = $fileinfo->getPathname(); + + if ($fileinfo->isFile() && is_executable($filePath)) { + $dirPath = dirname($filePath); + + foreach ($excludeDirs as $excludeDir) { + if (str_contains($dirPath, $excludeDir)) { + continue 2; + } + } - // Check allow list if (in_array($filePath, $execFileList, true)) { continue; } @@ -72,12 +85,13 @@ function findExecutableFiles($dir) // Main chdir(__DIR__ . '/../'); -$dirs = ['admin', 'app', 'system', 'tests', 'user_guide_src', 'utils', 'writable']; +$includeDirs = ['.github', 'admin', 'app', 'public', 'system', 'tests', 'user_guide_src', 'utils', 'writable']; +$excludeDirs = ['utils/vendor']; $executableFiles = []; -foreach ($dirs as $dir) { - $executableFiles = array_merge($executableFiles, findExecutableFiles($dir)); +foreach ($includeDirs as $dir) { + $executableFiles = array_merge($executableFiles, findExecutableFiles($dir, $excludeDirs)); } if ($executableFiles !== []) { From fce28583c0abd63a1a4c4e2761e3b61b0902b512 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Sun, 18 May 2025 17:56:50 +0800 Subject: [PATCH 10/59] docs: `CookieStore::get()` does not give an array of Cookie objects (#9566) --- user_guide_src/source/libraries/cookies/013.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/user_guide_src/source/libraries/cookies/013.php b/user_guide_src/source/libraries/cookies/013.php index 80052c7c9218..d27b248662cd 100644 --- a/user_guide_src/source/libraries/cookies/013.php +++ b/user_guide_src/source/libraries/cookies/013.php @@ -1,9 +1,6 @@ get(); // array of Cookie objects - -// alternatively, you can use the display method -cookies()->display(); +cookies()->display(); // array of Cookie objects // or even from the Response service('response')->getCookies(); From 4fd4be184de315462cb78a87eec72ad22f9b87e6 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Sun, 18 May 2025 17:57:14 +0800 Subject: [PATCH 11/59] test: fix `PublisherInputTest::testAddUri(s)` failing due to rate limiting (#9567) --- tests/system/Publisher/PublisherInputTest.php | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/system/Publisher/PublisherInputTest.php b/tests/system/Publisher/PublisherInputTest.php index 1336137defd4..b1046e19cf68 100644 --- a/tests/system/Publisher/PublisherInputTest.php +++ b/tests/system/Publisher/PublisherInputTest.php @@ -13,7 +13,10 @@ namespace CodeIgniter\Publisher; +use CodeIgniter\HTTP\CURLRequest; +use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\Test\CIUnitTestCase; +use Config\Services; use PHPUnit\Framework\Attributes\Group; /** @@ -136,16 +139,37 @@ public function testAddPathsRecursive(): void public function testAddUri(): void { + $mockCurl = $this->getMockBuilder(CURLRequest::class) + ->disableOriginalConstructor() + ->onlyMethods(['get']) + ->getMock(); + $mockResponse = $this->createStub(ResponseInterface::class); + + $mockResponse->method('getBody')->willReturn(''); + $mockCurl->method('get')->willReturn($mockResponse); + Services::injectMock('curlrequest', $mockCurl); + $publisher = new Publisher(); $publisher->addUri('https://raw.githubusercontent.com/codeigniter4/CodeIgniter4/develop/composer.json'); $scratch = $this->getPrivateProperty($publisher, 'scratch'); $this->assertSame([$scratch . 'composer.json'], $publisher->get()); + Services::resetSingle('curlrequest'); } public function testAddUris(): void { + $mockCurl = $this->getMockBuilder(CURLRequest::class) + ->disableOriginalConstructor() + ->onlyMethods(['get']) + ->getMock(); + $mockResponse = $this->createStub(ResponseInterface::class); + + $mockResponse->method('getBody')->willReturn(''); + $mockCurl->method('get')->willReturn($mockResponse); + Services::injectMock('curlrequest', $mockCurl); + $publisher = new Publisher(); $publisher->addUris([ 'https://raw.githubusercontent.com/codeigniter4/CodeIgniter4/develop/LICENSE', @@ -155,5 +179,6 @@ public function testAddUris(): void $scratch = $this->getPrivateProperty($publisher, 'scratch'); $this->assertSame([$scratch . 'LICENSE', $scratch . 'composer.json'], $publisher->get()); + Services::resetSingle('curlrequest'); } } From a21845c36002cf00e9986ed7211879a3a2d9b3c2 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Mon, 19 May 2025 18:41:57 +0800 Subject: [PATCH 12/59] refactor: update places to use `system/util_bootstrap.php` (#9568) --- phpstan-bootstrap.php | 2 +- psalm_autoload.php | 2 +- rector.php | 2 +- system/util_bootstrap.php | 4 ++++ user_guide_src/source/installation/running.rst | 9 +++++++-- utils/check_tabs_in_rst.php | 2 +- 6 files changed, 15 insertions(+), 6 deletions(-) diff --git a/phpstan-bootstrap.php b/phpstan-bootstrap.php index 99acf96227fe..ce8e4636bd11 100644 --- a/phpstan-bootstrap.php +++ b/phpstan-bootstrap.php @@ -1,6 +1,6 @@ withBootstrapFiles([ - __DIR__ . '/system/Test/bootstrap.php', + __DIR__ . '/system/util_bootstrap.php', ]) ->withPHPStanConfigs([ __DIR__ . '/phpstan.neon.dist', diff --git a/system/util_bootstrap.php b/system/util_bootstrap.php index 3152cc8d1839..5423954deb59 100644 --- a/system/util_bootstrap.php +++ b/system/util_bootstrap.php @@ -61,6 +61,10 @@ defined('CIPATH') || define('CIPATH', realpath($paths->systemDirectory . '/../') . DIRECTORY_SEPARATOR); defined('FCPATH') || define('FCPATH', PUBLICPATH); +if (is_dir($paths->testsDirectory . '/_support/') && ! defined('SUPPORTPATH')) { + define('SUPPORTPATH', realpath($paths->testsDirectory . '/_support/') . DIRECTORY_SEPARATOR); +} + if (is_dir(HOMEPATH . 'vendor/')) { define('VENDORPATH', realpath(HOMEPATH . 'vendor/') . DIRECTORY_SEPARATOR); define('COMPOSER_PATH', (string) realpath(HOMEPATH . 'vendor/autoload.php')); diff --git a/user_guide_src/source/installation/running.rst b/user_guide_src/source/installation/running.rst index 5b39f13f3df9..0fbc6fdc9c11 100644 --- a/user_guide_src/source/installation/running.rst +++ b/user_guide_src/source/installation/running.rst @@ -501,10 +501,15 @@ See :ref:`Deployment `. Bootstrapping the App ********************* -In some scenarios you will want to load the framework without actually running the whole +In some scenarios, you will want to load the framework without actually running the whole application. This is particularly useful for unit testing your project, but may also be handy for using third-party tools to analyze and modify your code. The framework comes -with a separate bootstrap script specifically for this scenario: **system/Test/bootstrap.php**. +with two separate bootstrap scripts specifically for these scenarios: + +- **system/Test/bootstrap.php**: This script is used primarily for unit testing. +- **system/util_bootstrap.php**: This script is used for other scripts that need access to the + framework. It is recommended for use in scripts that are not part for testing as this will NOT + fail gracefully if an exception is thrown. Most of the paths to your project are defined during the bootstrap process. You may use pre-defined constants to override these, but when using the defaults be sure that your diff --git a/utils/check_tabs_in_rst.php b/utils/check_tabs_in_rst.php index e99d08bf7645..69e8f92de1ee 100644 --- a/utils/check_tabs_in_rst.php +++ b/utils/check_tabs_in_rst.php @@ -13,7 +13,7 @@ namespace Utils; -require __DIR__ . '/../system/Test/bootstrap.php'; +require __DIR__ . '/../system/util_bootstrap.php'; use CodeIgniter\CLI\CLI; From 9d032efb2586601679c4da6947615769ee268109 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Mon, 19 May 2025 18:42:30 +0800 Subject: [PATCH 13/59] refactor: more accurate array PHPDocs of Cookie (#9569) --- system/Cookie/Cookie.php | 39 +++++++++++++++++++---- system/Cookie/CookieInterface.php | 22 +++++++++++-- tests/system/Cookie/CookieStoreTest.php | 2 +- utils/phpstan-baseline/argument.type.neon | 12 +------ utils/phpstan-baseline/loader.neon | 2 +- 5 files changed, 56 insertions(+), 21 deletions(-) diff --git a/system/Cookie/Cookie.php b/system/Cookie/Cookie.php index df75c03c7bcb..ad461c721112 100644 --- a/system/Cookie/Cookie.php +++ b/system/Cookie/Cookie.php @@ -99,7 +99,16 @@ class Cookie implements ArrayAccess, CloneableCookieInterface * Default attributes for a Cookie object. The keys here are the * lowercase attribute names. Do not camelCase! * - * @var array + * @var array{ + * prefix: string, + * expires: int, + * path: string, + * domain: string, + * secure: bool, + * httponly: bool, + * samesite: string, + * raw: bool, + * } */ private static array $defaults = [ 'prefix' => '', @@ -127,9 +136,27 @@ class Cookie implements ArrayAccess, CloneableCookieInterface * * This method is called from Response::__construct(). * - * @param array|CookieConfig $config + * @param array{ + * prefix?: string, + * expires?: int, + * path?: string, + * domain?: string, + * secure?: bool, + * httponly?: bool, + * samesite?: string, + * raw?: bool, + * }|CookieConfig $config * - * @return array The old defaults array. Useful for resetting. + * @return array{ + * prefix: string, + * expires: int, + * path: string, + * domain: string, + * secure: bool, + * httponly: bool, + * samesite: string, + * raw: bool, + * } The old defaults array. Useful for resetting. */ public static function setDefaults($config = []) { @@ -198,9 +225,9 @@ public static function fromHeaderString(string $cookie, bool $raw = false) /** * Construct a new Cookie instance. * - * @param string $name The cookie's name - * @param string $value The cookie's value - * @param array $options The cookie's options + * @param string $name The cookie's name + * @param string $value The cookie's value + * @param array{prefix?: string, max-age?: int|numeric-string, expires?: DateTimeInterface|int|string, path?: string, domain?: string, secure?: bool, httponly?: bool, samesite?: string, raw?: bool} $options The cookie's options * * @throws CookieException */ diff --git a/system/Cookie/CookieInterface.php b/system/Cookie/CookieInterface.php index c848fa8884ec..b4a7ea3d7320 100644 --- a/system/Cookie/CookieInterface.php +++ b/system/Cookie/CookieInterface.php @@ -145,7 +145,14 @@ public function isRaw(): bool; * Gets the options that are passable to the `setcookie` variant * available on PHP 7.3+ * - * @return array + * @return array{ + * expires: int, + * path: string, + * domain: string, + * secure: bool, + * httponly: bool, + * samesite: string, + * } */ public function getOptions(): array; @@ -164,7 +171,18 @@ public function __toString(); /** * Returns the array representation of the Cookie object. * - * @return array + * @return array{ + * name: string, + * value: string, + * prefix: string, + * raw: bool, + * expires: int, + * path: string, + * domain: string, + * secure: bool, + * httponly: bool, + * samesite: string, + * } */ public function toArray(): array; } diff --git a/tests/system/Cookie/CookieStoreTest.php b/tests/system/Cookie/CookieStoreTest.php index 7e752fde5da5..60498052ee93 100644 --- a/tests/system/Cookie/CookieStoreTest.php +++ b/tests/system/Cookie/CookieStoreTest.php @@ -78,7 +78,7 @@ public function testCookieStoreInitViaHeaders(): void public function testInvalidCookieStored(): void { $this->expectException(CookieException::class); - new CookieStore([new DateTimeImmutable('now')]); + new CookieStore([new DateTimeImmutable('now')]); // @phpstan-ignore argument.type } public function testPutRemoveCookiesInStore(): void diff --git a/utils/phpstan-baseline/argument.type.neon b/utils/phpstan-baseline/argument.type.neon index 7301faeae35d..34a1a8c9986e 100644 --- a/utils/phpstan-baseline/argument.type.neon +++ b/utils/phpstan-baseline/argument.type.neon @@ -1,4 +1,4 @@ -# total 138 errors +# total 136 errors parameters: ignoreErrors: @@ -82,16 +82,6 @@ parameters: count: 2 path: ../../tests/system/Config/FactoriesTest.php - - - message: '#^Parameter \#1 \$cookies of class CodeIgniter\\Cookie\\CookieStore constructor expects array\, array\ given\.$#' - count: 1 - path: ../../tests/system/Cookie/CookieStoreTest.php - - - - message: '#^Parameter \#3 \$options of class CodeIgniter\\Cookie\\Cookie constructor expects array\, array\ given\.$#' - count: 1 - path: ../../tests/system/Cookie/CookieTest.php - - message: '#^Parameter \#1 \$from of method CodeIgniter\\Database\\BaseBuilder\:\:from\(\) expects array\|string, null given\.$#' count: 1 diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index c715fc07bd3b..e54589fea853 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 3273 errors +# total 3271 errors includes: - argument.type.neon - assign.propertyType.neon From 9b14ad94fb8f1b28dffd36960ace0063c7af2b95 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Sat, 24 May 2025 22:26:27 +0800 Subject: [PATCH 14/59] refactor: use native phpdocs wherever possible (#9571) * refactor: use native phpdocs wherever possible * Fix psalm * Fix rector --- app/Config/Cookie.php | 2 +- psalm_autoload.php | 7 +++- system/API/ResponseTrait.php | 6 ++-- system/Autoloader/Autoloader.php | 3 +- system/BaseModel.php | 3 +- system/CodeIgniter.php | 4 +-- .../Translation/LocalizationFinder.php | 3 +- system/Common.php | 30 +++++++---------- system/DataCaster/DataCaster.php | 7 ++-- system/DataConverter/DataConverter.php | 15 ++++----- system/Database/BaseConnection.php | 33 +++++++------------ system/Database/BasePreparedQuery.php | 9 ++--- system/Database/BaseResult.php | 32 +++++++----------- system/Database/ConnectionInterface.php | 18 ++++------ system/Database/Forge.php | 3 +- system/Database/MySQLi/Forge.php | 3 +- system/Database/OCI8/Forge.php | 3 +- system/Database/Postgre/Connection.php | 3 +- system/Database/Postgre/Forge.php | 3 +- system/Database/PreparedQueryInterface.php | 3 +- system/Database/ResultInterface.php | 15 ++++----- system/Database/SQLSRV/Forge.php | 3 +- system/Database/SQLite3/Forge.php | 4 +-- system/Database/SQLite3/Table.php | 3 +- system/Files/File.php | 4 +-- system/Filters/Filters.php | 20 +++++------ system/HTTP/CLIRequest.php | 3 -- system/HTTP/IncomingRequest.php | 3 -- system/HTTP/RequestTrait.php | 17 ++++------ system/HTTP/SiteURI.php | 9 +++-- system/Model.php | 3 +- system/RESTful/ResourceController.php | 3 +- system/Test/Mock/MockInputOutput.php | 6 ++-- system/Traits/ConditionalTrait.php | 16 ++++----- system/View/Cell.php | 3 +- system/View/Filters.php | 4 +-- system/View/Parser.php | 7 ++-- system/View/RendererInterface.php | 14 ++++---- system/View/Table.php | 3 +- system/View/View.php | 12 +++---- .../booleanAnd.rightNotBoolean.neon | 8 +++++ utils/phpstan-baseline/if.condNotBoolean.neon | 8 +++++ utils/phpstan-baseline/loader.neon | 2 ++ .../missingType.iterableValue.neon | 12 +------ 44 files changed, 155 insertions(+), 217 deletions(-) create mode 100644 utils/phpstan-baseline/booleanAnd.rightNotBoolean.neon create mode 100644 utils/phpstan-baseline/if.condNotBoolean.neon diff --git a/app/Config/Cookie.php b/app/Config/Cookie.php index 84ccc0e99d80..3bad184797ce 100644 --- a/app/Config/Cookie.php +++ b/app/Config/Cookie.php @@ -85,7 +85,7 @@ class Cookie extends BaseConfig * (empty string) means default SameSite attribute set by browsers (`Lax`) * will be set on cookies. If set to `None`, `$secure` must also be set. * - * @phpstan-var 'None'|'Lax'|'Strict'|'' + * @var ''|'Lax'|'None'|'Strict' */ public string $samesite = 'Lax'; diff --git a/psalm_autoload.php b/psalm_autoload.php index 5585212190dd..d8d92a892ce8 100644 --- a/psalm_autoload.php +++ b/psalm_autoload.php @@ -24,8 +24,13 @@ } $dirs = [ - 'tests/_support/Controllers', 'tests/_support/_controller', + 'tests/_support/Controllers', + 'tests/_support/Entity', + 'tests/_support/Entity/Cast', + 'tests/_support/Models', + 'tests/_support/Validation', + 'tests/_support/View', 'tests/system/Config/fixtures', ]; diff --git a/system/API/ResponseTrait.php b/system/API/ResponseTrait.php index 5f2e068b3918..a2e93bfd309f 100644 --- a/system/API/ResponseTrait.php +++ b/system/API/ResponseTrait.php @@ -69,8 +69,7 @@ trait ResponseTrait * Either 'json' or 'xml'. If null is set, it will be determined through * content negotiation. * - * @var string|null - * @phpstan-var 'html'|'json'|'xml'|null + * @var 'html'|'json'|'xml'|null */ protected $format = 'json'; @@ -348,8 +347,7 @@ protected function format($data = null) /** * Sets the format the response should be in. * - * @param string|null $format Response format - * @phpstan-param 'json'|'xml' $format + * @param 'json'|'xml' $format Response format * * @return $this */ diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index 2e628f1590d6..86d922c33302 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -219,8 +219,7 @@ public function addNamespace($namespace, ?string $path = null) * * If a prefix param is set, returns only paths to the given prefix. * - * @return array>|list - * @phpstan-return ($prefix is null ? array> : list) + * @return ($prefix is null ? array> : list) */ public function getNamespace(?string $prefix = null) { diff --git a/system/BaseModel.php b/system/BaseModel.php index cf8df09998f3..a5762a23be7d 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -793,8 +793,7 @@ public function getInsertID() * @phpstan-param row_array|object|null $row * @param bool $returnID Whether insert ID should be returned or not. * - * @return bool|int|string insert ID or true on success. false on failure. - * @phpstan-return ($returnID is true ? int|string|false : bool) + * @return ($returnID is true ? false|int|string : bool) * * @throws ReflectionException */ diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 2c89a5e2e4f1..560925d5f623 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -141,7 +141,7 @@ class CodeIgniter * web: Invoked by HTTP request * php-cli: Invoked by CLI via `php public/index.php` * - * @phpstan-var 'php-cli'|'web' + * @var 'php-cli'|'web'|null */ protected ?string $context = null; @@ -1128,7 +1128,7 @@ protected function callExit($code) /** * Sets the app context. * - * @phpstan-param 'php-cli'|'web' $context + * @param 'php-cli'|'web' $context * * @return $this */ diff --git a/system/Commands/Translation/LocalizationFinder.php b/system/Commands/Translation/LocalizationFinder.php index 3307dab8a4e4..e6557b74d1c4 100644 --- a/system/Commands/Translation/LocalizationFinder.php +++ b/system/Commands/Translation/LocalizationFinder.php @@ -361,8 +361,7 @@ private function isSubDirectory(string $directory, string $rootDirectory): bool /** * @param list $files * - * @return array - * @phpstan-return array{'foundLanguageKeys': array>, 'badLanguageKeys': array>, 'countFiles': int} + * @return array{'foundLanguageKeys': array>, 'badLanguageKeys': array>, 'countFiles': int} */ private function findLanguageKeysInFiles(array $files): array { diff --git a/system/Common.php b/system/Common.php index 910ec9b1cc28..3f69838e0b80 100644 --- a/system/Common.php +++ b/system/Common.php @@ -69,8 +69,7 @@ function app_timezone(): string * cache()->save('foo', 'bar'); * $foo = cache('bar'); * - * @return array|bool|CacheInterface|float|int|object|string|null - * @phpstan-return ($key is null ? CacheInterface : array|bool|float|int|object|string|null) + * @return ($key is null ? CacheInterface : mixed) */ function cache(?string $key = null) { @@ -201,8 +200,7 @@ function command(string $command) * * @param class-string|string $name * - * @return ConfigTemplate|null - * @phpstan-return ($name is class-string ? ConfigTemplate : object|null) + * @return ($name is class-string ? ConfigTemplate : object|null) */ function config(string $name, bool $getShared = true) { @@ -404,11 +402,11 @@ function env(string $key, $default = null) * If $data is an array, then it loops over it, escaping each * 'value' of the key/value pairs. * - * @param array|string $data - * @phpstan-param 'html'|'js'|'css'|'url'|'attr'|'raw' $context - * @param string|null $encoding Current encoding for escaping. - * If not UTF-8, we convert strings from this encoding - * pre-escaping and back to this encoding post-escaping. + * @param array|string $data + * @param 'attr'|'css'|'html'|'js'|'raw'|'url' $context + * @param string|null $encoding Current encoding for escaping. + * If not UTF-8, we convert strings from this encoding + * pre-escaping and back to this encoding post-escaping. * * @return array|string * @@ -796,8 +794,7 @@ function log_message(string $level, string $message, array $context = []): void * * @param class-string|string $name * - * @return ModelTemplate|null - * @phpstan-return ($name is class-string ? ModelTemplate : object|null) + * @return ($name is class-string ? ModelTemplate : object|null) */ function model(string $name, bool $getShared = true, ?ConnectionInterface &$conn = null) { @@ -810,9 +807,8 @@ function model(string $name, bool $getShared = true, ?ConnectionInterface &$conn * Provides access to "old input" that was set in the session * during a redirect()->withInput(). * - * @param string|null $default - * @param false|string $escape - * @phpstan-param false|'attr'|'css'|'html'|'js'|'raw'|'url' $escape + * @param string|null $default + * @param 'attr'|'css'|'html'|'js'|'raw'|'url'|false $escape * * @return array|string|null */ @@ -965,8 +961,7 @@ function route_to(string $method, ...$params) * session()->set('foo', 'bar'); * $foo = session('bar'); * - * @return array|bool|float|int|object|Session|string|null - * @phpstan-return ($val is null ? Session : array|bool|float|int|object|string|null) + * @return ($val is null ? Session : mixed) */ function session(?string $val = null) { @@ -1123,8 +1118,7 @@ function stringify_attributes($attributes, bool $js = false): string * @param non-empty-string|null $name * @param (callable(): mixed)|null $callable * - * @return mixed|Timer - * @phpstan-return ($name is null ? Timer : ($callable is (callable(): mixed) ? mixed : Timer)) + * @return ($name is null ? Timer : ($callable is (callable(): mixed) ? mixed : Timer)) */ function timer(?string $name = null, ?callable $callable = null) { diff --git a/system/DataCaster/DataCaster.php b/system/DataCaster/DataCaster.php index ae3f40f4dc25..81ebe233c125 100644 --- a/system/DataCaster/DataCaster.php +++ b/system/DataCaster/DataCaster.php @@ -113,10 +113,9 @@ public function setTypes(array $types): static * Add ? at the beginning of the type (i.e. ?string) to get `null` * instead of casting $value when $value is null. * - * @param mixed $value The value to convert - * @param string $field The field name - * @param string $method Allowed to "get" and "set" - * @phpstan-param 'get'|'set' $method + * @param mixed $value The value to convert + * @param string $field The field name + * @param 'get'|'set' $method Allowed to "get" and "set" */ public function castAs(mixed $value, string $field, string $method = 'get'): mixed { diff --git a/system/DataConverter/DataConverter.php b/system/DataConverter/DataConverter.php index 43b42a08bbdf..fa8353e83abd 100644 --- a/system/DataConverter/DataConverter.php +++ b/system/DataConverter/DataConverter.php @@ -20,9 +20,9 @@ /** * PHP data <==> DataSource data converter * - * @see \CodeIgniter\DataConverter\DataConverterTest - * * @template TEntity of object + * + * @see \CodeIgniter\DataConverter\DataConverterTest */ final class DataConverter { @@ -52,14 +52,14 @@ public function __construct( * Static reconstruct method name or closure to reconstruct an object. * Used by reconstruct(). * - * @phpstan-var (Closure(array): TEntity)|string|null + * @var (Closure(array): TEntity)|string|null */ private readonly Closure|string|null $reconstructor = 'reconstruct', /** * Extract method name or closure to extract data from an object. * Used by extract(). * - * @phpstan-var (Closure(TEntity, bool, bool): array)|string|null + * @var (Closure(TEntity, bool, bool): array)|string|null */ private readonly Closure|string|null $extractor = null, ) { @@ -105,11 +105,10 @@ public function toDataSource(array $phpData): array /** * Takes database data array and creates a specified type object. * - * @param class-string $classname - * @phpstan-param class-string $classname - * @param array $row Raw data from database + * @param class-string $classname + * @param array $row Raw data from database * - * @phpstan-return TEntity + * @return TEntity * * @internal */ diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index b274b02aa5a8..1ae432dcb79c 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -206,16 +206,14 @@ abstract class BaseConnection implements ConnectionInterface /** * Connection ID * - * @var false|object|resource - * @phpstan-var false|TConnection + * @var false|TConnection */ public $connID = false; /** * Result ID * - * @var false|object|resource - * @phpstan-var false|TResult + * @var false|TResult */ public $resultID = false; @@ -498,8 +496,7 @@ abstract protected function _close(); /** * Create a persistent database connection. * - * @return false|object|resource - * @phpstan-return false|TConnection + * @return false|TConnection */ public function persistentConnect() { @@ -512,8 +509,7 @@ public function persistentConnect() * get that connection. If you pass either alias in and only a single * connection is present, it must return the sole connection. * - * @return false|object|resource - * @phpstan-return TConnection + * @return false|TConnection */ public function getConnection(?string $alias = null) { @@ -592,8 +588,7 @@ public function addTableAlias(string $alias) /** * Executes the query against the database. * - * @return false|object|resource - * @phpstan-return false|TResult + * @return false|TResult */ abstract protected function execute(string $sql); @@ -607,8 +602,7 @@ abstract protected function execute(string $sql); * * @param array|string|null $binds * - * @return BaseResult|bool|Query BaseResult when “read” type query, bool when “write” type query, Query when prepared query - * @phpstan-return BaseResult|bool|Query + * @return BaseResult|bool|Query * * @todo BC set $queryClass default as null in 4.1 */ @@ -726,8 +720,7 @@ public function query(string $sql, $binds = null, bool $setEscapeFlags = true, s * is performed, nor are transactions handled. Simply takes a raw * query string and returns the database-specific result id. * - * @return false|object|resource - * @phpstan-return false|TResult + * @return false|TResult */ public function simpleQuery(string $sql) { @@ -1065,8 +1058,7 @@ public function getConnectDuration(int $decimals = 6): string * @param bool $protectIdentifiers Protect table or column names? * @param bool $fieldExists Supplied $item contains a column name? * - * @return array|string - * @phpstan-return ($item is array ? array : string) + * @return ($item is array ? array : string) */ public function protectIdentifiers($item, bool $prefixSingle = false, ?bool $protectIdentifiers = null, bool $fieldExists = true) { @@ -1270,8 +1262,7 @@ private function escapeTableName(TableName $tableName): string * * @param array|string $item * - * @return array|string - * @phpstan-return ($item is array ? array : string) + * @return ($item is array ? array : string) */ public function escapeIdentifiers($item) { @@ -1355,8 +1346,7 @@ abstract public function affectedRows(): int; * * @param array|bool|float|int|object|string|null $str * - * @return array|float|int|string - * @phpstan-return ($str is array ? array : float|int|string) + * @return ($str is array ? array : float|int|string) */ public function escape($str) { @@ -1787,8 +1777,7 @@ public function isWriteType($sql): bool * * Must return an array with keys 'code' and 'message': * - * @return array - * @phpstan-return array{code: int|string|null, message: string|null} + * @return array{code: int|string|null, message: string|null} */ abstract public function error(): array; diff --git a/system/Database/BasePreparedQuery.php b/system/Database/BasePreparedQuery.php index e9017bcf5ff9..3632ccb18c0c 100644 --- a/system/Database/BasePreparedQuery.php +++ b/system/Database/BasePreparedQuery.php @@ -31,8 +31,7 @@ abstract class BasePreparedQuery implements PreparedQueryInterface /** * The prepared statement itself. * - * @var object|resource|null - * @phpstan-var TStatement|null + * @var TStatement|null */ protected $statement; @@ -61,8 +60,7 @@ abstract class BasePreparedQuery implements PreparedQueryInterface /** * A reference to the db connection to use. * - * @var BaseConnection - * @phpstan-var BaseConnection + * @var BaseConnection */ protected $db; @@ -112,8 +110,7 @@ abstract public function _prepare(string $sql, array $options = []); * Takes a new set of data and runs it against the currently * prepared query. Upon success, will return a Results object. * - * @return bool|ResultInterface - * @phpstan-return bool|ResultInterface + * @return bool|ResultInterface * * @throws DatabaseException */ diff --git a/system/Database/BaseResult.php b/system/Database/BaseResult.php index efbc16722148..fc2393edffee 100644 --- a/system/Database/BaseResult.php +++ b/system/Database/BaseResult.php @@ -27,16 +27,14 @@ abstract class BaseResult implements ResultInterface /** * Connection ID * - * @var object|resource - * @phpstan-var TConnection + * @var TConnection */ public $connID; /** * Result ID * - * @var false|object|resource - * @phpstan-var false|TResult + * @var false|TResult */ public $resultID; @@ -85,10 +83,8 @@ abstract class BaseResult implements ResultInterface /** * Constructor * - * @param object|resource $connID - * @param object|resource $resultID - * @phpstan-param TConnection $connID - * @phpstan-param TResult $resultID + * @param TConnection $connID + * @param TResult $resultID */ public function __construct(&$connID, &$resultID) { @@ -119,7 +115,7 @@ public function getResult(string $type = 'object'): array /** * Returns the results as an array of custom objects. * - * @phpstan-param class-string $className + * @param class-string $className * * @return array */ @@ -211,8 +207,7 @@ public function getResultArray(): array * * If no results, an empty array is returned. * - * @return array - * @phpstan-return list + * @return list */ public function getResultObject(): array { @@ -258,12 +253,10 @@ public function getResultObject(): array * * @template T of object * - * @param int|string $n The index of the results to return, or column name. - * @param string $type The type of result object. 'array', 'object' or class name. - * @phpstan-param class-string|'array'|'object' $type + * @param int|string $n The index of the results to return, or column name. + * @param 'array'|'object'|class-string $type The type of result object. 'array', 'object' or class name. * - * @return array|float|int|object|stdClass|string|null - * @phpstan-return ($n is string ? float|int|string|null : ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null))) + * @return ($n is string ? float|int|string|null : ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null))) */ public function getRow($n = 0, string $type = 'object') { @@ -300,11 +293,10 @@ public function getRow($n = 0, string $type = 'object') * * @template T of object * - * @param int $n The index of the results to return. - * @phpstan-param class-string $className + * @param int $n The index of the results to return. + * @param class-string $className * - * @return object|null - * @phpstan-return T|null + * @return T|null */ public function getCustomRowObject(int $n, string $className) { diff --git a/system/Database/ConnectionInterface.php b/system/Database/ConnectionInterface.php index 2f1c932c461d..8e4834486261 100644 --- a/system/Database/ConnectionInterface.php +++ b/system/Database/ConnectionInterface.php @@ -32,16 +32,14 @@ public function initialize(); /** * Connect to the database. * - * @return false|object|resource - * @phpstan-return false|TConnection + * @return false|TConnection */ public function connect(bool $persistent = false); /** * Create a persistent database connection. * - * @return false|object|resource - * @phpstan-return false|TConnection + * @return false|TConnection */ public function persistentConnect(); @@ -59,8 +57,7 @@ public function reconnect(); * get that connection. If you pass either alias in and only a single * connection is present, it must return the sole connection. * - * @return false|object|resource - * @phpstan-return false|TConnection + * @return false|TConnection */ public function getConnection(?string $alias = null); @@ -105,8 +102,7 @@ public function getVersion(): string; * * @param array|string|null $binds * - * @return BaseResult|bool|Query - * @phpstan-return BaseResult|bool|Query + * @return BaseResult|bool|Query */ public function query(string $sql, $binds = null); @@ -115,8 +111,7 @@ public function query(string $sql, $binds = null); * is performed, nor are transactions handled. Simply takes a raw * query string and returns the database-specific result id. * - * @return false|object|resource - * @phpstan-return false|TResult + * @return false|TResult */ public function simpleQuery(string $sql); @@ -144,8 +139,7 @@ public function getLastQuery(); * * @param array|bool|float|int|object|string|null $str * - * @return array|float|int|string - * @phpstan-return ($str is array ? array : float|int|string) + * @return ($str is array ? array : float|int|string) */ public function escape($str); diff --git a/system/Database/Forge.php b/system/Database/Forge.php index 8405d9f7143f..f6eb617fd389 100644 --- a/system/Database/Forge.php +++ b/system/Database/Forge.php @@ -846,8 +846,7 @@ public function modifyColumn(string $table, $fields): bool * @param array|string $processedFields Processed column definitions * or column names to DROP * - * @return false|list|string|null SQL string - * @phpstan-return ($alterType is 'DROP' ? string : list|false|null) + * @return ($alterType is 'DROP' ? string : false|list|null) */ protected function _alterTable(string $alterType, string $table, $processedFields) { diff --git a/system/Database/MySQLi/Forge.php b/system/Database/MySQLi/Forge.php index fe207f5267a5..9482f7e2329a 100644 --- a/system/Database/MySQLi/Forge.php +++ b/system/Database/MySQLi/Forge.php @@ -135,8 +135,7 @@ protected function _createTableAttributes(array $attributes): string * @param array|string $processedFields Processed column definitions * or column names to DROP * - * @return list|string SQL string - * @phpstan-return ($alterType is 'DROP' ? string : list) + * @return ($alterType is 'DROP' ? string : list) */ protected function _alterTable(string $alterType, string $table, $processedFields) { diff --git a/system/Database/OCI8/Forge.php b/system/Database/OCI8/Forge.php index f761045d9784..8f71169115ce 100644 --- a/system/Database/OCI8/Forge.php +++ b/system/Database/OCI8/Forge.php @@ -100,8 +100,7 @@ class Forge extends BaseForge * @param array|string $processedFields Processed column definitions * or column names to DROP * - * @return list|string SQL string - * @phpstan-return ($alterType is 'DROP' ? string : list) + * @return ($alterType is 'DROP' ? string : list) */ protected function _alterTable(string $alterType, string $table, $processedFields) { diff --git a/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php index 584c452b233d..9b480975de2e 100644 --- a/system/Database/Postgre/Connection.php +++ b/system/Database/Postgre/Connection.php @@ -242,8 +242,7 @@ public function affectedRows(): int * * @param array|bool|float|int|object|string|null $str * - * @return array|float|int|string - * @phpstan-return ($str is array ? array : float|int|string) + * @return ($str is array ? array : float|int|string) */ public function escape($str) { diff --git a/system/Database/Postgre/Forge.php b/system/Database/Postgre/Forge.php index b0a38c90ed23..8b18d7d48ca7 100644 --- a/system/Database/Postgre/Forge.php +++ b/system/Database/Postgre/Forge.php @@ -86,8 +86,7 @@ protected function _createTableAttributes(array $attributes): string * @param array|string $processedFields Processed column definitions * or column names to DROP * - * @return false|list|string SQL string or false - * @phpstan-return ($alterType is 'DROP' ? string : list|false) + * @return ($alterType is 'DROP' ? string : false|list) */ protected function _alterTable(string $alterType, string $table, $processedFields) { diff --git a/system/Database/PreparedQueryInterface.php b/system/Database/PreparedQueryInterface.php index 6ac69904e7f9..c49c2e7bb551 100644 --- a/system/Database/PreparedQueryInterface.php +++ b/system/Database/PreparedQueryInterface.php @@ -26,8 +26,7 @@ interface PreparedQueryInterface * Takes a new set of data and runs it against the currently * prepared query. Upon success, will return a Results object. * - * @return bool|ResultInterface - * @phpstan-return bool|ResultInterface + * @return bool|ResultInterface */ public function execute(...$data); diff --git a/system/Database/ResultInterface.php b/system/Database/ResultInterface.php index e6383cab6585..89febfef835c 100644 --- a/system/Database/ResultInterface.php +++ b/system/Database/ResultInterface.php @@ -61,12 +61,10 @@ public function getResultObject(): array; * * @template T of object * - * @param int|string $n The index of the results to return, or column name. - * @param string $type The type of result object. 'array', 'object' or class name. - * @phpstan-param class-string|'array'|'object' $type + * @param int|string $n The index of the results to return, or column name. + * @param 'array'|'object'|class-string $type The type of result object. 'array', 'object' or class name. * - * @return array|float|int|object|stdClass|string|null - * @phpstan-return ($n is string ? float|int|string|null : ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null))) + * @return ($n is string ? float|int|string|null : ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null))) */ public function getRow($n = 0, string $type = 'object'); @@ -77,11 +75,10 @@ public function getRow($n = 0, string $type = 'object'); * * @template T of object * - * @param int $n The index of the results to return. - * @phpstan-param class-string $className + * @param int $n The index of the results to return. + * @param class-string $className * - * @return object|null - * @phpstan-return T|null + * @return T|null */ public function getCustomRowObject(int $n, string $className); diff --git a/system/Database/SQLSRV/Forge.php b/system/Database/SQLSRV/Forge.php index 7b1da6e85ba7..f35d8e7bbca2 100644 --- a/system/Database/SQLSRV/Forge.php +++ b/system/Database/SQLSRV/Forge.php @@ -180,8 +180,7 @@ protected function _createTableAttributes(array $attributes): string * @param array|string $processedFields Processed column definitions * or column names to DROP * - * @return false|list|string SQL string or false - * @phpstan-return ($alterType is 'DROP' ? string : list|false) + * @return ($alterType is 'DROP' ? string : false|list) */ protected function _alterTable(string $alterType, string $table, $processedFields) { diff --git a/system/Database/SQLite3/Forge.php b/system/Database/SQLite3/Forge.php index 31c2a7ed58a6..2bb9386f6c9e 100644 --- a/system/Database/SQLite3/Forge.php +++ b/system/Database/SQLite3/Forge.php @@ -139,9 +139,7 @@ public function dropColumn(string $table, $columnNames): bool * @param array|string $processedFields Processed column definitions * or column names to DROP * - * @return array|string|null - * @return list|string|null SQL string or null - * @phpstan-return ($alterType is 'DROP' ? string : list|null) + * @return ($alterType is 'DROP' ? string : list|null) */ protected function _alterTable(string $alterType, string $table, $processedFields) { diff --git a/system/Database/SQLite3/Table.php b/system/Database/SQLite3/Table.php index 674d26b7d6f2..ea84e0011e3f 100644 --- a/system/Database/SQLite3/Table.php +++ b/system/Database/SQLite3/Table.php @@ -376,8 +376,7 @@ protected function copyData() * * @param array|bool $fields * - * @return mixed - * @phpstan-return ($fields is array ? array : mixed) + * @return ($fields is array ? array : mixed) */ protected function formatFields($fields) { diff --git a/system/Files/File.php b/system/Files/File.php index 23eb324d2684..be7f84f8a20b 100644 --- a/system/Files/File.php +++ b/system/Files/File.php @@ -73,7 +73,7 @@ public function getSize() /** * Retrieve the file size by unit, calculated in IEC standards with 1024 as base value. * - * @phpstan-param positive-int $precision + * @param positive-int $precision */ public function getSizeByBinaryUnit(FileSizeUnit $unit = FileSizeUnit::B, int $precision = 3): int|string { @@ -83,7 +83,7 @@ public function getSizeByBinaryUnit(FileSizeUnit $unit = FileSizeUnit::B, int $p /** * Retrieve the file size by unit, calculated in metric standards with 1000 as base value. * - * @phpstan-param positive-int $precision + * @param positive-int $precision */ public function getSizeByMetricUnit(FileSizeUnit $unit = FileSizeUnit::B, int $precision = 3): int|string { diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 412a9f457d53..9a253a42b359 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -206,8 +206,8 @@ public function setResponse(ResponseInterface $response) * Runs through all the filters (except "Required Filters") for the specified * URI and position. * - * @param string $uri URI path relative to baseURL - * @phpstan-param 'before'|'after' $position + * @param string $uri URI path relative to baseURL + * @param 'after'|'before' $position * * @return RequestInterface|ResponseInterface|string|null * @@ -310,7 +310,7 @@ private function createFilter(string $className): FilterInterface /** * Returns the "Required Filters" class list. * - * @phpstan-param 'before'|'after' $position + * @param 'after'|'before' $position * * @return list}> [[classname, arguments], ...] */ @@ -340,7 +340,7 @@ public function getRequiredClasses(string $position): array /** * Runs "Required Filters" for the specified position. * - * @phpstan-param 'before'|'after' $position + * @param 'after'|'before' $position * * @return RequestInterface|ResponseInterface|string|null * @@ -367,7 +367,7 @@ public function runRequired(string $position = 'before') /** * Returns "Required Filters" for the specified position. * - * @phpstan-param 'before'|'after' $position + * @param 'after'|'before' $position * * @internal */ @@ -544,7 +544,7 @@ public function getFiltersClass(): array * MUST be called prior to initialize(); * Intended for use within routes files. * - * @phpstan-param 'before'|'after' $position + * @param 'after'|'before' $position * * @return $this */ @@ -574,9 +574,9 @@ public function addFilter(string $class, ?string $alias = null, string $position * after the filter name, followed by a comma-separated list of arguments that * are passed to the filter when executed. * - * @param string $filter filter_name or filter_name:arguments like 'role:admin,manager' - * or filter classname. - * @phpstan-param 'before'|'after' $position + * @param string $filter filter_name or filter_name:arguments like 'role:admin,manager' + * or filter classname. + * @param 'after'|'before' $position */ private function enableFilter(string $filter, string $position = 'before'): void { @@ -824,7 +824,7 @@ protected function processFilters(?string $uri = null) /** * Maps filter aliases to the equivalent filter classes * - * @phpstan-param 'before'|'after' $position + * @param 'after'|'before' $position * * @return void * diff --git a/system/HTTP/CLIRequest.php b/system/HTTP/CLIRequest.php index 6ef97170914e..bde4e1b8e3f8 100644 --- a/system/HTTP/CLIRequest.php +++ b/system/HTTP/CLIRequest.php @@ -314,9 +314,6 @@ public function getLocale(): string /** * Checks this request type. - * - * @param string $type HTTP verb or 'json' or 'ajax' - * @phpstan-param string|'get'|'post'|'put'|'delete'|'head'|'patch'|'options'|'json'|'ajax' $type */ public function is(string $type): bool { diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index ba6f61d7349f..72b1ce0ca5df 100644 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -348,9 +348,6 @@ public function negotiate(string $type, array $supported, bool $strictMatch = fa /** * Checks this request type. - * - * @param string $type HTTP verb or 'json' or 'ajax' - * @phpstan-param string|'get'|'post'|'put'|'delete'|'head'|'patch'|'options'|'json'|'ajax' $type */ public function is(string $type): bool { diff --git a/system/HTTP/RequestTrait.php b/system/HTTP/RequestTrait.php index 39f4269ef1df..3c3da161a3be 100644 --- a/system/HTTP/RequestTrait.php +++ b/system/HTTP/RequestTrait.php @@ -224,9 +224,8 @@ public function getEnv($index = null, $filter = null, $flags = null) /** * Allows manually setting the value of PHP global, like $_GET, $_POST, etc. * - * @param string $name Supergrlobal name (lowercase) - * @phpstan-param 'get'|'post'|'request'|'cookie'|'server' $name - * @param mixed $value + * @param 'cookie'|'get'|'post'|'request'|'server' $name Superglobal name (lowercase) + * @param mixed $value * * @return $this */ @@ -247,11 +246,10 @@ public function setGlobal(string $name, $value) * * http://php.net/manual/en/filter.filters.sanitize.php * - * @param string $name Supergrlobal name (lowercase) - * @phpstan-param 'get'|'post'|'request'|'cookie'|'server' $name - * @param array|int|string|null $index - * @param int|null $filter Filter constant - * @param array|int|null $flags Options + * @param 'cookie'|'get'|'post'|'request'|'server' $name Superglobal name (lowercase) + * @param array|int|string|null $index + * @param int|null $filter Filter constant + * @param array|int|null $flags Options * * @return array|bool|float|int|object|string|null */ @@ -341,8 +339,7 @@ public function fetchGlobal(string $name, $index = null, ?int $filter = null, $f * Saves a copy of the current state of one of several PHP globals, * so we can retrieve them later. * - * @param string $name Superglobal name (lowercase) - * @phpstan-param 'get'|'post'|'request'|'cookie'|'server' $name + * @param 'cookie'|'get'|'post'|'request'|'server' $name Superglobal name (lowercase) * * @return void */ diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index 6e5206a0d493..d6653b10f537 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -85,11 +85,10 @@ class SiteURI extends URI private string $routePath; /** - * @param string $relativePath URI path relative to baseURL. May include - * queries or fragments. - * @param string|null $host Optional current hostname. - * @param string|null $scheme Optional scheme. 'http' or 'https'. - * @phpstan-param 'http'|'https'|null $scheme + * @param string $relativePath URI path relative to baseURL. May include + * queries or fragments. + * @param string|null $host Optional current hostname. + * @param 'http'|'https'|null $scheme Optional scheme. 'http' or 'https'. */ public function __construct( App $configApp, diff --git a/system/Model.php b/system/Model.php index 473584cb8163..802dd84dcdca 100644 --- a/system/Model.php +++ b/system/Model.php @@ -778,8 +778,7 @@ protected function shouldUpdate($row): bool * @phpstan-param row_array|object|null $row * @param bool $returnID Whether insert ID should be returned or not. * - * @return bool|int|string - * @phpstan-return ($returnID is true ? int|string|false : bool) + * @return ($returnID is true ? false|int|string : bool) * * @throws ReflectionException */ diff --git a/system/RESTful/ResourceController.php b/system/RESTful/ResourceController.php index 072b52ee8260..ed72df212b07 100644 --- a/system/RESTful/ResourceController.php +++ b/system/RESTful/ResourceController.php @@ -106,8 +106,7 @@ public function delete($id = null) /** * Set/change the expected response representation for returned objects * - * @param string $format Response format - * @phpstan-param 'json'|'xml' $format + * @param 'json'|'xml' $format Response format * * @return void */ diff --git a/system/Test/Mock/MockInputOutput.php b/system/Test/Mock/MockInputOutput.php index 7f4370a2d22f..0333bf27ccdf 100644 --- a/system/Test/Mock/MockInputOutput.php +++ b/system/Test/Mock/MockInputOutput.php @@ -31,16 +31,14 @@ final class MockInputOutput extends InputOutput /** * Output lines. * - * @var array - * @phpstan-var list + * @var list */ private array $outputs = []; /** * Sets user inputs. * - * @param array $inputs - * @phpstan-param list $inputs + * @param list $inputs */ public function setInputs(array $inputs): void { diff --git a/system/Traits/ConditionalTrait.php b/system/Traits/ConditionalTrait.php index a778e9375a36..027890309ba9 100644 --- a/system/Traits/ConditionalTrait.php +++ b/system/Traits/ConditionalTrait.php @@ -20,10 +20,10 @@ trait ConditionalTrait * * @template TWhen of mixed * - * @phpstan-param TWhen $condition - * @phpstan-param callable(self, TWhen): mixed $callback - * @phpstan-param (callable(self): mixed)|null $defaultCallback - * @param array|bool|float|int|object|resource|string|null $condition + * @param TWhen $condition + * @param callable(self, TWhen): mixed $callback + * @param (callable(self): mixed)|null $defaultCallback + * @param mixed $condition * * @return $this */ @@ -43,10 +43,10 @@ public function when($condition, callable $callback, ?callable $defaultCallback * * @template TWhenNot of mixed * - * @phpstan-param TWhenNot $condition - * @phpstan-param callable(self, TWhenNot): mixed $callback - * @phpstan-param (callable(self): mixed)|null $defaultCallback - * @param array|bool|float|int|object|resource|string|null $condition + * @param TWhenNot $condition + * @param callable(self, TWhenNot): mixed $callback + * @param (callable(self): mixed)|null $defaultCallback + * @param mixed $condition * * @return $this */ diff --git a/system/View/Cell.php b/system/View/Cell.php index f2801e4194ec..79e573f5103e 100644 --- a/system/View/Cell.php +++ b/system/View/Cell.php @@ -116,8 +116,7 @@ public function render(string $library, $params = null, int $ttl = 0, ?string $c * If a string, it should be in the format "key1=value key2=value". * It will be split and returned as an array. * - * @param array|string|null $params - * @phpstan-param array|string|float|null $params + * @param array|float|string|null $params * * @return array */ diff --git a/system/View/Filters.php b/system/View/Filters.php index 0be48b6de1b4..c74e385cdb96 100644 --- a/system/View/Filters.php +++ b/system/View/Filters.php @@ -77,8 +77,8 @@ public static function default($value, string $default): string /** * Escapes the given value with our `esc()` helper function. * - * @param string $value - * @phpstan-param 'html'|'js'|'css'|'url'|'attr'|'raw' $context + * @param string $value + * @param 'attr'|'css'|'html'|'js'|'raw'|'url' $context */ public static function esc($value, string $context = 'html'): string { diff --git a/system/View/Parser.php b/system/View/Parser.php index a2393bb4b7e5..e2a093904cc7 100644 --- a/system/View/Parser.php +++ b/system/View/Parser.php @@ -198,10 +198,9 @@ public function renderString(string $template, ?array $options = null, ?bool $sa * so that the variable is correctly handled within the * parsing itself, and contexts (including raw) are respected. * - * @param array $data - * @param non-empty-string|null $context The context to escape it for. - * If 'raw', no escaping will happen. - * @phpstan-param null|'html'|'js'|'css'|'url'|'attr'|'raw' $context + * @param array $data + * @param 'attr'|'css'|'html'|'js'|'raw'|'url'|null $context The context to escape it for. + * If 'raw', no escaping will happen. */ public function setData(array $data = [], ?string $context = null): RendererInterface { diff --git a/system/View/RendererInterface.php b/system/View/RendererInterface.php index 5fdf8794f12d..d42dcee88442 100644 --- a/system/View/RendererInterface.php +++ b/system/View/RendererInterface.php @@ -46,10 +46,9 @@ public function renderString(string $view, ?array $options = null, bool $saveDat /** * Sets several pieces of view data at once. * - * @param array $data - * @param non-empty-string|null $context The context to escape it for. - * If 'raw', no escaping will happen. - * @phpstan-param null|'html'|'js'|'css'|'url'|'attr'|'raw' $context + * @param array $data + * @param 'attr'|'css'|'html'|'js'|'raw'|'url'|null $context The context to escape it for. + * If 'raw', no escaping will happen. * * @return RendererInterface */ @@ -58,10 +57,9 @@ public function setData(array $data = [], ?string $context = null); /** * Sets a single piece of view data. * - * @param mixed $value - * @param non-empty-string|null $context The context to escape it for. - * If 'raw', no escaping will happen. - * @phpstan-param null|'html'|'js'|'css'|'url'|'attr'|'raw' $context + * @param mixed $value + * @param 'attr'|'css'|'html'|'js'|'raw'|'url'|null $context The context to escape it for. + * If 'raw', no escaping will happen. * * @return RendererInterface */ diff --git a/system/View/Table.php b/system/View/Table.php index a063e71c5a85..df4b870b8ba9 100644 --- a/system/View/Table.php +++ b/system/View/Table.php @@ -108,8 +108,7 @@ public function __construct($config = []) /** * Set the template * - * @param array $template - * @phpstan-param array|string $template + * @param array|string $template * * @return bool */ diff --git a/system/View/View.php b/system/View/View.php index 6a282096ccb4..0f10b55ef5ab 100644 --- a/system/View/View.php +++ b/system/View/View.php @@ -335,9 +335,8 @@ public function excerpt(string $string, int $length = 20): string /** * Sets several pieces of view data at once. * - * @param non-empty-string|null $context The context to escape it for. - * If 'raw', no escaping will happen. - * @phpstan-param null|'html'|'js'|'css'|'url'|'attr'|'raw' $context + * @param 'attr'|'css'|'html'|'js'|'raw'|'url'|null $context The context to escape it for. + * If 'raw', no escaping will happen. */ public function setData(array $data = [], ?string $context = null): RendererInterface { @@ -354,10 +353,9 @@ public function setData(array $data = [], ?string $context = null): RendererInte /** * Sets a single piece of view data. * - * @param mixed $value - * @param non-empty-string|null $context The context to escape it for. - * If 'raw', no escaping will happen. - * @phpstan-param null|'html'|'js'|'css'|'url'|'attr'|'raw' $context + * @param mixed $value + * @param 'attr'|'css'|'html'|'js'|'raw'|'url'|null $context The context to escape it for. + * If 'raw', no escaping will happen. */ public function setVar(string $name, $value = null, ?string $context = null): RendererInterface { diff --git a/utils/phpstan-baseline/booleanAnd.rightNotBoolean.neon b/utils/phpstan-baseline/booleanAnd.rightNotBoolean.neon new file mode 100644 index 000000000000..44f6a2a063b0 --- /dev/null +++ b/utils/phpstan-baseline/booleanAnd.rightNotBoolean.neon @@ -0,0 +1,8 @@ +# total 1 error + +parameters: + ignoreErrors: + - + message: '#^Only booleans are allowed in &&, mixed given on the right side\.$#' + count: 1 + path: ../../system/View/Parser.php diff --git a/utils/phpstan-baseline/if.condNotBoolean.neon b/utils/phpstan-baseline/if.condNotBoolean.neon new file mode 100644 index 000000000000..e11e57b224cc --- /dev/null +++ b/utils/phpstan-baseline/if.condNotBoolean.neon @@ -0,0 +1,8 @@ +# total 1 error + +parameters: + ignoreErrors: + - + message: '#^Only booleans are allowed in an if condition, mixed given\.$#' + count: 1 + path: ../../system/View/View.php diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index e54589fea853..0f432eeac1e8 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -2,6 +2,7 @@ includes: - argument.type.neon - assign.propertyType.neon + - booleanAnd.rightNotBoolean.neon - codeigniter.cacheHandlerInstance.neon - codeigniter.getReassignArray.neon - codeigniter.modelArgumentType.neon @@ -9,6 +10,7 @@ includes: - codeigniter.superglobalAccessAssign.neon - deadCode.unreachable.neon - empty.notAllowed.neon + - if.condNotBoolean.neon - method.alreadyNarrowedType.neon - method.childParameterType.neon - method.childReturnType.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 8ecc1166e8bd..e972a65176a2 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 1576 errors +# total 1574 errors parameters: ignoreErrors: @@ -532,11 +532,6 @@ parameters: count: 1 path: ../../system/Commands/Utilities/Routes/FilterFinder.php - - - message: '#^Function cache\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Common.php - - message: '#^Function class_uses_recursive\(\) return type has no value type specified in iterable type array\.$#' count: 1 @@ -587,11 +582,6 @@ parameters: count: 1 path: ../../system/Common.php - - - message: '#^Function session\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Common.php - - message: '#^Function single_service\(\) has parameter \$params with no value type specified in iterable type array\.$#' count: 1 From 3638a496fdecfcbdca6d9029569bb1a2e0046b17 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Mon, 26 May 2025 08:57:55 +0200 Subject: [PATCH 15/59] fix: use native PHP truthiness for condition evaluation in when()/whenNot() (#9576) --- system/Traits/ConditionalTrait.php | 4 +- tests/system/Database/Builder/WhenTest.php | 58 +++++++++++++++++++ user_guide_src/source/changelogs/v4.6.2.rst | 1 + .../source/database/query_builder.rst | 7 ++- 4 files changed, 65 insertions(+), 5 deletions(-) diff --git a/system/Traits/ConditionalTrait.php b/system/Traits/ConditionalTrait.php index 027890309ba9..6835de6c3a4f 100644 --- a/system/Traits/ConditionalTrait.php +++ b/system/Traits/ConditionalTrait.php @@ -29,7 +29,7 @@ trait ConditionalTrait */ public function when($condition, callable $callback, ?callable $defaultCallback = null): self { - if ($condition !== '' && $condition !== false && $condition !== null) { + if ((bool) $condition) { $callback($this, $condition); } elseif ($defaultCallback !== null) { $defaultCallback($this); @@ -52,7 +52,7 @@ public function when($condition, callable $callback, ?callable $defaultCallback */ public function whenNot($condition, callable $callback, ?callable $defaultCallback = null): self { - if ($condition === '' || $condition === null || $condition === false || $condition === '0') { + if (! (bool) $condition) { $callback($this, $condition); } elseif ($defaultCallback !== null) { $defaultCallback($this); diff --git a/tests/system/Database/Builder/WhenTest.php b/tests/system/Database/Builder/WhenTest.php index a7fede7879d3..87f9b3ff233d 100644 --- a/tests/system/Database/Builder/WhenTest.php +++ b/tests/system/Database/Builder/WhenTest.php @@ -15,7 +15,9 @@ use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockConnection; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use stdClass; /** * @internal @@ -101,6 +103,23 @@ public function testWhenPassesParemeters(): void $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); } + #[DataProvider('provideConditionValues')] + public function testWhenRunsDefaultCallbackBasedOnCondition(mixed $condition, bool $expectDefault): void + { + $builder = $this->db->table('jobs'); + + $builder = $builder->when($condition, static function ($query): void { + $query->select('id'); + }, static function ($query): void { + $query->select('name'); + }); + + $expected = $expectDefault ? 'name' : 'id'; + $expectedSQL = 'SELECT "' . $expected . '" FROM "jobs"'; + + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + public function testWhenNotFalse(): void { $builder = $this->db->table('jobs'); @@ -166,4 +185,43 @@ public function testWhenNotPassesParemeters(): void $expectedSQL = 'SELECT * FROM "jobs" WHERE "name" = \'0\''; $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); } + + #[DataProvider('provideConditionValues')] + public function testWhenNotRunsDefaultCallbackBasedOnCondition(mixed $condition, bool $expectDefault): void + { + $builder = $this->db->table('jobs'); + + $builder = $builder->whenNot($condition, static function ($query): void { + $query->select('id'); + }, static function ($query): void { + $query->select('name'); + }); + + $expected = $expectDefault ? 'id' : 'name'; + $expectedSQL = 'SELECT "' . $expected . '" FROM "jobs"'; + + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + + /** + * @return array + */ + public static function provideConditionValues(): array + { + return [ + 'false' => [false, true], // [condition, expectedDefaultCallbackRuns] + 'int 0' => [0, true], + 'float 0.0' => [0.0, true], + 'empty string' => ['', true], + 'string 0' => ['0', true], + 'empty array' => [[], true], + 'null' => [null, true], + 'true' => [true, false], + 'int 1' => [1, false], + 'float 1.1' => [1.1, false], + 'non-empty string' => ['foo', false], + 'non-empty array' => [[1], false], + 'object' => [new stdClass(), false], + ]; + } } diff --git a/user_guide_src/source/changelogs/v4.6.2.rst b/user_guide_src/source/changelogs/v4.6.2.rst index 089fb01e1514..8be89ae72b32 100644 --- a/user_guide_src/source/changelogs/v4.6.2.rst +++ b/user_guide_src/source/changelogs/v4.6.2.rst @@ -35,6 +35,7 @@ Deprecations Bugs Fixed ********** +- **Database:** Fixed a bug where ``when()`` and ``whenNot()`` in ``ConditionalTrait`` incorrectly evaluated certain falsy values (such as ``[]``, ``0``, ``0.0``, and ``'0'``) as truthy, causing callbacks to be executed unexpectedly. These methods now cast the condition to a boolean using ``(bool)`` to ensure consistent behavior with PHP's native truthiness. - **Security:** Fixed a bug where the ``sanitize_filename()`` function from the Security helper would throw an error when used in CLI requests. See the repo's diff --git a/user_guide_src/source/database/query_builder.rst b/user_guide_src/source/database/query_builder.rst index 31515768d530..e901869fb5f7 100644 --- a/user_guide_src/source/database/query_builder.rst +++ b/user_guide_src/source/database/query_builder.rst @@ -1272,9 +1272,10 @@ $builder->when() .. versionadded:: 4.3.0 This allows modifying the query based on a condition without breaking out of the -query builder chain. The first parameter is the condition, and it should evaluate -to a boolean. The second parameter is a callable that will be ran -when the condition is true. +query builder chain. The first parameter is the condition, and it is evaluated +using PHP's native boolean logic - meaning that values like ``false``, ``null``, +``0``, ``'0'``, ``0.0``, empty string ``''`` and empty array ``[]`` will be considered false. +The second parameter is a callable that will be ran when the condition is true. For example, you might only want to apply a given WHERE statement based on the value sent within an HTTP request: From 045e12b67fdfffcea7aff1f93f490b5191ea1aa7 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Mon, 26 May 2025 08:59:05 +0200 Subject: [PATCH 16/59] fix: throw exception for unsupported DB drivers in session (#9574) --- system/Config/Services.php | 5 +++++ tests/system/Config/ServicesTest.php | 21 +++++++++++++++++++++ user_guide_src/source/changelogs/v4.6.2.rst | 1 + 3 files changed, 27 insertions(+) diff --git a/system/Config/Services.php b/system/Config/Services.php index f5c5a2db4a99..c9266a892e38 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -683,6 +683,11 @@ public static function session(?SessionConfig $config = null, bool $getShared = $driverName = MySQLiHandler::class; } elseif ($driverPlatform === 'Postgre') { $driverName = PostgreHandler::class; + } else { + throw new InvalidArgumentException(sprintf( + 'Invalid session database handler "%s" provided. Only "MySQLi" and "Postgre" are supported.', + $driverPlatform, + )); } } diff --git a/tests/system/Config/ServicesTest.php b/tests/system/Config/ServicesTest.php index 0e1c80afc75f..162d95705933 100644 --- a/tests/system/Config/ServicesTest.php +++ b/tests/system/Config/ServicesTest.php @@ -36,6 +36,7 @@ use CodeIgniter\Router\RouteCollection; use CodeIgniter\Router\Router; use CodeIgniter\Security\Security; +use CodeIgniter\Session\Handlers\DatabaseHandler; use CodeIgniter\Session\Session; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockResponse; @@ -46,6 +47,7 @@ use CodeIgniter\View\Cell; use CodeIgniter\View\Parser; use Config\App; +use Config\Database as DatabaseConfig; use Config\Exceptions; use Config\Security as SecurityConfig; use Config\Session as ConfigSession; @@ -275,6 +277,25 @@ public function testNewSessionWithInvalidHandler(string $driver): void Services::session($config, false); } + #[PreserveGlobalState(false)] + #[RunInSeparateProcess] + public function testNewSessionWithInvalidDatabaseHandler(): void + { + $driver = config(DatabaseConfig::class)->tests['DBDriver']; + + if (in_array($driver, ['MySQLi', 'Postgre'], true)) { + $this->markTestSkipped('This test case does not work with MySQLi and Postgre'); + } + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage(sprintf('Invalid session database handler "%s" provided. Only "MySQLi" and "Postgre" are supported.', $driver)); + + $config = new ConfigSession(); + + $config->driver = DatabaseHandler::class; + Services::session($config, false); + } + /** * @return iterable */ diff --git a/user_guide_src/source/changelogs/v4.6.2.rst b/user_guide_src/source/changelogs/v4.6.2.rst index 8be89ae72b32..c55eda3e3787 100644 --- a/user_guide_src/source/changelogs/v4.6.2.rst +++ b/user_guide_src/source/changelogs/v4.6.2.rst @@ -37,6 +37,7 @@ Bugs Fixed - **Database:** Fixed a bug where ``when()`` and ``whenNot()`` in ``ConditionalTrait`` incorrectly evaluated certain falsy values (such as ``[]``, ``0``, ``0.0``, and ``'0'``) as truthy, causing callbacks to be executed unexpectedly. These methods now cast the condition to a boolean using ``(bool)`` to ensure consistent behavior with PHP's native truthiness. - **Security:** Fixed a bug where the ``sanitize_filename()`` function from the Security helper would throw an error when used in CLI requests. +- **Session:** Fixed a bug where using the ``DatabaseHandler`` with an unsupported database driver (such as ``SQLSRV``, ``OCI8``, or ``SQLite3``) did not throw an appropriate error. See the repo's `CHANGELOG.md `_ From edb04a742ca29fcaea61187d8db8e8bb158cb7a8 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Mon, 26 May 2025 15:11:59 +0800 Subject: [PATCH 17/59] refactor: fix `notIdentical.alwaysTrue` error (#9579) * refactor: fix `notIdentical.alwaysTrue` error * Fix psalm --- psalm_autoload.php | 71 ++++++++----------- system/BaseModel.php | 4 +- utils/phpstan-baseline/loader.neon | 3 +- .../notIdentical.alwaysTrue.neon | 8 --- 4 files changed, 33 insertions(+), 53 deletions(-) delete mode 100644 utils/phpstan-baseline/notIdentical.alwaysTrue.neon diff --git a/psalm_autoload.php b/psalm_autoload.php index d8d92a892ce8..6ebe830423b5 100644 --- a/psalm_autoload.php +++ b/psalm_autoload.php @@ -4,49 +4,40 @@ require __DIR__ . '/system/util_bootstrap.php'; -$helperDirs = [ +$directories = [ 'system/Helpers', -]; - -foreach ($helperDirs as $dir) { - $dir = __DIR__ . '/' . $dir; - if (! is_dir($dir)) { - continue; - } - - chdir($dir); - - foreach (glob('*_helper.php') as $filename) { - $filePath = realpath($dir . '/' . $filename); - - require_once $filePath; - } -} - -$dirs = [ - 'tests/_support/_controller', - 'tests/_support/Controllers', - 'tests/_support/Entity', - 'tests/_support/Entity/Cast', - 'tests/_support/Models', - 'tests/_support/Validation', - 'tests/_support/View', + 'tests/_support', 'tests/system/Config/fixtures', ]; +$excludeDirs = [ + 'tests/_support/Config', + 'tests/_support/View/Cells', + 'tests/_support/View/Views', +]; -foreach ($dirs as $dir) { - $dir = __DIR__ . '/' . $dir; - if (! is_dir($dir)) { - continue; - } - - chdir($dir); - - foreach (glob('*.php') as $filename) { - $filePath = realpath($dir . '/' . $filename); - - require_once $filePath; +foreach ($directories as $directory) { + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( + $directory, + RecursiveDirectoryIterator::UNIX_PATHS | RecursiveDirectoryIterator::CURRENT_AS_FILEINFO, + ), + RecursiveIteratorIterator::CHILD_FIRST, + ); + + /** @var SplFileInfo $file */ + foreach ($iterator as $file) { + if (! $file->isFile()) { + continue; + } + + if (in_array($file->getPath(), $excludeDirs, true)) { + continue; + } + + if ($file->getExtension() !== 'php') { + continue; + } + + require_once $file->getPathname(); } } - -chdir(__DIR__); diff --git a/system/BaseModel.php b/system/BaseModel.php index a5762a23be7d..ca0e20c32829 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -1093,9 +1093,7 @@ public function updateBatch(?array $set = null, ?string $index = null, int $batc $row = $this->doProtectFields($row); // Restore updateIndex value in case it was wiped out - if ($updateIndex !== null) { - $row[$index] = $updateIndex; - } + $row[$index] = $updateIndex; $row = $this->setUpdatedField($row, $this->setDate()); } diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 0f432eeac1e8..58f4f745ccd6 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 3271 errors +# total 3270 errors includes: - argument.type.neon - assign.propertyType.neon @@ -19,7 +19,6 @@ includes: - missingType.iterableValue.neon - missingType.parameter.neon - missingType.property.neon - - notIdentical.alwaysTrue.neon - nullCoalesce.property.neon - offsetAccess.notFound.neon - property.defaultValue.neon diff --git a/utils/phpstan-baseline/notIdentical.alwaysTrue.neon b/utils/phpstan-baseline/notIdentical.alwaysTrue.neon deleted file mode 100644 index 48feec028165..000000000000 --- a/utils/phpstan-baseline/notIdentical.alwaysTrue.neon +++ /dev/null @@ -1,8 +0,0 @@ -# total 1 error - -parameters: - ignoreErrors: - - - message: '#^Strict comparison using \!\=\= between mixed and null will always evaluate to true\.$#' - count: 1 - path: ../../system/BaseModel.php From 8c3f3e73307cd5303f4b7ae932852e84d2bceed8 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Mon, 26 May 2025 15:55:49 +0800 Subject: [PATCH 18/59] refactor: fix phpstan errors in `Events` (#9580) --- system/Events/Events.php | 34 ++++++++++--------- utils/phpstan-baseline/loader.neon | 2 +- .../missingType.callable.neon | 12 +------ .../missingType.iterableValue.neon | 17 +--------- .../ternary.shortNotAllowed.neon | 7 +--- 5 files changed, 22 insertions(+), 50 deletions(-) diff --git a/system/Events/Events.php b/system/Events/Events.php index ebe13d8682c1..ae68e20573ed 100644 --- a/system/Events/Events.php +++ b/system/Events/Events.php @@ -29,7 +29,7 @@ class Events /** * The list of listeners. * - * @var array + * @var array, 2: list}> */ protected static $listeners = []; @@ -53,7 +53,7 @@ class Events * Stores information about the events * for display in the debug toolbar. * - * @var list> + * @var list */ protected static $performanceLog = []; @@ -84,15 +84,12 @@ public static function initialize() $files = service('locator')->search('Config/Events.php'); } - $files = array_filter(array_map(static function (string $file): false|string { - if (is_file($file)) { - return realpath($file) ?: $file; - } - - return false; // @codeCoverageIgnore - }, $files)); + $files = array_filter(array_map( + static fn (string $file): false|string => realpath($file), + $files, + )); - static::$files = array_unique(array_merge($files, [$events])); + static::$files = array_values(array_unique(array_merge($files, [$events]))); foreach (static::$files as $file) { include $file; @@ -110,9 +107,9 @@ public static function initialize() * Events::on('create', [$myInstance, 'myMethod']); // Method on an existing instance * Events::on('create', function() {}); // Closure * - * @param string $eventName - * @param callable $callback - * @param int $priority + * @param string $eventName + * @param callable(mixed): mixed $callback + * @param int $priority * * @return void */ @@ -138,7 +135,7 @@ public static function on($eventName, $callback, $priority = self::PRIORITY_NORM * b) a method returns false, at which point execution of subscribers stops. * * @param string $eventName - * @param mixed $arguments + * @param mixed ...$arguments */ public static function trigger($eventName, ...$arguments): bool { @@ -175,6 +172,8 @@ public static function trigger($eventName, ...$arguments): bool * sorted by priority. * * @param string $eventName + * + * @return list */ public static function listeners($eventName): array { @@ -200,7 +199,8 @@ public static function listeners($eventName): array * If the listener couldn't be found, returns FALSE, else TRUE if * it was removed. * - * @param string $eventName + * @param string $eventName + * @param callable(mixed): mixed $listener */ public static function removeListener($eventName, callable $listener): bool { @@ -244,6 +244,8 @@ public static function removeAllListeners($eventName = null) /** * Sets the path to the file that routes are read from. * + * @param list $files + * * @return void */ public static function setFiles(array $files) @@ -276,7 +278,7 @@ public static function simulate(bool $choice = true) /** * Getter for the performance log records. * - * @return list> + * @return list */ public static function getPerformanceLogs() { diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 58f4f745ccd6..f94bfb305432 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 3270 errors +# total 3264 errors includes: - argument.type.neon - assign.propertyType.neon diff --git a/utils/phpstan-baseline/missingType.callable.neon b/utils/phpstan-baseline/missingType.callable.neon index cc111b14651e..9429a25441eb 100644 --- a/utils/phpstan-baseline/missingType.callable.neon +++ b/utils/phpstan-baseline/missingType.callable.neon @@ -1,17 +1,7 @@ -# total 14 errors +# total 12 errors parameters: ignoreErrors: - - - message: '#^Method CodeIgniter\\Events\\Events\:\:on\(\) has parameter \$callback with no signature specified for callable\.$#' - count: 1 - path: ../../system/Events/Events.php - - - - message: '#^Method CodeIgniter\\Events\\Events\:\:removeListener\(\) has parameter \$listener with no signature specified for callable\.$#' - count: 1 - path: ../../system/Events/Events.php - - message: '#^Class CodeIgniter\\Model has PHPDoc tag @method for method when\(\) parameter \#2 \$callback with no signature specified for callable\.$#' count: 1 diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index e972a65176a2..80bfc79d9b97 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 1574 errors +# total 1571 errors parameters: ignoreErrors: @@ -2972,21 +2972,6 @@ parameters: count: 1 path: ../../system/Entity/Entity.php - - - message: '#^Method CodeIgniter\\Events\\Events\:\:listeners\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Events/Events.php - - - - message: '#^Method CodeIgniter\\Events\\Events\:\:setFiles\(\) has parameter \$files with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Events/Events.php - - - - message: '#^Property CodeIgniter\\Events\\Events\:\:\$listeners type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Events/Events.php - - message: '#^Method CodeIgniter\\Exceptions\\PageNotFoundException\:\:lang\(\) has parameter \$args with no value type specified in iterable type array\.$#' count: 1 diff --git a/utils/phpstan-baseline/ternary.shortNotAllowed.neon b/utils/phpstan-baseline/ternary.shortNotAllowed.neon index a04074775f2e..18051955d6f7 100644 --- a/utils/phpstan-baseline/ternary.shortNotAllowed.neon +++ b/utils/phpstan-baseline/ternary.shortNotAllowed.neon @@ -1,4 +1,4 @@ -# total 36 errors +# total 35 errors parameters: ignoreErrors: @@ -32,11 +32,6 @@ parameters: count: 1 path: ../../system/Debug/Toolbar/Collectors/Database.php - - - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' - count: 1 - path: ../../system/Events/Events.php - - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' count: 3 From c65b238b161c05b581841159ece014b22cae42e6 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Mon, 26 May 2025 18:02:34 +0800 Subject: [PATCH 19/59] refactor: fix non-booleans in if conditions (#9578) --- system/View/Parser.php | 10 +++++++--- system/View/View.php | 4 +++- utils/phpstan-baseline/booleanAnd.rightNotBoolean.neon | 8 -------- utils/phpstan-baseline/if.condNotBoolean.neon | 8 -------- utils/phpstan-baseline/loader.neon | 4 +--- 5 files changed, 11 insertions(+), 23 deletions(-) delete mode 100644 utils/phpstan-baseline/booleanAnd.rightNotBoolean.neon delete mode 100644 utils/phpstan-baseline/if.condNotBoolean.neon diff --git a/system/View/Parser.php b/system/View/Parser.php index e2a093904cc7..85d0ff8468db 100644 --- a/system/View/Parser.php +++ b/system/View/Parser.php @@ -117,10 +117,14 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n $cacheName = $options['cache_name'] ?? str_replace('.php', '', $view); // Was it cached? - if (isset($options['cache']) && ($output = cache($cacheName))) { - $this->logPerformance($start, microtime(true), $view); + if (isset($options['cache'])) { + $output = cache($cacheName); + + if (is_string($output) && $output !== '') { + $this->logPerformance($start, microtime(true), $view); - return $output; + return $output; + } } $file = $this->viewPath . $view; diff --git a/system/View/View.php b/system/View/View.php index 0f10b55ef5ab..184f2528c32c 100644 --- a/system/View/View.php +++ b/system/View/View.php @@ -186,7 +186,9 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n $this->renderVars['cacheName'] = $cacheName; - if ($output = cache($this->renderVars['cacheName'])) { + $output = cache($this->renderVars['cacheName']); + + if (is_string($output) && $output !== '') { $this->logPerformance( $this->renderVars['start'], microtime(true), diff --git a/utils/phpstan-baseline/booleanAnd.rightNotBoolean.neon b/utils/phpstan-baseline/booleanAnd.rightNotBoolean.neon deleted file mode 100644 index 44f6a2a063b0..000000000000 --- a/utils/phpstan-baseline/booleanAnd.rightNotBoolean.neon +++ /dev/null @@ -1,8 +0,0 @@ -# total 1 error - -parameters: - ignoreErrors: - - - message: '#^Only booleans are allowed in &&, mixed given on the right side\.$#' - count: 1 - path: ../../system/View/Parser.php diff --git a/utils/phpstan-baseline/if.condNotBoolean.neon b/utils/phpstan-baseline/if.condNotBoolean.neon deleted file mode 100644 index e11e57b224cc..000000000000 --- a/utils/phpstan-baseline/if.condNotBoolean.neon +++ /dev/null @@ -1,8 +0,0 @@ -# total 1 error - -parameters: - ignoreErrors: - - - message: '#^Only booleans are allowed in an if condition, mixed given\.$#' - count: 1 - path: ../../system/View/View.php diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index f94bfb305432..2102bc809eb3 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,8 +1,7 @@ -# total 3264 errors +# total 3262 errors includes: - argument.type.neon - assign.propertyType.neon - - booleanAnd.rightNotBoolean.neon - codeigniter.cacheHandlerInstance.neon - codeigniter.getReassignArray.neon - codeigniter.modelArgumentType.neon @@ -10,7 +9,6 @@ includes: - codeigniter.superglobalAccessAssign.neon - deadCode.unreachable.neon - empty.notAllowed.neon - - if.condNotBoolean.neon - method.alreadyNarrowedType.neon - method.childParameterType.neon - method.childReturnType.neon From c3cc3fe902c629b45ea158a2313fd4ba1b339e7d Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Tue, 27 May 2025 18:31:53 +0800 Subject: [PATCH 20/59] refactor: fix and micro-optimize code in `Format` (#9583) --- system/Format/Exceptions/FormatException.php | 2 +- system/Format/Format.php | 13 +---- system/Format/FormatterInterface.php | 4 +- system/Format/JSONFormatter.php | 8 +-- system/Format/XMLFormatter.php | 7 +-- tests/system/Format/JSONFormatterTest.php | 54 +++++++------------ tests/system/Format/XMLFormatterTest.php | 6 +++ utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 32 +---------- 9 files changed, 40 insertions(+), 88 deletions(-) diff --git a/system/Format/Exceptions/FormatException.php b/system/Format/Exceptions/FormatException.php index 46daad558c50..cde333b1292c 100644 --- a/system/Format/Exceptions/FormatException.php +++ b/system/Format/Exceptions/FormatException.php @@ -37,7 +37,7 @@ public static function forInvalidFormatter(string $class) * Thrown in JSONFormatter when the json_encode produces * an error code other than JSON_ERROR_NONE and JSON_ERROR_RECURSION. * - * @param string $error The error message + * @param string|null $error The error message * * @return static */ diff --git a/system/Format/Format.php b/system/Format/Format.php index 4a9cb60981e6..6100b1a586a8 100644 --- a/system/Format/Format.php +++ b/system/Format/Format.php @@ -23,19 +23,8 @@ */ class Format { - /** - * Configuration instance - * - * @var FormatConfig - */ - protected $config; - - /** - * Constructor. - */ - public function __construct(FormatConfig $config) + public function __construct(protected FormatConfig $config) { - $this->config = $config; } /** diff --git a/system/Format/FormatterInterface.php b/system/Format/FormatterInterface.php index 0f005568c787..0c2492b90a77 100644 --- a/system/Format/FormatterInterface.php +++ b/system/Format/FormatterInterface.php @@ -21,9 +21,9 @@ interface FormatterInterface /** * Takes the given data and formats it. * - * @param array|object|string $data + * @param array|object|string $data * - * @return false|string + * @return false|non-empty-string */ public function format($data); } diff --git a/system/Format/JSONFormatter.php b/system/Format/JSONFormatter.php index 7d2ad5259ce3..a6ba87cd724a 100644 --- a/system/Format/JSONFormatter.php +++ b/system/Format/JSONFormatter.php @@ -26,9 +26,9 @@ class JSONFormatter implements FormatterInterface /** * Takes the given data and formats it. * - * @param array|bool|float|int|object|string|null $data + * @param array|object|string $data * - * @return false|string (JSON string | false) + * @return false|non-empty-string */ public function format($data) { @@ -37,7 +37,9 @@ public function format($data) $options = $config->formatterOptions['application/json'] ?? JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES; $options |= JSON_PARTIAL_OUTPUT_ON_ERROR; - $options = ENVIRONMENT === 'production' ? $options : $options | JSON_PRETTY_PRINT; + if (ENVIRONMENT !== 'production') { + $options |= JSON_PRETTY_PRINT; + } $result = json_encode($data, $options, 512); diff --git a/system/Format/XMLFormatter.php b/system/Format/XMLFormatter.php index c85eae5feffe..efef70cec2cb 100644 --- a/system/Format/XMLFormatter.php +++ b/system/Format/XMLFormatter.php @@ -27,9 +27,9 @@ class XMLFormatter implements FormatterInterface /** * Takes the given data and formats it. * - * @param array|bool|float|int|object|string|null $data + * @param array|object|string $data * - * @return false|string (XML string | false) + * @return false|non-empty-string */ public function format($data) { @@ -56,7 +56,8 @@ public function format($data) * * @see http://www.codexworld.com/convert-array-to-xml-in-php/ * - * @param SimpleXMLElement $output + * @param array $data + * @param SimpleXMLElement $output * * @return void */ diff --git a/tests/system/Format/JSONFormatterTest.php b/tests/system/Format/JSONFormatterTest.php index 7ac97091a4c7..027c618dc4f0 100644 --- a/tests/system/Format/JSONFormatterTest.php +++ b/tests/system/Format/JSONFormatterTest.php @@ -13,8 +13,9 @@ namespace CodeIgniter\Format; -use CodeIgniter\Exceptions\RuntimeException; +use CodeIgniter\Format\Exceptions\FormatException; use CodeIgniter\Test\CIUnitTestCase; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; /** @@ -31,51 +32,34 @@ protected function setUp(): void $this->jsonFormatter = new JSONFormatter(); } - public function testBasicJSON(): void + /** + * @param array $data + */ + #[DataProvider('provideFormattingToJson')] + public function testFormattingToJson(array $data, string $expected): void { - $data = [ - 'foo' => 'bar', - ]; - - $expected = '{ - "foo": "bar" -}'; - $this->assertSame($expected, $this->jsonFormatter->format($data)); } - public function testUnicodeOutput(): void + /** + * @return iterable, 1: string}> + */ + public static function provideFormattingToJson(): iterable { - $data = [ - 'foo' => 'База данни грешка', - ]; + yield 'empty array' => [[], '[]']; - $expected = '{ - "foo": "База данни грешка" -}'; + yield 'simple array' => [['foo' => 'bar'], "{\n \"foo\": \"bar\"\n}"]; - $this->assertSame($expected, $this->jsonFormatter->format($data)); - } + yield 'unicode array' => [['foo' => 'База данни грешка'], "{\n \"foo\": \"База данни грешка\"\n}"]; - public function testKeepsURLs(): void - { - $data = [ - 'foo' => 'https://www.example.com/foo/bar', - ]; - - $expected = '{ - "foo": "https://www.example.com/foo/bar" -}'; - - $this->assertSame($expected, $this->jsonFormatter->format($data)); + yield 'url array' => [['foo' => 'https://www.example.com/foo/bar'], "{\n \"foo\": \"https://www.example.com/foo/bar\"\n}"]; } - public function testJSONError(): void + public function testJSONFormatterThrowsError(): void { - $this->expectException(RuntimeException::class); + $this->expectException(FormatException::class); + $this->expectExceptionMessage('Malformed UTF-8 characters, possibly incorrectly encoded'); - $data = ["\xB1\x31"]; - $expected = 'Boom'; - $this->assertSame($expected, $this->jsonFormatter->format($data)); + $this->assertSame('Boom', $this->jsonFormatter->format(["\xB1\x31"])); } } diff --git a/tests/system/Format/XMLFormatterTest.php b/tests/system/Format/XMLFormatterTest.php index 0615a1eafd91..107418a5d68c 100644 --- a/tests/system/Format/XMLFormatterTest.php +++ b/tests/system/Format/XMLFormatterTest.php @@ -104,6 +104,9 @@ public function testValidatingXmlTags(): void $this->assertSame($expected, $this->xmlFormatter->format($data)); } + /** + * @param array $input + */ #[DataProvider('provideValidatingInvalidTags')] public function testValidatingInvalidTags(string $expected, array $input): void { @@ -116,6 +119,9 @@ public function testValidatingInvalidTags(string $expected, array $input): void $this->assertSame($expectedXML, $this->xmlFormatter->format($input)); } + /** + * @return iterable}> + */ public static function provideValidatingInvalidTags(): iterable { return [ diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 2102bc809eb3..4cb13113bbe1 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 3262 errors +# total 3256 errors includes: - argument.type.neon - assign.propertyType.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 80bfc79d9b97..be97474954ac 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 1571 errors +# total 1565 errors parameters: ignoreErrors: @@ -3052,26 +3052,6 @@ parameters: count: 1 path: ../../system/Filters/PerformanceMetrics.php - - - message: '#^Method CodeIgniter\\Format\\FormatterInterface\:\:format\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Format/FormatterInterface.php - - - - message: '#^Method CodeIgniter\\Format\\JSONFormatter\:\:format\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Format/JSONFormatter.php - - - - message: '#^Method CodeIgniter\\Format\\XMLFormatter\:\:arrayToXML\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Format/XMLFormatter.php - - - - message: '#^Method CodeIgniter\\Format\\XMLFormatter\:\:format\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Format/XMLFormatter.php - - message: '#^Method CodeIgniter\\HTTP\\CLIRequest\:\:getArgs\(\) return type has no value type specified in iterable type array\.$#' count: 1 @@ -6517,16 +6497,6 @@ parameters: count: 1 path: ../../tests/system/Filters/InvalidCharsTest.php - - - message: '#^Method CodeIgniter\\Format\\XMLFormatterTest\:\:provideValidatingInvalidTags\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Format/XMLFormatterTest.php - - - - message: '#^Method CodeIgniter\\Format\\XMLFormatterTest\:\:testValidatingInvalidTags\(\) has parameter \$input with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Format/XMLFormatterTest.php - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequestTest\:\:provideCanGrabGetRawInputVar\(\) return type has no value type specified in iterable type iterable\.$#' count: 1 From 327c7e136b2ee48cd6f4d040fa9681be2d136335 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Tue, 27 May 2025 19:41:55 +0800 Subject: [PATCH 21/59] refactor: add assert on `LiveModelTestCase::createModel()` (#9584) --- tests/_support/Models/UserModel.php | 3 ++ tests/system/Models/FindModelTest.php | 3 +- tests/system/Models/LiveModelTestCase.php | 8 +++++ utils/phpstan-baseline/argument.type.neon | 7 +---- utils/phpstan-baseline/loader.neon | 3 +- utils/phpstan-baseline/method.notFound.neon | 7 +---- .../phpstan-baseline/property.nonObject.neon | 31 ++----------------- utils/phpstan-baseline/varTag.type.neon | 13 -------- 8 files changed, 19 insertions(+), 56 deletions(-) delete mode 100644 utils/phpstan-baseline/varTag.type.neon diff --git a/tests/_support/Models/UserModel.php b/tests/_support/Models/UserModel.php index f940b5874ba5..8e3a1b34e13f 100644 --- a/tests/_support/Models/UserModel.php +++ b/tests/_support/Models/UserModel.php @@ -15,6 +15,9 @@ use CodeIgniter\Model; +/** + * @method int affectedRows() + */ class UserModel extends Model { protected $table = 'user'; diff --git a/tests/system/Models/FindModelTest.php b/tests/system/Models/FindModelTest.php index 43fb96d76b53..71a085dc3950 100644 --- a/tests/system/Models/FindModelTest.php +++ b/tests/system/Models/FindModelTest.php @@ -17,6 +17,7 @@ use CodeIgniter\Exceptions\ModelException; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use stdClass; use Tests\Support\Entity\UserWithCasts; use Tests\Support\Models\JobModel; use Tests\Support\Models\SecondaryModel; @@ -115,7 +116,7 @@ public function testFindRespectsSoftDeletes(): void $this->createModel(UserModel::class); $user = $this->model->find(4); - $this->assertEmpty($user); + $this->assertNotInstanceOf(stdClass::class, $user); $user = $this->model->withDeleted()->find(4); $count = is_object($user) ? 1 : 0; diff --git a/tests/system/Models/LiveModelTestCase.php b/tests/system/Models/LiveModelTestCase.php index ad250ecc73b7..342daecdf302 100644 --- a/tests/system/Models/LiveModelTestCase.php +++ b/tests/system/Models/LiveModelTestCase.php @@ -52,6 +52,14 @@ protected function tearDown(): void /** * Create an instance of Model for use in testing. + * + * @template T of Model + * + * @param class-string $modelName + * + * @return T + * + * @phpstan-assert T $this->model */ protected function createModel(string $modelName, ?BaseConnection $db = null): Model { diff --git a/utils/phpstan-baseline/argument.type.neon b/utils/phpstan-baseline/argument.type.neon index 34a1a8c9986e..2d74da4c8139 100644 --- a/utils/phpstan-baseline/argument.type.neon +++ b/utils/phpstan-baseline/argument.type.neon @@ -1,4 +1,4 @@ -# total 136 errors +# total 135 errors parameters: ignoreErrors: @@ -247,11 +247,6 @@ parameters: count: 1 path: ../../tests/system/Models/UpdateModelTest.php - - - message: '#^Parameter \#1 \$row of method CodeIgniter\\BaseModel\:\:save\(\) expects array\\|object, list\\|null given\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php - - message: '#^Parameter \#1 \$format of method CodeIgniter\\RESTful\\ResourceController\:\:setFormat\(\) expects ''json''\|''xml'', ''Nonsense'' given\.$#' count: 1 diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 4cb13113bbe1..c978950648ed 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 3256 errors +# total 3234 errors includes: - argument.type.neon - assign.propertyType.neon @@ -25,4 +25,3 @@ includes: - property.phpDocType.neon - staticMethod.notFound.neon - ternary.shortNotAllowed.neon - - varTag.type.neon diff --git a/utils/phpstan-baseline/method.notFound.neon b/utils/phpstan-baseline/method.notFound.neon index 445f071fb3c2..8bc0b8ae54c0 100644 --- a/utils/phpstan-baseline/method.notFound.neon +++ b/utils/phpstan-baseline/method.notFound.neon @@ -1,4 +1,4 @@ -# total 82 errors +# total 81 errors parameters: ignoreErrors: @@ -152,11 +152,6 @@ parameters: count: 3 path: ../../tests/system/Images/GDHandlerTest.php - - - message: '#^Call to an undefined method CodeIgniter\\Model\:\:affectedRows\(\)\.$#' - count: 1 - path: ../../tests/system/Models/AffectedRowsTest.php - - message: '#^Call to an undefined method CodeIgniter\\Model\:\:getLastQuery\(\)\.$#' count: 1 diff --git a/utils/phpstan-baseline/property.nonObject.neon b/utils/phpstan-baseline/property.nonObject.neon index ccb1a0cb3f42..33d6299465f8 100644 --- a/utils/phpstan-baseline/property.nonObject.neon +++ b/utils/phpstan-baseline/property.nonObject.neon @@ -1,4 +1,4 @@ -# total 54 errors +# total 36 errors parameters: ignoreErrors: @@ -29,24 +29,9 @@ parameters: - message: '#^Cannot access property \$id on array\.$#' - count: 8 - path: ../../tests/system/Models/FindModelTest.php - - - - message: '#^Cannot access property \$name on array\.$#' - count: 5 - path: ../../tests/system/Models/FindModelTest.php - - - - message: '#^Cannot access property \$total on array\.$#' - count: 1 + count: 2 path: ../../tests/system/Models/FindModelTest.php - - - message: '#^Cannot access property \$country on array\.$#' - count: 1 - path: ../../tests/system/Models/InsertModelTest.php - - message: '#^Cannot access property \$created_at on array\.$#' count: 3 @@ -59,16 +44,6 @@ parameters: - message: '#^Cannot access property \$description on array\.$#' - count: 3 - path: ../../tests/system/Models/SaveModelTest.php - - - - message: '#^Cannot access property \$id on array\.$#' - count: 1 - path: ../../tests/system/Models/SaveModelTest.php - - - - message: '#^Cannot access property \$name on array\.$#' count: 1 path: ../../tests/system/Models/SaveModelTest.php @@ -94,7 +69,7 @@ parameters: - message: '#^Cannot access property \$value on list\\.$#' - count: 2 + count: 1 path: ../../tests/system/Models/UpdateModelTest.php - diff --git a/utils/phpstan-baseline/varTag.type.neon b/utils/phpstan-baseline/varTag.type.neon deleted file mode 100644 index 4be1089ace00..000000000000 --- a/utils/phpstan-baseline/varTag.type.neon +++ /dev/null @@ -1,13 +0,0 @@ -# total 2 errors - -parameters: - ignoreErrors: - - - message: '#^PHPDoc tag @var with type Tests\\Support\\Entity\\UserWithCasts is not subtype of type list\\|null\.$#' - count: 1 - path: ../../tests/system/Models/FindModelTest.php - - - - message: '#^PHPDoc tag @var with type stdClass is not subtype of type array\{\}\|null\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php From 7e19473702b4eebafdc615ec780dd8ee8de7bc8a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 May 2025 15:39:20 +0000 Subject: [PATCH 22/59] chore(deps-dev): update rector/rector requirement from 2.0.16 to 2.0.17 Updates the requirements on [rector/rector](https://github.com/rectorphp/rector) to permit the latest version. - [Release notes](https://github.com/rectorphp/rector/releases) - [Commits](https://github.com/rectorphp/rector/compare/2.0.16...2.0.17) --- updated-dependencies: - dependency-name: rector/rector dependency-version: 2.0.17 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d91d2df06b27..01148efe985a 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "phpunit/phpcov": "^9.0.2 || ^10.0", "phpunit/phpunit": "^10.5.16 || ^11.2", "predis/predis": "^3.0", - "rector/rector": "2.0.16", + "rector/rector": "2.0.17", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { From 315f55b14fc7f96109f9dd5c3510077e4ea4e68f Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Sat, 31 May 2025 09:04:29 +0200 Subject: [PATCH 23/59] docs: update description for the (:num) placeholder in routes (#9588) --- user_guide_src/source/incoming/routing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/incoming/routing.rst b/user_guide_src/source/incoming/routing.rst index 6a48e95fe7a2..4f4afce136f5 100644 --- a/user_guide_src/source/incoming/routing.rst +++ b/user_guide_src/source/incoming/routing.rst @@ -177,7 +177,7 @@ Placeholders Description ============ =========================================================================================================== (:any) will match all characters from that point to the end of the URI. This may include multiple URI segments. (:segment) will match any character except for a forward slash (``/``) restricting the result to a single segment. -(:num) will match any integer. +(:num) will match any positive integer. (:alpha) will match any string of alphabetic characters (:alphanum) will match any string of alphabetic characters or integers, or any combination of the two. (:hash) is the same as ``(:segment)``, but can be used to easily see which routes use hashed ids. From 63ab555933485a938091ac95f2f1fb9f5a6fe3a5 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Sat, 31 May 2025 15:04:47 +0800 Subject: [PATCH 24/59] refactor: fix various phpstan errors in Log component (#9581) * refactor Logs collector * refactor Logger config * refactor Logger * refactor BaseHandler * refactor FileHandler * refactor Mock FileHandler * refactor ErrorlogHandler * refactor ChromeLoggerHandler * regenerate baseline * Fix per review --- app/Config/Logger.php | 3 +- system/Debug/Toolbar/Collectors/Logs.php | 18 ++- system/Log/Handlers/BaseHandler.php | 4 +- system/Log/Handlers/ChromeLoggerHandler.php | 29 ++-- system/Log/Handlers/ErrorlogHandler.php | 6 +- system/Log/Handlers/FileHandler.php | 20 +-- system/Log/Logger.php | 96 +++++-------- system/Test/Mock/MockFileLogger.php | 9 +- system/Test/Mock/MockLogger.php | 126 +++++++++--------- .../Log/Handlers/ErrorlogHandlerTest.php | 2 + tests/system/Log/LoggerTest.php | 13 +- utils/phpstan-baseline/argument.type.neon | 32 +---- utils/phpstan-baseline/empty.notAllowed.neon | 12 +- utils/phpstan-baseline/loader.neon | 2 +- .../method.childParameterType.neon | 7 +- .../missingType.iterableValue.neon | 92 +------------ .../missingType.property.neon | 22 +-- .../nullCoalesce.property.neon | 7 +- 18 files changed, 157 insertions(+), 343 deletions(-) diff --git a/app/Config/Logger.php b/app/Config/Logger.php index ab6997e52fb9..799dc2c39080 100644 --- a/app/Config/Logger.php +++ b/app/Config/Logger.php @@ -4,6 +4,7 @@ use CodeIgniter\Config\BaseConfig; use CodeIgniter\Log\Handlers\FileHandler; +use CodeIgniter\Log\Handlers\HandlerInterface; class Logger extends BaseConfig { @@ -73,7 +74,7 @@ class Logger extends BaseConfig * Handlers are executed in the order defined in this array, starting with * the handler on top and continuing down. * - * @var array|string>> + * @var array, array|string>> */ public array $handlers = [ /* diff --git a/system/Debug/Toolbar/Collectors/Logs.php b/system/Debug/Toolbar/Collectors/Logs.php index f36dfb1d12b5..c6e22fc0bbd2 100644 --- a/system/Debug/Toolbar/Collectors/Logs.php +++ b/system/Debug/Toolbar/Collectors/Logs.php @@ -45,12 +45,14 @@ class Logs extends BaseCollector /** * Our collected data. * - * @var array + * @var list */ protected $data; /** - * Returns the data of this collector to be formatted in the toolbar + * Returns the data of this collector to be formatted in the toolbar. + * + * @return array{logs: list} */ public function display(): array { @@ -66,7 +68,7 @@ public function isEmpty(): bool { $this->collectLogs(); - return empty($this->data); + return $this->data !== []; } /** @@ -82,14 +84,18 @@ public function icon(): string /** * Ensures the data has been collected. * - * @return array + * @return list */ protected function collectLogs() { - if (! empty($this->data)) { + if ($this->data !== []) { return $this->data; } - return $this->data = service('logger', true)->logCache ?? []; + $cache = service('logger')->logCache; + + $this->data = $cache ?? []; + + return $this->data; } } diff --git a/system/Log/Handlers/BaseHandler.php b/system/Log/Handlers/BaseHandler.php index 34f9fe2ecd6c..2b82f0f49225 100644 --- a/system/Log/Handlers/BaseHandler.php +++ b/system/Log/Handlers/BaseHandler.php @@ -21,7 +21,7 @@ abstract class BaseHandler implements HandlerInterface /** * Handles * - * @var array + * @var list */ protected $handles; @@ -33,7 +33,7 @@ abstract class BaseHandler implements HandlerInterface protected $dateFormat = 'Y-m-d H:i:s'; /** - * Constructor + * @param array{handles?: list} $config */ public function __construct(array $config) { diff --git a/system/Log/Handlers/ChromeLoggerHandler.php b/system/Log/Handlers/ChromeLoggerHandler.php index 5813f338b389..8d763399b513 100644 --- a/system/Log/Handlers/ChromeLoggerHandler.php +++ b/system/Log/Handlers/ChromeLoggerHandler.php @@ -16,8 +16,6 @@ use CodeIgniter\HTTP\ResponseInterface; /** - * Class ChromeLoggerHandler - * * Allows for logging items to the Chrome console for debugging. * Requires the ChromeLogger extension installed in your browser. * @@ -41,7 +39,16 @@ class ChromeLoggerHandler extends BaseHandler /** * The final data that is sent to the browser. * - * @var array + * @var array{ + * version: float, + * columns: list, + * rows: list, + * 1: string, + * 2: string, + * }>, + * request_uri?: string, + * } */ protected $json = [ 'version' => self::VERSION, @@ -63,7 +70,7 @@ class ChromeLoggerHandler extends BaseHandler /** * Maps the log levels to the ChromeLogger types. * - * @var array + * @var array */ protected $levels = [ 'emergency' => 'error', @@ -77,7 +84,7 @@ class ChromeLoggerHandler extends BaseHandler ]; /** - * Constructor + * @param array{handles?: list} $config */ public function __construct(array $config = []) { @@ -97,10 +104,8 @@ public function __construct(array $config = []) */ public function handle($level, $message): bool { - // Format our message $message = $this->format($message); - // Generate Backtrace info $backtrace = debug_backtrace(0, $this->backtraceLevel); $backtrace = end($backtrace); @@ -116,11 +121,7 @@ public function handle($level, $message): bool $type = $this->levels[$level]; } - $this->json['rows'][] = [ - [$message], - $backtraceMessage, - $type, - ]; + $this->json['rows'][] = [[$message], $backtraceMessage, $type]; $this->sendLogs(); @@ -130,9 +131,9 @@ public function handle($level, $message): bool /** * Converts the object to display nicely in the Chrome Logger UI. * - * @param array|int|object|string $object + * @param object|string $object * - * @return array + * @return array|string */ protected function format($object) { diff --git a/system/Log/Handlers/ErrorlogHandler.php b/system/Log/Handlers/ErrorlogHandler.php index 8eee392c6b6f..a7e820419fee 100644 --- a/system/Log/Handlers/ErrorlogHandler.php +++ b/system/Log/Handlers/ErrorlogHandler.php @@ -38,14 +38,14 @@ class ErrorlogHandler extends BaseHandler * Says where the error should go. Currently supported are * 0 (`TYPE_OS`) and 4 (`TYPE_SAPI`). * - * @var int + * @var 0|4 */ protected $messageType = 0; /** * Constructor. * - * @param list $config + * @param array{handles?: list, messageType?: int} $config */ public function __construct(array $config = []) { @@ -79,6 +79,8 @@ public function handle($level, $message): bool /** * Extracted call to `error_log()` in order to be tested. * + * @param 0|4 $messageType + * * @codeCoverageIgnore */ protected function errorLog(string $message, int $messageType): bool diff --git a/system/Log/Handlers/FileHandler.php b/system/Log/Handlers/FileHandler.php index 64d1485f0f7a..d3132bf878ac 100644 --- a/system/Log/Handlers/FileHandler.php +++ b/system/Log/Handlers/FileHandler.php @@ -45,18 +45,22 @@ class FileHandler extends BaseHandler protected $filePermissions; /** - * Constructor + * @param array{handles?: list, path?: string, fileExtension?: string, filePermissions?: int} $config */ public function __construct(array $config = []) { parent::__construct($config); - $this->path = empty($config['path']) ? WRITEPATH . 'logs/' : $config['path']; + $defaults = ['path' => WRITEPATH . 'logs/', 'fileExtension' => 'log', 'filePermissions' => 0644]; + $config = [...$defaults, ...$config]; - $this->fileExtension = empty($config['fileExtension']) ? 'log' : $config['fileExtension']; - $this->fileExtension = ltrim($this->fileExtension, '.'); + $this->path = $config['path'] === '' ? $defaults['path'] : $config['path']; - $this->filePermissions = $config['filePermissions'] ?? 0644; + $this->fileExtension = $config['fileExtension'] === '' + ? $defaults['fileExtension'] + : ltrim($config['fileExtension'], '.'); + + $this->filePermissions = $config['filePermissions']; } /** @@ -108,10 +112,8 @@ public function handle($level, $message): bool for ($written = 0, $length = strlen($msg); $written < $length; $written += $result) { if (($result = fwrite($fp, substr($msg, $written))) === false) { - // if we get this far, we'll never see this during travis-ci - // @codeCoverageIgnoreStart - break; - // @codeCoverageIgnoreEnd + // if we get this far, we'll never see this during unit testing + break; // @codeCoverageIgnore } } diff --git a/system/Log/Logger.php b/system/Log/Logger.php index 7c14fcdaeb5b..3770e3a6567f 100644 --- a/system/Log/Logger.php +++ b/system/Log/Logger.php @@ -54,11 +54,11 @@ class Logger implements LoggerInterface ]; /** - * Array of levels to be logged. - * The rest will be ignored. - * Set in Config/logger.php + * Array of levels to be logged. The rest will be ignored. * - * @var array + * Set in app/Config/Logger.php + * + * @var list */ protected $loggableLevels = []; @@ -86,7 +86,7 @@ class Logger implements LoggerInterface /** * Caches instances of the handlers. * - * @var array + * @var array, HandlerInterface> */ protected $handlers = []; @@ -96,14 +96,14 @@ class Logger implements LoggerInterface * value is an associative array of configuration * items. * - * @var array|string>> + * @var array, array|string>> */ protected $handlerConfig = []; /** * Caches logging calls for debugbar. * - * @var array + * @var list */ public $logCache; @@ -123,32 +123,34 @@ class Logger implements LoggerInterface */ public function __construct($config, bool $debug = CI_DEBUG) { - $this->loggableLevels = is_array($config->threshold) ? $config->threshold : range(1, (int) $config->threshold); + $loggableLevels = is_array($config->threshold) ? $config->threshold : range(1, (int) $config->threshold); // Now convert loggable levels to strings. // We only use numbers to make the threshold setting convenient for users. - if ($this->loggableLevels !== []) { - $temp = []; + foreach ($loggableLevels as $level) { + /** @var false|string $stringLevel */ + $stringLevel = array_search($level, $this->logLevels, true); - foreach ($this->loggableLevels as $level) { - $temp[] = array_search((int) $level, $this->logLevels, true); + if ($stringLevel === false) { + continue; } - $this->loggableLevels = $temp; - unset($temp); + $this->loggableLevels[] = $stringLevel; } - $this->dateFormat = $config->dateFormat ?? $this->dateFormat; + if (isset($config->dateFormat)) { + $this->dateFormat = $config->dateFormat; + } - if (! is_array($config->handlers) || $config->handlers === []) { + if ($config->handlers === []) { throw LogException::forNoHandlers('LoggerConfig'); } // Save the handler configuration for later. // Instances will be created on demand. $this->handlerConfig = $config->handlers; + $this->cacheLogs = $debug; - $this->cacheLogs = $debug; if ($this->cacheLogs) { $this->logCache = []; } @@ -156,8 +158,6 @@ public function __construct($config, bool $debug = CI_DEBUG) /** * System is unusable. - * - * @param string $message */ public function emergency(string|Stringable $message, array $context = []): void { @@ -169,8 +169,6 @@ public function emergency(string|Stringable $message, array $context = []): void * * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. - * - * @param string $message */ public function alert(string|Stringable $message, array $context = []): void { @@ -181,8 +179,6 @@ public function alert(string|Stringable $message, array $context = []): void * Critical conditions. * * Example: Application component unavailable, unexpected exception. - * - * @param string $message */ public function critical(string|Stringable $message, array $context = []): void { @@ -192,8 +188,6 @@ public function critical(string|Stringable $message, array $context = []): void /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. - * - * @param string $message */ public function error(string|Stringable $message, array $context = []): void { @@ -205,8 +199,6 @@ public function error(string|Stringable $message, array $context = []): void * * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. - * - * @param string $message */ public function warning(string|Stringable $message, array $context = []): void { @@ -215,8 +207,6 @@ public function warning(string|Stringable $message, array $context = []): void /** * Normal but significant events. - * - * @param string $message */ public function notice(string|Stringable $message, array $context = []): void { @@ -227,8 +217,6 @@ public function notice(string|Stringable $message, array $context = []): void * Interesting events. * * Example: User logs in, SQL logs. - * - * @param string $message */ public function info(string|Stringable $message, array $context = []): void { @@ -237,8 +225,6 @@ public function info(string|Stringable $message, array $context = []): void /** * Detailed debug information. - * - * @param string $message */ public function debug(string|Stringable $message, array $context = []): void { @@ -248,8 +234,7 @@ public function debug(string|Stringable $message, array $context = []): void /** * Logs with an arbitrary level. * - * @param string $level - * @param string $message + * @param mixed $level */ public function log($level, string|Stringable $message, array $context = []): void { @@ -257,24 +242,18 @@ public function log($level, string|Stringable $message, array $context = []): vo $level = array_search((int) $level, $this->logLevels, true); } - // Is the level a valid level? if (! array_key_exists($level, $this->logLevels)) { throw LogException::forInvalidLogLevel($level); } - // Does the app want to log this right now? if (! in_array($level, $this->loggableLevels, true)) { return; } - // Parse our placeholders $message = $this->interpolate($message, $context); if ($this->cacheLogs) { - $this->logCache[] = [ - 'level' => $level, - 'msg' => $message, - ]; + $this->logCache[] = ['level' => $level, 'msg' => $message]; } foreach ($this->handlerConfig as $className => $config) { @@ -282,17 +261,13 @@ public function log($level, string|Stringable $message, array $context = []): vo $this->handlers[$className] = new $className($config); } - /** - * @var HandlerInterface $handler - */ $handler = $this->handlers[$className]; if (! $handler->canHandle($level)) { continue; } - // If the handler returns false, then we - // don't execute any other handlers. + // If the handler returns false, then we don't execute any other handlers. if (! $handler->setDateFormat($this->dateFormat)->handle($level, $message)) { break; } @@ -311,7 +286,8 @@ public function log($level, string|Stringable $message, array $context = []): vo * {file} * {line} * - * @param string $message + * @param string|Stringable $message + * @param array $context * * @return string */ @@ -321,7 +297,6 @@ protected function interpolate($message, array $context = []) return print_r($message, true); } - // build a replacement array with braces around the context keys $replace = []; foreach ($context as $key => $val) { @@ -335,7 +310,6 @@ protected function interpolate($message, array $context = []) $replace['{' . $key . '}'] = $val; } - // Add special placeholders $replace['{post_vars}'] = '$_POST: ' . print_r($_POST, true); $replace['{get_vars}'] = '$_GET: ' . print_r($_GET, true); $replace['{env}'] = ENVIRONMENT; @@ -362,7 +336,6 @@ protected function interpolate($message, array $context = []) $replace['{session_vars}'] = '$_SESSION: ' . print_r($_SESSION, true); } - // interpolate replacement values into the message and return return strtr($message, $replace); } @@ -370,6 +343,8 @@ protected function interpolate($message, array $context = []) * Determines the file and line that the logging call * was made from by analyzing the backtrace. * Find the earliest stack frame that is part of our logging system. + * + * @return array{string, int|string} */ public function determineFile(): array { @@ -386,28 +361,19 @@ public function determineFile(): array 'notice', ]; - // Generate Backtrace info - $trace = \debug_backtrace(0); + $trace = debug_backtrace(0); - // So we search from the bottom (earliest) of the stack frames - $stackFrames = \array_reverse($trace); + $stackFrames = array_reverse($trace); - // Find the first reference to a Logger class method foreach ($stackFrames as $frame) { - if (\in_array($frame['function'], $logFunctions, true)) { + if (in_array($frame['function'], $logFunctions, true)) { $file = isset($frame['file']) ? clean_path($frame['file']) : 'unknown'; $line = $frame['line'] ?? 'unknown'; - return [ - $file, - $line, - ]; + return [$file, $line]; } } - return [ - 'unknown', - 'unknown', - ]; + return ['unknown', 'unknown']; } } diff --git a/system/Test/Mock/MockFileLogger.php b/system/Test/Mock/MockFileLogger.php index 8737e724b413..5a815b223196 100644 --- a/system/Test/Mock/MockFileLogger.php +++ b/system/Test/Mock/MockFileLogger.php @@ -16,21 +16,24 @@ use CodeIgniter\Log\Handlers\FileHandler; /** - * Class MockFileLogger - * * Extends FileHandler, exposing some inner workings */ class MockFileLogger extends FileHandler { /** * Where would the log be written? + * + * @var string */ public $destination; + /** + * @param array{handles?: list, path?: string, fileExtension?: string, filePermissions?: int} $config + */ public function __construct(array $config) { parent::__construct($config); - $this->handles = $config['handles'] ?? []; + $this->destination = $this->path . 'log-' . date('Y-m-d') . '.' . $this->fileExtension; } } diff --git a/system/Test/Mock/MockLogger.php b/system/Test/Mock/MockLogger.php index 5130b2eabada..411c1a66a6be 100644 --- a/system/Test/Mock/MockLogger.php +++ b/system/Test/Mock/MockLogger.php @@ -13,77 +13,79 @@ namespace CodeIgniter\Test\Mock; +use CodeIgniter\Log\Handlers\HandlerInterface; +use Config\Logger; use Tests\Support\Log\Handlers\TestHandler; -class MockLogger +class MockLogger extends Logger { - /* - |-------------------------------------------------------------------------- - | Error Logging Threshold - |-------------------------------------------------------------------------- - | - | You can enable error logging by setting a threshold over zero. The - | threshold determines what gets logged. Any values below or equal to the - | threshold will be logged. Threshold options are: - | - | 0 = Disables logging, Error logging TURNED OFF - | 1 = Emergency Messages - System is unusable - | 2 = Alert Messages - Action Must Be Taken Immediately - | 3 = Critical Messages - Application component unavailable, unexpected exception. - | 4 = Runtime Errors - Don't need immediate action, but should be monitored. - | 5 = Warnings - Exceptional occurrences that are not errors. - | 6 = Notices - Normal but significant events. - | 7 = Info - Interesting events, like user logging in, etc. - | 8 = Debug - Detailed debug information. - | 9 = All Messages - | - | You can also pass an array with threshold levels to show individual error types - | - | array(1, 2, 3, 8) = Emergency, Alert, Critical, and Debug messages - | - | For a live site you'll usually enable Critical or higher (3) to be logged otherwise - | your log files will fill up very fast. - | + /** + *-------------------------------------------------------------------------- + * Error Logging Threshold + *-------------------------------------------------------------------------- + * + * You can enable error logging by setting a threshold over zero. The + * threshold determines what gets logged. Any values below or equal to the + * threshold will be logged. Threshold options are: + * + * 0 = Disables logging, Error logging TURNED OFF + * 1 = Emergency Messages - System is unusable + * 2 = Alert Messages - Action Must Be Taken Immediately + * 3 = Critical Messages - Application component unavailable, unexpected exception. + * 4 = Runtime Errors - Don't need immediate action, but should be monitored. + * 5 = Warnings - Exceptional occurrences that are not errors. + * 6 = Notices - Normal but significant events. + * 7 = Info - Interesting events, like user logging in, etc. + * 8 = Debug - Detailed debug information. + * 9 = All Messages + * + * You can also pass an array with threshold levels to show individual error types + * + * array(1, 2, 3, 8) = Emergency, Alert, Critical, and Debug messages + * + * For a live site you'll usually enable Critical or higher (3) to be logged otherwise + * your log files will fill up very fast. + * + * @var int|list */ - public $threshold = 9; - /* - |-------------------------------------------------------------------------- - | Date Format for Logs - |-------------------------------------------------------------------------- - | - | Each item that is logged has an associated date. You can use PHP date - | codes to set your own date formatting - | + /** + *-------------------------------------------------------------------------- + * Date Format for Logs + *-------------------------------------------------------------------------- + * + * Each item that is logged has an associated date. You can use PHP date + * codes to set your own date formatting */ - public $dateFormat = 'Y-m-d'; + public string $dateFormat = 'Y-m-d'; - /* - |-------------------------------------------------------------------------- - | Log Handlers - |-------------------------------------------------------------------------- - | - | The logging system supports multiple actions to be taken when something - | is logged. This is done by allowing for multiple Handlers, special classes - | designed to write the log to their chosen destinations, whether that is - | a file on the server, a cloud-based service, or even taking actions such - | as emailing the dev team. - | - | Each handler is defined by the class name used for that handler, and it - | MUST implement the CodeIgniter\Log\Handlers\HandlerInterface interface. - | - | The value of each key is an array of configuration items that are sent - | to the constructor of each handler. The only required configuration item - | is the 'handles' element, which must be an array of integer log levels. - | This is most easily handled by using the constants defined in the - | Psr\Log\LogLevel class. - | - | Handlers are executed in the order defined in this array, starting with - | the handler on top and continuing down. - | + /** + *-------------------------------------------------------------------------- + * Log Handlers + *-------------------------------------------------------------------------- + * + * The logging system supports multiple actions to be taken when something + * is logged. This is done by allowing for multiple Handlers, special classes + * designed to write the log to their chosen destinations, whether that is + * a file on the server, a cloud-based service, or even taking actions such + * as emailing the dev team. + * + * Each handler is defined by the class name used for that handler, and it + * MUST implement the CodeIgniter\Log\Handlers\HandlerInterface interface. + * + * The value of each key is an array of configuration items that are sent + * to the constructor of each handler. The only required configuration item + * is the 'handles' element, which must be an array of integer log levels. + * This is most easily handled by using the constants defined in the + * Psr\Log\LogLevel class. + * + * Handlers are executed in the order defined in this array, starting with + * the handler on top and continuing down. + * + * @var array, array|string>> */ - public $handlers = [ + public array $handlers = [ // File Handler TestHandler::class => [ // The log levels that this handler will handle. diff --git a/tests/system/Log/Handlers/ErrorlogHandlerTest.php b/tests/system/Log/Handlers/ErrorlogHandlerTest.php index 609cea39af7d..2383138d7306 100644 --- a/tests/system/Log/Handlers/ErrorlogHandlerTest.php +++ b/tests/system/Log/Handlers/ErrorlogHandlerTest.php @@ -39,6 +39,8 @@ public function testErrorLoggingWithErrorLog(): void } /** + * @param array{handles?: list, messageType?: int} $config + * * @return ErrorlogHandler&MockObject */ private function getMockedHandler(array $config = []) diff --git a/tests/system/Log/LoggerTest.php b/tests/system/Log/LoggerTest.php index 695e99fa3ef1..623b665cfda3 100644 --- a/tests/system/Log/LoggerTest.php +++ b/tests/system/Log/LoggerTest.php @@ -23,7 +23,6 @@ use ReflectionMethod; use ReflectionNamedType; use Tests\Support\Log\Handlers\TestHandler; -use TypeError; /** * @internal @@ -42,7 +41,7 @@ protected function tearDown(): void public function testThrowsExceptionWithBadHandlerSettings(): void { $config = new LoggerConfig(); - $config->handlers = null; + $config->handlers = []; $this->expectException(FrameworkException::class); $this->expectExceptionMessage(lang('Core.noHandlers', ['LoggerConfig'])); @@ -427,16 +426,6 @@ public function testLogLevels(): void $this->assertSame($expected, $logs[0]); } - public function testNonStringMessage(): void - { - $this->expectException(TypeError::class); - - $config = new LoggerConfig(); - $logger = new Logger($config); - - $logger->log(5, $config); - } - public function testDetermineFileNoStackTrace(): void { $config = new LoggerConfig(); diff --git a/utils/phpstan-baseline/argument.type.neon b/utils/phpstan-baseline/argument.type.neon index 2d74da4c8139..9fe27bf262a7 100644 --- a/utils/phpstan-baseline/argument.type.neon +++ b/utils/phpstan-baseline/argument.type.neon @@ -1,4 +1,4 @@ -# total 135 errors +# total 90 errors parameters: ignoreErrors: @@ -27,11 +27,6 @@ parameters: count: 1 path: ../../system/Database/SQLite3/Builder.php - - - message: '#^Parameter \#2 \$message_type of function error_log expects 0\|1\|3\|4, int given\.$#' - count: 1 - path: ../../system/Log/Handlers/ErrorlogHandler.php - - message: '#^Parameter \#2 \$to of method CodeIgniter\\Router\\RouteCollection\:\:add\(\) expects array\|\(Closure\(mixed \.\.\.\)\: \(CodeIgniter\\HTTP\\ResponseInterface\|string\|void\)\)\|string, Closure\(mixed\)\: \(CodeIgniter\\HTTP\\DownloadResponse\|null\) given\.$#' count: 1 @@ -142,11 +137,6 @@ parameters: count: 1 path: ../../tests/system/HTTP/HeaderTest.php - - - message: '#^Parameter \#1 \$config of class CodeIgniter\\Log\\Logger constructor expects Config\\Logger, CodeIgniter\\Test\\Mock\\MockLogger given\.$#' - count: 1 - path: ../../tests/system/HTTP/RedirectExceptionTest.php - - message: '#^Parameter \#3 \$expire of method CodeIgniter\\HTTP\\Response\:\:setCookie\(\) expects int, string given\.$#' count: 1 @@ -212,21 +202,6 @@ parameters: count: 1 path: ../../tests/system/Log/Handlers/ChromeLoggerHandlerTest.php - - - message: '#^Parameter \#1 \$config of class CodeIgniter\\Log\\Logger constructor expects Config\\Logger, CodeIgniter\\Test\\Mock\\MockLogger given\.$#' - count: 25 - path: ../../tests/system/Log/LoggerTest.php - - - - message: '#^Parameter \#1 \$level of method CodeIgniter\\Log\\Logger\:\:log\(\) expects string, int given\.$#' - count: 2 - path: ../../tests/system/Log/LoggerTest.php - - - - message: '#^Parameter \#2 \$message of method CodeIgniter\\Log\\Logger\:\:log\(\) expects string\|Stringable, CodeIgniter\\Test\\Mock\\MockLogger given\.$#' - count: 1 - path: ../../tests/system/Log/LoggerTest.php - - message: '#^Parameter \#1 \$row of method CodeIgniter\\BaseModel\:\:save\(\) expects array\\|object, array\\> given\.$#' count: 1 @@ -257,11 +232,6 @@ parameters: count: 1 path: ../../tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php - - - message: '#^Parameter \#1 \$config of class CodeIgniter\\Log\\Logger constructor expects Config\\Logger, CodeIgniter\\Test\\Mock\\MockLogger given\.$#' - count: 15 - path: ../../tests/system/Test/ControllerTestTraitTest.php - - message: '#^Parameter \#1 \$request of method CodeIgniter\\CodeIgniter\:\:setRequest\(\) expects CodeIgniter\\HTTP\\CLIRequest\|CodeIgniter\\HTTP\\IncomingRequest, CodeIgniter\\HTTP\\Request given\.$#' count: 1 diff --git a/utils/phpstan-baseline/empty.notAllowed.neon b/utils/phpstan-baseline/empty.notAllowed.neon index ea06afa8b720..ebcb9dafe4e1 100644 --- a/utils/phpstan-baseline/empty.notAllowed.neon +++ b/utils/phpstan-baseline/empty.notAllowed.neon @@ -1,4 +1,4 @@ -# total 251 errors +# total 247 errors parameters: ignoreErrors: @@ -182,11 +182,6 @@ parameters: count: 2 path: ../../system/Debug/Toolbar.php - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 2 - path: ../../system/Debug/Toolbar/Collectors/Logs.php - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' count: 12 @@ -292,11 +287,6 @@ parameters: count: 1 path: ../../system/Images/Image.php - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 2 - path: ../../system/Log/Handlers/FileHandler.php - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' count: 1 diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index c978950648ed..9edbf37e3bc2 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 3234 errors +# total 3161 errors includes: - argument.type.neon - assign.propertyType.neon diff --git a/utils/phpstan-baseline/method.childParameterType.neon b/utils/phpstan-baseline/method.childParameterType.neon index 0d10e9cd49d6..d6446dcbb7bd 100644 --- a/utils/phpstan-baseline/method.childParameterType.neon +++ b/utils/phpstan-baseline/method.childParameterType.neon @@ -1,4 +1,4 @@ -# total 9 errors +# total 8 errors parameters: ignoreErrors: @@ -41,8 +41,3 @@ parameters: message: '#^Parameter \#1 \$value \(int\) of method CodeIgniter\\Entity\\Cast\\IntBoolCast\:\:get\(\) should be contravariant with parameter \$value \(array\|bool\|float\|int\|object\|string\|null\) of method CodeIgniter\\Entity\\Cast\\CastInterface\:\:get\(\)$#' count: 1 path: ../../system/Entity/Cast/IntBoolCast.php - - - - message: '#^Parameter \#1 \$level \(string\) of method CodeIgniter\\Log\\Logger\:\:log\(\) should be contravariant with parameter \$level \(mixed\) of method Psr\\Log\\LoggerInterface\:\:log\(\)$#' - count: 1 - path: ../../system/Log/Logger.php diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index be97474954ac..c0eff3f00d6d 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 1565 errors +# total 1547 errors parameters: ignoreErrors: @@ -2497,21 +2497,6 @@ parameters: count: 1 path: ../../system/Debug/Toolbar/Collectors/History.php - - - message: '#^Method CodeIgniter\\Debug\\Toolbar\\Collectors\\Logs\:\:collectLogs\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar/Collectors/Logs.php - - - - message: '#^Method CodeIgniter\\Debug\\Toolbar\\Collectors\\Logs\:\:display\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar/Collectors/Logs.php - - - - message: '#^Property CodeIgniter\\Debug\\Toolbar\\Collectors\\Logs\:\:\$data type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar/Collectors/Logs.php - - message: '#^Method CodeIgniter\\Debug\\Toolbar\\Collectors\\Timers\:\:formatTimelineData\(\) return type has no value type specified in iterable type array\.$#' count: 1 @@ -4532,71 +4517,6 @@ parameters: count: 1 path: ../../system/Language/Language.php - - - message: '#^Method CodeIgniter\\Log\\Handlers\\BaseHandler\:\:__construct\(\) has parameter \$config with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Log/Handlers/BaseHandler.php - - - - message: '#^Property CodeIgniter\\Log\\Handlers\\BaseHandler\:\:\$handles type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Log/Handlers/BaseHandler.php - - - - message: '#^Method CodeIgniter\\Log\\Handlers\\ChromeLoggerHandler\:\:__construct\(\) has parameter \$config with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Log/Handlers/ChromeLoggerHandler.php - - - - message: '#^Method CodeIgniter\\Log\\Handlers\\ChromeLoggerHandler\:\:format\(\) has parameter \$object with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Log/Handlers/ChromeLoggerHandler.php - - - - message: '#^Method CodeIgniter\\Log\\Handlers\\ChromeLoggerHandler\:\:format\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Log/Handlers/ChromeLoggerHandler.php - - - - message: '#^Property CodeIgniter\\Log\\Handlers\\ChromeLoggerHandler\:\:\$json type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Log/Handlers/ChromeLoggerHandler.php - - - - message: '#^Property CodeIgniter\\Log\\Handlers\\ChromeLoggerHandler\:\:\$levels type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Log/Handlers/ChromeLoggerHandler.php - - - - message: '#^Method CodeIgniter\\Log\\Handlers\\FileHandler\:\:__construct\(\) has parameter \$config with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Log/Handlers/FileHandler.php - - - - message: '#^Method CodeIgniter\\Log\\Logger\:\:determineFile\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Log/Logger.php - - - - message: '#^Method CodeIgniter\\Log\\Logger\:\:interpolate\(\) has parameter \$context with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Log/Logger.php - - - - message: '#^Property CodeIgniter\\Log\\Logger\:\:\$handlers type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Log/Logger.php - - - - message: '#^Property CodeIgniter\\Log\\Logger\:\:\$logCache type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Log/Logger.php - - - - message: '#^Property CodeIgniter\\Log\\Logger\:\:\$loggableLevels type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Log/Logger.php - - message: '#^Method CodeIgniter\\Model\:\:__call\(\) has parameter \$params with no value type specified in iterable type array\.$#' count: 1 @@ -5262,11 +5182,6 @@ parameters: count: 1 path: ../../system/Test/Mock/MockEvents.php - - - message: '#^Method CodeIgniter\\Test\\Mock\\MockFileLogger\:\:__construct\(\) has parameter \$config with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Mock/MockFileLogger.php - - message: '#^Method CodeIgniter\\Test\\Mock\\MockInputOutput\:\:getOutputs\(\) return type has no value type specified in iterable type array\.$#' count: 1 @@ -6932,11 +6847,6 @@ parameters: count: 1 path: ../../tests/system/Language/LanguageTest.php - - - message: '#^Method CodeIgniter\\Log\\Handlers\\ErrorlogHandlerTest\:\:getMockedHandler\(\) has parameter \$config with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Log/Handlers/ErrorlogHandlerTest.php - - message: '#^Method CodeIgniter\\Models\\DeleteModelTest\:\:emptyPkValues\(\) return type has no value type specified in iterable type iterable\.$#' count: 1 diff --git a/utils/phpstan-baseline/missingType.property.neon b/utils/phpstan-baseline/missingType.property.neon index e4842f9f4d25..a739a7b09849 100644 --- a/utils/phpstan-baseline/missingType.property.neon +++ b/utils/phpstan-baseline/missingType.property.neon @@ -1,4 +1,4 @@ -# total 119 errors +# total 115 errors parameters: ignoreErrors: @@ -52,26 +52,6 @@ parameters: count: 1 path: ../../system/Test/Mock/MockCURLRequest.php - - - message: '#^Property CodeIgniter\\Test\\Mock\\MockFileLogger\:\:\$destination has no type specified\.$#' - count: 1 - path: ../../system/Test/Mock/MockFileLogger.php - - - - message: '#^Property CodeIgniter\\Test\\Mock\\MockLogger\:\:\$dateFormat has no type specified\.$#' - count: 1 - path: ../../system/Test/Mock/MockLogger.php - - - - message: '#^Property CodeIgniter\\Test\\Mock\\MockLogger\:\:\$handlers has no type specified\.$#' - count: 1 - path: ../../system/Test/Mock/MockLogger.php - - - - message: '#^Property CodeIgniter\\Test\\Mock\\MockLogger\:\:\$threshold has no type specified\.$#' - count: 1 - path: ../../system/Test/Mock/MockLogger.php - - message: '#^Property CodeIgniter\\Test\\Mock\\MockServices\:\:\$classmap has no type specified\.$#' count: 1 diff --git a/utils/phpstan-baseline/nullCoalesce.property.neon b/utils/phpstan-baseline/nullCoalesce.property.neon index 6caf5f23ef1d..88cdd2e7254e 100644 --- a/utils/phpstan-baseline/nullCoalesce.property.neon +++ b/utils/phpstan-baseline/nullCoalesce.property.neon @@ -1,12 +1,7 @@ -# total 20 errors +# total 19 errors parameters: ignoreErrors: - - - message: '#^Property CodeIgniter\\Log\\Logger\:\:\$logCache \(array\) on left side of \?\? is not nullable\.$#' - count: 1 - path: ../../system/Debug/Toolbar/Collectors/Logs.php - - message: '#^Property CodeIgniter\\Files\\File\:\:\$size \(int\) on left side of \?\? is not nullable\.$#' count: 1 From acb5470c4f6a06bc6dda977baaf90ee72cfdc809 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Sat, 31 May 2025 09:05:30 +0200 Subject: [PATCH 25/59] fix: add error handling for corrupted cache files in FileHandler (#9586) --- system/Cache/Handlers/FileHandler.php | 14 +++++++++-- .../system/Cache/Handlers/FileHandlerTest.php | 24 +++++++++++++++++++ user_guide_src/source/changelogs/v4.6.2.rst | 1 + 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/system/Cache/Handlers/FileHandler.php b/system/Cache/Handlers/FileHandler.php index 4fd03e7921b0..c496143f62d9 100644 --- a/system/Cache/Handlers/FileHandler.php +++ b/system/Cache/Handlers/FileHandler.php @@ -217,7 +217,7 @@ public function isSupported(): bool /** * Does the heavy lifting of actually retrieving the file and - * verifying it's age. + * verifying its age. * * @return array{data: mixed, ttl: int, time: int}|false */ @@ -227,7 +227,17 @@ protected function getItem(string $filename) return false; } - $data = @unserialize(file_get_contents($this->path . $filename)); + $content = @file_get_contents($this->path . $filename); + + if ($content === false) { + return false; + } + + try { + $data = unserialize($content); + } catch (Throwable) { + return false; + } if (! is_array($data)) { return false; diff --git a/tests/system/Cache/Handlers/FileHandlerTest.php b/tests/system/Cache/Handlers/FileHandlerTest.php index 4c45a0712f64..ed163071aede 100644 --- a/tests/system/Cache/Handlers/FileHandlerTest.php +++ b/tests/system/Cache/Handlers/FileHandlerTest.php @@ -363,6 +363,30 @@ public function testGetMetaDataMiss(): void { $this->assertFalse($this->handler->getMetaData(self::$dummy)); } + + #[RequiresOperatingSystem('Linux|Darwin')] + public function testGetUnreadableFile(): void + { + $this->handler->save(self::$key1, 'value'); + + $filePath = $this->config->file['storePath'] . DIRECTORY_SEPARATOR . $this->config->prefix . self::$key1; + + // Make the file unreadable + chmod($filePath, 0000); + + $this->assertNull($this->handler->get(self::$key1)); + } + + public function testGetItemWithCorruptedData(): void + { + $filePath = $this->config->file['storePath'] . DIRECTORY_SEPARATOR . $this->config->prefix . self::$key1; + + file_put_contents($filePath, 'corrupted_serialized_data_that_cannot_be_unserialized'); + + $this->assertFileExists($filePath); + + $this->assertNull($this->handler->get(self::$key1)); + } } final class BaseTestFileHandler extends FileHandler diff --git a/user_guide_src/source/changelogs/v4.6.2.rst b/user_guide_src/source/changelogs/v4.6.2.rst index c55eda3e3787..936c0e709046 100644 --- a/user_guide_src/source/changelogs/v4.6.2.rst +++ b/user_guide_src/source/changelogs/v4.6.2.rst @@ -35,6 +35,7 @@ Deprecations Bugs Fixed ********** +- **Cache:** Fixed a bug where a corrupted or unreadable cache file could cause an unhandled exception in ``FileHandler::getItem()``. - **Database:** Fixed a bug where ``when()`` and ``whenNot()`` in ``ConditionalTrait`` incorrectly evaluated certain falsy values (such as ``[]``, ``0``, ``0.0``, and ``'0'``) as truthy, causing callbacks to be executed unexpectedly. These methods now cast the condition to a boolean using ``(bool)`` to ensure consistent behavior with PHP's native truthiness. - **Security:** Fixed a bug where the ``sanitize_filename()`` function from the Security helper would throw an error when used in CLI requests. - **Session:** Fixed a bug where using the ``DatabaseHandler`` with an unsupported database driver (such as ``SQLSRV``, ``OCI8``, or ``SQLite3``) did not throw an appropriate error. From 58f6b16ce993f808e6a0204f4136173f6d53a661 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Sun, 1 Jun 2025 01:10:48 +0800 Subject: [PATCH 26/59] refactor: partial fix errors on Email (#9582) --- system/Email/Email.php | 111 +++++++++--------- utils/phpstan-baseline/empty.notAllowed.neon | 4 +- utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 27 +---- 4 files changed, 62 insertions(+), 82 deletions(-) diff --git a/system/Email/Email.php b/system/Email/Email.php index 2a0facc0ca08..727419832c62 100644 --- a/system/Email/Email.php +++ b/system/Email/Email.php @@ -68,7 +68,7 @@ class Email /** * Which method to use for sending e-mails. * - * @var string 'mail', 'sendmail' or 'smtp' + * @var 'mail'|'sendmail'|'smtp' */ public $protocol = 'mail'; @@ -117,9 +117,11 @@ class Email /** * SMTP Encryption * - * @var string '', 'tls' or 'ssl'. 'tls' will issue a STARTTLS command - * to the server. 'ssl' means implicit SSL. Connection on port - * 465 should set this to ''. + * * `tls` - will issue a STARTTLS command to the server + * * `ssl` - means implicit SSL + * * `''` - for connection on port 465 + * + * @var ''|'ssl'|'tls' */ public $SMTPCrypto = ''; @@ -142,7 +144,7 @@ class Email /** * Message format. * - * @var string 'text' or 'html' + * @var 'html'|'text' */ public $mailType = 'text'; @@ -170,7 +172,7 @@ class Email /** * X-Priority header value. * - * @var int 1-5 + * @var int<1, 5> */ public $priority = 3; @@ -180,7 +182,7 @@ class Email * * @see http://www.ietf.org/rfc/rfc822.txt * - * @var string "\r\n" or "\n" + * @var "\r\n"|"n" */ public $newline = "\r\n"; @@ -195,7 +197,7 @@ class Email * * @see http://www.ietf.org/rfc/rfc822.txt * - * @var string + * @var "\r\n"|"n" */ public $CRLF = "\r\n"; @@ -268,7 +270,7 @@ class Email /** * Mail encoding * - * @var string '8bit' or '7bit' + * @var '7bit'|'8bit' */ protected $encoding = '8bit'; @@ -342,7 +344,7 @@ class Email * * @see Email::$protocol * - * @var array + * @var list */ protected $protocols = [ 'mail', @@ -368,7 +370,7 @@ class Email * * @see Email::$encoding * - * @var array + * @var list */ protected $bitDepths = [ '7bit', @@ -380,7 +382,7 @@ class Email * * Actual values to send with the X-Priority header * - * @var array + * @var array */ protected $priorities = [ 1 => '1 (Highest)', @@ -414,7 +416,7 @@ public function __construct($config = null) * * @param array|\Config\Email|null $config * - * @return Email + * @return $this */ public function initialize($config) { @@ -445,7 +447,7 @@ public function initialize($config) /** * @param bool $clearAttachments * - * @return Email + * @return $this */ public function clear($clearAttachments = false) { @@ -463,7 +465,7 @@ public function clear($clearAttachments = false) $this->setHeader('Date', $this->setDate()); - if ($clearAttachments !== false) { + if ($clearAttachments) { $this->attachments = []; } @@ -473,13 +475,13 @@ public function clear($clearAttachments = false) /** * @param string $from * @param string $name - * @param string|null $returnPath Return-Path + * @param string|null $returnPath * - * @return Email + * @return $this */ public function setFrom($from, $name = '', $returnPath = null) { - if (preg_match('/\<(.*)\>/', $from, $match)) { + if (preg_match('/\<(.*)\>/', $from, $match) === 1) { $from = $match[1]; } @@ -504,9 +506,8 @@ public function setFrom($from, $name = '', $returnPath = null) } $this->setHeader('From', $name . ' <' . $from . '>'); - if (! isset($returnPath)) { - $returnPath = $from; - } + $returnPath ??= $from; + $this->setHeader('Return-Path', '<' . $returnPath . '>'); $this->tmpArchive['returnPath'] = $returnPath; @@ -517,11 +518,11 @@ public function setFrom($from, $name = '', $returnPath = null) * @param string $replyto * @param string $name * - * @return Email + * @return $this */ public function setReplyTo($replyto, $name = '') { - if (preg_match('/\<(.*)\>/', $replyto, $match)) { + if (preg_match('/\<(.*)\>/', $replyto, $match) === 1) { $replyto = $match[1]; } @@ -550,7 +551,7 @@ public function setReplyTo($replyto, $name = '') /** * @param array|string $to * - * @return Email + * @return $this */ public function setTo($to) { @@ -573,7 +574,7 @@ public function setTo($to) /** * @param string $cc * - * @return Email + * @return $this */ public function setCC($cc) { @@ -598,7 +599,7 @@ public function setCC($cc) * @param string $bcc * @param string $limit * - * @return Email + * @return $this */ public function setBCC($bcc, $limit = '') { @@ -626,7 +627,7 @@ public function setBCC($bcc, $limit = '') /** * @param string $subject * - * @return Email + * @return $this */ public function setSubject($subject) { @@ -641,7 +642,7 @@ public function setSubject($subject) /** * @param string $body * - * @return Email + * @return $this */ public function setMessage($body) { @@ -734,7 +735,7 @@ public function setAttachmentCID($filename) * @param string $header * @param string $value * - * @return Email + * @return $this */ public function setHeader($header, $value) { @@ -744,14 +745,16 @@ public function setHeader($header, $value) } /** - * @param array|string $email + * @param list|string $email * - * @return array + * @return list */ protected function stringToArray($email) { if (! is_array($email)) { - return (str_contains($email, ',')) ? preg_split('/[\s,]/', $email, -1, PREG_SPLIT_NO_EMPTY) : (array) trim($email); + return str_contains($email, ',') + ? preg_split('/[\s,]/', $email, -1, PREG_SPLIT_NO_EMPTY) + : (array) trim($email); } return $email; @@ -760,7 +763,7 @@ protected function stringToArray($email) /** * @param string $str * - * @return Email + * @return $this */ public function setAltMessage($str) { @@ -772,11 +775,11 @@ public function setAltMessage($str) /** * @param string $type * - * @return Email + * @return $this */ public function setMailType($type = 'text') { - $this->mailType = ($type === 'html') ? 'html' : 'text'; + $this->mailType = $type === 'html' ? 'html' : 'text'; return $this; } @@ -784,7 +787,7 @@ public function setMailType($type = 'text') /** * @param bool $wordWrap * - * @return Email + * @return $this */ public function setWordWrap($wordWrap = true) { @@ -796,7 +799,7 @@ public function setWordWrap($wordWrap = true) /** * @param string $protocol * - * @return Email + * @return $this */ public function setProtocol($protocol = 'mail') { @@ -808,7 +811,7 @@ public function setProtocol($protocol = 'mail') /** * @param int $n * - * @return Email + * @return $this */ public function setPriority($n = 3) { @@ -820,7 +823,7 @@ public function setPriority($n = 3) /** * @param string $newline * - * @return Email + * @return $this */ public function setNewline($newline = "\n") { @@ -832,11 +835,11 @@ public function setNewline($newline = "\n") /** * @param string $CRLF * - * @return Email + * @return $this */ public function setCRLF($CRLF = "\n") { - $this->CRLF = ($CRLF !== "\n" && $CRLF !== "\r\n" && $CRLF !== "\r") ? "\n" : $CRLF; + $this->CRLF = ! in_array($CRLF, ["\n", "\r\n", "\r"], true) ? "\n" : $CRLF; return $this; } @@ -891,10 +894,10 @@ protected function getEncoding() protected function getContentType() { if ($this->mailType === 'html') { - return empty($this->attachments) ? 'html' : 'html-attach'; + return $this->attachments === [] ? 'html' : 'html-attach'; } - if ($this->mailType === 'text' && ! empty($this->attachments)) { + if ($this->mailType === 'text' && $this->attachments !== []) { return 'plain-attach'; } @@ -996,8 +999,8 @@ public function cleanEmail($email) */ protected function getAltMessage() { - if (! empty($this->altMessage)) { - return ($this->wordWrap) ? $this->wordWrap($this->altMessage, 76) : $this->altMessage; + if ($this->altMessage !== '') { + return $this->wordWrap ? $this->wordWrap($this->altMessage, 76) : $this->altMessage; } $body = preg_match('/\(.*)\<\/body\>/si', $this->body, $match) ? $match[1] : $this->body; @@ -1009,7 +1012,7 @@ protected function getAltMessage() $body = preg_replace('| +|', ' ', $body); - return ($this->wordWrap) ? $this->wordWrap($body, 76) : $body; + return $this->wordWrap ? $this->wordWrap($body, 76) : $body; } /** @@ -1020,8 +1023,10 @@ protected function getAltMessage() */ public function wordWrap($str, $charlim = null) { - if (empty($charlim)) { - $charlim = empty($this->wrapChars) ? 76 : $this->wrapChars; + $charlim ??= 0; + + if ($charlim === 0) { + $charlim = $this->wrapChars === 0 ? 76 : $this->wrapChars; } if (str_contains($str, "\r")) { @@ -1269,13 +1274,13 @@ protected function buildMessage() } /** - * @param mixed $type + * @param string $type * * @return bool */ protected function attachmentsHaveMultipart($type) { - foreach ($this->attachments as &$attachment) { + foreach ($this->attachments as $attachment) { if ($attachment['multipart'] === $type) { return true; } @@ -1303,14 +1308,14 @@ protected function appendAttachments(&$body, $boundary, $multipart = null) . 'Content-Type: ' . $attachment['type'] . '; name="' . $name . '"' . $this->newline . 'Content-Disposition: ' . $attachment['disposition'] . ';' . $this->newline . 'Content-Transfer-Encoding: base64' . $this->newline - . (empty($attachment['cid']) ? '' : 'Content-ID: <' . $attachment['cid'] . '>' . $this->newline) + . ($attachment['cid'] === '' ? '' : 'Content-ID: <' . $attachment['cid'] . '>' . $this->newline) . $this->newline . $attachment['content'] . $this->newline; } // $name won't be set if no attachments were appended, // and therefore a boundary wouldn't be necessary - if (! empty($name)) { + if (isset($name)) { $body .= '--' . $boundary . '--'; } } @@ -2218,7 +2223,7 @@ protected function mimeTypes($ext = '') public function __destruct() { - if (is_resource($this->SMTPConnect)) { + if ($this->SMTPConnect !== null) { try { $this->sendCommand('quit'); } catch (ErrorException $e) { diff --git a/utils/phpstan-baseline/empty.notAllowed.neon b/utils/phpstan-baseline/empty.notAllowed.neon index ebcb9dafe4e1..91a3123c5d6c 100644 --- a/utils/phpstan-baseline/empty.notAllowed.neon +++ b/utils/phpstan-baseline/empty.notAllowed.neon @@ -1,4 +1,4 @@ -# total 247 errors +# total 240 errors parameters: ignoreErrors: @@ -184,7 +184,7 @@ parameters: - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 12 + count: 5 path: ../../system/Email/Email.php - diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 9edbf37e3bc2..5c7183708c35 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 3161 errors +# total 3149 errors includes: - argument.type.neon - assign.propertyType.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index c0eff3f00d6d..9e13a5c4ff23 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 1547 errors +# total 1542 errors parameters: ignoreErrors: @@ -2552,16 +2552,6 @@ parameters: count: 1 path: ../../system/Email/Email.php - - - message: '#^Method CodeIgniter\\Email\\Email\:\:stringToArray\(\) has parameter \$email with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Email/Email.php - - - - message: '#^Method CodeIgniter\\Email\\Email\:\:stringToArray\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Email/Email.php - - message: '#^Method CodeIgniter\\Email\\Email\:\:validateEmail\(\) has parameter \$email with no value type specified in iterable type array\.$#' count: 1 @@ -2587,11 +2577,6 @@ parameters: count: 1 path: ../../system/Email/Email.php - - - message: '#^Property CodeIgniter\\Email\\Email\:\:\$bitDepths type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Email/Email.php - - message: '#^Property CodeIgniter\\Email\\Email\:\:\$debugMessage type has no value type specified in iterable type array\.$#' count: 1 @@ -2602,16 +2587,6 @@ parameters: count: 1 path: ../../system/Email/Email.php - - - message: '#^Property CodeIgniter\\Email\\Email\:\:\$priorities type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Email/Email.php - - - - message: '#^Property CodeIgniter\\Email\\Email\:\:\$protocols type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Email/Email.php - - message: '#^Property CodeIgniter\\Email\\Email\:\:\$recipients type has no value type specified in iterable type array\.$#' count: 1 From 113a0fdf523c32cad9253fc648b746a0477fa83a Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Sat, 31 May 2025 19:55:33 +0200 Subject: [PATCH 27/59] fix: crrect getHostname() fallback logic in Email class (#9587) --- system/Email/Email.php | 12 +++-- tests/system/Email/EmailTest.php | 51 +++++++++++++++++++ user_guide_src/source/changelogs/v4.6.2.rst | 1 + .../codeigniter.superglobalAccess.neon | 12 +---- utils/phpstan-baseline/loader.neon | 2 +- 5 files changed, 62 insertions(+), 16 deletions(-) diff --git a/system/Email/Email.php b/system/Email/Email.php index 727419832c62..3e4cd5671a97 100644 --- a/system/Email/Email.php +++ b/system/Email/Email.php @@ -2142,12 +2142,16 @@ protected function getSMTPData() */ protected function getHostname() { - if (isset($_SERVER['SERVER_NAME'])) { - return $_SERVER['SERVER_NAME']; + $superglobals = service('superglobals'); + + $serverName = $superglobals->server('SERVER_NAME'); + if (! in_array($serverName, [null, ''], true)) { + return $serverName; } - if (isset($_SERVER['SERVER_ADDR'])) { - return '[' . $_SERVER['SERVER_ADDR'] . ']'; + $serverAddr = $superglobals->server('SERVER_ADDR'); + if (! in_array($serverAddr, [null, ''], true)) { + return '[' . $serverAddr . ']'; } $hostname = gethostname(); diff --git a/tests/system/Email/EmailTest.php b/tests/system/Email/EmailTest.php index ca26743c3ddc..63e82a0a72ba 100644 --- a/tests/system/Email/EmailTest.php +++ b/tests/system/Email/EmailTest.php @@ -16,9 +16,11 @@ use CodeIgniter\Events\Events; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockEmail; +use CodeIgniter\Test\ReflectionHelper; use ErrorException; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use ReflectionException; /** * @internal @@ -26,6 +28,8 @@ #[Group('Others')] final class EmailTest extends CIUnitTestCase { + use ReflectionHelper; + public function testEmailValidation(): void { $config = config('Email'); @@ -215,4 +219,51 @@ public function testSetAttachmentCIDBufferString(): void $email->archive['body'], ); } + + /** + * @throws ReflectionException + */ + public function testGetHostnameUsesServerName(): void + { + $email = $this->createMockEmail(); + + $superglobals = service('superglobals'); + $superglobals->setServer('SERVER_NAME', 'example.test'); + + $getHostname = self::getPrivateMethodInvoker($email, 'getHostname'); + + $this->assertSame('example.test', $getHostname()); + } + + /** + * @throws ReflectionException + */ + public function testGetHostnameUsesServerAddr(): void + { + $email = $this->createMockEmail(); + + $superglobals = service('superglobals'); + $superglobals->setServer('SERVER_NAME', ''); + $superglobals->setServer('SERVER_ADDR', '192.168.1.10'); + + $getHostname = self::getPrivateMethodInvoker($email, 'getHostname'); + + $this->assertSame('[192.168.1.10]', $getHostname()); + } + + /** + * @throws ReflectionException + */ + public function testGetHostnameFallsBackToGethostnameFunction(): void + { + $email = $this->createMockEmail(); + + $superglobals = service('superglobals'); + $superglobals->setServer('SERVER_NAME', ''); + $superglobals->setServer('SERVER_ADDR', ''); + + $getHostname = self::getPrivateMethodInvoker($email, 'getHostname'); + + $this->assertSame(gethostname(), $getHostname()); + } } diff --git a/user_guide_src/source/changelogs/v4.6.2.rst b/user_guide_src/source/changelogs/v4.6.2.rst index 936c0e709046..676d1a731ee9 100644 --- a/user_guide_src/source/changelogs/v4.6.2.rst +++ b/user_guide_src/source/changelogs/v4.6.2.rst @@ -37,6 +37,7 @@ Bugs Fixed - **Cache:** Fixed a bug where a corrupted or unreadable cache file could cause an unhandled exception in ``FileHandler::getItem()``. - **Database:** Fixed a bug where ``when()`` and ``whenNot()`` in ``ConditionalTrait`` incorrectly evaluated certain falsy values (such as ``[]``, ``0``, ``0.0``, and ``'0'``) as truthy, causing callbacks to be executed unexpectedly. These methods now cast the condition to a boolean using ``(bool)`` to ensure consistent behavior with PHP's native truthiness. +- **Email:** Fixed a bug where ``Email::getHostname()`` failed to use ``$_SERVER['SERVER_ADDR']`` when ``$_SERVER['SERVER_NAME']`` was not set. - **Security:** Fixed a bug where the ``sanitize_filename()`` function from the Security helper would throw an error when used in CLI requests. - **Session:** Fixed a bug where using the ``DatabaseHandler`` with an unsupported database driver (such as ``SQLSRV``, ``OCI8``, or ``SQLite3``) did not throw an appropriate error. diff --git a/utils/phpstan-baseline/codeigniter.superglobalAccess.neon b/utils/phpstan-baseline/codeigniter.superglobalAccess.neon index a12ee94e1fff..893b1cd96c62 100644 --- a/utils/phpstan-baseline/codeigniter.superglobalAccess.neon +++ b/utils/phpstan-baseline/codeigniter.superglobalAccess.neon @@ -1,4 +1,4 @@ -# total 83 errors +# total 79 errors parameters: ignoreErrors: @@ -67,16 +67,6 @@ parameters: count: 1 path: ../../system/Config/Services.php - - - message: '#^Accessing offset ''SERVER_ADDR'' directly on \$_SERVER is discouraged\.$#' - count: 2 - path: ../../system/Email/Email.php - - - - message: '#^Accessing offset ''SERVER_NAME'' directly on \$_SERVER is discouraged\.$#' - count: 2 - path: ../../system/Email/Email.php - - message: '#^Accessing offset ''HTTP_USER_AGENT'' directly on \$_SERVER is discouraged\.$#' count: 2 diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 5c7183708c35..643d42e8c95f 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 3149 errors +# total 3145 errors includes: - argument.type.neon - assign.propertyType.neon From 88ec911ab2a9f9368e7dfb526cfc92f47115e4e0 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Tue, 3 Jun 2025 14:53:07 +0800 Subject: [PATCH 28/59] refactor: fix phpstan errors in `ResponseTrait` (#9591) --- system/API/ResponseTrait.php | 37 +-- tests/system/API/ResponseTraitTest.php | 61 +++-- .../phpstan-baseline/assign.propertyType.neon | 7 +- .../codeigniter.superglobalAccessAssign.neon | 7 +- utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 257 +----------------- .../missingType.parameter.neon | 57 +--- .../missingType.property.neon | 17 +- 8 files changed, 65 insertions(+), 380 deletions(-) diff --git a/system/API/ResponseTrait.php b/system/API/ResponseTrait.php index a2e93bfd309f..318af4b28bea 100644 --- a/system/API/ResponseTrait.php +++ b/system/API/ResponseTrait.php @@ -13,8 +13,10 @@ namespace CodeIgniter\API; +use CodeIgniter\Format\Format; use CodeIgniter\Format\FormatterInterface; use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; /** @@ -22,8 +24,10 @@ * consistent HTTP responses under a variety of common * situations when working as an API. * - * @property bool $stringAsHtml Whether to treat string data as HTML in JSON response. - * Setting `true` is only for backward compatibility. + * @property RequestInterface $request + * @property ResponseInterface $response + * @property bool $stringAsHtml Whether to treat string data as HTML in JSON response. + * Setting `true` is only for backward compatibility. */ trait ResponseTrait { @@ -84,7 +88,7 @@ trait ResponseTrait * Provides a single, simple method to return an API response, formatted * to match the requested format, with proper content-type and status code. * - * @param array|string|null $data + * @param array|string|null $data * * @return ResponseInterface */ @@ -118,9 +122,9 @@ protected function respond($data = null, ?int $status = null, string $message = /** * Used for generic failures that no custom methods exist for. * - * @param array|string $messages - * @param int $status HTTP status code - * @param string|null $code Custom, API-specific, error code + * @param list|string $messages + * @param int $status HTTP status code + * @param string|null $code Custom, API-specific, error code * * @return ResponseInterface */ @@ -146,7 +150,7 @@ protected function fail($messages, int $status = 400, ?string $code = null, stri /** * Used after successfully creating a new resource. * - * @param array|string|null $data + * @param array|string|null $data * * @return ResponseInterface */ @@ -158,7 +162,7 @@ protected function respondCreated($data = null, string $message = '') /** * Used after a resource has been successfully deleted. * - * @param array|string|null $data + * @param array|string|null $data * * @return ResponseInterface */ @@ -170,7 +174,7 @@ protected function respondDeleted($data = null, string $message = '') /** * Used after a resource has been successfully updated. * - * @param array|string|null $data + * @param array|string|null $data * * @return ResponseInterface */ @@ -287,15 +291,17 @@ protected function failServerError(string $description = 'Internal Server Error' * Handles formatting a response. Currently, makes some heavy assumptions * and needs updating! :) * - * @param array|string|null $data + * @param array|string|null $data * * @return string|null */ protected function format($data = null) { + /** @var Format $format */ $format = service('format'); - $mime = ($this->format === null) ? $format->getConfig()->supportedResponseFormats[0] + $mime = $this->format === null + ? $format->getConfig()->supportedResponseFormats[0] : "application/{$this->format}"; // Determine correct response type through content negotiation if not explicitly declared @@ -313,14 +319,10 @@ protected function format($data = null) $this->response->setContentType($mime); // if we don't have a formatter, make one - if (! isset($this->formatter)) { - // if no formatter, use the default - $this->formatter = $format->getFormatter($mime); - } + $this->formatter ??= $format->getFormatter($mime); $asHtml = $this->stringAsHtml ?? false; - // Returns as HTML. if ( ($mime === 'application/json' && $asHtml && is_string($data)) || ($mime !== 'application/json' && is_string($data)) @@ -338,6 +340,7 @@ protected function format($data = null) if ($mime !== 'application/json') { // Recursively convert objects into associative arrays // Conversion not required for JSONFormatter + /** @var array|string|null $data */ $data = json_decode(json_encode($data), true); } @@ -353,7 +356,7 @@ protected function format($data = null) */ protected function setResponseFormat(?string $format = null) { - $this->format = ($format === null) ? null : strtolower($format); + $this->format = $format === null ? null : strtolower($format); return $this; } diff --git a/tests/system/API/ResponseTraitTest.php b/tests/system/API/ResponseTraitTest.php index bc61dd06c81d..0443d90dc391 100644 --- a/tests/system/API/ResponseTraitTest.php +++ b/tests/system/API/ResponseTraitTest.php @@ -17,6 +17,8 @@ use CodeIgniter\Format\FormatterInterface; use CodeIgniter\Format\JSONFormatter; use CodeIgniter\Format\XMLFormatter; +use CodeIgniter\HTTP\RequestInterface; +use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\HTTP\SiteURI; use CodeIgniter\HTTP\UserAgent; use CodeIgniter\Test\CIUnitTestCase; @@ -24,6 +26,7 @@ use CodeIgniter\Test\Mock\MockResponse; use Config\App; use Config\Cookie; +use Config\Services; use PHPUnit\Framework\Attributes\Group; use stdClass; @@ -82,6 +85,12 @@ private function createCookieConfig(): Cookie return $cookie; } + /** + * @param array $userHeaders + * + * @phpstan-assert RequestInterface $this->request + * @phpstan-assert ResponseInterface $this->response + */ private function createRequestAndResponse(string $routePath = '', array $userHeaders = []): void { $config = $this->createAppConfig(); @@ -97,29 +106,28 @@ private function createRequestAndResponse(string $routePath = '', array $userHea $this->response = new MockResponse($config); } - // Insert headers into request. - $headers = [ - 'Accept' => 'text/html', - ]; - $headers = array_merge($headers, $userHeaders); + $headers = array_merge(['Accept' => 'text/html'], $userHeaders); foreach ($headers as $key => $value) { $this->request->setHeader($key, $value); } } + /** + * @param array $userHeaders + */ protected function makeController(string $routePath = '', array $userHeaders = []): object { $this->createRequestAndResponse($routePath, $userHeaders); - // Create the controller class finally. return new class ($this->request, $this->response, $this->formatter) { use ResponseTrait; - protected $formatter; - - public function __construct(protected $request, protected $response, $formatter) - { + public function __construct( + protected RequestInterface $request, + protected ResponseInterface $response, + ?FormatterInterface $formatter, + ) { $this->formatter = $formatter; } @@ -173,11 +181,13 @@ public function testNoFormatterWithStringAsHtmlTrue(): void $controller = new class ($this->request, $this->response, $this->formatter) { use ResponseTrait; - protected $formatter; protected bool $stringAsHtml = true; - public function __construct(protected $request, protected $response, $formatter) - { + public function __construct( + protected RequestInterface $request, + protected ResponseInterface $response, + ?FormatterInterface $formatter, + ) { $this->formatter = $formatter; } }; @@ -291,11 +301,13 @@ public function testRespondSetsCorrectBodyAndStatusWithStringAsHtmlTrue(): void $controller = new class ($this->request, $this->response, $this->formatter) { use ResponseTrait; - protected $formatter; protected bool $stringAsHtml = true; - public function __construct(protected $request, protected $response, $formatter) - { + public function __construct( + protected RequestInterface $request, + protected ResponseInterface $response, + ?FormatterInterface $formatter, + ) { $this->formatter = $formatter; } }; @@ -546,8 +558,8 @@ public function testValidContentTypes(): void private function tryValidContentType(string $mimeType, string $contentType): void { - $original = $_SERVER; - $_SERVER['CONTENT_TYPE'] = $mimeType; + $originalContentType = Services::superglobals()->server('CONTENT_TYPE') ?? ''; + Services::superglobals()->setServer('CONTENT_TYPE', $mimeType); $this->makeController('', ['Accept' => $mimeType]); $this->assertSame( @@ -563,7 +575,7 @@ private function tryValidContentType(string $mimeType, string $contentType): voi 'Response header pre-response...', ); - $_SERVER = $original; + Services::superglobals()->setServer('CONTENT_TYPE', $originalContentType); } public function testValidResponses(): void @@ -609,9 +621,11 @@ public function testFormatByRequestNegotiateIfFormatIsNotJsonOrXML(): void $controller = new class ($request, $response) { use ResponseTrait; - public function __construct(protected $request, protected $response) - { - $this->format = 'txt'; + public function __construct( + protected RequestInterface $request, + protected ResponseInterface $response, + ) { + $this->format = 'txt'; // @phpstan-ignore assign.propertyType (needed for testing) } }; @@ -659,6 +673,9 @@ public function testXMLResponseFormat(): void $this->assertSame($xmlFormatter->format($data), $this->response->getXML()); } + /** + * @param list $args + */ private function invoke(object $controller, string $method, array $args = []): object { $method = self::getPrivateMethodInvoker($controller, $method); diff --git a/utils/phpstan-baseline/assign.propertyType.neon b/utils/phpstan-baseline/assign.propertyType.neon index 76c1a55f3756..813993f45e10 100644 --- a/utils/phpstan-baseline/assign.propertyType.neon +++ b/utils/phpstan-baseline/assign.propertyType.neon @@ -1,4 +1,4 @@ -# total 29 errors +# total 28 errors parameters: ignoreErrors: @@ -7,11 +7,6 @@ parameters: count: 1 path: ../../system/Controller.php - - - message: '#^Property class@anonymous/tests/system/API/ResponseTraitTest\.php\:609\:\:\$format \(''html''\|''json''\|''xml''\|null\) does not accept ''txt''\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - message: '#^Property CodeIgniter\\Commands\\Utilities\\Routes\\FilterFinderTest\:\:\$response \(CodeIgniter\\HTTP\\Response\) does not accept CodeIgniter\\HTTP\\ResponseInterface\.$#' count: 1 diff --git a/utils/phpstan-baseline/codeigniter.superglobalAccessAssign.neon b/utils/phpstan-baseline/codeigniter.superglobalAccessAssign.neon index 665b2a986ce5..3322897a77f8 100644 --- a/utils/phpstan-baseline/codeigniter.superglobalAccessAssign.neon +++ b/utils/phpstan-baseline/codeigniter.superglobalAccessAssign.neon @@ -1,4 +1,4 @@ -# total 582 errors +# total 581 errors parameters: ignoreErrors: @@ -17,11 +17,6 @@ parameters: count: 3 path: ../../system/HTTP/IncomingRequest.php - - - message: '#^Assigning string directly on offset ''CONTENT_TYPE'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - message: '#^Assigning 3 directly on offset ''argc'' of \$_SERVER is discouraged\.$#' count: 1 diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 643d42e8c95f..4754c3cd6c9b 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 3145 errors +# total 3075 errors includes: - argument.type.neon - assign.propertyType.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 9e13a5c4ff23..5e998fc3949e 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 1542 errors +# total 1491 errors parameters: ignoreErrors: @@ -2252,36 +2252,6 @@ parameters: count: 1 path: ../../system/Debug/BaseExceptionHandler.php - - - message: '#^Method CodeIgniter\\Debug\\ExceptionHandler\:\:fail\(\) has parameter \$messages with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/ExceptionHandler.php - - - - message: '#^Method CodeIgniter\\Debug\\ExceptionHandler\:\:format\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/ExceptionHandler.php - - - - message: '#^Method CodeIgniter\\Debug\\ExceptionHandler\:\:respond\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/ExceptionHandler.php - - - - message: '#^Method CodeIgniter\\Debug\\ExceptionHandler\:\:respondCreated\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/ExceptionHandler.php - - - - message: '#^Method CodeIgniter\\Debug\\ExceptionHandler\:\:respondDeleted\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/ExceptionHandler.php - - - - message: '#^Method CodeIgniter\\Debug\\ExceptionHandler\:\:respondUpdated\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/ExceptionHandler.php - - message: '#^Method CodeIgniter\\Debug\\Exceptions\:\:collectVars\(\) return type has no value type specified in iterable type array\.$#' count: 1 @@ -2292,16 +2262,6 @@ parameters: count: 1 path: ../../system/Debug/Exceptions.php - - - message: '#^Method CodeIgniter\\Debug\\Exceptions\:\:fail\(\) has parameter \$messages with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Exceptions.php - - - - message: '#^Method CodeIgniter\\Debug\\Exceptions\:\:format\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Exceptions.php - - message: '#^Method CodeIgniter\\Debug\\Exceptions\:\:maskData\(\) has parameter \$args with no value type specified in iterable type array\.$#' count: 1 @@ -2337,26 +2297,6 @@ parameters: count: 1 path: ../../system/Debug/Exceptions.php - - - message: '#^Method CodeIgniter\\Debug\\Exceptions\:\:respond\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Exceptions.php - - - - message: '#^Method CodeIgniter\\Debug\\Exceptions\:\:respondCreated\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Exceptions.php - - - - message: '#^Method CodeIgniter\\Debug\\Exceptions\:\:respondDeleted\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Exceptions.php - - - - message: '#^Method CodeIgniter\\Debug\\Exceptions\:\:respondUpdated\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Exceptions.php - - message: '#^Property CodeIgniter\\Debug\\Iterator\:\:\$results type has no value type specified in iterable type array\.$#' count: 1 @@ -4607,36 +4547,6 @@ parameters: count: 1 path: ../../system/Publisher/Publisher.php - - - message: '#^Method CodeIgniter\\RESTful\\ResourceController\:\:fail\(\) has parameter \$messages with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/RESTful/ResourceController.php - - - - message: '#^Method CodeIgniter\\RESTful\\ResourceController\:\:format\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/RESTful/ResourceController.php - - - - message: '#^Method CodeIgniter\\RESTful\\ResourceController\:\:respond\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/RESTful/ResourceController.php - - - - message: '#^Method CodeIgniter\\RESTful\\ResourceController\:\:respondCreated\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/RESTful/ResourceController.php - - - - message: '#^Method CodeIgniter\\RESTful\\ResourceController\:\:respondDeleted\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/RESTful/ResourceController.php - - - - message: '#^Method CodeIgniter\\RESTful\\ResourceController\:\:respondUpdated\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/RESTful/ResourceController.php - - message: '#^Method CodeIgniter\\Router\\AutoRouter\:\:getRoute\(\) return type has no value type specified in iterable type array\.$#' count: 1 @@ -5172,36 +5082,6 @@ parameters: count: 1 path: ../../system/Test/Mock/MockLanguage.php - - - message: '#^Method CodeIgniter\\Test\\Mock\\MockResourcePresenter\:\:fail\(\) has parameter \$messages with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Mock/MockResourcePresenter.php - - - - message: '#^Method CodeIgniter\\Test\\Mock\\MockResourcePresenter\:\:format\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Mock/MockResourcePresenter.php - - - - message: '#^Method CodeIgniter\\Test\\Mock\\MockResourcePresenter\:\:respond\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Mock/MockResourcePresenter.php - - - - message: '#^Method CodeIgniter\\Test\\Mock\\MockResourcePresenter\:\:respondCreated\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Mock/MockResourcePresenter.php - - - - message: '#^Method CodeIgniter\\Test\\Mock\\MockResourcePresenter\:\:respondDeleted\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Mock/MockResourcePresenter.php - - - - message: '#^Method CodeIgniter\\Test\\Mock\\MockResourcePresenter\:\:respondUpdated\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Mock/MockResourcePresenter.php - - message: '#^Method CodeIgniter\\Test\\Mock\\MockResult\:\:getFieldData\(\) return type has no value type specified in iterable type array\.$#' count: 1 @@ -5852,141 +5732,6 @@ parameters: count: 1 path: ../../system/View/Parser.php - - - message: '#^Method CodeIgniter\\API\\ResponseTraitTest\:\:createRequestAndResponse\(\) has parameter \$userHeaders with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method CodeIgniter\\API\\ResponseTraitTest\:\:invoke\(\) has parameter \$args with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method CodeIgniter\\API\\ResponseTraitTest\:\:makeController\(\) has parameter \$userHeaders with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:116\:\:fail\(\) has parameter \$messages with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:116\:\:format\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:116\:\:respond\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:116\:\:respondCreated\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:116\:\:respondDeleted\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:116\:\:respondUpdated\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:173\:\:fail\(\) has parameter \$messages with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:173\:\:format\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:173\:\:respond\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:173\:\:respondCreated\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:173\:\:respondDeleted\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:173\:\:respondUpdated\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:291\:\:fail\(\) has parameter \$messages with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:291\:\:format\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:291\:\:respond\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:291\:\:respondCreated\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:291\:\:respondDeleted\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:291\:\:respondUpdated\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:609\:\:fail\(\) has parameter \$messages with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:609\:\:format\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:609\:\:respond\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:609\:\:respondCreated\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:609\:\:respondDeleted\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:609\:\:respondUpdated\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - message: '#^Method CodeIgniter\\AutoReview\\ComposerJsonTest\:\:checkConfig\(\) has parameter \$fromComponent with no value type specified in iterable type array\.$#' count: 1 diff --git a/utils/phpstan-baseline/missingType.parameter.neon b/utils/phpstan-baseline/missingType.parameter.neon index a21867833318..ec340a803f7c 100644 --- a/utils/phpstan-baseline/missingType.parameter.neon +++ b/utils/phpstan-baseline/missingType.parameter.neon @@ -1,4 +1,4 @@ -# total 47 errors +# total 36 errors parameters: ignoreErrors: @@ -12,61 +12,6 @@ parameters: count: 1 path: ../../system/Database/PreparedQueryInterface.php - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:116\:\:__construct\(\) has parameter \$formatter with no type specified\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:116\:\:__construct\(\) has parameter \$request with no type specified\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:116\:\:__construct\(\) has parameter \$response with no type specified\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:173\:\:__construct\(\) has parameter \$formatter with no type specified\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:173\:\:__construct\(\) has parameter \$request with no type specified\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:173\:\:__construct\(\) has parameter \$response with no type specified\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:291\:\:__construct\(\) has parameter \$formatter with no type specified\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:291\:\:__construct\(\) has parameter \$request with no type specified\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:291\:\:__construct\(\) has parameter \$response with no type specified\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:609\:\:__construct\(\) has parameter \$request with no type specified\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Method class@anonymous/tests/system/API/ResponseTraitTest\.php\:609\:\:__construct\(\) has parameter \$response with no type specified\.$#' - count: 1 - path: ../../tests/system/API/ResponseTraitTest.php - - message: '#^Method CodeIgniter\\Commands\\Utilities\\Routes\\AutoRouterImproved\\Controllers\\Dash_folder\\Dash_controller\:\:getDash_method\(\) has parameter \$p1 with no type specified\.$#' count: 1 diff --git a/utils/phpstan-baseline/missingType.property.neon b/utils/phpstan-baseline/missingType.property.neon index a739a7b09849..dd29a69724d9 100644 --- a/utils/phpstan-baseline/missingType.property.neon +++ b/utils/phpstan-baseline/missingType.property.neon @@ -1,4 +1,4 @@ -# total 115 errors +# total 109 errors parameters: ignoreErrors: @@ -67,21 +67,6 @@ parameters: count: 1 path: ../../system/Test/Mock/MockSession.php - - - message: '#^Property class@anonymous/tests/system/API/ResponseTraitTest\.php\:116\:\:\$formatter has no type specified\.$#' - count: 2 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Property class@anonymous/tests/system/API/ResponseTraitTest\.php\:173\:\:\$formatter has no type specified\.$#' - count: 2 - path: ../../tests/system/API/ResponseTraitTest.php - - - - message: '#^Property class@anonymous/tests/system/API/ResponseTraitTest\.php\:291\:\:\$formatter has no type specified\.$#' - count: 2 - path: ../../tests/system/API/ResponseTraitTest.php - - message: '#^Property CodeIgniter\\Config\\Factory@anonymous/tests/system/Config/FactoriesTest\.php\:89\:\:\$widgets has no type specified\.$#' count: 1 From a19b44c2300611a45ae041b745d14c5c68b442c6 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Sun, 1 Jun 2025 09:48:45 +0800 Subject: [PATCH 29/59] chore: fix psalm error on 4.7 (#9590) --- psalm_autoload.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/psalm_autoload.php b/psalm_autoload.php index 6ebe830423b5..852ef4d6aab3 100644 --- a/psalm_autoload.php +++ b/psalm_autoload.php @@ -10,10 +10,13 @@ 'tests/system/Config/fixtures', ]; $excludeDirs = [ - 'tests/_support/Config', 'tests/_support/View/Cells', 'tests/_support/View/Views', ]; +$excludeFiles = [ + 'tests/_support/Config/Filters.php', + 'tests/_support/Config/Routes.php', +]; foreach ($directories as $directory) { $iterator = new RecursiveIteratorIterator( @@ -38,6 +41,10 @@ continue; } + if (in_array($file->getPathname(), $excludeFiles, true)) { + continue; + } + require_once $file->getPathname(); } } From c2f9731f8a7ca87dbc99bbf8d2efa6f6955151af Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Wed, 4 Jun 2025 01:33:46 +0800 Subject: [PATCH 30/59] refactor: precise PHPDocs for Autoloader (#9593) --- system/Autoloader/Autoloader.php | 48 +++++++++---------- system/Autoloader/FileLocator.php | 25 +++++----- system/Autoloader/FileLocatorCached.php | 2 +- system/Autoloader/FileLocatorInterface.php | 8 ++-- tests/system/Autoloader/AutoloaderTest.php | 2 +- utils/phpstan-baseline/loader.neon | 2 +- .../method.alreadyNarrowedType.neon | 7 +-- 7 files changed, 45 insertions(+), 49 deletions(-) diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index 86d922c33302..65c535e8611e 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -21,7 +21,7 @@ use Config\Autoload; use Config\Kint as KintConfig; use Config\Modules; -use Kint; +use Kint\Kint; use Kint\Renderer\CliRenderer; use Kint\Renderer\RichRenderer; @@ -67,21 +67,21 @@ class Autoloader /** * Stores namespaces as key, and path as values. * - * @var array> + * @var array> */ protected $prefixes = []; /** * Stores class name as key, and path as values. * - * @var array + * @var array */ protected $classmap = []; /** * Stores files as a list. * - * @var list + * @var list */ protected $files = []; @@ -89,7 +89,7 @@ class Autoloader * Stores helper list. * Always load the URL helper, it should be used in most apps. * - * @var list + * @var list */ protected $helpers = ['url']; @@ -147,36 +147,35 @@ private function loadComposerAutoloader(Modules $modules): void // Should we load through Composer's namespaces, also? if ($modules->discoverInComposer) { - // @phpstan-ignore-next-line - $this->loadComposerNamespaces($composer, $modules->composerPackages ?? []); + $composerPackages = $modules->composerPackages; + $this->loadComposerNamespaces($composer, $composerPackages ?? []); } unset($composer); } /** - * Register the loader with the SPL autoloader stack. + * Register the loader with the SPL autoloader stack + * in the following order: + * + * 1. Classmap loader + * 2. PSR-4 autoloader + * 3. Non-class files * * @return void */ public function register() { - // Register classmap loader for the files in our class map. spl_autoload_register($this->loadClassmap(...), true); - - // Register the PSR-4 autoloader. spl_autoload_register($this->loadClass(...), true); - // Load our non-class files foreach ($this->files as $file) { $this->includeFile($file); } } /** - * Unregister autoloader. - * - * This method is for testing. + * Unregisters the autoloader from the SPL autoload stack. */ public function unregister(): void { @@ -187,7 +186,7 @@ public function unregister(): void /** * Registers namespaces with the autoloader. * - * @param array|string>|string $namespace + * @param array|non-empty-string>|non-empty-string $namespace * * @return $this */ @@ -219,7 +218,7 @@ public function addNamespace($namespace, ?string $path = null) * * If a prefix param is set, returns only paths to the given prefix. * - * @return ($prefix is null ? array> : list) + * @return ($prefix is null ? array> : list) */ public function getNamespace(?string $prefix = null) { @@ -247,6 +246,8 @@ public function removeNamespace(string $namespace) /** * Load a class using available class mapping. * + * @param class-string $class The fully qualified class name. + * * @internal For `spl_autoload_register` use. */ public function loadClassmap(string $class): void @@ -261,9 +262,9 @@ public function loadClassmap(string $class): void /** * Loads the class file for a given class name. * - * @internal For `spl_autoload_register` use. + * @param class-string $class The fully qualified class name. * - * @param string $class The fully qualified class name. + * @internal For `spl_autoload_register` use. */ public function loadClass(string $class): void { @@ -273,9 +274,9 @@ public function loadClass(string $class): void /** * Loads the class file for a given class name. * - * @param string $class The fully-qualified class name + * @param class-string $class The fully qualified class name. * - * @return false|string The mapped file name on success, or boolean false on fail + * @return false|non-empty-string The mapped file name on success, or boolean false on fail */ protected function loadInNamespace(string $class) { @@ -293,21 +294,20 @@ protected function loadInNamespace(string $class) $filePath = $directory . $relativeClassPath . '.php'; $filename = $this->includeFile($filePath); - if ($filename) { + if ($filename !== false) { return $filename; } } } } - // never found a mapped file return false; } /** * A central way to include a file. Split out primarily for testing purposes. * - * @return false|string The filename on success, false if the file is not loaded + * @return false|non-empty-string The filename on success, false if the file is not loaded */ protected function includeFile(string $file) { diff --git a/system/Autoloader/FileLocator.php b/system/Autoloader/FileLocator.php index 13ecb817a56c..f67708a4e836 100644 --- a/system/Autoloader/FileLocator.php +++ b/system/Autoloader/FileLocator.php @@ -44,7 +44,7 @@ public function __construct(Autoloader $autoloader) * Attempts to locate a file by examining the name for a namespace * and looking through the PSR-4 namespaced files that we know about. * - * @param string $file The relative file path or namespaced file to + * @param non-empty-string $file The relative file path or namespaced file to * locate. If not namespaced, search in the app * folder. * @param non-empty-string|null $folder The folder within the namespace that we should @@ -53,7 +53,7 @@ public function __construct(Autoloader $autoloader) * folder. * @param string $ext The file extension the file should have. * - * @return false|string The path to the file, or false if not found. + * @return false|non-empty-string The path to the file, or false if not found. */ public function locateFile(string $file, ?string $folder = null, string $ext = 'php') { @@ -156,9 +156,11 @@ public function getClassname(string $file): string $dlm = false; } - if (($tokens[$i - 2][0] === T_CLASS || (isset($tokens[$i - 2][1]) && $tokens[$i - 2][1] === 'phpclass')) + if ( + ($tokens[$i - 2][0] === T_CLASS || (isset($tokens[$i - 2][1]) && $tokens[$i - 2][1] === 'phpclass')) && $tokens[$i - 1][0] === T_WHITESPACE - && $token[0] === T_STRING) { + && $token[0] === T_STRING + ) { $className = $token[1]; break; } @@ -184,7 +186,7 @@ public function getClassname(string $file): string * 'app/Modules/bar/Config/Routes.php', * ] * - * @return list + * @return list */ public function search(string $path, string $ext = 'php', bool $prioritizeApp = true): array { @@ -213,7 +215,6 @@ public function search(string $path, string $ext = 'php', bool $prioritizeApp = $foundPaths = [...$foundPaths, ...$appPaths]; } - // Remove any duplicates return array_values(array_unique($foundPaths)); } @@ -236,13 +237,12 @@ protected function ensureExt(string $path, string $ext): string /** * Return the namespace mappings we know about. * - * @return array> + * @return list */ protected function getNamespaces() { $namespaces = []; - // Save system for last $system = []; foreach ($this->autoloader->getNamespace() as $prefix => $paths) { @@ -263,6 +263,7 @@ protected function getNamespaces() } } + // Save system for last return array_merge($namespaces, $system); } @@ -295,19 +296,17 @@ public function findQualifiedNameFromPath(string $path) ); // Remove the file extension (.php) - /** @var class-string */ + /** @var class-string $className */ $className = mb_substr($className, 0, -4); if (in_array($className, $this->invalidClassnames, true)) { continue; } - // Check if this exists if (class_exists($className)) { return $className; } - // If the class does not exist, it is an invalid classname. $this->invalidClassnames[] = $className; } } @@ -353,11 +352,11 @@ public function listFiles(string $path): array * Scans the provided namespace, returning a list of all files * that are contained within the sub path specified by $path. * - * @return list List of file paths + * @return list List of file paths */ public function listNamespaceFiles(string $prefix, string $path): array { - if ($path === '' || ($prefix === '')) { + if ($path === '' || $prefix === '') { return []; } diff --git a/system/Autoloader/FileLocatorCached.php b/system/Autoloader/FileLocatorCached.php index 8b17399444c6..0aa267b2a84b 100644 --- a/system/Autoloader/FileLocatorCached.php +++ b/system/Autoloader/FileLocatorCached.php @@ -117,7 +117,7 @@ public function getClassname(string $file): string } /** - * @return list + * @return list */ public function search(string $path, string $ext = 'php', bool $prioritizeApp = true): array { diff --git a/system/Autoloader/FileLocatorInterface.php b/system/Autoloader/FileLocatorInterface.php index 8f6129abee17..0422ec2519db 100644 --- a/system/Autoloader/FileLocatorInterface.php +++ b/system/Autoloader/FileLocatorInterface.php @@ -23,7 +23,7 @@ interface FileLocatorInterface * Attempts to locate a file by examining the name for a namespace * and looking through the PSR-4 namespaced files that we know about. * - * @param string $file The relative file path or namespaced file to + * @param non-empty-string $file The relative file path or namespaced file to * locate. If not namespaced, search in the app * folder. * @param non-empty-string|null $folder The folder within the namespace that we should @@ -32,12 +32,14 @@ interface FileLocatorInterface * folder. * @param string $ext The file extension the file should have. * - * @return false|string The path to the file, or false if not found. + * @return false|non-empty-string The path to the file, or false if not found. */ public function locateFile(string $file, ?string $folder = null, string $ext = 'php'); /** * Examines a file and returns the fully qualified class name. + * + * @param non-empty-string $file */ public function getClassname(string $file): string; @@ -54,7 +56,7 @@ public function getClassname(string $file): string; * 'app/Modules/bar/Config/Routes.php', * ] * - * @return list + * @return list */ public function search(string $path, string $ext = 'php', bool $prioritizeApp = true): array; diff --git a/tests/system/Autoloader/AutoloaderTest.php b/tests/system/Autoloader/AutoloaderTest.php index af5340400160..cd73356958fe 100644 --- a/tests/system/Autoloader/AutoloaderTest.php +++ b/tests/system/Autoloader/AutoloaderTest.php @@ -394,7 +394,7 @@ public function testAutoloaderLoadsNonClassFiles(): void $this->assertTrue(function_exists('autoload_foo')); $this->assertSame('I am autoloaded by Autoloader through $files!', autoload_foo()); - $this->assertTrue(defined('AUTOLOAD_CONSTANT')); + $this->assertTrue(defined('AUTOLOAD_CONSTANT')); // @phpstan-ignore method.alreadyNarrowedType $this->assertSame('foo', AUTOLOAD_CONSTANT); $loader->unregister(); diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 4754c3cd6c9b..00cce6e6c73b 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 3075 errors +# total 3074 errors includes: - argument.type.neon - assign.propertyType.neon diff --git a/utils/phpstan-baseline/method.alreadyNarrowedType.neon b/utils/phpstan-baseline/method.alreadyNarrowedType.neon index 23132319175b..f980738a073a 100644 --- a/utils/phpstan-baseline/method.alreadyNarrowedType.neon +++ b/utils/phpstan-baseline/method.alreadyNarrowedType.neon @@ -1,4 +1,4 @@ -# total 24 errors +# total 23 errors parameters: ignoreErrors: @@ -7,11 +7,6 @@ parameters: count: 1 path: ../../admin/starter/tests/unit/HealthTest.php - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertTrue\(\) with bool will always evaluate to true\.$#' - count: 1 - path: ../../tests/system/Autoloader/AutoloaderTest.php - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertIsBool\(\) with bool will always evaluate to true\.$#' count: 1 From af6835887823c3045657df78b065ed8ac80e3e99 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Wed, 4 Jun 2025 01:57:36 +0800 Subject: [PATCH 31/59] refactor: fix phpstan errors in mock classes (#9594) --- system/Database/BaseBuilder.php | 2 +- system/Database/BaseResult.php | 6 +- system/Database/MySQLi/Builder.php | 2 +- system/Database/Postgre/Builder.php | 2 +- system/Database/SQLite3/Builder.php | 2 +- system/HTTP/CURLRequest.php | 2 + system/Test/Mock/MockBuilder.php | 3 + system/Test/Mock/MockCLIConfig.php | 21 ++++--- system/Test/Mock/MockCURLRequest.php | 11 +++- system/Test/Mock/MockCache.php | 5 +- system/Test/Mock/MockConnection.php | 49 ++++++++++----- system/Test/Mock/MockEvents.php | 4 +- system/Test/Mock/MockInputOutput.php | 2 + system/Test/Mock/MockResult.php | 14 +++-- system/Test/Mock/MockServices.php | 7 +++ system/Test/Mock/MockSession.php | 4 +- tests/system/Database/BaseConnectionTest.php | 33 +++++++--- tests/system/Database/Builder/InsertTest.php | 8 +-- tests/system/Database/Builder/UpdateTest.php | 4 +- utils/phpstan-baseline/loader.neon | 2 +- .../method.childReturnType.neon | 32 +--------- .../missingType.iterableValue.neon | 62 +------------------ .../missingType.property.neon | 32 +--------- 23 files changed, 126 insertions(+), 183 deletions(-) diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index cba7f5cbb8b4..82442fbca2b1 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -256,7 +256,7 @@ class BaseBuilder * Specifies which sql statements * support the ignore option. * - * @var array + * @var array */ protected $supportedIgnoreStatements = []; diff --git a/system/Database/BaseResult.php b/system/Database/BaseResult.php index fc2393edffee..c0bdc2aa1025 100644 --- a/system/Database/BaseResult.php +++ b/system/Database/BaseResult.php @@ -529,9 +529,9 @@ abstract protected function fetchAssoc(); /** * Returns the result set as an object. * - * Overridden by child classes. + * @param class-string $className * - * @return Entity|false|object|stdClass + * @return false|object */ - abstract protected function fetchObject(string $className = 'stdClass'); + abstract protected function fetchObject(string $className = stdClass::class); } diff --git a/system/Database/MySQLi/Builder.php b/system/Database/MySQLi/Builder.php index b8d0fa545d4f..a9e248fc4584 100644 --- a/system/Database/MySQLi/Builder.php +++ b/system/Database/MySQLi/Builder.php @@ -33,7 +33,7 @@ class Builder extends BaseBuilder * Specifies which sql statements * support the ignore option. * - * @var array + * @var array */ protected $supportedIgnoreStatements = [ 'update' => 'IGNORE', diff --git a/system/Database/Postgre/Builder.php b/system/Database/Postgre/Builder.php index 8d0d569d5e28..126ab5741892 100644 --- a/system/Database/Postgre/Builder.php +++ b/system/Database/Postgre/Builder.php @@ -36,7 +36,7 @@ class Builder extends BaseBuilder * Specifies which sql statements * support the ignore option. * - * @var array + * @var array */ protected $supportedIgnoreStatements = [ 'insert' => 'ON CONFLICT DO NOTHING', diff --git a/system/Database/SQLite3/Builder.php b/system/Database/SQLite3/Builder.php index 3aa13edeb96a..4f8dff97a0ea 100644 --- a/system/Database/SQLite3/Builder.php +++ b/system/Database/SQLite3/Builder.php @@ -49,7 +49,7 @@ class Builder extends BaseBuilder ]; /** - * @var array + * @var array */ protected $supportedIgnoreStatements = [ 'insert' => 'OR IGNORE', diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php index 8a21d77262a0..5df355d58b84 100644 --- a/system/HTTP/CURLRequest.php +++ b/system/HTTP/CURLRequest.php @@ -696,6 +696,8 @@ protected function setCURLOptions(array $curlOptions = [], array $config = []) * Does the actual work of initializing cURL, setting the options, * and grabbing the output. * + * @param array $curlOptions + * * @codeCoverageIgnore */ protected function sendRequest(array $curlOptions = []): string diff --git a/system/Test/Mock/MockBuilder.php b/system/Test/Mock/MockBuilder.php index e947f0ef5fb7..f95f09495a98 100644 --- a/system/Test/Mock/MockBuilder.php +++ b/system/Test/Mock/MockBuilder.php @@ -17,6 +17,9 @@ class MockBuilder extends BaseBuilder { + /** + * @var array + */ protected $supportedIgnoreStatements = [ 'update' => 'IGNORE', 'insert' => 'IGNORE', diff --git a/system/Test/Mock/MockCLIConfig.php b/system/Test/Mock/MockCLIConfig.php index c62df3d9f43a..99e3597d353a 100644 --- a/system/Test/Mock/MockCLIConfig.php +++ b/system/Test/Mock/MockCLIConfig.php @@ -17,14 +17,19 @@ class MockCLIConfig extends App { - public string $baseURL = 'http://example.com/'; - public string $uriProtocol = 'REQUEST_URI'; - public array $proxyIPs = []; - public string $CSRFTokenName = 'csrf_test_name'; - public string $CSRFCookieName = 'csrf_cookie_name'; - public int $CSRFExpire = 7200; - public bool $CSRFRegenerate = true; - public $CSRFExcludeURIs = ['http://example.com']; + public string $baseURL = 'http://example.com/'; + public string $uriProtocol = 'REQUEST_URI'; + public array $proxyIPs = []; + public string $CSRFTokenName = 'csrf_test_name'; + public string $CSRFCookieName = 'csrf_cookie_name'; + public int $CSRFExpire = 7200; + public bool $CSRFRegenerate = true; + + /** + * @var list + */ + public array $CSRFExcludeURIs = ['http://example.com']; + public string $CSRFSameSite = 'Lax'; public bool $CSPEnabled = false; public string $defaultLocale = 'en'; diff --git a/system/Test/Mock/MockCURLRequest.php b/system/Test/Mock/MockCURLRequest.php index 0f0b203a9a14..059b83114927 100644 --- a/system/Test/Mock/MockCURLRequest.php +++ b/system/Test/Mock/MockCURLRequest.php @@ -23,7 +23,14 @@ */ class MockCURLRequest extends CURLRequest { + /** + * @var array + */ public $curl_options; + + /** + * @var string + */ protected $output = ''; /** @@ -38,11 +45,13 @@ public function setOutput($output) return $this; } + /** + * @param array $curlOptions + */ protected function sendRequest(array $curlOptions = []): string { $this->response = clone $this->responseOrig; - // Save so we can access later. $this->curl_options = $curlOptions; return $this->output; diff --git a/system/Test/Mock/MockCache.php b/system/Test/Mock/MockCache.php index b410df874243..67f8aee86ad1 100644 --- a/system/Test/Mock/MockCache.php +++ b/system/Test/Mock/MockCache.php @@ -214,8 +214,9 @@ public function getCacheInfo() /** * Returns detailed information about the specific item in the cache. * - * @return array|null Returns null if the item does not exist, otherwise array - * with at least the 'expire' key for absolute epoch expiry (or null). + * @return array{expire: int|null}|null Returns null if the item does not exist, + * otherwise, array with the 'expire' key for + * absolute epoch expiry (or null). */ public function getMetaData(string $key) { diff --git a/system/Test/Mock/MockConnection.php b/system/Test/Mock/MockConnection.php index 7870d51b42cb..2801a0674ad5 100644 --- a/system/Test/Mock/MockConnection.php +++ b/system/Test/Mock/MockConnection.php @@ -18,6 +18,7 @@ use CodeIgniter\Database\BaseResult; use CodeIgniter\Database\Query; use CodeIgniter\Database\TableName; +use stdClass; /** * @extends BaseConnection @@ -25,7 +26,10 @@ class MockConnection extends BaseConnection { /** - * @var array{connect?: mixed, execute?: bool|object} + * @var array{ + * connect?: object|resource|false|list, + * execute?: object|resource|false, + * } */ protected $returnValues = []; @@ -36,11 +40,18 @@ class MockConnection extends BaseConnection */ protected $schema; + /** + * @var string + */ public $database; + + /** + * @var Query + */ public $lastQuery; /** - * @param mixed $return + * @param false|list|object|resource $return * * @return $this */ @@ -59,14 +70,15 @@ public function shouldReturn(string $method, $return) * Should automatically handle different connections for read/write * queries if needed. * - * @param mixed ...$binds + * @param mixed $binds * - * @return BaseResult|bool|Query + * @return BaseResult|bool|Query * * @todo BC set $queryClass default as null in 4.1 */ public function query(string $sql, $binds = null, bool $setEscapeFlags = true, string $queryClass = '') { + /** @var class-string $queryClass */ $queryClass = str_replace('Connection', 'Query', static::class); $query = new $queryClass($this); @@ -81,23 +93,24 @@ public function query(string $sql, $binds = null, bool $setEscapeFlags = true, s $this->lastQuery = $query; - // Run the query - if (false === ($this->resultID = $this->simpleQuery($query->getQuery()))) { + $this->resultID = $this->simpleQuery($query->getQuery()); + + if ($this->resultID === false) { $query->setDuration($startTime, $startTime); // @todo deal with errors - return false; } $query->setDuration($startTime); // resultID is not false, so it must be successful - if ($query->isWriteType($sql)) { + if ($query->isWriteType()) { return true; } // query is not write-type, so it must be read-type query; return QueryResult + /** @var class-string $resultClass */ $resultClass = str_replace('Connection', 'Result', static::class); return new $resultClass($this->connID, $this->resultID); @@ -106,7 +119,7 @@ public function query(string $sql, $binds = null, bool $setEscapeFlags = true, s /** * Connect to the database. * - * @return mixed + * @return false|object|resource */ public function connect(bool $persistent = false) { @@ -153,7 +166,7 @@ public function getVersion(): string /** * Executes the query against the database. * - * @return bool|object + * @return false|object|resource */ protected function execute(string $sql) { @@ -171,9 +184,7 @@ public function affectedRows(): int /** * Returns the last error code and message. * - * Must return an array with keys 'code' and 'message': - * - * return ['code' => null, 'message' => null); + * @return array{code: int, message: string} */ public function error(): array { @@ -183,9 +194,6 @@ public function error(): array ]; } - /** - * Insert ID - */ public function insertID(): int { return $this->connID->insert_id; @@ -211,16 +219,25 @@ protected function _listColumns($table = ''): string return ''; } + /** + * @return list + */ protected function _fieldData(string $table): array { return []; } + /** + * @return array + */ protected function _indexData(string $table): array { return []; } + /** + * @return array + */ protected function _foreignKeyData(string $table): array { return []; diff --git a/system/Test/Mock/MockEvents.php b/system/Test/Mock/MockEvents.php index bb0b4d9fb8bd..4ed1ea67ccce 100644 --- a/system/Test/Mock/MockEvents.php +++ b/system/Test/Mock/MockEvents.php @@ -18,7 +18,7 @@ class MockEvents extends Events { /** - * @return array + * @return array, 2: list}> */ public function getListeners() { @@ -26,7 +26,7 @@ public function getListeners() } /** - * @return array + * @return list */ public function getEventsFile() { diff --git a/system/Test/Mock/MockInputOutput.php b/system/Test/Mock/MockInputOutput.php index 0333bf27ccdf..6aa4779fb742 100644 --- a/system/Test/Mock/MockInputOutput.php +++ b/system/Test/Mock/MockInputOutput.php @@ -78,6 +78,8 @@ public function getOutput(?int $index = null): string /** * Returns the outputs array. + * + * @return list */ public function getOutputs(): array { diff --git a/system/Test/Mock/MockResult.php b/system/Test/Mock/MockResult.php index 9c5f55e98fdc..bc5cd8ac2054 100644 --- a/system/Test/Mock/MockResult.php +++ b/system/Test/Mock/MockResult.php @@ -31,6 +31,8 @@ public function getFieldCount(): int /** * Generates an array of column names in the result set. + * + * @return array{} */ public function getFieldNames(): array { @@ -39,6 +41,8 @@ public function getFieldNames(): array /** * Generates an array of objects representing field meta-data. + * + * @return array{} */ public function getFieldData(): array { @@ -73,7 +77,7 @@ public function dataSeek($n = 0) * * Overridden by driver classes. * - * @return mixed + * @return array{} */ protected function fetchAssoc() { @@ -83,13 +87,11 @@ protected function fetchAssoc() /** * Returns the result set as an object. * - * Overridden by child classes. - * - * @param string $className + * @param class-string $className * - * @return object|stdClass + * @return object */ - protected function fetchObject($className = 'stdClass') + protected function fetchObject($className = stdClass::class) { return new $className(); } diff --git a/system/Test/Mock/MockServices.php b/system/Test/Mock/MockServices.php index 5d1076a6dffb..c71bb612c6a7 100644 --- a/system/Test/Mock/MockServices.php +++ b/system/Test/Mock/MockServices.php @@ -18,9 +18,16 @@ class MockServices extends BaseService { + /** + * @var array + */ public $psr4 = [ 'Tests/Support' => TESTPATH . '_support/', ]; + + /** + * @var array + */ public $classmap = []; public function __construct() diff --git a/system/Test/Mock/MockSession.php b/system/Test/Mock/MockSession.php index 13eca596c9c7..b581f81dfafb 100644 --- a/system/Test/Mock/MockSession.php +++ b/system/Test/Mock/MockSession.php @@ -30,9 +30,9 @@ class MockSession extends Session * * @var list */ - public $cookies = []; + public array $cookies = []; - public $didRegenerate = false; + public bool $didRegenerate = false; /** * Sets the driver as the session handler in PHP. diff --git a/tests/system/Database/BaseConnectionTest.php b/tests/system/Database/BaseConnectionTest.php index ba55129cc723..a9fcab80622b 100644 --- a/tests/system/Database/BaseConnectionTest.php +++ b/tests/system/Database/BaseConnectionTest.php @@ -108,24 +108,39 @@ public function testConnectionThrowExceptionWhenCannotConnect(): void public function testCanConnectAndStoreConnection(): void { + $conn = new class () {}; + $db = new MockConnection($this->options); - $db->shouldReturn('connect', 123)->initialize(); + $db->shouldReturn('connect', $conn)->initialize(); - $this->assertSame(123, $db->getConnection()); + $this->assertSame($conn, $db->getConnection()); } public function testCanConnectToFailoverWhenNoConnectionAvailable(): void { - $options = $this->options; - $options['failover'] = [$this->failoverOptions]; + $options = [ + ...$this->options, + ...['failover' => [$this->failoverOptions]], + ]; - $db = new class ($options) extends MockConnection { - protected $returnValues = [ - 'connect' => [false, 345], - ]; + $conn = new class () {}; + + $db = new class ($options, $conn) extends MockConnection { + /** + * @param array $params + */ + public function __construct(array $params, object $return) + { + // need to call it here before any initialization + // we cannot do it directly in the property as objects + // cannot be set directly in properties + $this->shouldReturn('connect', [false, $return]); + + parent::__construct($params); + } }; - $this->assertSame(345, $db->getConnection()); + $this->assertSame($conn, $db->getConnection()); $this->assertSame('failover', $db->username); } diff --git a/tests/system/Database/Builder/InsertTest.php b/tests/system/Database/Builder/InsertTest.php index 4a5d7913e4ab..e8761b9de52b 100644 --- a/tests/system/Database/Builder/InsertTest.php +++ b/tests/system/Database/Builder/InsertTest.php @@ -162,7 +162,7 @@ public function testInsertBatch(): void ], ]; - $this->db->shouldReturn('execute', 1)->shouldReturn('affectedRows', 1); + $this->db->shouldReturn('execute', new class () {}); $builder->insertBatch($insertData, true); $query = $this->db->getLastQuery(); @@ -197,7 +197,7 @@ public function testInsertBatchIgnore(): void ], ]; - $this->db->shouldReturn('execute', 1)->shouldReturn('affectedRows', 1); + $this->db->shouldReturn('execute', new class () {}); $builder->ignore()->insertBatch($insertData, true, 1); $query = $this->db->getLastQuery(); @@ -231,7 +231,7 @@ public function testInsertBatchWithoutEscape(): void ], ]; - $this->db->shouldReturn('execute', 1)->shouldReturn('affectedRows', 1); + $this->db->shouldReturn('execute', new class () {}); $builder->insertBatch($insertData, false); $query = $this->db->getLastQuery(); @@ -255,7 +255,7 @@ public function testInsertBatchWithFieldsEndingInNumbers(): void ['ip' => '4.4.4.0', 'ip2' => '4.4.4.2'], ]; - $this->db->shouldReturn('execute', 1)->shouldReturn('affectedRows', 1); + $this->db->shouldReturn('execute', new class () {}); $builder->insertBatch($data, true); $query = $this->db->getLastQuery(); diff --git a/tests/system/Database/Builder/UpdateTest.php b/tests/system/Database/Builder/UpdateTest.php index b13739dfbd58..eb1cfc7d9c92 100644 --- a/tests/system/Database/Builder/UpdateTest.php +++ b/tests/system/Database/Builder/UpdateTest.php @@ -229,7 +229,7 @@ public function testUpdateBatch(): void ], ]; - $this->db->shouldReturn('execute', 1)->shouldReturn('affectedRows', 1); + $this->db->shouldReturn('execute', new class () {}); $builder->updateBatch($updateData, 'id'); $query = $this->db->getLastQuery(); @@ -268,7 +268,7 @@ public function testSetUpdateBatchWithoutEscape(): void ], ], 'id', $escape); - $this->db->shouldReturn('execute', 1)->shouldReturn('affectedRows', 1); + $this->db->shouldReturn('execute', new class () {}); $builder->updateBatch(null, 'id'); $query = $this->db->getLastQuery(); diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 00cce6e6c73b..607eb550c6c6 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 3074 errors +# total 3049 errors includes: - argument.type.neon - assign.propertyType.neon diff --git a/utils/phpstan-baseline/method.childReturnType.neon b/utils/phpstan-baseline/method.childReturnType.neon index f66d38d4d6e4..c48fbca29d99 100644 --- a/utils/phpstan-baseline/method.childReturnType.neon +++ b/utils/phpstan-baseline/method.childReturnType.neon @@ -1,4 +1,4 @@ -# total 37 errors +# total 30 errors parameters: ignoreErrors: @@ -151,33 +151,3 @@ parameters: message: '#^Return type \(array\|bool\|float\|int\|object\|string\|null\) of method CodeIgniter\\Model\:\:__call\(\) should be covariant with return type \(\$this\(CodeIgniter\\BaseModel\)\|null\) of method CodeIgniter\\BaseModel\:\:__call\(\)$#' count: 1 path: ../../system/Model.php - - - - message: '#^Return type \(array\{code\: int\|string\|null, message\: string\|null\}\) of method CodeIgniter\\Test\\Mock\\MockConnection\:\:error\(\) should be covariant with return type \(array\\) of method CodeIgniter\\Database\\ConnectionInterface\\:\:error\(\)$#' - count: 1 - path: ../../system/Test/Mock/MockConnection.php - - - - message: '#^Return type \(bool\|CodeIgniter\\Database\\BaseResult\|CodeIgniter\\Database\\Query\) of method CodeIgniter\\Test\\Mock\\MockConnection\:\:query\(\) should be covariant with return type \(bool\|CodeIgniter\\Database\\BaseResult\\|CodeIgniter\\Database\\Query\) of method CodeIgniter\\Database\\BaseConnection\\:\:query\(\)$#' - count: 1 - path: ../../system/Test/Mock/MockConnection.php - - - - message: '#^Return type \(bool\|CodeIgniter\\Database\\BaseResult\|CodeIgniter\\Database\\Query\) of method CodeIgniter\\Test\\Mock\\MockConnection\:\:query\(\) should be covariant with return type \(bool\|CodeIgniter\\Database\\BaseResult\\|CodeIgniter\\Database\\Query\) of method CodeIgniter\\Database\\ConnectionInterface\\:\:query\(\)$#' - count: 1 - path: ../../system/Test/Mock/MockConnection.php - - - - message: '#^Return type \(bool\|object\) of method CodeIgniter\\Test\\Mock\\MockConnection\:\:execute\(\) should be covariant with return type \(object\|resource\|false\) of method CodeIgniter\\Database\\BaseConnection\\:\:execute\(\)$#' - count: 1 - path: ../../system/Test/Mock/MockConnection.php - - - - message: '#^Return type \(mixed\) of method CodeIgniter\\Test\\Mock\\MockConnection\:\:connect\(\) should be covariant with return type \(object\|resource\|false\) of method CodeIgniter\\Database\\ConnectionInterface\\:\:connect\(\)$#' - count: 2 - path: ../../system/Test/Mock/MockConnection.php - - - - message: '#^Return type \(mixed\) of method CodeIgniter\\Test\\Mock\\MockResult\:\:fetchAssoc\(\) should be covariant with return type \(array\|false\|null\) of method CodeIgniter\\Database\\BaseResult\\:\:fetchAssoc\(\)$#' - count: 1 - path: ../../system/Test/Mock/MockResult.php diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 5e998fc3949e..6d974d9bb30b 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 1491 errors +# total 1479 errors parameters: ignoreErrors: @@ -1157,11 +1157,6 @@ parameters: count: 1 path: ../../system/Database/BaseBuilder.php - - - message: '#^Property CodeIgniter\\Database\\BaseBuilder\:\:\$supportedIgnoreStatements type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - message: '#^Class CodeIgniter\\Database\\BaseConnection has PHPDoc tag @property\-read for property \$aliasedTables with no value type specified in iterable type array\.$#' count: 1 @@ -1662,11 +1657,6 @@ parameters: count: 1 path: ../../system/Database/MigrationRunner.php - - - message: '#^Property CodeIgniter\\Database\\MySQLi\\Builder\:\:\$supportedIgnoreStatements type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/MySQLi/Builder.php - - message: '#^Method CodeIgniter\\Database\\MySQLi\\Forge\:\:_alterTable\(\) has parameter \$processedFields with no value type specified in iterable type array\.$#' count: 1 @@ -1827,11 +1817,6 @@ parameters: count: 1 path: ../../system/Database/Postgre/Builder.php - - - message: '#^Property CodeIgniter\\Database\\Postgre\\Builder\:\:\$supportedIgnoreStatements type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Postgre/Builder.php - - message: '#^Method CodeIgniter\\Database\\Postgre\\Connection\:\:escape\(\) has parameter \$str with no value type specified in iterable type array\.$#' count: 1 @@ -2117,11 +2102,6 @@ parameters: count: 1 path: ../../system/Database/SQLite3/Builder.php - - - message: '#^Property CodeIgniter\\Database\\SQLite3\\Builder\:\:\$supportedIgnoreStatements type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLite3/Builder.php - - message: '#^Method CodeIgniter\\Database\\SQLite3\\Forge\:\:_alterTable\(\) has parameter \$processedFields with no value type specified in iterable type array\.$#' count: 1 @@ -3137,11 +3117,6 @@ parameters: count: 1 path: ../../system/HTTP/CURLRequest.php - - - message: '#^Method CodeIgniter\\HTTP\\CURLRequest\:\:sendRequest\(\) has parameter \$curlOptions with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CURLRequest.php - - message: '#^Method CodeIgniter\\HTTP\\CURLRequest\:\:setCURLOptions\(\) has parameter \$config with no value type specified in iterable type array\.$#' count: 1 @@ -5047,31 +5022,6 @@ parameters: count: 1 path: ../../system/Test/Fabricator.php - - - message: '#^Property CodeIgniter\\Test\\Mock\\MockBuilder\:\:\$supportedIgnoreStatements type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Mock/MockBuilder.php - - - - message: '#^Method CodeIgniter\\Test\\Mock\\MockCURLRequest\:\:sendRequest\(\) has parameter \$curlOptions with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Mock/MockCURLRequest.php - - - - message: '#^Method CodeIgniter\\Test\\Mock\\MockCache\:\:getMetaData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Mock/MockCache.php - - - - message: '#^Method CodeIgniter\\Test\\Mock\\MockEvents\:\:getListeners\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Mock/MockEvents.php - - - - message: '#^Method CodeIgniter\\Test\\Mock\\MockInputOutput\:\:getOutputs\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Mock/MockInputOutput.php - - message: '#^Method CodeIgniter\\Test\\Mock\\MockLanguage\:\:requireFile\(\) return type has no value type specified in iterable type array\.$#' count: 1 @@ -5082,16 +5032,6 @@ parameters: count: 1 path: ../../system/Test/Mock/MockLanguage.php - - - message: '#^Method CodeIgniter\\Test\\Mock\\MockResult\:\:getFieldData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Mock/MockResult.php - - - - message: '#^Method CodeIgniter\\Test\\Mock\\MockResult\:\:getFieldNames\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Mock/MockResult.php - - message: '#^Method CodeIgniter\\Test\\TestResponse\:\:assertJSONExact\(\) has parameter \$test with no value type specified in iterable type array\.$#' count: 1 diff --git a/utils/phpstan-baseline/missingType.property.neon b/utils/phpstan-baseline/missingType.property.neon index dd29a69724d9..46719d10694c 100644 --- a/utils/phpstan-baseline/missingType.property.neon +++ b/utils/phpstan-baseline/missingType.property.neon @@ -1,4 +1,4 @@ -# total 109 errors +# total 103 errors parameters: ignoreErrors: @@ -37,36 +37,6 @@ parameters: count: 1 path: ../../system/Session/Handlers/ArrayHandler.php - - - message: '#^Property CodeIgniter\\Test\\Mock\\MockCLIConfig\:\:\$CSRFExcludeURIs has no type specified\.$#' - count: 1 - path: ../../system/Test/Mock/MockCLIConfig.php - - - - message: '#^Property CodeIgniter\\Test\\Mock\\MockCURLRequest\:\:\$curl_options has no type specified\.$#' - count: 1 - path: ../../system/Test/Mock/MockCURLRequest.php - - - - message: '#^Property CodeIgniter\\Test\\Mock\\MockCURLRequest\:\:\$output has no type specified\.$#' - count: 1 - path: ../../system/Test/Mock/MockCURLRequest.php - - - - message: '#^Property CodeIgniter\\Test\\Mock\\MockServices\:\:\$classmap has no type specified\.$#' - count: 1 - path: ../../system/Test/Mock/MockServices.php - - - - message: '#^Property CodeIgniter\\Test\\Mock\\MockServices\:\:\$psr4 has no type specified\.$#' - count: 1 - path: ../../system/Test/Mock/MockServices.php - - - - message: '#^Property CodeIgniter\\Test\\Mock\\MockSession\:\:\$didRegenerate has no type specified\.$#' - count: 1 - path: ../../system/Test/Mock/MockSession.php - - message: '#^Property CodeIgniter\\Config\\Factory@anonymous/tests/system/Config/FactoriesTest\.php\:89\:\:\$widgets has no type specified\.$#' count: 1 From f737307925d59036e495590c1732932410240faf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 15:51:58 +0000 Subject: [PATCH 32/59] chore(deps-dev): update rector/rector requirement from 2.0.17 to 2.0.18 Updates the requirements on [rector/rector](https://github.com/rectorphp/rector) to permit the latest version. - [Release notes](https://github.com/rectorphp/rector/releases) - [Commits](https://github.com/rectorphp/rector/compare/2.0.17...2.0.18) --- updated-dependencies: - dependency-name: rector/rector dependency-version: 2.0.18 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 01148efe985a..e2c5d4c79491 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "phpunit/phpcov": "^9.0.2 || ^10.0", "phpunit/phpunit": "^10.5.16 || ^11.2", "predis/predis": "^3.0", - "rector/rector": "2.0.17", + "rector/rector": "2.0.18", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { From 863faeeabbfa95783ba300c736afd933ae853646 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Sun, 22 Jun 2025 16:20:39 +0200 Subject: [PATCH 33/59] fix: encapsulation violation in `BasePreparedQuery` class (#9603) * fix: add BaseConnection::handleTransStatus() method to fix protected property access * fix test --- system/Database/BaseConnection.php | 16 ++++++-- system/Database/BasePreparedQuery.php | 4 +- .../Database/Live/PreparedQueryTest.php | 40 +++++++++++++++++++ user_guide_src/source/changelogs/v4.6.2.rst | 1 + 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index 1ae432dcb79c..f09175d59b46 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -652,9 +652,7 @@ public function query(string $sql, $binds = null, bool $setEscapeFlags = true, s $query->setDuration($startTime, $startTime); // This will trigger a rollback if transactions are being used - if ($this->transDepth !== 0) { - $this->transStatus = false; - } + $this->handleTransStatus(); if ( $this->DBDebug @@ -904,6 +902,18 @@ public function resetTransStatus(): static return $this; } + /** + * Handle transaction status when a query fails + * + * @internal This method is for internal database component use only + */ + public function handleTransStatus(): void + { + if ($this->transDepth !== 0) { + $this->transStatus = false; + } + } + /** * Begin Transaction */ diff --git a/system/Database/BasePreparedQuery.php b/system/Database/BasePreparedQuery.php index 3632ccb18c0c..d5bb40bebd0a 100644 --- a/system/Database/BasePreparedQuery.php +++ b/system/Database/BasePreparedQuery.php @@ -134,9 +134,7 @@ public function execute(...$data) $query->setDuration($startTime, $startTime); // This will trigger a rollback if transactions are being used - if ($this->db->transDepth !== 0) { - $this->db->transStatus = false; - } + $this->db->handleTransStatus(); if ($this->db->DBDebug) { // We call this function in order to roll-back queries diff --git a/tests/system/Database/Live/PreparedQueryTest.php b/tests/system/Database/Live/PreparedQueryTest.php index f5fe6115b111..7549e0b75ee0 100644 --- a/tests/system/Database/Live/PreparedQueryTest.php +++ b/tests/system/Database/Live/PreparedQueryTest.php @@ -304,4 +304,44 @@ public function testInsertBinaryData(): void $this->assertSame(strlen($fileContent), strlen($file)); } + + public function testHandleTransStatusMarksTransactionFailedDuringTransaction(): void + { + $this->db->transStart(); + + // Verify we're in a transaction + $this->assertSame(1, $this->db->transDepth); + + // Prepare a query that will fail (duplicate key) + $this->query = $this->db->prepare(static fn ($db) => $db->table('without_auto_increment')->insert([ + 'key' => 'a', + 'value' => 'b', + ])); + + $this->disableDBDebug(); + + $this->assertTrue($this->query->execute('test_key', 'test_value')); + $this->assertTrue($this->db->transStatus()); + + $this->seeInDatabase($this->db->DBPrefix . 'without_auto_increment', [ + 'key' => 'test_key', + 'value' => 'test_value' + ]); + + $this->assertFalse($this->query->execute('test_key', 'different_value')); + $this->assertFalse($this->db->transStatus()); + + $this->enableDBDebug(); + + // Complete the transaction - should rollback due to failed status + $this->assertFalse($this->db->transComplete()); + + // Verify the first insert was rolled back + $this->dontSeeInDatabase($this->db->DBPrefix . 'without_auto_increment', [ + 'key' => 'test_key', + 'value' => 'test_value' + ]); + + $this->db->resetTransStatus(); + } } diff --git a/user_guide_src/source/changelogs/v4.6.2.rst b/user_guide_src/source/changelogs/v4.6.2.rst index 676d1a731ee9..11d11fd8a1f5 100644 --- a/user_guide_src/source/changelogs/v4.6.2.rst +++ b/user_guide_src/source/changelogs/v4.6.2.rst @@ -37,6 +37,7 @@ Bugs Fixed - **Cache:** Fixed a bug where a corrupted or unreadable cache file could cause an unhandled exception in ``FileHandler::getItem()``. - **Database:** Fixed a bug where ``when()`` and ``whenNot()`` in ``ConditionalTrait`` incorrectly evaluated certain falsy values (such as ``[]``, ``0``, ``0.0``, and ``'0'``) as truthy, causing callbacks to be executed unexpectedly. These methods now cast the condition to a boolean using ``(bool)`` to ensure consistent behavior with PHP's native truthiness. +- **Database:** Fixed encapsulation violation in ``BasePreparedQuery`` when accessing ``BaseConnection::transStatus`` protected property. - **Email:** Fixed a bug where ``Email::getHostname()`` failed to use ``$_SERVER['SERVER_ADDR']`` when ``$_SERVER['SERVER_NAME']`` was not set. - **Security:** Fixed a bug where the ``sanitize_filename()`` function from the Security helper would throw an error when used in CLI requests. - **Session:** Fixed a bug where using the ``DatabaseHandler`` with an unsupported database driver (such as ``SQLSRV``, ``OCI8``, or ``SQLite3``) did not throw an appropriate error. From e29ce0262a5f51fce11304549dfe4d6bf24e70bc Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Sun, 22 Jun 2025 16:21:23 +0200 Subject: [PATCH 34/59] fix: URI authority generation for schemes without default ports (#9605) * fix: URI authority generation for schemes without default ports * fix typo --- system/HTTP/URI.php | 2 +- tests/system/HTTP/URITest.php | 29 +++++++++++++++++++++ user_guide_src/source/changelogs/v4.6.2.rst | 1 + 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index b714995ce143..5bcf11de655a 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -376,7 +376,7 @@ public function getAuthority(bool $ignorePort = false): string } // Don't add port if it's a standard port for this scheme - if ((int) $this->port !== 0 && ! $ignorePort && $this->port !== $this->defaultPorts[$this->scheme]) { + if ((int) $this->port !== 0 && ! $ignorePort && $this->port !== ($this->defaultPorts[$this->scheme] ?? null)) { $authority .= ':' . $this->port; } diff --git a/tests/system/HTTP/URITest.php b/tests/system/HTTP/URITest.php index 945b132876ef..3564ae6c658e 100644 --- a/tests/system/HTTP/URITest.php +++ b/tests/system/HTTP/URITest.php @@ -622,6 +622,22 @@ public static function provideAuthorityReturnsExceptedValues(): iterable 'http://me@foo.com:3000/bar', 'me@foo.com:3000', ], + 'rtsp-with-port' => [ + 'rtsp://localhost:1234/stream', + 'localhost:1234', + ], + 'rtsp-no-port' => [ + 'rtsp://localhost/stream', + 'localhost', + ], + 'custom-scheme-with-port' => [ + 'myscheme://server:9999/resource', + 'server:9999', + ], + 'custom-scheme-no-port' => [ + 'myscheme://server/resource', + 'server', + ], ]; } @@ -1226,4 +1242,17 @@ public function testForceGlobalSecureRequestsAndNonHTTPProtocol(): void $this->assertSame($expected, (string) $uri); } + + /** + * @see https://github.com/codeigniter4/CodeIgniter4/issues/9604 + */ + public function testAuthorityIncludesPortForCustomSchemes(): void + { + $url = 'rtsp://localhost:1234/stream'; + $uri = new URI($url); + + $this->assertSame('rtsp://localhost:1234/stream', (string) $uri); + $this->assertSame('localhost:1234', $uri->getAuthority()); + $this->assertSame(1234, $uri->getPort()); + } } diff --git a/user_guide_src/source/changelogs/v4.6.2.rst b/user_guide_src/source/changelogs/v4.6.2.rst index 11d11fd8a1f5..f39a0c46cf81 100644 --- a/user_guide_src/source/changelogs/v4.6.2.rst +++ b/user_guide_src/source/changelogs/v4.6.2.rst @@ -41,6 +41,7 @@ Bugs Fixed - **Email:** Fixed a bug where ``Email::getHostname()`` failed to use ``$_SERVER['SERVER_ADDR']`` when ``$_SERVER['SERVER_NAME']`` was not set. - **Security:** Fixed a bug where the ``sanitize_filename()`` function from the Security helper would throw an error when used in CLI requests. - **Session:** Fixed a bug where using the ``DatabaseHandler`` with an unsupported database driver (such as ``SQLSRV``, ``OCI8``, or ``SQLite3``) did not throw an appropriate error. +- **URI:** Fixed a bug in ``URI::getAuthority()`` where schemes without defined default ports (like ``rtsp://``) would cause issues due to missing array key handling. See the repo's `CHANGELOG.md `_ From 80005b2b952dc23afb452070b79471dd80037dae Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Sun, 22 Jun 2025 16:22:01 +0200 Subject: [PATCH 35/59] docs: note removal of `AbstractRenderer::SORT_FULL` and `$richSort` in Kint config (#9606) * docs: note removal of AbstractRenderer::SORT_FULL and richSort in Kint v6 * add more detailed info * re-add Kint to the list of changed files --- user_guide_src/source/installation/upgrade_460.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/user_guide_src/source/installation/upgrade_460.rst b/user_guide_src/source/installation/upgrade_460.rst index bb78d0f47e32..354d542e3cc4 100644 --- a/user_guide_src/source/installation/upgrade_460.rst +++ b/user_guide_src/source/installation/upgrade_460.rst @@ -210,6 +210,8 @@ Config - ``Config\Feature::$strictLocaleNegotiation`` has been added. - app/Config/Routing.php - ``Config\Routing::$translateUriToCamelCase`` has been changed to ``true``. +- app/Config/Kint.php + - ``Config\Kint::$richSort`` has been removed. Kint in v6 no longer uses ``AbstractRenderer::SORT_FULL``. Leaving this property in your code will cause a runtime error due to the undefined constant. All Changes =========== From c7f2c80aa7e2aacddfbfac2d73d65b0205641ca8 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Wed, 25 Jun 2025 01:38:41 +0800 Subject: [PATCH 36/59] chore: fix rector crash (#9608) --- phpstan-bootstrap.php | 15 +++++++++++++-- rector.php | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/phpstan-bootstrap.php b/phpstan-bootstrap.php index ce8e4636bd11..0f45bf0cb559 100644 --- a/phpstan-bootstrap.php +++ b/phpstan-bootstrap.php @@ -2,6 +2,17 @@ require __DIR__ . '/system/util_bootstrap.php'; -if (! defined('OCI_COMMIT_ON_SUCCESS')) { - define('OCI_COMMIT_ON_SUCCESS', 32); +defined('OCI_COMMIT_ON_SUCCESS') || define('OCI_COMMIT_ON_SUCCESS', 32); + +foreach ([ + 'app/Config', +] as $directory) { + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory)); + + /** @var SplFileInfo $file */ + foreach ($iterator as $file) { + if ($file->isFile() && $file->getExtension() === 'php') { + require_once $file->getRealPath(); + } + } } diff --git a/rector.php b/rector.php index 4f1504f270be..c814a7402c4d 100644 --- a/rector.php +++ b/rector.php @@ -71,7 +71,7 @@ ]) // do you need to include constants, class aliases or custom autoloader? files listed will be executed ->withBootstrapFiles([ - __DIR__ . '/system/util_bootstrap.php', + __DIR__ . '/phpstan-bootstrap.php', ]) ->withPHPStanConfigs([ __DIR__ . '/phpstan.neon.dist', From 764cd550db23d06d7680fde859a9e9dfba3a88f2 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Wed, 25 Jun 2025 02:14:41 +0800 Subject: [PATCH 37/59] chore: update phpstan baseline (#9609) --- system/Boot.php | 3 +- .../codeigniter.superglobalAccess.neon | 12 +- .../codeigniter.superglobalAccessAssign.neon | 237 +----------------- utils/phpstan-baseline/loader.neon | 2 +- 4 files changed, 4 insertions(+), 250 deletions(-) diff --git a/system/Boot.php b/system/Boot.php index 720c65facb43..ba3675516b16 100644 --- a/system/Boot.php +++ b/system/Boot.php @@ -373,9 +373,8 @@ protected static function initializeConsole(): Console $console = new Console(); // Show basic information before we do anything else. - // @phpstan-ignore-next-line if (is_int($suppress = array_search('--no-header', $_SERVER['argv'], true))) { - unset($_SERVER['argv'][$suppress]); // @phpstan-ignore-line + unset($_SERVER['argv'][$suppress]); $suppress = true; } diff --git a/utils/phpstan-baseline/codeigniter.superglobalAccess.neon b/utils/phpstan-baseline/codeigniter.superglobalAccess.neon index 893b1cd96c62..05153d87fb35 100644 --- a/utils/phpstan-baseline/codeigniter.superglobalAccess.neon +++ b/utils/phpstan-baseline/codeigniter.superglobalAccess.neon @@ -1,4 +1,4 @@ -# total 79 errors +# total 77 errors parameters: ignoreErrors: @@ -12,11 +12,6 @@ parameters: count: 1 path: ../../system/CLI/CLI.php - - - message: '#^Accessing offset ''argv'' directly on \$_SERVER is discouraged\.$#' - count: 1 - path: ../../system/CLI/CLI.php - - message: '#^Accessing offset ''encryption\.key'' directly on \$_SERVER is discouraged\.$#' count: 1 @@ -152,11 +147,6 @@ parameters: count: 1 path: ../../tests/system/CLI/ConsoleTest.php - - - message: '#^Accessing offset ''argv'' directly on \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/CLI/ConsoleTest.php - - message: '#^Accessing offset ''encryption\.key'' directly on \$_SERVER is discouraged\.$#' count: 1 diff --git a/utils/phpstan-baseline/codeigniter.superglobalAccessAssign.neon b/utils/phpstan-baseline/codeigniter.superglobalAccessAssign.neon index 3322897a77f8..4e5600bcc34c 100644 --- a/utils/phpstan-baseline/codeigniter.superglobalAccessAssign.neon +++ b/utils/phpstan-baseline/codeigniter.superglobalAccessAssign.neon @@ -1,4 +1,4 @@ -# total 581 errors +# total 466 errors parameters: ignoreErrors: @@ -17,46 +17,11 @@ parameters: count: 3 path: ../../system/HTTP/IncomingRequest.php - - - message: '#^Assigning 3 directly on offset ''argc'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/CLI/CLITest.php - - - - message: '#^Assigning array\{''ignored'', ''b'', ''c'', ''\-\-parm'', ''pvalue'', ''d'', ''\-\-p2'', ''\-\-p3'', ''value 3''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/CLI/CLITest.php - - - - message: '#^Assigning array\{''ignored'', ''b'', ''c'', ''\-\-parm'', ''pvalue'', ''d''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/CLI/CLITest.php - - - - message: '#^Assigning array\{''ignored'', ''b'', ''c'', ''d'', ''\-\-parm'', ''pvalue'', ''d2'', ''da\-sh'', ''\-\-fix'', ''\-\-opt\-in'', ''sure''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/CLI/CLITest.php - - - - message: '#^Assigning array\{''ignored'', ''b'', ''c''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/CLI/CLITest.php - - message: '#^Assigning ''http\://example\.com/'' directly on offset ''app\.baseURL'' of \$_SERVER is discouraged\.$#' count: 1 path: ../../tests/system/CLI/ConsoleTest.php - - - message: '#^Assigning int\<1, max\> directly on offset ''argc'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/CLI/ConsoleTest.php - - - - message: '#^Assigning non\-empty\-array\ directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/CLI/ConsoleTest.php - - message: '#^Assigning ''/index\.php'' directly on offset ''SCRIPT_NAME'' of \$_SERVER is discouraged\.$#' count: 1 @@ -67,11 +32,6 @@ parameters: count: 1 path: ../../tests/system/Cache/ResponseCacheTest.php - - - message: '#^Assigning non\-empty\-list\ directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/Cache/ResponseCacheTest.php - - message: '#^Assigning non\-falsy\-string directly on offset ''REQUEST_URI'' of \$_SERVER is discouraged\.$#' count: 1 @@ -152,46 +112,6 @@ parameters: count: 1 path: ../../tests/system/CodeIgniterTest.php - - - message: '#^Assigning 1 directly on offset ''argc'' of \$_SERVER is discouraged\.$#' - count: 5 - path: ../../tests/system/CodeIgniterTest.php - - - - message: '#^Assigning 2 directly on offset ''argc'' of \$_SERVER is discouraged\.$#' - count: 26 - path: ../../tests/system/CodeIgniterTest.php - - - - message: '#^Assigning array\{''index\.php'', ''/''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 11 - path: ../../tests/system/CodeIgniterTest.php - - - - message: '#^Assigning array\{''index\.php'', ''cli''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/CodeIgniterTest.php - - - - message: '#^Assigning array\{''index\.php'', ''example''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 6 - path: ../../tests/system/CodeIgniterTest.php - - - - message: '#^Assigning array\{''index\.php'', ''image''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/CodeIgniterTest.php - - - - message: '#^Assigning array\{''index\.php'', ''pages/about''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 7 - path: ../../tests/system/CodeIgniterTest.php - - - - message: '#^Assigning array\{''index\.php''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 5 - path: ../../tests/system/CodeIgniterTest.php - - message: '#^Assigning non\-falsy\-string directly on offset ''REQUEST_URI'' of \$_SERVER is discouraged\.$#' count: 1 @@ -242,16 +162,6 @@ parameters: count: 38 path: ../../tests/system/Filters/FiltersTest.php - - - message: '#^Assigning 2 directly on offset ''argc'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/Filters/FiltersTest.php - - - - message: '#^Assigning array\{''spark'', ''list''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/Filters/FiltersTest.php - - message: '#^Assigning ''POST'' directly on offset ''REQUEST_METHOD'' of \$_SERVER is discouraged\.$#' count: 1 @@ -267,56 +177,6 @@ parameters: count: 1 path: ../../tests/system/HTTP/CLIRequestTest.php - - - message: '#^Assigning array\{''index\.php'', ''users'', ''21'', ''abc \< def'', ''McDonald\\''s'', ''\aaa\''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/HTTP/CLIRequestTest.php - - - - message: '#^Assigning array\{''index\.php'', ''users'', ''21'', ''pro\-file'', ''\-\-foo'', ''bar'', ''\-\-baz'', ''queue some stuff''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/HTTP/CLIRequestTest.php - - - - message: '#^Assigning array\{''index\.php'', ''users'', ''21'', ''profile'', ''\-\-foo'', ''bar'', ''\-\-baz'', ''queue some stuff''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/HTTP/CLIRequestTest.php - - - - message: '#^Assigning array\{''index\.php'', ''users'', ''21'', ''profile'', ''\-\-foo'', ''bar'', ''\-\-foo\-bar'', ''yes''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/HTTP/CLIRequestTest.php - - - - message: '#^Assigning array\{''index\.php'', ''users'', ''21'', ''profile'', ''\-\-foo'', ''bar''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 2 - path: ../../tests/system/HTTP/CLIRequestTest.php - - - - message: '#^Assigning array\{''index\.php'', ''users'', ''21'', ''profile'', ''\-\-foo'', ''oops'', ''bar'', ''\-\-baz'', ''queue some stuff''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/HTTP/CLIRequestTest.php - - - - message: '#^Assigning array\{''index\.php'', ''users'', ''21'', ''profile'', ''\-\-foo'', ''oops\-bar'', ''\-\-baz'', ''queue some stuff''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/HTTP/CLIRequestTest.php - - - - message: '#^Assigning array\{''index\.php'', ''users'', ''21'', ''profile'', ''\-foo'', ''bar''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/HTTP/CLIRequestTest.php - - - - message: '#^Assigning array\{''index\.php'', ''users'', ''21'', ''profile''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/HTTP/CLIRequestTest.php - - - - message: '#^Assigning array\{''spark'', ''command'', ''param1'', ''param2'', ''\-\-opt1'', ''opt1val'', ''\-\-opt\-2'', ''opt 2 val'', ''param3''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/HTTP/CLIRequestTest.php - - message: '#^Assigning ''10'' directly on offset ''HTTP_CONTENT_LENGTH'' of \$_SERVER is discouraged\.$#' count: 1 @@ -1052,46 +912,6 @@ parameters: count: 1 path: ../../tests/system/RESTful/ResourceControllerTest.php - - - message: '#^Assigning 2 directly on offset ''argc'' of \$_SERVER is discouraged\.$#' - count: 2 - path: ../../tests/system/RESTful/ResourceControllerTest.php - - - - message: '#^Assigning 3 directly on offset ''argc'' of \$_SERVER is discouraged\.$#' - count: 5 - path: ../../tests/system/RESTful/ResourceControllerTest.php - - - - message: '#^Assigning 4 directly on offset ''argc'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/RESTful/ResourceControllerTest.php - - - - message: '#^Assigning array\{''index\.php'', ''work'', ''1'', ''edit''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/RESTful/ResourceControllerTest.php - - - - message: '#^Assigning array\{''index\.php'', ''work'', ''1''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/RESTful/ResourceControllerTest.php - - - - message: '#^Assigning array\{''index\.php'', ''work'', ''123''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 3 - path: ../../tests/system/RESTful/ResourceControllerTest.php - - - - message: '#^Assigning array\{''index\.php'', ''work'', ''new''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/RESTful/ResourceControllerTest.php - - - - message: '#^Assigning array\{''index\.php'', ''work''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 2 - path: ../../tests/system/RESTful/ResourceControllerTest.php - - message: '#^Assigning ''/work'' directly on offset ''REQUEST_URI'' of \$_SERVER is discouraged\.$#' count: 1 @@ -1147,61 +967,6 @@ parameters: count: 3 path: ../../tests/system/RESTful/ResourcePresenterTest.php - - - message: '#^Assigning 2 directly on offset ''argc'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/RESTful/ResourcePresenterTest.php - - - - message: '#^Assigning 3 directly on offset ''argc'' of \$_SERVER is discouraged\.$#' - count: 4 - path: ../../tests/system/RESTful/ResourcePresenterTest.php - - - - message: '#^Assigning 4 directly on offset ''argc'' of \$_SERVER is discouraged\.$#' - count: 3 - path: ../../tests/system/RESTful/ResourcePresenterTest.php - - - - message: '#^Assigning array\{''index\.php'', ''work'', ''create''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/RESTful/ResourcePresenterTest.php - - - - message: '#^Assigning array\{''index\.php'', ''work'', ''delete'', ''123''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/RESTful/ResourcePresenterTest.php - - - - message: '#^Assigning array\{''index\.php'', ''work'', ''edit'', ''1'', ''edit''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/RESTful/ResourcePresenterTest.php - - - - message: '#^Assigning array\{''index\.php'', ''work'', ''new''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/RESTful/ResourcePresenterTest.php - - - - message: '#^Assigning array\{''index\.php'', ''work'', ''remove'', ''123''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/RESTful/ResourcePresenterTest.php - - - - message: '#^Assigning array\{''index\.php'', ''work'', ''show'', ''1''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/RESTful/ResourcePresenterTest.php - - - - message: '#^Assigning array\{''index\.php'', ''work'', ''update'', ''123''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/RESTful/ResourcePresenterTest.php - - - - message: '#^Assigning array\{''index\.php'', ''work''\} directly on offset ''argv'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/RESTful/ResourcePresenterTest.php - - message: '#^Assigning ''adm\.example\.com'' directly on offset ''HTTP_HOST'' of \$_SERVER is discouraged\.$#' count: 4 diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 607eb550c6c6..03b3f826d494 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 3049 errors +# total 2932 errors includes: - argument.type.neon - assign.propertyType.neon From 488ace9334528bff89af51d42ff34a6ff7c88e46 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Wed, 25 Jun 2025 23:39:29 +0800 Subject: [PATCH 38/59] refactor: fix various phpstan errors in Cache (#9610) --- app/Config/Cache.php | 6 +- system/Cache/CacheFactory.php | 2 +- system/Cache/CacheInterface.php | 20 +- system/Cache/Exceptions/CacheException.php | 11 +- system/Cache/FactoriesCache.php | 16 +- .../FactoriesCache/FileVarExportHandler.php | 10 +- system/Cache/Handlers/BaseHandler.php | 9 +- system/Cache/Handlers/FileHandler.php | 61 +++-- system/Cache/Handlers/MemcachedHandler.php | 19 +- system/Cache/Handlers/PredisHandler.php | 8 +- system/Cache/Handlers/RedisHandler.php | 8 +- system/Cache/ResponseCache.php | 29 +-- system/Helpers/filesystem_helper.php | 23 +- system/View/Cell.php | 4 +- .../AbstractFactoriesCacheHandlerTestCase.php | 82 +++++++ tests/system/Cache/CacheFactoryTest.php | 16 +- tests/system/Cache/CacheMockTest.php | 1 + .../Cache/FactoriesCacheFileHandlerTest.php | 12 +- ...FactoriesCacheFileVarExportHandlerTest.php | 63 +---- .../system/Cache/Handlers/BaseHandlerTest.php | 20 +- .../Cache/Handlers/DummyHandlerTest.php | 5 +- .../system/Cache/Handlers/FileHandlerTest.php | 47 ++-- .../Cache/Handlers/MemcachedHandlerTest.php | 13 +- .../Cache/Handlers/PredisHandlerTest.php | 12 +- .../Cache/Handlers/RedisHandlerTest.php | 11 +- tests/system/Cache/ResponseCacheTest.php | 177 ++++++-------- .../codeigniter.cacheHandlerInstance.neon | 33 --- .../codeigniter.getReassignArray.neon | 7 +- .../codeigniter.superglobalAccessAssign.neon | 17 +- utils/phpstan-baseline/empty.notAllowed.neon | 7 +- utils/phpstan-baseline/loader.neon | 3 +- .../missingType.iterableValue.neon | 217 +----------------- utils/phpstan-baseline/property.notFound.neon | 7 +- .../ternary.shortNotAllowed.neon | 7 +- 34 files changed, 346 insertions(+), 637 deletions(-) create mode 100644 tests/system/Cache/AbstractFactoriesCacheHandlerTestCase.php delete mode 100644 utils/phpstan-baseline/codeigniter.cacheHandlerInstance.neon diff --git a/app/Config/Cache.php b/app/Config/Cache.php index e6efa3acf6f5..1169c95ff7ee 100644 --- a/app/Config/Cache.php +++ b/app/Config/Cache.php @@ -78,7 +78,7 @@ class Cache extends BaseConfig * Your file storage preferences can be specified below, if you are using * the File driver. * - * @var array + * @var array{storePath?: string, mode?: int} */ public array $file = [ 'storePath' => WRITEPATH . 'cache/', @@ -95,7 +95,7 @@ class Cache extends BaseConfig * * @see https://codeigniter.com/user_guide/libraries/caching.html#memcached * - * @var array + * @var array{host?: string, port?: int, weight?: int, raw?: bool} */ public array $memcached = [ 'host' => '127.0.0.1', @@ -112,7 +112,7 @@ class Cache extends BaseConfig * Your Redis server can be specified below, if you are using * the Redis or Predis drivers. * - * @var array + * @var array{host?: string, password?: string|null, port?: int, timeout?: int, database?: int} */ public array $redis = [ 'host' => '127.0.0.1', diff --git a/system/Cache/CacheFactory.php b/system/Cache/CacheFactory.php index fb60d73c5cd7..e84254142125 100644 --- a/system/Cache/CacheFactory.php +++ b/system/Cache/CacheFactory.php @@ -75,7 +75,7 @@ public static function getHandler(Cache $config, ?string $handler = null, ?strin } } - // If $adapter->initialization throws a CriticalError exception, we will attempt to + // If $adapter->initialize() throws a CriticalError exception, we will attempt to // use the $backup handler, if that also fails, we resort to the dummy handler. try { $adapter->initialize(); diff --git a/system/Cache/CacheInterface.php b/system/Cache/CacheInterface.php index d76d5aa15ba3..0f94cd56f930 100644 --- a/system/Cache/CacheInterface.php +++ b/system/Cache/CacheInterface.php @@ -13,9 +13,6 @@ namespace CodeIgniter\Cache; -/** - * Cache interface - */ interface CacheInterface { /** @@ -30,16 +27,16 @@ public function initialize(); * * @param string $key Cache item name * - * @return array|bool|float|int|object|string|null + * @return mixed */ public function get(string $key); /** * Saves an item to the cache store. * - * @param string $key Cache item name - * @param array|bool|float|int|object|string|null $value The data to save - * @param int $ttl Time To Live, in seconds (default 60) + * @param string $key Cache item name + * @param mixed $value The data to save + * @param int $ttl Time To Live, in seconds (default 60) * * @return bool Success or failure */ @@ -87,7 +84,7 @@ public function clean(); * The information returned and the structure of the data * varies depending on the handler. * - * @return array|false|object|null + * @return array|false|object|null */ public function getCacheInfo(); @@ -96,10 +93,9 @@ public function getCacheInfo(); * * @param string $key Cache item name. * - * @return array|false|null - * Returns null if the item does not exist, otherwise array - * with at least the 'expire' key for absolute epoch expiry (or null). - * Some handlers may return false when an item does not exist, which is deprecated. + * @return array|false|null Returns null if the item does not exist, otherwise array + * with at least the 'expire' key for absolute epoch expiry (or null). + * Some handlers may return false when an item does not exist, which is deprecated. */ public function getMetaData(string $key); diff --git a/system/Cache/Exceptions/CacheException.php b/system/Cache/Exceptions/CacheException.php index 650dad64d649..c4871a00e8b4 100644 --- a/system/Cache/Exceptions/CacheException.php +++ b/system/Cache/Exceptions/CacheException.php @@ -16,9 +16,6 @@ use CodeIgniter\Exceptions\DebugTraceableTrait; use CodeIgniter\Exceptions\RuntimeException; -/** - * CacheException - */ class CacheException extends RuntimeException { use DebugTraceableTrait; @@ -26,7 +23,7 @@ class CacheException extends RuntimeException /** * Thrown when handler has no permission to write cache. * - * @return CacheException + * @return static */ public static function forUnableToWrite(string $path) { @@ -36,7 +33,7 @@ public static function forUnableToWrite(string $path) /** * Thrown when an unrecognized handler is used. * - * @return CacheException + * @return static */ public static function forInvalidHandlers() { @@ -46,7 +43,7 @@ public static function forInvalidHandlers() /** * Thrown when no backup handler is setup in config. * - * @return CacheException + * @return static */ public static function forNoBackup() { @@ -56,7 +53,7 @@ public static function forNoBackup() /** * Thrown when specified handler was not found. * - * @return CacheException + * @return static */ public static function forHandlerNotFound() { diff --git a/system/Cache/FactoriesCache.php b/system/Cache/FactoriesCache.php index e4b7488f43df..6e49996c5d45 100644 --- a/system/Cache/FactoriesCache.php +++ b/system/Cache/FactoriesCache.php @@ -18,15 +18,9 @@ final class FactoriesCache { - /** - * @var CacheInterface|FileVarExportHandler - */ - private $cache; - - /** - * @param CacheInterface|FileVarExportHandler|null $cache - */ - public function __construct($cache = null) + private readonly CacheInterface|FileVarExportHandler $cache; + + public function __construct(CacheInterface|FileVarExportHandler|null $cache = null) { $this->cache = $cache ?? new FileVarExportHandler(); } @@ -51,7 +45,9 @@ public function load(string $component): bool { $key = $this->getCacheKey($component); - if (! $data = $this->cache->get($key)) { + $data = $this->cache->get($key); + + if (! is_array($data) || $data === []) { return false; } diff --git a/system/Cache/FactoriesCache/FileVarExportHandler.php b/system/Cache/FactoriesCache/FileVarExportHandler.php index b7c1a043c86e..092cd67ebd80 100644 --- a/system/Cache/FactoriesCache/FileVarExportHandler.php +++ b/system/Cache/FactoriesCache/FileVarExportHandler.php @@ -17,10 +17,7 @@ final class FileVarExportHandler { private string $path = WRITEPATH . 'cache'; - /** - * @param array|bool|float|int|object|string|null $val - */ - public function save(string $key, $val): void + public function save(string $key, mixed $val): void { $val = var_export($val, true); @@ -36,10 +33,7 @@ public function delete(string $key): void @unlink($this->path . "/{$key}"); } - /** - * @return array|bool|float|int|object|string|null - */ - public function get(string $key) + public function get(string $key): mixed { return @include $this->path . "/{$key}"; } diff --git a/system/Cache/Handlers/BaseHandler.php b/system/Cache/Handlers/BaseHandler.php index ebe0ca2f43ba..db482952022a 100644 --- a/system/Cache/Handlers/BaseHandler.php +++ b/system/Cache/Handlers/BaseHandler.php @@ -53,7 +53,7 @@ abstract class BaseHandler implements CacheInterface * Keys that exceed MAX_KEY_LENGTH are hashed. * From https://github.com/symfony/cache/blob/7b024c6726af21fd4984ac8d1eae2b9f3d90de88/CacheItem.php#L158 * - * @param string $key The key to validate + * @param mixed $key The key to validate * @param string $prefix Optional prefix to include in length calculations * * @throws InvalidArgumentException When $key is not valid @@ -67,7 +67,8 @@ public static function validateKey($key, $prefix = ''): string throw new InvalidArgumentException('Cache key cannot be empty.'); } - $reserved = config(Cache::class)->reservedCharacters ?? self::RESERVED_CHARACTERS; + $reserved = config(Cache::class)->reservedCharacters; + if ($reserved !== '' && strpbrk($key, $reserved) !== false) { throw new InvalidArgumentException('Cache key contains reserved characters ' . $reserved); } @@ -83,7 +84,7 @@ public static function validateKey($key, $prefix = ''): string * @param int $ttl Time to live * @param Closure(): mixed $callback Callback return value * - * @return array|bool|float|int|object|string|null + * @return mixed */ public function remember(string $key, int $ttl, Closure $callback) { @@ -103,7 +104,7 @@ public function remember(string $key, int $ttl, Closure $callback) * * @param string $pattern Cache items glob-style pattern * - * @return int|never + * @return int * * @throws Exception */ diff --git a/system/Cache/Handlers/FileHandler.php b/system/Cache/Handlers/FileHandler.php index c496143f62d9..010fc86d9cdd 100644 --- a/system/Cache/Handlers/FileHandler.php +++ b/system/Cache/Handlers/FileHandler.php @@ -54,14 +54,19 @@ class FileHandler extends BaseHandler */ public function __construct(Cache $config) { - $this->path = ! empty($config->file['storePath']) ? $config->file['storePath'] : WRITEPATH . 'cache'; - $this->path = rtrim($this->path, '/') . '/'; + $options = [ + ...['storePath' => WRITEPATH . 'cache', 'mode' => 0640], + ...$config->file, + ]; + + $this->path = $options['storePath'] !== '' ? $options['storePath'] : WRITEPATH . 'cache'; + $this->path = rtrim($this->path, '\\/') . '/'; if (! is_really_writable($this->path)) { throw CacheException::forUnableToWrite($this->path); } - $this->mode = $config->file['mode'] ?? 0640; + $this->mode = $options['mode']; $this->prefix = $config->prefix; helper('filesystem'); @@ -342,33 +347,46 @@ protected function deleteFiles(string $path, bool $delDir = false, bool $htdocs * @param bool $topLevelOnly Look only at the top level directory specified? * @param bool $_recursion Internal variable to determine recursion status - do not use in calls * - * @return array|false + * @return array|false */ protected function getDirFileInfo(string $sourceDir, bool $topLevelOnly = true, bool $_recursion = false) { - static $_filedata = []; - $relativePath = $sourceDir; + static $filedata = []; + + $relativePath = $sourceDir; + $filePointer = @opendir($sourceDir); - if ($fp = @opendir($sourceDir)) { + if (! is_bool($filePointer)) { // reset the array and make sure $sourceDir has a trailing slash on the initial call if ($_recursion === false) { - $_filedata = []; - $sourceDir = rtrim(realpath($sourceDir) ?: $sourceDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + $filedata = []; + + $resolvedSrc = realpath($sourceDir); + $resolvedSrc = $resolvedSrc === false ? $sourceDir : $resolvedSrc; + + $sourceDir = rtrim($resolvedSrc, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; } // Used to be foreach (scandir($sourceDir, 1) as $file), but scandir() is simply not as fast - while (false !== ($file = readdir($fp))) { + while (false !== $file = readdir($filePointer)) { if (is_dir($sourceDir . $file) && $file[0] !== '.' && $topLevelOnly === false) { $this->getDirFileInfo($sourceDir . $file . DIRECTORY_SEPARATOR, $topLevelOnly, true); } elseif (! is_dir($sourceDir . $file) && $file[0] !== '.') { - $_filedata[$file] = $this->getFileInfo($sourceDir . $file); - $_filedata[$file]['relative_path'] = $relativePath; + $filedata[$file] = $this->getFileInfo($sourceDir . $file); + + $filedata[$file]['relative_path'] = $relativePath; } } - closedir($fp); + closedir($filePointer); - return $_filedata; + return $filedata; } return false; @@ -382,10 +400,19 @@ protected function getDirFileInfo(string $sourceDir, bool $topLevelOnly = true, * * @deprecated 4.6.1 Use `get_file_info()` instead. * - * @param string $file Path to file - * @param array|string $returnedValues Array or comma separated string of information returned + * @param string $file Path to file + * @param list|string $returnedValues Array or comma separated string of information returned * - * @return array|false + * @return array{ + * name?: string, + * server_path?: string, + * size?: int, + * date?: int, + * readable?: bool, + * writable?: bool, + * executable?: bool, + * fileperms?: int + * }|false */ protected function getFileInfo(string $file, $returnedValues = ['name', 'server_path', 'size', 'date']) { diff --git a/system/Cache/Handlers/MemcachedHandler.php b/system/Cache/Handlers/MemcachedHandler.php index c6bb6ddcadce..91ac38a9427d 100644 --- a/system/Cache/Handlers/MemcachedHandler.php +++ b/system/Cache/Handlers/MemcachedHandler.php @@ -38,7 +38,7 @@ class MemcachedHandler extends BaseHandler /** * Memcached Configuration * - * @var array + * @var array{host: string, port: int, weight: int, raw: bool} */ protected $config = [ 'host' => '127.0.0.1', @@ -76,43 +76,32 @@ public function initialize() { try { if (class_exists(Memcached::class)) { - // Create new instance of Memcached $this->memcached = new Memcached(); + if ($this->config['raw']) { $this->memcached->setOption(Memcached::OPT_BINARY_PROTOCOL, true); } - // Add server $this->memcached->addServer( $this->config['host'], $this->config['port'], $this->config['weight'], ); - // attempt to get status of servers $stats = $this->memcached->getStats(); // $stats should be an associate array with a key in the format of host:port. // If it doesn't have the key, we know the server is not working as expected. - if (! isset($stats[$this->config['host'] . ':' . $this->config['port']])) { + if (! is_array($stats) || ! isset($stats[$this->config['host'] . ':' . $this->config['port']])) { throw new CriticalError('Cache: Memcached connection failed.'); } } elseif (class_exists(Memcache::class)) { - // Create new instance of Memcache $this->memcached = new Memcache(); - // Check if we can connect to the server - $canConnect = $this->memcached->connect( - $this->config['host'], - $this->config['port'], - ); - - // If we can't connect, throw a CriticalError exception - if ($canConnect === false) { + if (! $this->memcached->connect($this->config['host'], $this->config['port'])) { throw new CriticalError('Cache: Memcache connection failed.'); } - // Add server, third parameter is persistence and defaults to TRUE. $this->memcached->addServer( $this->config['host'], $this->config['port'], diff --git a/system/Cache/Handlers/PredisHandler.php b/system/Cache/Handlers/PredisHandler.php index bdf5afe0f22f..250ea74a88e0 100644 --- a/system/Cache/Handlers/PredisHandler.php +++ b/system/Cache/Handlers/PredisHandler.php @@ -31,7 +31,13 @@ class PredisHandler extends BaseHandler /** * Default config * - * @var array + * @var array{ + * scheme: string, + * host: string, + * password: string|null, + * port: int, + * timeout: int + * } */ protected $config = [ 'scheme' => 'tcp', diff --git a/system/Cache/Handlers/RedisHandler.php b/system/Cache/Handlers/RedisHandler.php index 80c21e8559e8..3a13ee07f8e4 100644 --- a/system/Cache/Handlers/RedisHandler.php +++ b/system/Cache/Handlers/RedisHandler.php @@ -29,7 +29,13 @@ class RedisHandler extends BaseHandler /** * Default config * - * @var array + * @var array{ + * host: string, + * password: string|null, + * port: int, + * timeout: int, + * database: int, + * } */ protected $config = [ 'host' => '127.0.0.1', diff --git a/system/Cache/ResponseCache.php b/system/Cache/ResponseCache.php index e984b8e3606f..2c92dc5058be 100644 --- a/system/Cache/ResponseCache.php +++ b/system/Cache/ResponseCache.php @@ -40,12 +40,10 @@ final class ResponseCache * * @var bool|list */ - private $cacheQueryString = false; + private array|bool $cacheQueryString = false; /** - * Cache time to live. - * - * @var int seconds + * Cache time to live (TTL) in seconds. */ private int $ttl = 0; @@ -54,10 +52,7 @@ public function __construct(CacheConfig $config, private readonly CacheInterface $this->cacheQueryString = $config->cacheQueryString; } - /** - * @return $this - */ - public function setTtl(int $ttl) + public function setTtl(int $ttl): self { $this->ttl = $ttl; @@ -67,11 +62,9 @@ public function setTtl(int $ttl) /** * Generates the cache key to use from the current request. * - * @param CLIRequest|IncomingRequest $request - * * @internal for testing purposes only */ - public function generateCacheKey($request): string + public function generateCacheKey(CLIRequest|IncomingRequest $request): string { if ($request instanceof CLIRequest) { return md5($request->getPath()); @@ -79,7 +72,7 @@ public function generateCacheKey($request): string $uri = clone $request->getUri(); - $query = $this->cacheQueryString + $query = (bool) $this->cacheQueryString ? $uri->getQuery(is_array($this->cacheQueryString) ? ['only' => $this->cacheQueryString] : []) : ''; @@ -88,10 +81,8 @@ public function generateCacheKey($request): string /** * Caches the response. - * - * @param CLIRequest|IncomingRequest $request */ - public function make($request, ResponseInterface $response): bool + public function make(CLIRequest|IncomingRequest $request, ResponseInterface $response): bool { if ($this->ttl === 0) { return true; @@ -118,12 +109,12 @@ public function make($request, ResponseInterface $response): bool /** * Gets the cached response for the request. - * - * @param CLIRequest|IncomingRequest $request */ - public function get($request, ResponseInterface $response): ?ResponseInterface + public function get(CLIRequest|IncomingRequest $request, ResponseInterface $response): ?ResponseInterface { - if ($cachedResponse = $this->cache->get($this->generateCacheKey($request))) { + $cachedResponse = $this->cache->get($this->generateCacheKey($request)); + + if (is_string($cachedResponse) && $cachedResponse !== '') { $cachedResponse = unserialize($cachedResponse); if ( diff --git a/system/Helpers/filesystem_helper.php b/system/Helpers/filesystem_helper.php index aa7efbea0468..e7bd35441959 100644 --- a/system/Helpers/filesystem_helper.php +++ b/system/Helpers/filesystem_helper.php @@ -255,6 +255,14 @@ function get_filenames( * @param string $sourceDir Path to source * @param bool $topLevelOnly Look only at the top level directory specified? * @param bool $recursion Internal variable to determine recursion status - do not use in calls + * + * @return array */ function get_dir_file_info(string $sourceDir, bool $topLevelOnly = true, bool $recursion = false): array { @@ -298,10 +306,19 @@ function get_dir_file_info(string $sourceDir, bool $topLevelOnly = true, bool $r * Options are: name, server_path, size, date, readable, writable, executable, fileperms * Returns false if the file cannot be found. * - * @param string $file Path to file - * @param array|string $returnedValues Array or comma separated string of information returned + * @param string $file Path to file + * @param list|string $returnedValues Array or comma separated string of information returned * - * @return array|null + * @return array{ + * name?: string, + * server_path?: string, + * size?: int, + * date?: int, + * readable?: bool, + * writable?: bool, + * executable?: bool, + * fileperms?: int + * }|null */ function get_file_info(string $file, $returnedValues = ['name', 'server_path', 'size', 'date']) { diff --git a/system/View/Cell.php b/system/View/Cell.php index 79e573f5103e..8325accc7223 100644 --- a/system/View/Cell.php +++ b/system/View/Cell.php @@ -87,7 +87,9 @@ public function render(string $library, $params = null, int $ttl = 0, ?string $c // Is the output cached? $cacheName ??= str_replace(['\\', '/'], '', $class) . $method . md5(serialize($params)); - if ($output = $this->cache->get($cacheName)) { + $output = $this->cache->get($cacheName); + + if (is_string($output) && $output !== '') { return $output; } diff --git a/tests/system/Cache/AbstractFactoriesCacheHandlerTestCase.php b/tests/system/Cache/AbstractFactoriesCacheHandlerTestCase.php new file mode 100644 index 000000000000..7a75239c5b03 --- /dev/null +++ b/tests/system/Cache/AbstractFactoriesCacheHandlerTestCase.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Cache; + +use CodeIgniter\Cache\FactoriesCache\FileVarExportHandler; +use CodeIgniter\Config\Factories; +use CodeIgniter\Test\CIUnitTestCase; +use Config\App; + +/** + * @internal + */ +abstract class AbstractFactoriesCacheHandlerTestCase extends CIUnitTestCase +{ + protected FactoriesCache $cache; + protected CacheInterface|FileVarExportHandler $handler; + + abstract protected function createFactoriesCache(): void; + + public function testInstantiate(): void + { + $this->createFactoriesCache(); + $this->assertInstanceOf(FactoriesCache::class, $this->cache); + } + + public function testSave(): void + { + Factories::reset(); + Factories::config('App'); + + $this->createFactoriesCache(); + + $this->cache->save('config'); + + $cachedData = $this->handler->get('FactoriesCache_config'); + + $this->assertIsArray($cachedData); + $this->assertArrayHasKey('aliases', $cachedData); + $this->assertArrayHasKey('instances', $cachedData); + $this->assertArrayHasKey('App', $cachedData['aliases']); + } + + public function testLoad(): void + { + Factories::reset(); + + /** @var App $appConfig */ + $appConfig = Factories::config('App'); + + $appConfig->baseURL = 'http://test.example.jp/this-is-test/'; + + $this->createFactoriesCache(); + $this->cache->save('config'); + + Factories::reset(); + $this->cache->load('config'); + + /** @var App $appConfig */ + $appConfig = Factories::config('App'); + $this->assertSame('http://test.example.jp/this-is-test/', $appConfig->baseURL); + } + + public function testDelete(): void + { + $this->createFactoriesCache(); + + $this->cache->delete('config'); + + $this->assertFalse($this->cache->load('config')); + } +} diff --git a/tests/system/Cache/CacheFactoryTest.php b/tests/system/Cache/CacheFactoryTest.php index 8a76462b2cea..c51dce6830c3 100644 --- a/tests/system/Cache/CacheFactoryTest.php +++ b/tests/system/Cache/CacheFactoryTest.php @@ -26,15 +26,12 @@ final class CacheFactoryTest extends CIUnitTestCase { private static string $directory = 'CacheFactory'; - private CacheFactory $cacheFactory; private Cache $config; protected function setUp(): void { parent::setUp(); - $this->cacheFactory = new CacheFactory(); - // Initialize path $this->config = new Cache(); $this->config->file['storePath'] .= self::$directory; @@ -48,11 +45,6 @@ protected function tearDown(): void } } - public function testNew(): void - { - $this->assertInstanceOf(CacheFactory::class, $this->cacheFactory); - } - public function testGetHandlerExceptionCacheInvalidHandlers(): void { $this->expectException(CacheException::class); @@ -60,7 +52,7 @@ public function testGetHandlerExceptionCacheInvalidHandlers(): void $this->config->validHandlers = []; - $this->cacheFactory->getHandler($this->config); + CacheFactory::getHandler($this->config); } public function testGetHandlerExceptionCacheHandlerNotFound(): void @@ -70,7 +62,7 @@ public function testGetHandlerExceptionCacheHandlerNotFound(): void unset($this->config->validHandlers[$this->config->handler]); - $this->cacheFactory->getHandler($this->config); + CacheFactory::getHandler($this->config); } public function testGetDummyHandler(): void @@ -81,7 +73,7 @@ public function testGetDummyHandler(): void $this->config->handler = 'dummy'; - $this->assertInstanceOf(DummyHandler::class, $this->cacheFactory->getHandler($this->config)); + $this->assertInstanceOf(DummyHandler::class, CacheFactory::getHandler($this->config)); // Initialize path $this->config = new Cache(); @@ -99,7 +91,7 @@ public function testHandlesBadHandler(): void if (is_windows()) { $this->markTestSkipped('Cannot test this properly on Windows.'); } else { - $this->assertInstanceOf(DummyHandler::class, $this->cacheFactory->getHandler($this->config, 'wincache', 'wincache')); + $this->assertInstanceOf(DummyHandler::class, CacheFactory::getHandler($this->config, 'wincache', 'wincache')); } // Initialize path diff --git a/tests/system/Cache/CacheMockTest.php b/tests/system/Cache/CacheMockTest.php index c867bebc4249..9fe4538c2760 100644 --- a/tests/system/Cache/CacheMockTest.php +++ b/tests/system/Cache/CacheMockTest.php @@ -35,6 +35,7 @@ public function testMockReturnsMockCacheClass(): void public function testMockCaching(): void { + /** @var MockCache $mock */ $mock = mock(CacheFactory::class); // Ensure it stores the value normally diff --git a/tests/system/Cache/FactoriesCacheFileHandlerTest.php b/tests/system/Cache/FactoriesCacheFileHandlerTest.php index 01e3276733e3..660c476cc799 100644 --- a/tests/system/Cache/FactoriesCacheFileHandlerTest.php +++ b/tests/system/Cache/FactoriesCacheFileHandlerTest.php @@ -13,24 +13,18 @@ namespace CodeIgniter\Cache; -use CodeIgniter\Cache\FactoriesCache\FileVarExportHandler; -use Config\Cache as CacheConfig; +use Config\Cache; use PHPUnit\Framework\Attributes\Group; /** * @internal */ #[Group('Others')] -final class FactoriesCacheFileHandlerTest extends FactoriesCacheFileVarExportHandlerTest +final class FactoriesCacheFileHandlerTest extends AbstractFactoriesCacheHandlerTestCase { - /** - * @var CacheInterface|FileVarExportHandler - */ - protected $handler; - protected function createFactoriesCache(): void { - $this->handler = CacheFactory::getHandler(new CacheConfig(), 'file'); + $this->handler = CacheFactory::getHandler(new Cache(), 'file'); $this->cache = new FactoriesCache($this->handler); } } diff --git a/tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php b/tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php index 8bf5c79d9209..c71c756f5cdd 100644 --- a/tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php +++ b/tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php @@ -14,78 +14,17 @@ namespace CodeIgniter\Cache; use CodeIgniter\Cache\FactoriesCache\FileVarExportHandler; -use CodeIgniter\Config\Factories; -use CodeIgniter\Test\CIUnitTestCase; -use Config\App; use PHPUnit\Framework\Attributes\Group; /** * @internal - * @no-final */ #[Group('Others')] -class FactoriesCacheFileVarExportHandlerTest extends CIUnitTestCase +final class FactoriesCacheFileVarExportHandlerTest extends AbstractFactoriesCacheHandlerTestCase { - protected FactoriesCache $cache; - - /** - * @var CacheInterface|FileVarExportHandler - */ - protected $handler; - protected function createFactoriesCache(): void { $this->handler = new FileVarExportHandler(); $this->cache = new FactoriesCache($this->handler); } - - public function testInstantiate(): void - { - $this->createFactoriesCache(); - - $this->assertInstanceOf(FactoriesCache::class, $this->cache); - } - - public function testSave(): void - { - Factories::reset(); - Factories::config('App'); - - $this->createFactoriesCache(); - - $this->cache->save('config'); - - $cachedData = $this->handler->get('FactoriesCache_config'); - - $this->assertArrayHasKey('aliases', $cachedData); - $this->assertArrayHasKey('instances', $cachedData); - $this->assertArrayHasKey('App', $cachedData['aliases']); - } - - public function testLoad(): void - { - Factories::reset(); - /** @var App $appConfig */ - $appConfig = Factories::config('App'); - $appConfig->baseURL = 'http://test.example.jp/this-is-test/'; - - $this->createFactoriesCache(); - $this->cache->save('config'); - - Factories::reset(); - - $this->cache->load('config'); - - $appConfig = Factories::config('App'); - $this->assertSame('http://test.example.jp/this-is-test/', $appConfig->baseURL); - } - - public function testDelete(): void - { - $this->createFactoriesCache(); - - $this->cache->delete('config'); - - $this->assertFalse($this->cache->load('config')); - } } diff --git a/tests/system/Cache/Handlers/BaseHandlerTest.php b/tests/system/Cache/Handlers/BaseHandlerTest.php index 06d3c4a98f8e..4cba697e5e00 100644 --- a/tests/system/Cache/Handlers/BaseHandlerTest.php +++ b/tests/system/Cache/Handlers/BaseHandlerTest.php @@ -25,11 +25,8 @@ #[Group('Others')] final class BaseHandlerTest extends CIUnitTestCase { - /** - * @param mixed $input - */ #[DataProvider('provideValidateKeyInvalidType')] - public function testValidateKeyInvalidType($input): void + public function testValidateKeyInvalidType(mixed $input): void { $this->expectException('InvalidArgumentException'); $this->expectExceptionMessage('Cache key must be a string'); @@ -37,14 +34,17 @@ public function testValidateKeyInvalidType($input): void BaseHandler::validateKey($input); } + /** + * @return iterable + */ public static function provideValidateKeyInvalidType(): iterable { - return [ - [true], - [false], - [null], - [42], - [new stdClass()], + yield from [ + 'true' => [true], + 'false' => [false], + 'null' => [null], + 'int' => [42], + 'object' => [new stdClass()], ]; } diff --git a/tests/system/Cache/Handlers/DummyHandlerTest.php b/tests/system/Cache/Handlers/DummyHandlerTest.php index c9c8a0fa0937..ab3b3ccd643d 100644 --- a/tests/system/Cache/Handlers/DummyHandlerTest.php +++ b/tests/system/Cache/Handlers/DummyHandlerTest.php @@ -13,7 +13,9 @@ namespace CodeIgniter\Cache\Handlers; +use CodeIgniter\Cache\CacheFactory; use CodeIgniter\Test\CIUnitTestCase; +use Config\Cache; use PHPUnit\Framework\Attributes\Group; /** @@ -26,8 +28,7 @@ final class DummyHandlerTest extends CIUnitTestCase protected function setUp(): void { - $this->handler = new DummyHandler(); - $this->handler->initialize(); + $this->handler = CacheFactory::getHandler(new Cache(), 'dummy'); } public function testNew(): void diff --git a/tests/system/Cache/Handlers/FileHandlerTest.php b/tests/system/Cache/Handlers/FileHandlerTest.php index ed163071aede..0819a5a9df06 100644 --- a/tests/system/Cache/Handlers/FileHandlerTest.php +++ b/tests/system/Cache/Handlers/FileHandlerTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Cache\Handlers; +use CodeIgniter\Cache\CacheFactory; use CodeIgniter\Cache\Exceptions\CacheException; use CodeIgniter\CLI\CLI; use CodeIgniter\I18n\Time; @@ -58,8 +59,7 @@ protected function setUp(): void mkdir($this->config->file['storePath'], 0777, true); } - $this->handler = new FileHandler($this->config); - $this->handler->initialize(); + $this->handler = CacheFactory::getHandler($this->config, 'file'); } protected function tearDown(): void @@ -98,19 +98,16 @@ public function testNewWithNonWritablePath(): void $this->expectException(CacheException::class); chmod($this->config->file['storePath'], 0444); - new FileHandler($this->config); + CacheFactory::getHandler($this->config, 'file'); } public function testSetDefaultPath(): void { // Initialize path - $config = new Cache(); - $config->file['storePath'] = null; + $config = new Cache(); + unset($config->file['storePath']); - $this->handler = new FileHandler($config); - $this->handler->initialize(); - - $this->assertInstanceOf(FileHandler::class, $this->handler); + $this->assertInstanceOf(FileHandler::class, CacheFactory::getHandler($config, 'file')); } /** @@ -245,9 +242,8 @@ public function testIncrement(): void public function testIncrementWithDefaultPrefix(): void { $this->config->prefix = 'test_'; - $this->handler = new FileHandler($this->config); - $this->handler->initialize(); + $this->handler = CacheFactory::getHandler($this->config, 'file'); $this->handler->save(self::$key1, 1); $this->handler->save(self::$key2, 'value'); @@ -320,9 +316,7 @@ public function testSaveMode(int $int, string $string): void $config = new Cache(); $config->file['mode'] = $int; - $this->handler = new FileHandler($config); - $this->handler->initialize(); - + $this->handler = CacheFactory::getHandler($config, 'file'); $this->handler->save(self::$key1, 'value'); $file = $config->file['storePath'] . DIRECTORY_SEPARATOR . self::$key1; @@ -346,10 +340,17 @@ public static function provideSaveMode(): iterable public function testFileHandler(): void { - $fileHandler = new BaseTestFileHandler(); + $cache = new Cache(); - $actual = $fileHandler->getFileInfoTest(); + $cache->validHandlers['file'] = BaseTestFileHandler::class; + + /** @var BaseTestFileHandler $fileHandler */ + $fileHandler = CacheFactory::getHandler($cache, 'file'); + $this->assertInstanceOf(BaseTestFileHandler::class, $fileHandler); + $actual = $fileHandler->getFileInfoTest(); + $this->assertIsArray($actual); + $this->assertArrayHasKey('name', $actual); $this->assertArrayHasKey('server_path', $actual); $this->assertArrayHasKey('size', $actual); $this->assertArrayHasKey('date', $actual); @@ -389,6 +390,9 @@ public function testGetItemWithCorruptedData(): void } } +/** + * @internal + */ final class BaseTestFileHandler extends FileHandler { private static string $directory = 'FileHandler'; @@ -405,7 +409,16 @@ public function __construct() } /** - * @return array|null + * @return array{ + * name: string, + * server_path: string, + * size: int, + * date: int, + * readable: bool, + * writable: bool, + * executable: bool, + * fileperms: int, + * }|null */ public function getFileInfoTest(): ?array { diff --git a/tests/system/Cache/Handlers/MemcachedHandlerTest.php b/tests/system/Cache/Handlers/MemcachedHandlerTest.php index c6cfdb8543ee..f4114d4742e7 100644 --- a/tests/system/Cache/Handlers/MemcachedHandlerTest.php +++ b/tests/system/Cache/Handlers/MemcachedHandlerTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Cache\Handlers; +use CodeIgniter\Cache\CacheFactory; use CodeIgniter\CLI\CLI; use CodeIgniter\Exceptions\BadMethodCallException; use CodeIgniter\I18n\Time; @@ -45,11 +46,7 @@ protected function setUp(): void $this->markTestSkipped('Memcached extension not loaded.'); } - $config = new Cache(); - - $this->handler = new MemcachedHandler($config); - - $this->handler->initialize(); + $this->handler = CacheFactory::getHandler(new Cache(), 'memcached'); } protected function tearDown(): void @@ -139,9 +136,8 @@ public function testIncrement(): void $config = new Cache(); $config->memcached['raw'] = true; - $memcachedHandler = new MemcachedHandler($config); - $memcachedHandler->initialize(); + $memcachedHandler = CacheFactory::getHandler($config, 'memcached'); $memcachedHandler->save(self::$key1, 1); $memcachedHandler->save(self::$key2, 'value'); @@ -158,9 +154,8 @@ public function testDecrement(): void $config = new Cache(); $config->memcached['raw'] = true; - $memcachedHandler = new MemcachedHandler($config); - $memcachedHandler->initialize(); + $memcachedHandler = CacheFactory::getHandler($config, 'memcached'); $memcachedHandler->save(self::$key1, 10); $memcachedHandler->save(self::$key2, 'value'); diff --git a/tests/system/Cache/Handlers/PredisHandlerTest.php b/tests/system/Cache/Handlers/PredisHandlerTest.php index 4b009582cd55..eec292f280ad 100644 --- a/tests/system/Cache/Handlers/PredisHandlerTest.php +++ b/tests/system/Cache/Handlers/PredisHandlerTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Cache\Handlers; +use CodeIgniter\Cache\CacheFactory; use CodeIgniter\CLI\CLI; use CodeIgniter\I18n\Time; use Config\Cache; @@ -42,11 +43,8 @@ protected function setUp(): void { parent::setUp(); - $this->config = new Cache(); - - $this->handler = new PredisHandler($this->config); - - $this->handler->initialize(); + $this->config = new Cache(); + $this->handler = CacheFactory::getHandler($this->config, 'predis'); } protected function tearDown(): void @@ -63,9 +61,7 @@ public function testNew(): void public function testDestruct(): void { - $this->handler = new PredisHandler($this->config); - $this->handler->initialize(); - + $this->handler = CacheFactory::getHandler($this->config, 'predis'); $this->assertInstanceOf(PredisHandler::class, $this->handler); } diff --git a/tests/system/Cache/Handlers/RedisHandlerTest.php b/tests/system/Cache/Handlers/RedisHandlerTest.php index 812985e65e8d..467abc675372 100644 --- a/tests/system/Cache/Handlers/RedisHandlerTest.php +++ b/tests/system/Cache/Handlers/RedisHandlerTest.php @@ -48,11 +48,8 @@ protected function setUp(): void $this->markTestSkipped('redis extension not loaded.'); } - $this->config = new Cache(); - - $this->handler = new RedisHandler($this->config); - - $this->handler->initialize(); + $this->config = new Cache(); + $this->handler = CacheFactory::getHandler($this->config, 'redis'); } protected function tearDown(): void @@ -69,9 +66,7 @@ public function testNew(): void public function testDestruct(): void { - $this->handler = new RedisHandler($this->config); - $this->handler->initialize(); - + $this->handler = CacheFactory::getHandler($this->config, 'redis'); $this->assertInstanceOf(RedisHandler::class, $this->handler); } diff --git a/tests/system/Cache/ResponseCacheTest.php b/tests/system/Cache/ResponseCacheTest.php index 11c82db71247..b3f0ec832905 100644 --- a/tests/system/Cache/ResponseCacheTest.php +++ b/tests/system/Cache/ResponseCacheTest.php @@ -21,8 +21,9 @@ use CodeIgniter\HTTP\SiteURI; use CodeIgniter\HTTP\UserAgent; use CodeIgniter\Test\CIUnitTestCase; -use Config\App as AppConfig; -use Config\Cache as CacheConfig; +use CodeIgniter\Test\Mock\MockCache; +use Config\App; +use Config\Cache; use ErrorException; use PHPUnit\Framework\Attributes\BackupGlobals; use PHPUnit\Framework\Attributes\Group; @@ -34,138 +35,112 @@ #[Group('Others')] final class ResponseCacheTest extends CIUnitTestCase { - private AppConfig $appConfig; - - protected function setUp(): void + /** + * @param array $query + */ + private function createIncomingRequest(string $uri = '', array $query = [], App $app = new App()): IncomingRequest { - parent::setUp(); - - $this->appConfig = new AppConfig(); - } + $superglobals = service('superglobals'); + $superglobals->setServer('REQUEST_URI', sprintf('/%s%s', $uri, $query !== [] ? '?' . http_build_query($query) : '')); + $superglobals->setServer('SCRIPT_NAME', '/index.php'); - private function createIncomingRequest( - string $uri = '', - array $query = [], - ?AppConfig $appConfig = null, - ): IncomingRequest { - $_POST = $_GET = $_SERVER = $_REQUEST = $_ENV = $_COOKIE = $_SESSION = []; + $siteUri = new SiteURI($app, $uri); - $_SERVER['REQUEST_URI'] = '/' . $uri . ($query !== [] ? '?' . http_build_query($query) : ''); - $_SERVER['SCRIPT_NAME'] = '/index.php'; - - $appConfig ??= $this->appConfig; - - $siteUri = new SiteURI($appConfig, $uri); if ($query !== []) { - $_GET = $_REQUEST = $query; $siteUri->setQueryArray($query); } - return new IncomingRequest( - $appConfig, - $siteUri, - null, - new UserAgent(), - ); + return new IncomingRequest($app, $siteUri, null, new UserAgent()); } /** * @param list $params */ - private function createCLIRequest(array $params = [], ?AppConfig $appConfig = null): CLIRequest + private function createCLIRequest(array $params = [], App $app = new App()): CLIRequest { - $_POST = $_GET = $_SERVER = $_REQUEST = $_ENV = $_COOKIE = $_SESSION = []; + $_SERVER['argv'] = ['public/index.php', ...$params]; - $_SERVER['argv'] = ['public/index.php', ...$params]; - $_SERVER['SCRIPT_NAME'] = 'public/index.php'; + $superglobals = service('superglobals'); + $superglobals->setServer('SCRIPT_NAME', 'public/index.php'); - $appConfig ??= $this->appConfig; - - return new CLIRequest($appConfig); + return new CLIRequest($app); } - private function createResponseCache(?CacheConfig $cacheConfig = null): ResponseCache + private function createResponseCache(Cache $cache = new Cache()): ResponseCache { - $cache = mock(CacheFactory::class); - - $cacheConfig ??= new CacheConfig(); + /** @var MockCache $mockCache */ + $mockCache = mock(CacheFactory::class); - return (new ResponseCache($cacheConfig, $cache))->setTtl(300); + return (new ResponseCache($cache, $mockCache))->setTtl(300); } public function testCachePageIncomingRequest(): void { $pageCache = $this->createResponseCache(); - $request = $this->createIncomingRequest('foo/bar'); - - $response = new Response($this->appConfig); + $response = new Response(new App()); $response->setHeader('ETag', 'abcd1234'); $response->setBody('The response body.'); - $return = $pageCache->make($request, $response); - - $this->assertTrue($return); + $this->assertTrue($pageCache->make( + $this->createIncomingRequest('foo/bar'), + $response, + )); // Check cache with a request with the same URI path. - $request = $this->createIncomingRequest('foo/bar'); - $cachedResponse = $pageCache->get($request, new Response($this->appConfig)); - + $cachedResponse = $pageCache->get($this->createIncomingRequest('foo/bar'), new Response(new App())); $this->assertInstanceOf(ResponseInterface::class, $cachedResponse); $this->assertSame('The response body.', $cachedResponse->getBody()); $this->assertSame('abcd1234', $cachedResponse->getHeaderLine('ETag')); // Check cache with a request with the same URI path and different query string. - $request = $this->createIncomingRequest('foo/bar', ['foo' => 'bar', 'bar' => 'baz']); - $cachedResponse = $pageCache->get($request, new Response($this->appConfig)); - + $cachedResponse = $pageCache->get( + $this->createIncomingRequest('foo/bar', ['foo' => 'bar', 'bar' => 'baz']), + new Response(new App()), + ); $this->assertInstanceOf(ResponseInterface::class, $cachedResponse); $this->assertSame('The response body.', $cachedResponse->getBody()); $this->assertSame('abcd1234', $cachedResponse->getHeaderLine('ETag')); // Check cache with another request with the different URI path. - $request = $this->createIncomingRequest('another'); - - $cachedResponse = $pageCache->get($request, new Response($this->appConfig)); - + $cachedResponse = $pageCache->get($this->createIncomingRequest('another'), new Response(new App())); $this->assertNotInstanceOf(ResponseInterface::class, $cachedResponse); } public function testCachePageIncomingRequestWithCacheQueryString(): void { - $cacheConfig = new CacheConfig(); - $cacheConfig->cacheQueryString = true; - $pageCache = $this->createResponseCache($cacheConfig); + $cache = new Cache(); + + $cache->cacheQueryString = true; + + $pageCache = $this->createResponseCache($cache); $request = $this->createIncomingRequest('foo/bar', ['foo' => 'bar', 'bar' => 'baz']); - $response = new Response($this->appConfig); + $response = new Response(new App()); $response->setHeader('ETag', 'abcd1234'); $response->setBody('The response body.'); - $return = $pageCache->make($request, $response); - - $this->assertTrue($return); + $this->assertTrue($pageCache->make($request, $response)); // Check cache with a request with the same URI path and same query string. - $this->createIncomingRequest('foo/bar', ['foo' => 'bar', 'bar' => 'baz']); - $cachedResponse = $pageCache->get($request, new Response($this->appConfig)); - + $cachedResponse = $pageCache->get( + $this->createIncomingRequest('foo/bar', ['foo' => 'bar', 'bar' => 'baz']), + new Response(new App()), + ); $this->assertInstanceOf(ResponseInterface::class, $cachedResponse); $this->assertSame('The response body.', $cachedResponse->getBody()); $this->assertSame('abcd1234', $cachedResponse->getHeaderLine('ETag')); // Check cache with a request with the same URI path and different query string. - $request = $this->createIncomingRequest('foo/bar', ['xfoo' => 'bar', 'bar' => 'baz']); - $cachedResponse = $pageCache->get($request, new Response($this->appConfig)); - + $cachedResponse = $pageCache->get( + $this->createIncomingRequest('foo/bar', ['xfoo' => 'bar', 'bar' => 'baz']), + new Response(new App()), + ); $this->assertNotInstanceOf(ResponseInterface::class, $cachedResponse); // Check cache with another request with the different URI path. - $request = $this->createIncomingRequest('another'); - - $cachedResponse = $pageCache->get($request, new Response($this->appConfig)); - + $cachedResponse = $pageCache->get($this->createIncomingRequest('another'), new Response(new App())); $this->assertNotInstanceOf(ResponseInterface::class, $cachedResponse); } @@ -173,19 +148,16 @@ public function testCachePageIncomingRequestWithHttpMethods(): void { $pageCache = $this->createResponseCache(); - $request = $this->createIncomingRequest('foo/bar'); - - $response = new Response($this->appConfig); + $response = new Response(new App()); $response->setBody('The response body.'); - $return = $pageCache->make($request, $response); - - $this->assertTrue($return); + $this->assertTrue($pageCache->make($this->createIncomingRequest('foo/bar'), $response)); // Check cache with a request with the same URI path and different HTTP method - $request = $this->createIncomingRequest('foo/bar')->withMethod('POST'); - $cachedResponse = $pageCache->get($request, new Response($this->appConfig)); - + $cachedResponse = $pageCache->get( + $this->createIncomingRequest('foo/bar')->withMethod('POST'), + new Response(new App()), + ); $this->assertNotInstanceOf(ResponseInterface::class, $cachedResponse); } @@ -193,27 +165,18 @@ public function testCachePageCLIRequest(): void { $pageCache = $this->createResponseCache(); - $request = $this->createCLIRequest(['foo', 'bar']); - - $response = new Response($this->appConfig); + $response = new Response(new App()); $response->setBody('The response body.'); - $return = $pageCache->make($request, $response); - - $this->assertTrue($return); + $this->assertTrue($pageCache->make($this->createCLIRequest(['foo', 'bar']), $response)); // Check cache with a request with the same params. - $request = $this->createCLIRequest(['foo', 'bar']); - $cachedResponse = $pageCache->get($request, new Response($this->appConfig)); - + $cachedResponse = $pageCache->get($this->createCLIRequest(['foo', 'bar']), new Response(new App())); $this->assertInstanceOf(ResponseInterface::class, $cachedResponse); $this->assertSame('The response body.', $cachedResponse->getBody()); // Check cache with another request with the different params. - $request = $this->createCLIRequest(['baz']); - - $cachedResponse = $pageCache->get($request, new Response($this->appConfig)); - + $cachedResponse = $pageCache->get($this->createCLIRequest(['baz']), new Response(new App())); $this->assertNotInstanceOf(ResponseInterface::class, $cachedResponse); } @@ -222,13 +185,13 @@ public function testUnserializeError(): void $this->expectException(ErrorException::class); $this->expectExceptionMessage('unserialize(): Error at offset 0 of 12 bytes'); - $cache = mock(CacheFactory::class); - $cacheConfig = new CacheConfig(); - $pageCache = new ResponseCache($cacheConfig, $cache); + /** @var MockCache $mockCache */ + $mockCache = mock(CacheFactory::class); + $pageCache = new ResponseCache(new Cache(), $mockCache); $request = $this->createIncomingRequest('foo/bar'); - $response = new Response($this->appConfig); + $response = new Response(new App()); $response->setHeader('ETag', 'abcd1234'); $response->setBody('The response body.'); @@ -237,10 +200,10 @@ public function testUnserializeError(): void $cacheKey = $pageCache->generateCacheKey($request); // Save invalid data. - $cache->save($cacheKey, 'Invalid data'); + $mockCache->save($cacheKey, 'Invalid data'); // Check cache with a request with the same URI path. - $pageCache->get($request, new Response($this->appConfig)); + $pageCache->get($request, new Response(new App())); } public function testInvalidCacheError(): void @@ -248,13 +211,13 @@ public function testInvalidCacheError(): void $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Error unserializing page cache'); - $cache = mock(CacheFactory::class); - $cacheConfig = new CacheConfig(); - $pageCache = new ResponseCache($cacheConfig, $cache); + /** @var MockCache $mockCache */ + $mockCache = mock(CacheFactory::class); + $pageCache = new ResponseCache(new Cache(), $mockCache); $request = $this->createIncomingRequest('foo/bar'); - $response = new Response($this->appConfig); + $response = new Response(new App()); $response->setHeader('ETag', 'abcd1234'); $response->setBody('The response body.'); @@ -263,9 +226,9 @@ public function testInvalidCacheError(): void $cacheKey = $pageCache->generateCacheKey($request); // Save invalid data. - $cache->save($cacheKey, serialize(['a' => '1'])); + $mockCache->save($cacheKey, serialize(['a' => '1'])); // Check cache with a request with the same URI path. - $pageCache->get($request, new Response($this->appConfig)); + $pageCache->get($request, new Response(new App())); } } diff --git a/utils/phpstan-baseline/codeigniter.cacheHandlerInstance.neon b/utils/phpstan-baseline/codeigniter.cacheHandlerInstance.neon deleted file mode 100644 index f9225bcfb5dd..000000000000 --- a/utils/phpstan-baseline/codeigniter.cacheHandlerInstance.neon +++ /dev/null @@ -1,33 +0,0 @@ -# total 14 errors - -parameters: - ignoreErrors: - - - message: '#^Calling new DummyHandler\(\) directly is incomplete to get the cache instance\.$#' - count: 1 - path: ../../tests/system/Cache/Handlers/DummyHandlerTest.php - - - - message: '#^Calling new BaseTestFileHandler\(\) directly is incomplete to get the cache instance\.$#' - count: 1 - path: ../../tests/system/Cache/Handlers/FileHandlerTest.php - - - - message: '#^Calling new FileHandler\(\) directly is incomplete to get the cache instance\.$#' - count: 5 - path: ../../tests/system/Cache/Handlers/FileHandlerTest.php - - - - message: '#^Calling new MemcachedHandler\(\) directly is incomplete to get the cache instance\.$#' - count: 3 - path: ../../tests/system/Cache/Handlers/MemcachedHandlerTest.php - - - - message: '#^Calling new PredisHandler\(\) directly is incomplete to get the cache instance\.$#' - count: 2 - path: ../../tests/system/Cache/Handlers/PredisHandlerTest.php - - - - message: '#^Calling new RedisHandler\(\) directly is incomplete to get the cache instance\.$#' - count: 2 - path: ../../tests/system/Cache/Handlers/RedisHandlerTest.php diff --git a/utils/phpstan-baseline/codeigniter.getReassignArray.neon b/utils/phpstan-baseline/codeigniter.getReassignArray.neon index e02f1107c29c..b661a9e9d7de 100644 --- a/utils/phpstan-baseline/codeigniter.getReassignArray.neon +++ b/utils/phpstan-baseline/codeigniter.getReassignArray.neon @@ -1,12 +1,7 @@ -# total 22 errors +# total 19 errors parameters: ignoreErrors: - - - message: '#^Re\-assigning arrays to \$_GET directly is discouraged\.$#' - count: 3 - path: ../../tests/system/Cache/ResponseCacheTest.php - - message: '#^Re\-assigning arrays to \$_GET directly is discouraged\.$#' count: 3 diff --git a/utils/phpstan-baseline/codeigniter.superglobalAccessAssign.neon b/utils/phpstan-baseline/codeigniter.superglobalAccessAssign.neon index 4e5600bcc34c..4a3113467afe 100644 --- a/utils/phpstan-baseline/codeigniter.superglobalAccessAssign.neon +++ b/utils/phpstan-baseline/codeigniter.superglobalAccessAssign.neon @@ -1,4 +1,4 @@ -# total 466 errors +# total 463 errors parameters: ignoreErrors: @@ -22,21 +22,6 @@ parameters: count: 1 path: ../../tests/system/CLI/ConsoleTest.php - - - message: '#^Assigning ''/index\.php'' directly on offset ''SCRIPT_NAME'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/Cache/ResponseCacheTest.php - - - - message: '#^Assigning ''public/index\.php'' directly on offset ''SCRIPT_NAME'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/Cache/ResponseCacheTest.php - - - - message: '#^Assigning non\-falsy\-string directly on offset ''REQUEST_URI'' of \$_SERVER is discouraged\.$#' - count: 1 - path: ../../tests/system/Cache/ResponseCacheTest.php - - message: '#^Assigning ''/'' directly on offset ''REQUEST_URI'' of \$_SERVER is discouraged\.$#' count: 2 diff --git a/utils/phpstan-baseline/empty.notAllowed.neon b/utils/phpstan-baseline/empty.notAllowed.neon index 91a3123c5d6c..9bdf7e5f5720 100644 --- a/utils/phpstan-baseline/empty.notAllowed.neon +++ b/utils/phpstan-baseline/empty.notAllowed.neon @@ -1,12 +1,7 @@ -# total 240 errors +# total 239 errors parameters: ignoreErrors: - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 1 - path: ../../system/Cache/Handlers/FileHandler.php - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' count: 1 diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 03b3f826d494..f48b806c95ed 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,8 +1,7 @@ -# total 2932 errors +# total 2866 errors includes: - argument.type.neon - assign.propertyType.neon - - codeigniter.cacheHandlerInstance.neon - codeigniter.getReassignArray.neon - codeigniter.modelArgumentType.neon - codeigniter.superglobalAccess.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 6d974d9bb30b..4a4d55f542f2 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 1479 errors +# total 1436 errors parameters: ignoreErrors: @@ -242,196 +242,6 @@ parameters: count: 1 path: ../../system/CLI/Console.php - - - message: '#^Method CodeIgniter\\Cache\\CacheInterface\:\:get\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/CacheInterface.php - - - - message: '#^Method CodeIgniter\\Cache\\CacheInterface\:\:getCacheInfo\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/CacheInterface.php - - - - message: '#^Method CodeIgniter\\Cache\\CacheInterface\:\:getMetaData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/CacheInterface.php - - - - message: '#^Method CodeIgniter\\Cache\\CacheInterface\:\:save\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/CacheInterface.php - - - - message: '#^Method CodeIgniter\\Cache\\FactoriesCache\\FileVarExportHandler\:\:get\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/FactoriesCache/FileVarExportHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\FactoriesCache\\FileVarExportHandler\:\:save\(\) has parameter \$val with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/FactoriesCache/FileVarExportHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\BaseHandler\:\:remember\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/BaseHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\DummyHandler\:\:get\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/DummyHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\DummyHandler\:\:getCacheInfo\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/DummyHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\DummyHandler\:\:getMetaData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/DummyHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\DummyHandler\:\:remember\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/DummyHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\DummyHandler\:\:save\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/DummyHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\FileHandler\:\:get\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/FileHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\FileHandler\:\:getCacheInfo\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/FileHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\FileHandler\:\:getDirFileInfo\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/FileHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\FileHandler\:\:getFileInfo\(\) has parameter \$returnedValues with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/FileHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\FileHandler\:\:getFileInfo\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/FileHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\FileHandler\:\:getMetaData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/FileHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\FileHandler\:\:save\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/FileHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\MemcachedHandler\:\:get\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/MemcachedHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\MemcachedHandler\:\:getCacheInfo\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/MemcachedHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\MemcachedHandler\:\:getMetaData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/MemcachedHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\MemcachedHandler\:\:save\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/MemcachedHandler.php - - - - message: '#^Property CodeIgniter\\Cache\\Handlers\\MemcachedHandler\:\:\$config type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/MemcachedHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\PredisHandler\:\:get\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/PredisHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\PredisHandler\:\:getCacheInfo\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/PredisHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\PredisHandler\:\:getMetaData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/PredisHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\PredisHandler\:\:save\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/PredisHandler.php - - - - message: '#^Property CodeIgniter\\Cache\\Handlers\\PredisHandler\:\:\$config type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/PredisHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\RedisHandler\:\:get\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/RedisHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\RedisHandler\:\:getCacheInfo\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/RedisHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\RedisHandler\:\:getMetaData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/RedisHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\RedisHandler\:\:save\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/RedisHandler.php - - - - message: '#^Property CodeIgniter\\Cache\\Handlers\\RedisHandler\:\:\$config type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/RedisHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\WincacheHandler\:\:get\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/WincacheHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\WincacheHandler\:\:getCacheInfo\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/WincacheHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\WincacheHandler\:\:getMetaData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/WincacheHandler.php - - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\WincacheHandler\:\:save\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cache/Handlers/WincacheHandler.php - - message: '#^Method CodeIgniter\\CodeIgniter\:\:getPerformanceStats\(\) return type has no value type specified in iterable type array\.$#' count: 1 @@ -3947,21 +3757,6 @@ parameters: count: 1 path: ../../system/Helpers/filesystem_helper.php - - - message: '#^Function get_dir_file_info\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/filesystem_helper.php - - - - message: '#^Function get_file_info\(\) has parameter \$returnedValues with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/filesystem_helper.php - - - - message: '#^Function get_file_info\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/filesystem_helper.php - - message: '#^Function get_filenames\(\) return type has no value type specified in iterable type array\.$#' count: 1 @@ -5742,16 +5537,6 @@ parameters: count: 1 path: ../../tests/system/CLI/CLITest.php - - - message: '#^Method CodeIgniter\\Cache\\Handlers\\BaseHandlerTest\:\:provideValidateKeyInvalidType\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Cache/Handlers/BaseHandlerTest.php - - - - message: '#^Method CodeIgniter\\Cache\\ResponseCacheTest\:\:createIncomingRequest\(\) has parameter \$query with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Cache/ResponseCacheTest.php - - message: '#^Method CodeIgniter\\CodeIgniterTest\:\:providePageCacheWithCacheQueryString\(\) return type has no value type specified in iterable type iterable\.$#' count: 1 diff --git a/utils/phpstan-baseline/property.notFound.neon b/utils/phpstan-baseline/property.notFound.neon index 9c0dfe2005a3..f7f0db25ce63 100644 --- a/utils/phpstan-baseline/property.notFound.neon +++ b/utils/phpstan-baseline/property.notFound.neon @@ -1,4 +1,4 @@ -# total 59 errors +# total 58 errors parameters: ignoreErrors: @@ -27,11 +27,6 @@ parameters: count: 1 path: ../../system/Session/Handlers/RedisHandler.php - - - message: '#^Access to an undefined property CodeIgniter\\Config\\BaseConfig\:\:\$baseURL\.$#' - count: 1 - path: ../../tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php - - message: '#^Access to an undefined property Tests\\Support\\Commands\\AppInfo\:\:\$foobar\.$#' count: 2 diff --git a/utils/phpstan-baseline/ternary.shortNotAllowed.neon b/utils/phpstan-baseline/ternary.shortNotAllowed.neon index 18051955d6f7..d0ed19c4567e 100644 --- a/utils/phpstan-baseline/ternary.shortNotAllowed.neon +++ b/utils/phpstan-baseline/ternary.shortNotAllowed.neon @@ -1,4 +1,4 @@ -# total 35 errors +# total 34 errors parameters: ignoreErrors: @@ -7,11 +7,6 @@ parameters: count: 2 path: ../../system/CLI/CLI.php - - - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' - count: 1 - path: ../../system/Cache/Handlers/FileHandler.php - - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' count: 1 From 7aa73ff14911f59c9bf1e474410a5608f740b712 Mon Sep 17 00:00:00 2001 From: michalsn Date: Wed, 25 Jun 2025 21:58:50 +0200 Subject: [PATCH 39/59] fix: apply rector rule TernaryImplodeToImplodeRector --- system/Validation/Validation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index f13af5179112..d3f2a3dc828f 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -869,7 +869,7 @@ public function getError(?string $field = null): string ARRAY_FILTER_USE_KEY, ); - return $errors === [] ? '' : implode("\n", $errors); + return implode("\n", $errors); } /** From 4b1fa12df9ed3dd9fd8292717df11fe911347bc3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Jun 2025 01:35:03 +0000 Subject: [PATCH 40/59] chore(deps-dev): update rector/rector requirement from 2.0.18 to 2.1.0 Updates the requirements on [rector/rector](https://github.com/rectorphp/rector) to permit the latest version. - [Release notes](https://github.com/rectorphp/rector/releases) - [Commits](https://github.com/rectorphp/rector/compare/2.0.18...2.1.0) --- updated-dependencies: - dependency-name: rector/rector dependency-version: 2.1.0 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e2c5d4c79491..15ae178d5c6c 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "phpunit/phpcov": "^9.0.2 || ^10.0", "phpunit/phpunit": "^10.5.16 || ^11.2", "predis/predis": "^3.0", - "rector/rector": "2.0.18", + "rector/rector": "2.1.0", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { From 4247be6fb468858fe6d4cc55ea405797c68b808f Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Sun, 29 Jun 2025 21:52:56 +0800 Subject: [PATCH 41/59] refactor: `Console::showHeader()` call `date()` only once (#9616) --- system/CLI/Console.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/system/CLI/Console.php b/system/CLI/Console.php index 325eaa7b455c..89415e265134 100644 --- a/system/CLI/Console.php +++ b/system/CLI/Console.php @@ -59,10 +59,9 @@ public function showHeader(bool $suppress = false) } CLI::write(sprintf( - 'CodeIgniter v%s Command Line Tool - Server Time: %s UTC%s', + 'CodeIgniter v%s Command Line Tool - Server Time: %s', CodeIgniter::CI_VERSION, - date('Y-m-d H:i:s'), - date('P'), + date('Y-m-d H:i:s \\U\\T\\CP'), ), 'green'); CLI::newLine(); } From 28e80960f6ca10c07c4888820a6b1b8a645aa188 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Mon, 30 Jun 2025 07:24:29 +0200 Subject: [PATCH 42/59] fix: correct path parsing in SiteURIFactory::parseRequestURI() (#9613) --- system/HTTP/SiteURIFactory.php | 9 ++ .../SiteURIFactoryDetectRoutePathTest.php | 93 +++++++++++++++++++ user_guide_src/source/changelogs/v4.6.2.rst | 1 + 3 files changed, 103 insertions(+) diff --git a/system/HTTP/SiteURIFactory.php b/system/HTTP/SiteURIFactory.php index c87898c0470e..2c94f23f4678 100644 --- a/system/HTTP/SiteURIFactory.php +++ b/system/HTTP/SiteURIFactory.php @@ -146,6 +146,15 @@ private function parseRequestURI(): string $path = implode('/', $keep); } + // Cleanup: if indexPage is still visible in the path, remove it + if ($this->appConfig->indexPage !== '' && str_starts_with($path, $this->appConfig->indexPage)) { + $remainingPath = substr($path, strlen($this->appConfig->indexPage)); + // Only remove if followed by '/' (route) or nothing (root) + if ($remainingPath === '' || str_starts_with($remainingPath, '/')) { + $path = ltrim($remainingPath, '/'); + } + } + // This section ensures that even on servers that require the URI to // contain the query string (Nginx) a correct URI is found, and also // fixes the QUERY_STRING Server var and $_GET array. diff --git a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php index d14a6e5e4151..fc717b78176f 100644 --- a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php +++ b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php @@ -311,4 +311,97 @@ public static function provideExtensionPHP(): iterable ], ]; } + + #[DataProvider('provideRequestURIRewrite')] + public function testRequestURIRewrite( + string $requestUri, + string $scriptName, + string $indexPage, + string $expected, + ): void { + $server = []; + $server['REQUEST_URI'] = $requestUri; + $server['SCRIPT_NAME'] = $scriptName; + + $appConfig = new App(); + $appConfig->indexPage = $indexPage; + + $factory = $this->createSiteURIFactory($server, $appConfig); + + $this->assertSame($expected, $factory->detectRoutePath('REQUEST_URI')); + } + + /** + * @return iterable + */ + public static function provideRequestURIRewrite(): iterable + { + return [ + 'rewrite_with_route' => [ + 'requestUri' => '/ci/index.php/sample/method', + 'scriptName' => '/ci/public/index.php', + 'indexPage' => 'index.php', + 'expected' => 'sample/method', + ], + 'rewrite_root' => [ + 'requestUri' => '/ci/index.php', + 'scriptName' => '/ci/public/index.php', + 'indexPage' => 'index.php', + 'expected' => '/', + ], + 'rewrite_no_index_page' => [ + 'requestUri' => '/ci/sample/method', + 'scriptName' => '/ci/public/index.php', + 'indexPage' => '', + 'expected' => 'sample/method', + ], + 'rewrite_nested_subfolder' => [ + 'requestUri' => '/projects/index.php/api/users/list', + 'scriptName' => '/projects/myapp/public/index.php', + 'indexPage' => 'index.php', + 'expected' => 'api/users/list', + ], + 'rewrite_multiple_public_folders' => [ + 'requestUri' => '/public-sites/myapp/index.php/content/view', + 'scriptName' => '/public-sites/myapp/public/index.php', + 'indexPage' => 'index.php', + 'expected' => 'content/view', + ], + 'rewrite_custom_app_folder' => [ + 'requestUri' => '/myapp/index.php/products/category/electronics', + 'scriptName' => '/myapp/web/index.php', + 'indexPage' => 'index.php', + 'expected' => 'products/category/electronics', + ], + 'multiple_index_php_in_path' => [ + 'requestUri' => '/app/index.php/user/index.php/profile', + 'scriptName' => '/app/public/index.php', + 'indexPage' => 'index.php', + 'expected' => 'user/index.php/profile', + ], + 'custom_index_page_name' => [ + 'requestUri' => '/ci/app.php/users/list', + 'scriptName' => '/ci/public/app.php', + 'indexPage' => 'app.php', + 'expected' => 'users/list', + ], + 'custom_index_page_root' => [ + 'requestUri' => '/project/main.php', + 'scriptName' => '/project/web/main.php', + 'indexPage' => 'main.php', + 'expected' => '/', + ], + 'partial_match_should_not_remove' => [ + 'requestUri' => '/app/myindex.php/route', + 'scriptName' => '/app/public/index.php', + 'indexPage' => 'index.php', + 'expected' => 'myindex.php/route', + ], + ]; + } } diff --git a/user_guide_src/source/changelogs/v4.6.2.rst b/user_guide_src/source/changelogs/v4.6.2.rst index f39a0c46cf81..e9d7a65eb99d 100644 --- a/user_guide_src/source/changelogs/v4.6.2.rst +++ b/user_guide_src/source/changelogs/v4.6.2.rst @@ -41,6 +41,7 @@ Bugs Fixed - **Email:** Fixed a bug where ``Email::getHostname()`` failed to use ``$_SERVER['SERVER_ADDR']`` when ``$_SERVER['SERVER_NAME']`` was not set. - **Security:** Fixed a bug where the ``sanitize_filename()`` function from the Security helper would throw an error when used in CLI requests. - **Session:** Fixed a bug where using the ``DatabaseHandler`` with an unsupported database driver (such as ``SQLSRV``, ``OCI8``, or ``SQLite3``) did not throw an appropriate error. +- **SiteURI:** Fixed a bug in ``SiteURIFactory::parseRequestURI()`` where serving the app from a subfolder using ``mod_rewrite`` while preserving the ``index.php`` file would cause incorrect route path detection. - **URI:** Fixed a bug in ``URI::getAuthority()`` where schemes without defined default ports (like ``rtsp://``) would cause issues due to missing array key handling. See the repo's From 78f38424cd7816eecc0bba401b7028749e00dda8 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Mon, 30 Jun 2025 08:21:58 +0200 Subject: [PATCH 43/59] fix: support for multibyte folder names when the app is served from a subfolder (#9615) --- system/HTTP/SiteURIFactory.php | 3 ++- .../HTTP/SiteURIFactoryDetectRoutePathTest.php | 12 ++++++++++++ user_guide_src/source/changelogs/v4.6.2.rst | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/system/HTTP/SiteURIFactory.php b/system/HTTP/SiteURIFactory.php index 2c94f23f4678..483c3ded5779 100644 --- a/system/HTTP/SiteURIFactory.php +++ b/system/HTTP/SiteURIFactory.php @@ -132,7 +132,8 @@ private function parseRequestURI(): string && pathinfo($this->superglobals->server('SCRIPT_NAME'), PATHINFO_EXTENSION) === 'php' ) { // Compare each segment, dropping them until there is no match - $segments = $keep = explode('/', $path); + $segments = explode('/', rawurldecode($path)); + $keep = explode('/', $path); foreach (explode('/', $this->superglobals->server('SCRIPT_NAME')) as $i => $segment) { // If these segments are not the same then we're done diff --git a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php index fc717b78176f..cbd309dffa7c 100644 --- a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php +++ b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php @@ -402,6 +402,18 @@ public static function provideRequestURIRewrite(): iterable 'indexPage' => 'index.php', 'expected' => 'myindex.php/route', ], + 'multibyte_characters' => [ + 'requestUri' => '/%ED%85%8C%EC%8A%A4%ED%8A%B81/index.php/route', + 'scriptName' => '/테스트1/public/index.php', + 'indexPage' => 'index.php', + 'expected' => 'route', + ], + 'multibyte_characters_with_nested_subfolder' => [ + 'requestUri' => '/%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82/%D1%82%D0%B5%D1%81%D1%821/index.php/route', + 'scriptName' => '/проект/тест1/public/index.php', + 'indexPage' => 'index.php', + 'expected' => 'route', + ], ]; } } diff --git a/user_guide_src/source/changelogs/v4.6.2.rst b/user_guide_src/source/changelogs/v4.6.2.rst index e9d7a65eb99d..360e592b680d 100644 --- a/user_guide_src/source/changelogs/v4.6.2.rst +++ b/user_guide_src/source/changelogs/v4.6.2.rst @@ -42,6 +42,7 @@ Bugs Fixed - **Security:** Fixed a bug where the ``sanitize_filename()`` function from the Security helper would throw an error when used in CLI requests. - **Session:** Fixed a bug where using the ``DatabaseHandler`` with an unsupported database driver (such as ``SQLSRV``, ``OCI8``, or ``SQLite3``) did not throw an appropriate error. - **SiteURI:** Fixed a bug in ``SiteURIFactory::parseRequestURI()`` where serving the app from a subfolder using ``mod_rewrite`` while preserving the ``index.php`` file would cause incorrect route path detection. +- **SiteURI:** Fixed a bug in ``SiteURIFactory::parseRequestURI()`` where folder names containing multibyte (non-ASCII) characters were not correctly resolved when the application was served from a subfolder. - **URI:** Fixed a bug in ``URI::getAuthority()`` where schemes without defined default ports (like ``rtsp://``) would cause issues due to missing array key handling. See the repo's From 3d019b0737d7362a519b9c77b3c72b0a5337207f Mon Sep 17 00:00:00 2001 From: Francesco Ciannavei <64163256+Franky5831@users.noreply.github.com> Date: Thu, 3 Jul 2025 21:01:45 +0200 Subject: [PATCH 44/59] docs: fix outdated documentation on how to exclude multiple test groups (#9621) --- tests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/README.md b/tests/README.md index c0c58040917b..3848a83540dd 100644 --- a/tests/README.md +++ b/tests/README.md @@ -105,7 +105,7 @@ Individual tests can be run by including the relative path to the test file. You can run the tests without running the live database and the live cache tests. ```console -./phpunit --exclude-group DatabaseLive,CacheLive +./phpunit --exclude-group DatabaseLive --exclude-group CacheLive ``` ## Generating Code Coverage From 228ba7db60dd57a9f177dea8e1ff6db57bdf3567 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Sat, 5 Jul 2025 17:26:11 +0200 Subject: [PATCH 45/59] docs: add detailed explanation about paginate() and beforeFind model event (#9618) --- user_guide_src/source/models/model.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/user_guide_src/source/models/model.rst b/user_guide_src/source/models/model.rst index bfef42f86d32..57fb52a024c3 100644 --- a/user_guide_src/source/models/model.rst +++ b/user_guide_src/source/models/model.rst @@ -1032,6 +1032,15 @@ afterUpdateBatch **data** = the key/value pairs being updated. **result** = the results of the ``updateBatch()`` method used through the Query Builder. ================= ========================================================================================================= +.. note:: When using the ``paginate()`` method in combination with the ``beforeFind`` event to modify the query, + the results may not behave as expected. + + This is because the ``beforeFind`` event only affects the actual retrieval of the results (``findAll()``), + but **not** the query used to count the total number of rows for pagination. + + As a result, the total row count used for generating pagination links may not reflect the modified query conditions, + leading to inconsistencies in pagination. + Modifying Find* Data ==================== From 1f2fdf7f052d331518634539215c7fa5022f4dc0 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Sun, 6 Jul 2025 01:21:23 +0800 Subject: [PATCH 46/59] style: fix code styles from php-cs-fixer v3.76 (#9626) * remove PHP_CS_FIXER_IGNORE_ENV * Bump versions in utils/composer.json * Manually fix cs errors * Remove deprecated fixers --- .github/workflows/test-coding-standards.yml | 2 - .php-cs-fixer.dist.php | 10 +--- system/BaseModel.php | 56 ++++++++----------- system/Model.php | 10 ++-- system/Test/Interfaces/FabricatorModel.php | 7 +-- utils/composer.json | 4 +- utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 7 +-- 8 files changed, 37 insertions(+), 61 deletions(-) diff --git a/.github/workflows/test-coding-standards.yml b/.github/workflows/test-coding-standards.yml index 75fbabf2f14a..c49fd416ea8e 100644 --- a/.github/workflows/test-coding-standards.yml +++ b/.github/workflows/test-coding-standards.yml @@ -65,5 +65,3 @@ jobs: - name: Run lint run: composer cs - env: - PHP_CS_FIXER_IGNORE_ENV: ${{ matrix.php-version == '8.4' }} diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index e9240c590917..f256758deb2b 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -13,8 +13,6 @@ use CodeIgniter\CodingStandard\CodeIgniter4; use Nexus\CsConfig\Factory; -use Nexus\CsConfig\Fixer\Comment\NoCodeSeparatorCommentFixer; -use Nexus\CsConfig\FixerGenerator; use PhpCsFixer\Finder; $finder = Finder::create() @@ -44,12 +42,8 @@ ]; $options = [ - 'cacheFile' => 'build/.php-cs-fixer.cache', - 'finder' => $finder, - 'customFixers' => FixerGenerator::create('utils/vendor/nexusphp/cs-config/src/Fixer', 'Nexus\\CsConfig\\Fixer'), - 'customRules' => [ - NoCodeSeparatorCommentFixer::name() => true, - ], + 'cacheFile' => 'build/.php-cs-fixer.cache', + 'finder' => $finder, ]; return Factory::create(new CodeIgniter4(), $overrides, $options)->forLibrary( diff --git a/system/BaseModel.php b/system/BaseModel.php index ca0e20c32829..4f1cf3dca18f 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -532,9 +532,8 @@ abstract protected function doOnlyDeleted(); * Compiles a replace and runs the query. * This method works only with dbCalls. * - * @param array|null $row Row data - * @phpstan-param row_array|null $row - * @param bool $returnSQL Set to true to return Query String + * @param row_array|null $row Row data + * @param bool $returnSQL Set to true to return Query String * * @return BaseResult|false|Query|string */ @@ -552,8 +551,7 @@ abstract protected function doErrors(); * Public getter to return the id value using the idValue() method. * For example with SQL this will return $data->$this->primaryKey. * - * @param array|object $row Row data - * @phpstan-param row_array|object $row + * @param object|row_array $row Row data * * @return array|int|string|null */ @@ -737,8 +735,7 @@ public function first() * you must ensure that the class will provide access to the class * variables, even if through a magic method. * - * @param array|object $row Row data - * @phpstan-param row_array|object $row + * @param object|row_array $row Row data * * @throws ReflectionException */ @@ -789,9 +786,8 @@ public function getInsertID() * Inserts data into the database. If an object is provided, * it will attempt to convert it to an array. * - * @param array|object|null $row Row data - * @phpstan-param row_array|object|null $row - * @param bool $returnID Whether insert ID should be returned or not. + * @param object|row_array|null $row Row data + * @param bool $returnID Whether insert ID should be returned or not. * * @return ($returnID is true ? false|int|string : bool) * @@ -866,8 +862,8 @@ public function insert($row = null, bool $returnID = true) /** * Set datetime to created field. * - * @phpstan-param row_array $row - * @param int|string $date timestamp or datetime string + * @param row_array $row + * @param int|string $date timestamp or datetime string */ protected function setCreatedField(array $row, $date): array { @@ -881,8 +877,8 @@ protected function setCreatedField(array $row, $date): array /** * Set datetime to updated field. * - * @phpstan-param row_array $row - * @param int|string $date timestamp or datetime string + * @param row_array $row + * @param int|string $date timestamp or datetime string */ protected function setUpdatedField(array $row, $date): array { @@ -896,11 +892,10 @@ protected function setUpdatedField(array $row, $date): array /** * Compiles batch insert runs the queries, validating each row prior. * - * @param list|null $set an associative array of insert values - * @phpstan-param list|null $set - * @param bool|null $escape Whether to escape values - * @param int $batchSize The size of the batch to run - * @param bool $testing True means only number of records is returned, false will execute the query + * @param list|null $set an associative array of insert values + * @param bool|null $escape Whether to escape values + * @param int $batchSize The size of the batch to run + * @param bool $testing True means only number of records is returned, false will execute the query * * @return bool|int Number of rows inserted or FALSE on failure * @@ -1042,11 +1037,10 @@ public function update($id = null, $row = null): bool /** * Compiles an update and runs the query. * - * @param list|null $set an associative array of insert values - * @phpstan-param list|null $set - * @param string|null $index The where key - * @param int $batchSize The size of the batch to run - * @param bool $returnSQL True means SQL is returned, false will execute the query + * @param list|null $set an associative array of insert values + * @param string|null $index The where key + * @param int $batchSize The size of the batch to run + * @param bool $returnSQL True means SQL is returned, false will execute the query * * @return false|int|list Number of rows affected or FALSE on failure, SQL array when testMode * @@ -1125,8 +1119,8 @@ public function updateBatch(?array $set = null, ?string $index = null, int $batc /** * Deletes a single record from the database where $id matches. * - * @param array|int|string|null $id The rows primary key(s) - * @param bool $purge Allows overriding the soft deletes setting. + * @param int|list|string|null $id The rows primary key(s) + * @param bool $purge Allows overriding the soft deletes setting. * * @return BaseResult|bool * @@ -1214,9 +1208,8 @@ public function onlyDeleted() /** * Compiles a replace and runs the query. * - * @param array|null $row Row data - * @phpstan-param row_array|null $row - * @param bool $returnSQL Set to true to return Query String + * @param row_array|null $row Row data + * @param bool $returnSQL Set to true to return Query String * * @return BaseResult|false|Query|string */ @@ -1810,9 +1803,8 @@ protected function objectToRawArray($object, bool $onlyChanged = true, bool $rec /** * Transform data to array. * - * @param array|object|null $row Row data - * @phpstan-param row_array|object|null $row - * @param string $type Type of data (insert|update) + * @param object|row_array|null $row Row data + * @param string $type Type of data (insert|update) * * @throws DataException * @throws InvalidArgumentException diff --git a/system/Model.php b/system/Model.php index 802dd84dcdca..44ed52a0edd0 100644 --- a/system/Model.php +++ b/system/Model.php @@ -551,9 +551,8 @@ protected function doOnlyDeleted() * Compiles a replace into string and runs the query * This method works only with dbCalls. * - * @param array|null $row Data - * @phpstan-param row_array|null $row - * @param bool $returnSQL Set to true to return Query String + * @param row_array|null $row Data + * @param bool $returnSQL Set to true to return Query String * * @return BaseResult|false|Query|string */ @@ -774,9 +773,8 @@ protected function shouldUpdate($row): bool * Inserts data into the database. If an object is provided, * it will attempt to convert it to an array. * - * @param array|object|null $row - * @phpstan-param row_array|object|null $row - * @param bool $returnID Whether insert ID should be returned or not. + * @param object|row_array|null $row + * @param bool $returnID Whether insert ID should be returned or not. * * @return ($returnID is true ? false|int|string : bool) * diff --git a/system/Test/Interfaces/FabricatorModel.php b/system/Test/Interfaces/FabricatorModel.php index a5860e22fb83..57c1b16cc2dc 100644 --- a/system/Test/Interfaces/FabricatorModel.php +++ b/system/Test/Interfaces/FabricatorModel.php @@ -39,7 +39,7 @@ interface FabricatorModel * * @param int|list|string|null $id One primary key or an array of primary keys * - * @phpstan-return ($id is int|string ? row_array|object|null : list) + * @return ($id is int|string ? object|row_array|null : list) */ public function find($id = null); @@ -47,9 +47,8 @@ public function find($id = null); * Inserts data into the current table. If an object is provided, * it will attempt to convert it to an array. * - * @param array|object|null $row - * @phpstan-param row_array|object|null $row - * @param bool $returnID Whether insert ID should be returned or not. + * @param object|row_array|null $row + * @param bool $returnID Whether insert ID should be returned or not. * * @return bool|int|string * diff --git a/utils/composer.json b/utils/composer.json index a204d68c0214..81445f4c704b 100644 --- a/utils/composer.json +++ b/utils/composer.json @@ -3,8 +3,8 @@ "php": "^8.1", "codeigniter/coding-standard": "^1.7", "ergebnis/composer-normalize": "^2.28", - "friendsofphp/php-cs-fixer": "^3.63.2", - "nexusphp/cs-config": "^3.6", + "friendsofphp/php-cs-fixer": "^3.76", + "nexusphp/cs-config": "^3.26", "phpmetrics/phpmetrics": "^2.8 || ^3.0rc6", "vimeo/psalm": "^5.0" }, diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index f48b806c95ed..cbafddf4c147 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 2866 errors +# total 2865 errors includes: - argument.type.neon - assign.propertyType.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 4a4d55f542f2..f8a533b18a39 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 1436 errors +# total 1435 errors parameters: ignoreErrors: @@ -27,11 +27,6 @@ parameters: count: 1 path: ../../system/BaseModel.php - - - message: '#^Method CodeIgniter\\BaseModel\:\:delete\(\) has parameter \$id with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/BaseModel.php - - message: '#^Method CodeIgniter\\BaseModel\:\:doDelete\(\) has parameter \$id with no value type specified in iterable type array\.$#' count: 1 From 0121e8da15374ed7397f2fbc68542da71fc7f5e2 Mon Sep 17 00:00:00 2001 From: Ping-yee <611077101@mail.nknu.edu.tw> Date: Wed, 9 Jul 2025 02:57:14 +0800 Subject: [PATCH 47/59] fix: use correct 24-hour time format. (#9628) --- app/Views/errors/html/error_exception.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Views/errors/html/error_exception.php b/app/Views/errors/html/error_exception.php index d5e0c2ec7ee0..2c4e00911365 100644 --- a/app/Views/errors/html/error_exception.php +++ b/app/Views/errors/html/error_exception.php @@ -24,7 +24,7 @@
- Displayed at — + Displayed at — PHP: — CodeIgniter: -- Environment: From 42f68a6291308cbba45942fecddf80850696a786 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Wed, 9 Jul 2025 07:07:06 +0200 Subject: [PATCH 48/59] style: fix code styles from php-cs-fixer v3.82.1 (#9629) * style: fix code styles from php-cs-fixer v3.82.1 * fix phpstan --- tests/system/CommonSingleServiceTest.php | 46 +-- tests/system/Config/MimesTest.php | 18 +- tests/system/Config/ServicesTest.php | 26 +- .../DataConverter/DataConverterTest.php | 20 +- tests/system/Database/BaseQueryTest.php | 52 ++-- tests/system/Database/Builder/WhenTest.php | 2 +- tests/system/Database/Builder/WhereTest.php | 24 +- tests/system/Database/Live/LikeTest.php | 4 +- tests/system/Email/EmailTest.php | 16 +- tests/system/Files/FileTest.php | 42 +-- tests/system/Filters/FiltersTest.php | 36 +-- tests/system/HTTP/HeaderTest.php | 8 +- tests/system/HTTP/IncomingRequestTest.php | 64 ++-- tests/system/HTTP/MessageTest.php | 38 +-- tests/system/HTTP/SiteURITest.php | 286 +++++++++--------- tests/system/HTTP/URITest.php | 136 ++++----- tests/system/Helpers/ArrayHelperTest.php | 68 ++--- tests/system/Helpers/InflectorHelperTest.php | 12 +- .../Helpers/URLHelper/CurrentUrlTest.php | 76 ++--- .../system/Helpers/URLHelper/MiscUrlTest.php | 126 ++++---- tests/system/Language/LanguageTest.php | 26 +- tests/system/Models/DeleteModelTest.php | 18 +- .../RouteCollectionReverseRouteTest.php | 18 +- tests/system/Router/RouteCollectionTest.php | 16 +- tests/system/Test/DOMParserTest.php | 18 +- tests/system/Test/FeatureTestTraitTest.php | 54 ++-- tests/system/Validation/FormatRulesTest.php | 42 +-- tests/system/Validation/RulesTest.php | 16 +- tests/system/Validation/ValidationTest.php | 13 +- tests/system/View/ParserTest.php | 26 +- 30 files changed, 677 insertions(+), 670 deletions(-) diff --git a/tests/system/CommonSingleServiceTest.php b/tests/system/CommonSingleServiceTest.php index 2a27c9b8486f..f3e3bb57b40b 100644 --- a/tests/system/CommonSingleServiceTest.php +++ b/tests/system/CommonSingleServiceTest.php @@ -75,29 +75,6 @@ public function testSingleServiceWithAtLeastOneParamSupplied(string $service): v } } - public function testSingleServiceWithAllParamsSupplied(): void - { - $cache1 = single_service('cache', null, true); - $cache2 = single_service('cache', null, true); - - assert($cache1 !== null); - assert($cache2 !== null); - - // Assert that even passing true as last param this will - // not create a shared instance. - $this->assertInstanceOf($cache1::class, $cache2); - $this->assertNotSame($cache1, $cache2); - } - - public function testSingleServiceWithGibberishGiven(): void - { - $this->assertNull(single_service('foo')); // @phpstan-ignore codeigniter.unknownServiceMethod - $this->assertNull(single_service('bar')); // @phpstan-ignore codeigniter.unknownServiceMethod - $this->assertNull(single_service('baz')); // @phpstan-ignore codeigniter.unknownServiceMethod - $this->assertNull(single_service('caches')); // @phpstan-ignore codeigniter.unknownServiceMethod - $this->assertNull(single_service('timers')); // @phpstan-ignore codeigniter.unknownServiceMethod - } - /** * @return iterable */ @@ -137,4 +114,27 @@ public static function provideServiceNames(): iterable yield from $services; } + + public function testSingleServiceWithAllParamsSupplied(): void + { + $cache1 = single_service('cache', null, true); + $cache2 = single_service('cache', null, true); + + assert($cache1 !== null); + assert($cache2 !== null); + + // Assert that even passing true as last param this will + // not create a shared instance. + $this->assertInstanceOf($cache1::class, $cache2); + $this->assertNotSame($cache1, $cache2); + } + + public function testSingleServiceWithGibberishGiven(): void + { + $this->assertNull(single_service('foo')); // @phpstan-ignore codeigniter.unknownServiceMethod + $this->assertNull(single_service('bar')); // @phpstan-ignore codeigniter.unknownServiceMethod + $this->assertNull(single_service('baz')); // @phpstan-ignore codeigniter.unknownServiceMethod + $this->assertNull(single_service('caches')); // @phpstan-ignore codeigniter.unknownServiceMethod + $this->assertNull(single_service('timers')); // @phpstan-ignore codeigniter.unknownServiceMethod + } } diff --git a/tests/system/Config/MimesTest.php b/tests/system/Config/MimesTest.php index bcf5923f5b9d..0b22a45e5735 100644 --- a/tests/system/Config/MimesTest.php +++ b/tests/system/Config/MimesTest.php @@ -24,6 +24,12 @@ #[Group('Others')] final class MimesTest extends CIUnitTestCase { + #[DataProvider('provideGuessExtensionFromType')] + public function testGuessExtensionFromType(?string $expected, string $mime): void + { + $this->assertSame($expected, Mimes::guessExtensionFromType($mime)); + } + public static function provideGuessExtensionFromType(): iterable { return [ @@ -50,10 +56,10 @@ public static function provideGuessExtensionFromType(): iterable ]; } - #[DataProvider('provideGuessExtensionFromType')] - public function testGuessExtensionFromType(?string $expected, string $mime): void + #[DataProvider('provideGuessTypeFromExtension')] + public function testGuessTypeFromExtension(?string $expected, string $ext): void { - $this->assertSame($expected, Mimes::guessExtensionFromType($mime)); + $this->assertSame($expected, Mimes::guessTypeFromExtension($ext)); } public static function provideGuessTypeFromExtension(): iterable @@ -81,10 +87,4 @@ public static function provideGuessTypeFromExtension(): iterable ], ]; } - - #[DataProvider('provideGuessTypeFromExtension')] - public function testGuessTypeFromExtension(?string $expected, string $ext): void - { - $this->assertSame($expected, Mimes::guessTypeFromExtension($ext)); - } } diff --git a/tests/system/Config/ServicesTest.php b/tests/system/Config/ServicesTest.php index 162d95705933..250474cbc0ea 100644 --- a/tests/system/Config/ServicesTest.php +++ b/tests/system/Config/ServicesTest.php @@ -263,7 +263,7 @@ public function testNewSessionWithNullConfig(): void $this->assertInstanceOf(Session::class, $actual); } - #[DataProvider('provideNewSessionInvalid')] + #[DataProvider('provideNewSessionWithInvalidHandler')] #[PreserveGlobalState(false)] #[RunInSeparateProcess] public function testNewSessionWithInvalidHandler(string $driver): void @@ -277,6 +277,18 @@ public function testNewSessionWithInvalidHandler(string $driver): void Services::session($config, false); } + /** + * @return iterable + */ + public static function provideNewSessionWithInvalidHandler(): iterable + { + yield 'just a string' => ['file']; + + yield 'inexistent class' => ['Foo']; + + yield 'other class' => [self::class]; + } + #[PreserveGlobalState(false)] #[RunInSeparateProcess] public function testNewSessionWithInvalidDatabaseHandler(): void @@ -296,18 +308,6 @@ public function testNewSessionWithInvalidDatabaseHandler(): void Services::session($config, false); } - /** - * @return iterable - */ - public static function provideNewSessionInvalid(): iterable - { - yield 'just a string' => ['file']; - - yield 'inexistent class' => ['Foo']; - - yield 'other class' => [self::class]; - } - #[PreserveGlobalState(false)] #[RunInSeparateProcess] public function testCallStatic(): void diff --git a/tests/system/DataConverter/DataConverterTest.php b/tests/system/DataConverter/DataConverterTest.php index 0e6e85a488c9..10a35e6909fc 100644 --- a/tests/system/DataConverter/DataConverterTest.php +++ b/tests/system/DataConverter/DataConverterTest.php @@ -50,16 +50,6 @@ public function testConvertDataFromDB(array $types, array $dbData, array $expect $this->assertSame($expected, $data); } - #[DataProvider('provideConvertDataToDB')] - public function testConvertDataToDB(array $types, array $phpData, array $expected): void - { - $converter = $this->createDataConverter($types); - - $data = $converter->toDataSource($phpData); - - $this->assertSame($expected, $data); - } - public static function provideConvertDataFromDB(): iterable { yield from [ @@ -206,6 +196,16 @@ public static function provideConvertDataFromDB(): iterable ]; } + #[DataProvider('provideConvertDataToDB')] + public function testConvertDataToDB(array $types, array $phpData, array $expected): void + { + $converter = $this->createDataConverter($types); + + $data = $converter->toDataSource($phpData); + + $this->assertSame($expected, $data); + } + public static function provideConvertDataToDB(): iterable { yield from [ diff --git a/tests/system/Database/BaseQueryTest.php b/tests/system/Database/BaseQueryTest.php index 3c48e2526652..c5265b85dfa7 100644 --- a/tests/system/Database/BaseQueryTest.php +++ b/tests/system/Database/BaseQueryTest.php @@ -107,6 +107,19 @@ public function testSwapPrefix(): void $this->assertSame($newSQL, $query->getQuery()); } + /** + * @param mixed $expected + * @param mixed $sql + */ + #[DataProvider('provideIsWriteType')] + public function testIsWriteType($expected, $sql): void + { + $query = new Query($this->db); + + $query->setQuery($sql); + $this->assertSame($expected, $query->isWriteType()); + } + public static function provideIsWriteType(): iterable { return [ @@ -185,19 +198,6 @@ public static function provideIsWriteType(): iterable ]; } - /** - * @param mixed $expected - * @param mixed $sql - */ - #[DataProvider('provideIsWriteType')] - public function testIsWriteType($expected, $sql): void - { - $query = new Query($this->db); - - $query->setQuery($sql); - $this->assertSame($expected, $query->isWriteType()); - } - public function testSingleBindingOutsideOfArray(): void { $query = new Query($this->db); @@ -579,6 +579,19 @@ public function testSwapPrefixAfterGetQuery(): void $this->assertSame($expected, $query->getQuery()); } + /** + * @param mixed $expected + * @param mixed $sql + */ + #[DataProvider('provideHighlightQueryKeywords')] + public function testHighlightQueryKeywords($expected, $sql): void + { + $query = new Query($this->db); + $query->setQuery($sql); + + $this->assertSame($expected, $query->debugToolbarDisplay()); + } + public static function provideHighlightQueryKeywords(): iterable { return [ @@ -596,17 +609,4 @@ public static function provideHighlightQueryKeywords(): iterable ], ]; } - - /** - * @param mixed $expected - * @param mixed $sql - */ - #[DataProvider('provideHighlightQueryKeywords')] - public function testHighlightQueryKeywords($expected, $sql): void - { - $query = new Query($this->db); - $query->setQuery($sql); - - $this->assertSame($expected, $query->debugToolbarDisplay()); - } } diff --git a/tests/system/Database/Builder/WhenTest.php b/tests/system/Database/Builder/WhenTest.php index 87f9b3ff233d..2fc4966accb8 100644 --- a/tests/system/Database/Builder/WhenTest.php +++ b/tests/system/Database/Builder/WhenTest.php @@ -206,7 +206,7 @@ public function testWhenNotRunsDefaultCallbackBasedOnCondition(mixed $condition, /** * @return array */ - public static function provideConditionValues(): array + public static function provideConditionValues(): iterable { return [ 'false' => [false, true], // [condition, expectedDefaultCallbackRuns] diff --git a/tests/system/Database/Builder/WhereTest.php b/tests/system/Database/Builder/WhereTest.php index 57980f28ee8c..2d0c2a3e1233 100644 --- a/tests/system/Database/Builder/WhereTest.php +++ b/tests/system/Database/Builder/WhereTest.php @@ -395,14 +395,6 @@ public function testWhereInSubQuery(): void $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); } - public static function provideWhereInvalidKeyThrowInvalidArgumentException(): iterable - { - return [ - 'null' => [null], - 'empty string' => [''], - ]; - } - /** * @param mixed $key */ @@ -415,12 +407,11 @@ public function testWhereInvalidKeyThrowInvalidArgumentException($key): void $builder->whereIn($key, ['Politician', 'Accountant']); } - public static function provideWhereInEmptyValuesThrowInvalidArgumentException(): iterable + public static function provideWhereInvalidKeyThrowInvalidArgumentException(): iterable { return [ - 'null' => [null], - 'not array' => ['not array'], - 'not instanceof \Closure' => [new stdClass()], + 'null' => [null], + 'empty string' => [''], ]; } @@ -436,6 +427,15 @@ public function testWhereInEmptyValuesThrowInvalidArgumentException($values): vo $builder->whereIn('name', $values); } + public static function provideWhereInEmptyValuesThrowInvalidArgumentException(): iterable + { + return [ + 'null' => [null], + 'not array' => ['not array'], + 'not instanceof \Closure' => [new stdClass()], + ]; + } + public function testWhereNotIn(): void { $builder = $this->db->table('jobs'); diff --git a/tests/system/Database/Live/LikeTest.php b/tests/system/Database/Live/LikeTest.php index 9591eef72ea8..c03dcd8d096c 100644 --- a/tests/system/Database/Live/LikeTest.php +++ b/tests/system/Database/Live/LikeTest.php @@ -76,7 +76,7 @@ public function testLikeCaseInsensitive(): void $this->assertSame('Developer', $job->name); } - #[DataProvider('provideMultibyteCharacters')] + #[DataProvider('provideLikeCaseInsensitiveWithMultibyteCharacter')] public function testLikeCaseInsensitiveWithMultibyteCharacter(string $match, string $result): void { $wai = $this->db->table('without_auto_increment')->like('value', $match, 'both', null, true)->get(); @@ -88,7 +88,7 @@ public function testLikeCaseInsensitiveWithMultibyteCharacter(string $match, str /** * @return iterable */ - public static function provideMultibyteCharacters(): iterable + public static function provideLikeCaseInsensitiveWithMultibyteCharacter(): iterable { yield from [ 'polish' => ['ŁĄ', 'multibyte characters pl'], diff --git a/tests/system/Email/EmailTest.php b/tests/system/Email/EmailTest.php index 63e82a0a72ba..8d865b58ddeb 100644 --- a/tests/system/Email/EmailTest.php +++ b/tests/system/Email/EmailTest.php @@ -39,14 +39,6 @@ public function testEmailValidation(): void $this->assertStringContainsString('Invalid email address: "invalid"', $email->printDebugger()); } - public static function provideEmailSendWithClearance(): iterable - { - return [ - 'autoclear' => [true], - 'not autoclear' => [false], - ]; - } - /** * @param bool $autoClear */ @@ -64,6 +56,14 @@ public function testEmailSendWithClearance($autoClear): void } } + public static function provideEmailSendWithClearance(): iterable + { + return [ + 'autoclear' => [true], + 'not autoclear' => [false], + ]; + } + public function testEmailSendStoresArchive(): void { $email = $this->createMockEmail(); diff --git a/tests/system/Files/FileTest.php b/tests/system/Files/FileTest.php index c74c6b63ffeb..8975ba0a6fd2 100644 --- a/tests/system/Files/FileTest.php +++ b/tests/system/Files/FileTest.php @@ -139,6 +139,27 @@ public function testGetSizeMetric(FileSizeUnit $unit): void $this->assertSame($size, $file->getSizeByMetricUnit($unit)); } + /** + * @return array> + */ + public static function provideGetSizeData(): iterable + { + return [ + 'returns KB binary' => [ + FileSizeUnit::KB, + ], + 'returns MB binary' => [ + FileSizeUnit::MB, + ], + 'returns GB binary' => [ + FileSizeUnit::GB, + ], + 'returns TB binary' => [ + FileSizeUnit::TB, + ], + ]; + } + public function testGetSizeMetricBytes(): void { $file = new File(SYSTEMPATH . 'Common.php'); @@ -168,25 +189,4 @@ public function testGetDestination(): void unlink(SYSTEMPATH . 'Common_Copy.php'); unlink(SYSTEMPATH . 'Common_Copy_5.php'); } - - /** - * @return array> - */ - public static function provideGetSizeData(): array - { - return [ - 'returns KB binary' => [ - FileSizeUnit::KB, - ], - 'returns MB binary' => [ - FileSizeUnit::MB, - ], - 'returns GB binary' => [ - FileSizeUnit::GB, - ], - 'returns TB binary' => [ - FileSizeUnit::TB, - ], - ]; - } } diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php index a6ef8abede3e..b2daf0ec2b07 100644 --- a/tests/system/Filters/FiltersTest.php +++ b/tests/system/Filters/FiltersTest.php @@ -210,24 +210,6 @@ public function testProcessMethodProcessGlobals(): void $this->assertSame($expected, $filters->initialize()->getFilters()); } - public static function provideProcessMethodProcessGlobalsWithExcept(): iterable - { - return [ - [ - ['admin/*'], - ], - [ - ['admin/*', 'foo/*'], - ], - [ - ['*'], - ], - [ - 'admin/*', - ], - ]; - } - /** * @param array|string $except */ @@ -265,6 +247,24 @@ public function testProcessMethodProcessGlobalsWithExcept($except): void $this->assertSame($expected, $filters->initialize($uri)->getFilters()); } + public static function provideProcessMethodProcessGlobalsWithExcept(): iterable + { + return [ + [ + ['admin/*'], + ], + [ + ['admin/*', 'foo/*'], + ], + [ + ['*'], + ], + [ + 'admin/*', + ], + ]; + } + public function testProcessMethodProcessesFiltersBefore(): void { $_SERVER['REQUEST_METHOD'] = 'GET'; diff --git a/tests/system/HTTP/HeaderTest.php b/tests/system/HTTP/HeaderTest.php index 3dab6faa3784..99ee23708780 100644 --- a/tests/system/HTTP/HeaderTest.php +++ b/tests/system/HTTP/HeaderTest.php @@ -240,7 +240,7 @@ public function testHeaderToStringShowsEntireHeader(): void /** * @param string $name */ - #[DataProvider('invalidNamesProvider')] + #[DataProvider('provideInvalidHeaderNames')] public function testInvalidHeaderNames($name): void { $this->expectException(InvalidArgumentException::class); @@ -251,7 +251,7 @@ public function testInvalidHeaderNames($name): void /** * @return list> */ - public static function invalidNamesProvider(): array + public static function provideInvalidHeaderNames(): iterable { return [ ["Content-Type\r\n\r\n"], @@ -277,7 +277,7 @@ public static function invalidNamesProvider(): array /** * @param array|string>|string|null $value */ - #[DataProvider('invalidValuesProvider')] + #[DataProvider('provideInvalidHeaderValues')] public function testInvalidHeaderValues($value): void { $this->expectException(InvalidArgumentException::class); @@ -288,7 +288,7 @@ public function testInvalidHeaderValues($value): void /** * @return list|string>> */ - public static function invalidValuesProvider(): array + public static function provideInvalidHeaderValues(): iterable { return [ ["Header\n Value"], diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index 337fc24384f9..1240d0885b23 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -579,6 +579,24 @@ public function testCanGrabGetRawInput(): void $this->assertSame($expected, $request->getRawInput()); } + /** + * @param string $rawstring + * @param mixed $var + * @param mixed $expected + * @param mixed $filter + * @param mixed $flag + */ + #[DataProvider('provideCanGrabGetRawInputVar')] + public function testCanGrabGetRawInputVar($rawstring, $var, $expected, $filter, $flag): void + { + $config = new App(); + $config->baseURL = 'http://example.com/'; + + $request = $this->createRequest($config, $rawstring); + + $this->assertSame($expected, $request->getRawInputVar($var, $filter, $flag)); + } + public static function provideCanGrabGetRawInputVar(): iterable { return [ @@ -658,24 +676,6 @@ public static function provideCanGrabGetRawInputVar(): iterable ]; } - /** - * @param string $rawstring - * @param mixed $var - * @param mixed $expected - * @param mixed $filter - * @param mixed $flag - */ - #[DataProvider('provideCanGrabGetRawInputVar')] - public function testCanGrabGetRawInputVar($rawstring, $var, $expected, $filter, $flag): void - { - $config = new App(); - $config->baseURL = 'http://example.com/'; - - $request = $this->createRequest($config, $rawstring); - - $this->assertSame($expected, $request->getRawInputVar($var, $filter, $flag)); - } - #[DataProvider('provideIsHTTPMethods')] public function testIsHTTPMethodLowerCase(string $value): void { @@ -902,20 +902,6 @@ public function testGetPostIndexNotExists(): void $this->assertNull($this->request->getGetPost('gc')); } - public static function provideExtensionPHP(): iterable - { - return [ - 'not /index.php' => [ - '/test.php', - '/', - ], - '/index.php' => [ - '/index.php', - '/', - ], - ]; - } - /** * @param mixed $path * @param mixed $detectPath @@ -932,6 +918,20 @@ public function testExtensionPHP($path, $detectPath): void $this->assertSame($detectPath, $request->detectPath()); } + public static function provideExtensionPHP(): iterable + { + return [ + 'not /index.php' => [ + '/test.php', + '/', + ], + '/index.php' => [ + '/index.php', + '/', + ], + ]; + } + public function testGetPath(): void { $request = $this->createRequest(null, null, 'fruits/banana'); diff --git a/tests/system/HTTP/MessageTest.php b/tests/system/HTTP/MessageTest.php index 5e3343416b12..28483674fee9 100644 --- a/tests/system/HTTP/MessageTest.php +++ b/tests/system/HTTP/MessageTest.php @@ -189,25 +189,6 @@ public function testSetHeaderArrayValues(): void $this->assertSame('json, html, xml', $this->message->getHeaderLine('Accept')); } - public static function provideArrayHeaderValue(): iterable - { - return [ - 'existing for next not append' => [ - [ - 'json', - 'html', - 'xml', - ], - ], - 'existing for next append' => [ - [ - 'json', - 'html', - ], - ], - ]; - } - /** * @param array $arrayHeaderValue */ @@ -232,6 +213,25 @@ public function testSetHeaderWithExistingArrayValuesAppendArrayValue($arrayHeade $this->assertSame('json, html, xml', $this->message->getHeaderLine('Accept')); } + public static function provideArrayHeaderValue(): iterable + { + return [ + 'existing for next not append' => [ + [ + 'json', + 'html', + 'xml', + ], + ], + 'existing for next append' => [ + [ + 'json', + 'html', + ], + ], + ]; + } + public function testSetHeaderWithExistingArrayValuesAppendNullValue(): void { $this->message->setHeader('Accept', ['json', 'html', 'xml']); diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php index 26d0017d7c34..1208edaf05b9 100644 --- a/tests/system/HTTP/SiteURITest.php +++ b/tests/system/HTTP/SiteURITest.php @@ -66,6 +66,149 @@ public static function provideConstructor(): iterable return array_merge(self::provideSetPath(), self::provideRelativePathWithQueryOrFragment()); } + public static function provideRelativePathWithQueryOrFragment(): iterable + { + return [ + 'one/two?foo=1&bar=2' => [ + 'http://example.com/', // $baseURL + 'index.php', // $indexPage + 'one/two?foo=1&bar=2', // $relativePath + 'http://example.com/index.php/one/two?foo=1&bar=2', // $expectedURI + 'one/two', // $expectedRoutePath + '/index.php/one/two', // $expectedPath + 'foo=1&bar=2', // $expectedQuery + '', // $expectedFragment + ['one', 'two'], // $expectedSegments + 2, // $expectedTotalSegments + ], + 'one/two#sec1' => [ + 'http://example.com/', + 'index.php', + 'one/two#sec1', + 'http://example.com/index.php/one/two#sec1', + 'one/two', + '/index.php/one/two', + '', + 'sec1', + ['one', 'two'], + 2, + ], + 'one/two?foo=1&bar=2#sec1' => [ + 'http://example.com/', + 'index.php', + 'one/two?foo=1&bar=2#sec1', + 'http://example.com/index.php/one/two?foo=1&bar=2#sec1', + 'one/two', + '/index.php/one/two', + 'foo=1&bar=2', + 'sec1', + ['one', 'two'], + 2, + ], + 'Subfolder: one/two?foo=1&bar=2' => [ + 'http://example.com/ci4/', + 'index.php', + 'one/two?foo=1&bar=2', + 'http://example.com/ci4/index.php/one/two?foo=1&bar=2', + 'one/two', + '/ci4/index.php/one/two', + 'foo=1&bar=2', + '', + ['one', 'two'], + 2, + ], + ]; + } + + public function testConstructorHost(): void + { + $config = new App(); + $config->allowedHostnames = ['sub.example.com']; + + $uri = new SiteURI($config, '', 'sub.example.com'); + + $this->assertInstanceOf(SiteURI::class, $uri); + $this->assertSame('http://sub.example.com/index.php', (string) $uri); + $this->assertSame('', $uri->getRoutePath()); + $this->assertSame('/index.php', $uri->getPath()); + $this->assertSame('http://sub.example.com/', $uri->getBaseURL()); + } + + public function testConstructorScheme(): void + { + $config = new App(); + + $uri = new SiteURI($config, '', null, 'https'); + + $this->assertInstanceOf(SiteURI::class, $uri); + $this->assertSame('https://example.com/index.php', (string) $uri); + $this->assertSame('https://example.com/', $uri->getBaseURL()); + } + + public function testConstructorEmptyScheme(): void + { + $config = new App(); + + $uri = new SiteURI($config, '', null, ''); + + $this->assertInstanceOf(SiteURI::class, $uri); + $this->assertSame('http://example.com/index.php', (string) $uri); + $this->assertSame('http://example.com/', $uri->getBaseURL()); + } + + public function testConstructorForceGlobalSecureRequests(): void + { + $config = new App(); + $config->forceGlobalSecureRequests = true; + + $uri = new SiteURI($config); + + $this->assertSame('https://example.com/index.php', (string) $uri); + $this->assertSame('https://example.com/', $uri->getBaseURL()); + } + + public function testConstructorInvalidBaseURL(): void + { + $this->expectException(ConfigException::class); + + $config = new App(); + $config->baseURL = 'invalid'; + + new SiteURI($config); + } + + #[DataProvider('provideSetPath')] + public function testSetPath( + string $baseURL, + string $indexPage, + string $relativePath, + string $expectedURI, + string $expectedRoutePath, + string $expectedPath, + string $expectedQuery, + string $expectedFragment, + array $expectedSegments, + int $expectedTotalSegments, + ): void { + $config = new App(); + $config->indexPage = $indexPage; + $config->baseURL = $baseURL; + + $uri = new SiteURI($config); + + $uri->setPath($relativePath); + + $this->assertSame($expectedURI, (string) $uri); + $this->assertSame($expectedRoutePath, $uri->getRoutePath()); + $this->assertSame($expectedPath, $uri->getPath()); + $this->assertSame($expectedQuery, $uri->getQuery()); + $this->assertSame($expectedFragment, $uri->getFragment()); + $this->assertSame($baseURL, $uri->getBaseURL()); + + $this->assertSame($expectedSegments, $uri->getSegments()); + $this->assertSame($expectedTotalSegments, $uri->getTotalSegments()); + } + public static function provideSetPath(): iterable { return [ @@ -215,149 +358,6 @@ public static function provideSetPath(): iterable ]; } - public static function provideRelativePathWithQueryOrFragment(): iterable - { - return [ - 'one/two?foo=1&bar=2' => [ - 'http://example.com/', // $baseURL - 'index.php', // $indexPage - 'one/two?foo=1&bar=2', // $relativePath - 'http://example.com/index.php/one/two?foo=1&bar=2', // $expectedURI - 'one/two', // $expectedRoutePath - '/index.php/one/two', // $expectedPath - 'foo=1&bar=2', // $expectedQuery - '', // $expectedFragment - ['one', 'two'], // $expectedSegments - 2, // $expectedTotalSegments - ], - 'one/two#sec1' => [ - 'http://example.com/', - 'index.php', - 'one/two#sec1', - 'http://example.com/index.php/one/two#sec1', - 'one/two', - '/index.php/one/two', - '', - 'sec1', - ['one', 'two'], - 2, - ], - 'one/two?foo=1&bar=2#sec1' => [ - 'http://example.com/', - 'index.php', - 'one/two?foo=1&bar=2#sec1', - 'http://example.com/index.php/one/two?foo=1&bar=2#sec1', - 'one/two', - '/index.php/one/two', - 'foo=1&bar=2', - 'sec1', - ['one', 'two'], - 2, - ], - 'Subfolder: one/two?foo=1&bar=2' => [ - 'http://example.com/ci4/', - 'index.php', - 'one/two?foo=1&bar=2', - 'http://example.com/ci4/index.php/one/two?foo=1&bar=2', - 'one/two', - '/ci4/index.php/one/two', - 'foo=1&bar=2', - '', - ['one', 'two'], - 2, - ], - ]; - } - - public function testConstructorHost(): void - { - $config = new App(); - $config->allowedHostnames = ['sub.example.com']; - - $uri = new SiteURI($config, '', 'sub.example.com'); - - $this->assertInstanceOf(SiteURI::class, $uri); - $this->assertSame('http://sub.example.com/index.php', (string) $uri); - $this->assertSame('', $uri->getRoutePath()); - $this->assertSame('/index.php', $uri->getPath()); - $this->assertSame('http://sub.example.com/', $uri->getBaseURL()); - } - - public function testConstructorScheme(): void - { - $config = new App(); - - $uri = new SiteURI($config, '', null, 'https'); - - $this->assertInstanceOf(SiteURI::class, $uri); - $this->assertSame('https://example.com/index.php', (string) $uri); - $this->assertSame('https://example.com/', $uri->getBaseURL()); - } - - public function testConstructorEmptyScheme(): void - { - $config = new App(); - - $uri = new SiteURI($config, '', null, ''); - - $this->assertInstanceOf(SiteURI::class, $uri); - $this->assertSame('http://example.com/index.php', (string) $uri); - $this->assertSame('http://example.com/', $uri->getBaseURL()); - } - - public function testConstructorForceGlobalSecureRequests(): void - { - $config = new App(); - $config->forceGlobalSecureRequests = true; - - $uri = new SiteURI($config); - - $this->assertSame('https://example.com/index.php', (string) $uri); - $this->assertSame('https://example.com/', $uri->getBaseURL()); - } - - public function testConstructorInvalidBaseURL(): void - { - $this->expectException(ConfigException::class); - - $config = new App(); - $config->baseURL = 'invalid'; - - new SiteURI($config); - } - - #[DataProvider('provideSetPath')] - public function testSetPath( - string $baseURL, - string $indexPage, - string $relativePath, - string $expectedURI, - string $expectedRoutePath, - string $expectedPath, - string $expectedQuery, - string $expectedFragment, - array $expectedSegments, - int $expectedTotalSegments, - ): void { - $config = new App(); - $config->indexPage = $indexPage; - $config->baseURL = $baseURL; - - $uri = new SiteURI($config); - - $uri->setPath($relativePath); - - $this->assertSame($expectedURI, (string) $uri); - $this->assertSame($expectedRoutePath, $uri->getRoutePath()); - $this->assertSame($expectedPath, $uri->getPath()); - $this->assertSame($expectedQuery, $uri->getQuery()); - $this->assertSame($expectedFragment, $uri->getFragment()); - $this->assertSame($baseURL, $uri->getBaseURL()); - - $this->assertSame($expectedSegments, $uri->getSegments()); - $this->assertSame($expectedTotalSegments, $uri->getTotalSegments()); - } - public function testSetSegment(): void { $config = new App(); diff --git a/tests/system/HTTP/URITest.php b/tests/system/HTTP/URITest.php index 3564ae6c658e..3199d9c1cb25 100644 --- a/tests/system/HTTP/URITest.php +++ b/tests/system/HTTP/URITest.php @@ -468,6 +468,19 @@ public static function provideSetPath(): iterable ]; } + /** + * @param string $path + * @param string $expected + */ + #[DataProvider('providePathGetsFiltered')] + public function testPathGetsFiltered($path, $expected): void + { + $uri = new URI(); + $uri->setPath($path); + + $this->assertSame($expected, $uri->getPath()); + } + public static function providePathGetsFiltered(): iterable { return [ @@ -510,19 +523,6 @@ public static function providePathGetsFiltered(): iterable ]; } - /** - * @param string $path - * @param string $expected - */ - #[DataProvider('providePathGetsFiltered')] - public function testPathGetsFiltered($path, $expected): void - { - $uri = new URI(); - $uri->setPath($path); - - $this->assertSame($expected, $uri->getPath()); - } - public function testSetFragmentSetsValue(): void { $url = 'http://example.com/path'; @@ -603,6 +603,18 @@ public function testSetQueryThrowsErrorWhenFragmentPresentSilent(): void $this->assertSame('', $uri->getQuery()); } + /** + * @param string $url + * @param string $expected + */ + #[DataProvider('provideAuthorityReturnsExceptedValues')] + public function testAuthorityReturnsExceptedValues($url, $expected): void + { + $uri = new URI($url); + + $this->assertSame($expected, $uri->getAuthority()); + } + public static function provideAuthorityReturnsExceptedValues(): iterable { return [ @@ -642,15 +654,17 @@ public static function provideAuthorityReturnsExceptedValues(): iterable } /** - * @param string $url - * @param string $expected + * @param string $scheme + * @param int $port */ - #[DataProvider('provideAuthorityReturnsExceptedValues')] - public function testAuthorityReturnsExceptedValues($url, $expected): void + #[DataProvider('provideAuthorityRemovesDefaultPorts')] + public function testAuthorityRemovesDefaultPorts($scheme, $port): void { + $url = "{$scheme}://example.com:{$port}/path"; $uri = new URI($url); - $this->assertSame($expected, $uri->getAuthority()); + $expected = "{$scheme}://example.com/path"; + $this->assertSame($expected, (string) $uri); } public static function provideAuthorityRemovesDefaultPorts(): iterable @@ -667,20 +681,6 @@ public static function provideAuthorityRemovesDefaultPorts(): iterable ]; } - /** - * @param string $scheme - * @param int $port - */ - #[DataProvider('provideAuthorityRemovesDefaultPorts')] - public function testAuthorityRemovesDefaultPorts($scheme, $port): void - { - $url = "{$scheme}://example.com:{$port}/path"; - $uri = new URI($url); - - $expected = "{$scheme}://example.com/path"; - $this->assertSame($expected, (string) $uri); - } - public function testSetAuthorityReconstitutes(): void { $authority = 'me@foo.com:3000'; @@ -691,6 +691,16 @@ public function testSetAuthorityReconstitutes(): void $this->assertSame($authority, $uri->getAuthority()); } + /** + * @param string $path + * @param string $expected + */ + #[DataProvider('provideRemoveDotSegments')] + public function testRemoveDotSegments($path, $expected): void + { + $this->assertSame($expected, URI::removeDotSegments($path)); + } + public static function provideRemoveDotSegments(): iterable { return [ @@ -790,13 +800,35 @@ public static function provideRemoveDotSegments(): iterable } /** - * @param string $path + * @param string $rel * @param string $expected */ - #[DataProvider('provideRemoveDotSegments')] - public function testRemoveDotSegments($path, $expected): void + #[DataProvider('defaultResolutions')] + public function testResolveRelativeURI($rel, $expected): void { - $this->assertSame($expected, URI::removeDotSegments($path)); + $base = 'http://a/b/c/d'; + $uri = new URI($base); + + $new = $uri->resolveRelativeURI($rel); + + $this->assertSame($expected, (string) $new); + } + + /** + * @param string $rel + * @param string $expected + */ + #[DataProvider('defaultResolutions')] + public function testResolveRelativeURIHTTPS($rel, $expected): void + { + $base = 'https://a/b/c/d'; + $expected = str_replace('http:', 'https:', $expected); + + $uri = new URI($base); + + $new = $uri->resolveRelativeURI($rel); + + $this->assertSame($expected, (string) $new); } public static function defaultResolutions(): iterable @@ -829,38 +861,6 @@ public static function defaultResolutions(): iterable ]; } - /** - * @param string $rel - * @param string $expected - */ - #[DataProvider('defaultResolutions')] - public function testResolveRelativeURI($rel, $expected): void - { - $base = 'http://a/b/c/d'; - $uri = new URI($base); - - $new = $uri->resolveRelativeURI($rel); - - $this->assertSame($expected, (string) $new); - } - - /** - * @param string $rel - * @param string $expected - */ - #[DataProvider('defaultResolutions')] - public function testResolveRelativeURIHTTPS($rel, $expected): void - { - $base = 'https://a/b/c/d'; - $expected = str_replace('http:', 'https:', $expected); - - $uri = new URI($base); - - $new = $uri->resolveRelativeURI($rel); - - $this->assertSame($expected, (string) $new); - } - public function testResolveRelativeURIWithNoBase(): void { $base = 'http://a'; diff --git a/tests/system/Helpers/ArrayHelperTest.php b/tests/system/Helpers/ArrayHelperTest.php index 77079d2a5a2b..0293cc6c826e 100644 --- a/tests/system/Helpers/ArrayHelperTest.php +++ b/tests/system/Helpers/ArrayHelperTest.php @@ -241,6 +241,32 @@ public function testArrayDeepSearch($key, $expected): void $this->assertSame($expected, $result); } + public static function provideArrayDeepSearch(): iterable + { + return [ + [ + 'key6441', + 'Value 6.4.4.1', + ], + [ + 'key64421', + null, + ], + [ + 42, + 'Value 42', + ], + [ + 'key644', + ['key6441' => 'Value 6.4.4.1'], + ], + [ + '', + null, + ], + ]; + } + public function testArrayDeepSearchReturnNullEmptyArray(): void { $data = []; @@ -308,32 +334,6 @@ public function testArraySortByMultipleKeysFailsInconsistentArraySizes($data): v array_sort_by_multiple_keys($data, $sortColumns); } - public static function provideArrayDeepSearch(): iterable - { - return [ - [ - 'key6441', - 'Value 6.4.4.1', - ], - [ - 'key64421', - null, - ], - [ - 42, - 'Value 42', - ], - [ - 'key644', - ['key6441' => 'Value 6.4.4.1'], - ], - [ - '', - null, - ], - ]; - } - public static function provideSortByMultipleKeys(): iterable { $seed = [ @@ -493,14 +493,6 @@ public function testArrayGroupByIncludeEmpty(array $indexes, array $data, array $this->assertSame($expected, $actual, 'array including empty not the same'); } - #[DataProvider('provideArrayGroupByExcludeEmpty')] - public function testArrayGroupByExcludeEmpty(array $indexes, array $data, array $expected): void - { - $actual = array_group_by($data, $indexes, false); - - $this->assertSame($expected, $actual, 'array excluding empty not the same'); - } - public static function provideArrayGroupByIncludeEmpty(): iterable { yield 'simple group-by test' => [ @@ -912,6 +904,14 @@ public static function provideArrayGroupByIncludeEmpty(): iterable ]; } + #[DataProvider('provideArrayGroupByExcludeEmpty')] + public function testArrayGroupByExcludeEmpty(array $indexes, array $data, array $expected): void + { + $actual = array_group_by($data, $indexes, false); + + $this->assertSame($expected, $actual, 'array excluding empty not the same'); + } + public static function provideArrayGroupByExcludeEmpty(): iterable { yield 'simple group-by test' => [ diff --git a/tests/system/Helpers/InflectorHelperTest.php b/tests/system/Helpers/InflectorHelperTest.php index f0df11977126..59068b8bf751 100644 --- a/tests/system/Helpers/InflectorHelperTest.php +++ b/tests/system/Helpers/InflectorHelperTest.php @@ -246,6 +246,12 @@ public function testDasherize(): void } } + #[DataProvider('provideOrdinal')] + public function testOrdinal(string $suffix, int $number): void + { + $this->assertSame($suffix, ordinal($number)); + } + public static function provideOrdinal(): iterable { return [ @@ -262,12 +268,6 @@ public static function provideOrdinal(): iterable ]; } - #[DataProvider('provideOrdinal')] - public function testOrdinal(string $suffix, int $number): void - { - $this->assertSame($suffix, ordinal($number)); - } - public function testOrdinalize(): void { $suffixedNumbers = [ diff --git a/tests/system/Helpers/URLHelper/CurrentUrlTest.php b/tests/system/Helpers/URLHelper/CurrentUrlTest.php index 1c7de84586e2..b802db349949 100644 --- a/tests/system/Helpers/URLHelper/CurrentUrlTest.php +++ b/tests/system/Helpers/URLHelper/CurrentUrlTest.php @@ -242,6 +242,44 @@ public function testUriStringSubfolderRelative(): void $this->assertSame('assets/image.jpg', uri_string()); } + #[DataProvider('provideUrlIs')] + public function testUrlIs(string $currentPath, string $testPath, bool $expected): void + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/' . $currentPath; + + $this->createRequest($this->config); + + $this->assertSame($expected, url_is($testPath)); + } + + #[DataProvider('provideUrlIs')] + public function testUrlIsNoIndex(string $currentPath, string $testPath, bool $expected): void + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/' . $currentPath; + + $this->config->indexPage = ''; + + $this->createRequest($this->config); + + $this->assertSame($expected, url_is($testPath)); + } + + #[DataProvider('provideUrlIs')] + public function testUrlIsWithSubfolder(string $currentPath, string $testPath, bool $expected): void + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['REQUEST_URI'] = '/' . $currentPath; + $_SERVER['SCRIPT_NAME'] = '/subfolder/index.php'; + + $this->config->baseURL = 'http://example.com/subfolder/'; + + $this->createRequest($this->config); + + $this->assertSame($expected, url_is($testPath)); + } + public static function provideUrlIs(): iterable { return [ @@ -282,42 +320,4 @@ public static function provideUrlIs(): iterable ], ]; } - - #[DataProvider('provideUrlIs')] - public function testUrlIs(string $currentPath, string $testPath, bool $expected): void - { - $_SERVER['HTTP_HOST'] = 'example.com'; - $_SERVER['REQUEST_URI'] = '/' . $currentPath; - - $this->createRequest($this->config); - - $this->assertSame($expected, url_is($testPath)); - } - - #[DataProvider('provideUrlIs')] - public function testUrlIsNoIndex(string $currentPath, string $testPath, bool $expected): void - { - $_SERVER['HTTP_HOST'] = 'example.com'; - $_SERVER['REQUEST_URI'] = '/' . $currentPath; - - $this->config->indexPage = ''; - - $this->createRequest($this->config); - - $this->assertSame($expected, url_is($testPath)); - } - - #[DataProvider('provideUrlIs')] - public function testUrlIsWithSubfolder(string $currentPath, string $testPath, bool $expected): void - { - $_SERVER['HTTP_HOST'] = 'example.com'; - $_SERVER['REQUEST_URI'] = '/' . $currentPath; - $_SERVER['SCRIPT_NAME'] = '/subfolder/index.php'; - - $this->config->baseURL = 'http://example.com/subfolder/'; - - $this->createRequest($this->config); - - $this->assertSame($expected, url_is($testPath)); - } } diff --git a/tests/system/Helpers/URLHelper/MiscUrlTest.php b/tests/system/Helpers/URLHelper/MiscUrlTest.php index 78c7fd5b0d24..81ec9770ea2a 100644 --- a/tests/system/Helpers/URLHelper/MiscUrlTest.php +++ b/tests/system/Helpers/URLHelper/MiscUrlTest.php @@ -128,6 +128,21 @@ public function testIndexPageAlt(): void $this->assertSame('banana.php', index_page($this->config)); } + /** + * @param mixed $expected + * @param mixed $uri + * @param mixed $title + * @param mixed $attributes + */ + #[DataProvider('provideAnchor')] + public function testAnchor($expected = '', $uri = '', $title = '', $attributes = ''): void + { + $uriString = 'http://example.com/'; + $this->createRequest($uriString); + + $this->assertSame($expected, anchor($uri, $title, $attributes, $this->config)); + } + // Test anchor public static function provideAnchor(): iterable @@ -179,9 +194,11 @@ public static function provideAnchor(): iterable * @param mixed $title * @param mixed $attributes */ - #[DataProvider('provideAnchor')] - public function testAnchor($expected = '', $uri = '', $title = '', $attributes = ''): void + #[DataProvider('provideAnchorNoindex')] + public function testAnchorNoindex($expected = '', $uri = '', $title = '', $attributes = ''): void { + $this->config->indexPage = ''; + $uriString = 'http://example.com/'; $this->createRequest($uriString); @@ -243,8 +260,8 @@ public static function provideAnchorNoindex(): iterable * @param mixed $title * @param mixed $attributes */ - #[DataProvider('provideAnchorNoindex')] - public function testAnchorNoindex($expected = '', $uri = '', $title = '', $attributes = ''): void + #[DataProvider('provideAnchorTargetted')] + public function testAnchorTargetted($expected = '', $uri = '', $title = '', $attributes = ''): void { $this->config->indexPage = ''; @@ -299,11 +316,9 @@ public static function provideAnchorTargetted(): iterable * @param mixed $title * @param mixed $attributes */ - #[DataProvider('provideAnchorTargetted')] - public function testAnchorTargetted($expected = '', $uri = '', $title = '', $attributes = ''): void + #[DataProvider('provideAnchorExamples')] + public function testAnchorExamples($expected = '', $uri = '', $title = '', $attributes = ''): void { - $this->config->indexPage = ''; - $uriString = 'http://example.com/'; $this->createRequest($uriString); @@ -344,13 +359,13 @@ public static function provideAnchorExamples(): iterable * @param mixed $title * @param mixed $attributes */ - #[DataProvider('provideAnchorExamples')] - public function testAnchorExamples($expected = '', $uri = '', $title = '', $attributes = ''): void + #[DataProvider('provideAnchorPopup')] + public function testAnchorPopup($expected = '', $uri = '', $title = '', $attributes = false): void { $uriString = 'http://example.com/'; $this->createRequest($uriString); - $this->assertSame($expected, anchor($uri, $title, $attributes, $this->config)); + $this->assertSame($expected, anchor_popup($uri, $title, $attributes, $this->config)); } // Test anchor_popup @@ -397,17 +412,17 @@ public static function provideAnchorPopup(): iterable /** * @param mixed $expected - * @param mixed $uri + * @param mixed $email * @param mixed $title * @param mixed $attributes */ - #[DataProvider('provideAnchorPopup')] - public function testAnchorPopup($expected = '', $uri = '', $title = '', $attributes = false): void + #[DataProvider('provideMailto')] + public function testMailto($expected = '', $email = '', $title = '', $attributes = ''): void { $uriString = 'http://example.com/'; $this->createRequest($uriString); - $this->assertSame($expected, anchor_popup($uri, $title, $attributes, $this->config)); + $this->assertSame($expected, mailto($email, $title, $attributes)); } // Test mailto @@ -439,13 +454,13 @@ public static function provideMailto(): iterable * @param mixed $title * @param mixed $attributes */ - #[DataProvider('provideMailto')] - public function testMailto($expected = '', $email = '', $title = '', $attributes = ''): void + #[DataProvider('provideSafeMailto')] + public function testSafeMailto($expected = '', $email = '', $title = '', $attributes = ''): void { $uriString = 'http://example.com/'; $this->createRequest($uriString); - $this->assertSame($expected, mailto($email, $title, $attributes)); + $this->assertSame($expected, safe_mailto($email, $title, $attributes)); } // Test safe_mailto @@ -471,21 +486,6 @@ public static function provideSafeMailto(): iterable ]; } - /** - * @param mixed $expected - * @param mixed $email - * @param mixed $title - * @param mixed $attributes - */ - #[DataProvider('provideSafeMailto')] - public function testSafeMailto($expected = '', $email = '', $title = '', $attributes = ''): void - { - $uriString = 'http://example.com/'; - $this->createRequest($uriString); - - $this->assertSame($expected, safe_mailto($email, $title, $attributes)); - } - public function testSafeMailtoWithCsp(): void { $this->config->CSPEnabled = true; @@ -496,6 +496,16 @@ public function testSafeMailtoWithCsp(): void $this->assertMatchesRegularExpression('/