From b992c3b8e085fc628024278753c71f0115f86413 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Fri, 12 Dec 2025 21:09:01 +0800 Subject: [PATCH 01/29] docs: add changelog and upgrade for v4.6.5 (#9838) --- user_guide_src/source/changelogs/index.rst | 1 + user_guide_src/source/changelogs/v4.6.5.rst | 35 ++++++++++++ .../source/installation/upgrade_465.rst | 55 +++++++++++++++++++ .../source/installation/upgrading.rst | 1 + 4 files changed, 92 insertions(+) create mode 100644 user_guide_src/source/changelogs/v4.6.5.rst create mode 100644 user_guide_src/source/installation/upgrade_465.rst diff --git a/user_guide_src/source/changelogs/index.rst b/user_guide_src/source/changelogs/index.rst index e11d2e67cc19..70892e3f2900 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.5 v4.6.4 v4.6.3 v4.6.2 diff --git a/user_guide_src/source/changelogs/v4.6.5.rst b/user_guide_src/source/changelogs/v4.6.5.rst new file mode 100644 index 000000000000..0ae6d47fe1be --- /dev/null +++ b/user_guide_src/source/changelogs/v4.6.5.rst @@ -0,0 +1,35 @@ +############# +Version 4.6.5 +############# + +Release Date: Unreleased + +**4.6.5 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_465.rst b/user_guide_src/source/installation/upgrade_465.rst new file mode 100644 index 000000000000..abf6d7ca5b47 --- /dev/null +++ b/user_guide_src/source/installation/upgrade_465.rst @@ -0,0 +1,55 @@ +############################# +Upgrading from 4.6.4 to 4.6.5 +############################# + +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 692666df08c2..2f37b8bfb9f9 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_465 upgrade_464 upgrade_463 upgrade_462 From 4a7309f981f3cf00997b2e0f03f596bf2f805cb4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Dec 2025 18:01:48 +0100 Subject: [PATCH 02/29] chore(deps): bump actions/cache from 4 to 5 (#9839) Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/cache dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/reusable-coveralls.yml | 4 ++-- .github/workflows/reusable-phpunit-test.yml | 4 ++-- .github/workflows/reusable-serviceless-phpunit-test.yml | 4 ++-- .github/workflows/test-coding-standards.yml | 2 +- .github/workflows/test-deptrac.yml | 4 ++-- .github/workflows/test-phpstan.yml | 4 ++-- .github/workflows/test-psalm.yml | 4 ++-- .github/workflows/test-rector.yml | 4 ++-- 8 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/reusable-coveralls.yml b/.github/workflows/reusable-coveralls.yml index 0625e042bad8..93488356d626 100644 --- a/.github/workflows/reusable-coveralls.yml +++ b/.github/workflows/reusable-coveralls.yml @@ -43,7 +43,7 @@ jobs: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV - name: Cache dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ env.COMPOSER_CACHE_FILES_DIR }} key: ${{ github.job }}-php-${{ inputs.php-version }}-${{ hashFiles('**/composer.*') }} @@ -52,7 +52,7 @@ jobs: ${{ github.job }}- - name: Cache PHPUnit's static analysis cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: build/.phpunit.cache/code-coverage key: phpunit-code-coverage-${{ hashFiles('**/phpunit.*') }} diff --git a/.github/workflows/reusable-phpunit-test.yml b/.github/workflows/reusable-phpunit-test.yml index 3ce33ecb7000..8637fa4d0dbc 100644 --- a/.github/workflows/reusable-phpunit-test.yml +++ b/.github/workflows/reusable-phpunit-test.yml @@ -173,7 +173,7 @@ jobs: echo "ARTIFACT_NAME=${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}-db-${{ inputs.db-platform || 'none' }}" >> $GITHUB_ENV - name: Cache dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ env.COMPOSER_CACHE_FILES_DIR }} key: ${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}-db-${{ inputs.db-platform || 'none' }}-${{ hashFiles('**/composer.*') }} @@ -184,7 +184,7 @@ jobs: - name: Cache PHPUnit's static analysis cache if: ${{ inputs.enable-artifact-upload }} - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: build/.phpunit.cache/code-coverage key: phpunit-code-coverage-${{ hashFiles('**/phpunit.*') }} diff --git a/.github/workflows/reusable-serviceless-phpunit-test.yml b/.github/workflows/reusable-serviceless-phpunit-test.yml index 209430eb5b00..43cb7bee2c10 100644 --- a/.github/workflows/reusable-serviceless-phpunit-test.yml +++ b/.github/workflows/reusable-serviceless-phpunit-test.yml @@ -85,7 +85,7 @@ jobs: echo "ARTIFACT_NAME=${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}" >> $GITHUB_ENV - name: Cache Composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ env.COMPOSER_CACHE_FILES_DIR }} key: ${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}-${{ hashFiles('**/composer.*') }} @@ -95,7 +95,7 @@ jobs: - name: Cache PHPUnit's static analysis cache if: ${{ inputs.enable-artifact-upload }} - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: build/.phpunit.cache/code-coverage key: phpunit-code-coverage-${{ hashFiles('**/phpunit.*') }} diff --git a/.github/workflows/test-coding-standards.yml b/.github/workflows/test-coding-standards.yml index 804c587e7f77..c122f1e25b62 100644 --- a/.github/workflows/test-coding-standards.yml +++ b/.github/workflows/test-coding-standards.yml @@ -52,7 +52,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV - name: Cache dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ env.COMPOSER_CACHE_FILES_DIR }} key: ${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }} diff --git a/.github/workflows/test-deptrac.yml b/.github/workflows/test-deptrac.yml index f66e1057b8d4..a301f3552d55 100644 --- a/.github/workflows/test-deptrac.yml +++ b/.github/workflows/test-deptrac.yml @@ -59,7 +59,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV - name: Cache dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ env.COMPOSER_CACHE_FILES_DIR }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} @@ -69,7 +69,7 @@ jobs: run: mkdir -p build/ - name: Cache Deptrac results - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: build key: ${{ runner.os }}-deptrac-${{ github.sha }} diff --git a/.github/workflows/test-phpstan.yml b/.github/workflows/test-phpstan.yml index d4657396da6c..5634b634d482 100644 --- a/.github/workflows/test-phpstan.yml +++ b/.github/workflows/test-phpstan.yml @@ -71,7 +71,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV - name: Cache dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ env.COMPOSER_CACHE_FILES_DIR }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} @@ -81,7 +81,7 @@ jobs: run: mkdir -p build/phpstan - name: Cache PHPStan result cache directory - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: build/phpstan key: ${{ runner.os }}-phpstan-${{ github.sha }} diff --git a/.github/workflows/test-psalm.yml b/.github/workflows/test-psalm.yml index 0e0a865843ba..a2ad9668bd91 100644 --- a/.github/workflows/test-psalm.yml +++ b/.github/workflows/test-psalm.yml @@ -49,7 +49,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV - name: Cache composer dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ env.COMPOSER_CACHE_FILES_DIR }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}-${{ hashFiles('**/composer.lock') }} @@ -59,7 +59,7 @@ jobs: run: mkdir -p build/psalm - name: Cache Psalm results - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: build/psalm key: ${{ runner.os }}-psalm-${{ github.sha }} diff --git a/.github/workflows/test-rector.yml b/.github/workflows/test-rector.yml index 74a46590b80a..72bbf8198875 100644 --- a/.github/workflows/test-rector.yml +++ b/.github/workflows/test-rector.yml @@ -72,7 +72,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV - name: Cache dependencies - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ env.COMPOSER_CACHE_FILES_DIR }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} @@ -82,7 +82,7 @@ jobs: run: composer update --ansi --no-interaction - name: Rector Cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: /tmp/rector key: ${{ runner.os }}-rector-${{ github.run_id }} From ff579c33a1cbe6b67361ab28095be6da2d6ae6d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 18:41:13 +0100 Subject: [PATCH 03/29] chore(deps): bump actions/upload-artifact from 5 to 6 (#9843) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy-userguide-latest.yml | 2 +- .github/workflows/reusable-phpunit-test.yml | 2 +- .github/workflows/reusable-serviceless-phpunit-test.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-userguide-latest.yml b/.github/workflows/deploy-userguide-latest.yml index 5365d4e94d78..8fee24da6299 100644 --- a/.github/workflows/deploy-userguide-latest.yml +++ b/.github/workflows/deploy-userguide-latest.yml @@ -57,7 +57,7 @@ jobs: # Create an artifact of the html output - name: Upload artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: HTML Documentation path: user_guide_src/build/html/ diff --git a/.github/workflows/reusable-phpunit-test.yml b/.github/workflows/reusable-phpunit-test.yml index 8637fa4d0dbc..16494d9f4c57 100644 --- a/.github/workflows/reusable-phpunit-test.yml +++ b/.github/workflows/reusable-phpunit-test.yml @@ -212,7 +212,7 @@ jobs: - name: Upload coverage results as artifact if: ${{ inputs.enable-artifact-upload }} - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: ${{ env.ARTIFACT_NAME }} path: build/cov/coverage-${{ env.ARTIFACT_NAME }}.cov diff --git a/.github/workflows/reusable-serviceless-phpunit-test.yml b/.github/workflows/reusable-serviceless-phpunit-test.yml index 43cb7bee2c10..2bd34967cdf1 100644 --- a/.github/workflows/reusable-serviceless-phpunit-test.yml +++ b/.github/workflows/reusable-serviceless-phpunit-test.yml @@ -122,7 +122,7 @@ jobs: - name: Upload coverage results as artifact if: ${{ inputs.enable-artifact-upload }} - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: ${{ env.ARTIFACT_NAME }} path: build/cov/coverage-${{ env.ARTIFACT_NAME }}.cov From e56ba934f2ba6b7a2869c7d8f4f179f0f2188e73 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 18:41:28 +0100 Subject: [PATCH 04/29] chore(deps): bump actions/download-artifact from 6 to 7 (#9842) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/reusable-coveralls.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/reusable-coveralls.yml b/.github/workflows/reusable-coveralls.yml index 93488356d626..e8af4347f9f6 100644 --- a/.github/workflows/reusable-coveralls.yml +++ b/.github/workflows/reusable-coveralls.yml @@ -30,7 +30,7 @@ jobs: coverage: xdebug - name: Download coverage files - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: path: build/cov From 79bed83164d4f4ead2adb0748f7d4fc54710a8d7 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 24 Dec 2025 07:37:17 +0700 Subject: [PATCH 05/29] chore: Prepare to update to rector 2.3.0 and migrate FileWithoutNamespace to FileNode --- composer.json | 2 +- .../Rector/UnderscoreToCamelCaseVariableNameRector.php | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 48007df18b3d..7ee9c0a858ee 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.2.14", + "rector/rector": "dev-main", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { diff --git a/utils/src/Rector/UnderscoreToCamelCaseVariableNameRector.php b/utils/src/Rector/UnderscoreToCamelCaseVariableNameRector.php index 4471bdb7167c..42c677a2de84 100644 --- a/utils/src/Rector/UnderscoreToCamelCaseVariableNameRector.php +++ b/utils/src/Rector/UnderscoreToCamelCaseVariableNameRector.php @@ -23,7 +23,7 @@ use PhpParser\Node\Stmt\Namespace_; use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory; use Rector\Php\ReservedKeywordAnalyzer; -use Rector\PhpParser\Node\CustomNode\FileWithoutNamespace; +use Rector\PhpParser\Node\FileNode; use Rector\Rector\AbstractRector; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; @@ -77,14 +77,18 @@ public function run($aB) */ public function getNodeTypes(): array { - return [FileWithoutNamespace::class, Namespace_::class]; + return [FileNode::class, Namespace_::class]; } /** - * @param ClassMethod|Closure|FileWithoutNamespace|Function_|Namespace_ $node + * @param ClassMethod|Closure|FileNode|Function_|Namespace_ $node */ public function refactor(Node $node): ?Node { + if ($node instanceof FileNode && $node->isNamespaced()) { + return null; + } + if ($node->stmts === null) { return null; } From 359c5aee80f4e3e4e6ff0a3c06378a781ea4fb8e Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 24 Dec 2025 07:38:44 +0700 Subject: [PATCH 06/29] chore: add comment for namespaced filenode --- utils/src/Rector/UnderscoreToCamelCaseVariableNameRector.php | 1 + 1 file changed, 1 insertion(+) diff --git a/utils/src/Rector/UnderscoreToCamelCaseVariableNameRector.php b/utils/src/Rector/UnderscoreToCamelCaseVariableNameRector.php index 42c677a2de84..8bf02002a267 100644 --- a/utils/src/Rector/UnderscoreToCamelCaseVariableNameRector.php +++ b/utils/src/Rector/UnderscoreToCamelCaseVariableNameRector.php @@ -86,6 +86,7 @@ public function getNodeTypes(): array public function refactor(Node $node): ?Node { if ($node instanceof FileNode && $node->isNamespaced()) { + // handled in Namespace_ node return null; } From 83bef105a7da5c86e7006b5d09d4365e0a931a62 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 24 Dec 2025 07:43:02 +0700 Subject: [PATCH 07/29] chore: clean up param doc --- utils/src/Rector/UnderscoreToCamelCaseVariableNameRector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/src/Rector/UnderscoreToCamelCaseVariableNameRector.php b/utils/src/Rector/UnderscoreToCamelCaseVariableNameRector.php index 8bf02002a267..070522314ee3 100644 --- a/utils/src/Rector/UnderscoreToCamelCaseVariableNameRector.php +++ b/utils/src/Rector/UnderscoreToCamelCaseVariableNameRector.php @@ -81,7 +81,7 @@ public function getNodeTypes(): array } /** - * @param ClassMethod|Closure|FileNode|Function_|Namespace_ $node + * @param FileNode|Namespace_ $node */ public function refactor(Node $node): ?Node { From 4b4b3c65271daf8ff6e5e7bfe26499e45baaba14 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Fri, 26 Dec 2025 06:34:46 +0700 Subject: [PATCH 08/29] bump to rector 2.3.0 Signed-off-by: Abdul Malik Ikhsan --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 7ee9c0a858ee..c1254bf5f583 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": "dev-main", + "rector/rector": "2.3.0", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { From f77d6b8a071584dad38fe33450237a45815ea248 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Mon, 29 Dec 2025 09:33:38 +0330 Subject: [PATCH 09/29] docs: clarify Session Testing behavior and assertion scope (#9857) * docs: link to Session Testing * docs: add session testing * docs: add all session testing code * style: run cs-fix * docs: fix load session config --- user_guide_src/source/libraries/sessions.rst | 3 +- user_guide_src/source/testing/feature.rst | 2 + user_guide_src/source/testing/index.rst | 41 +++++------ user_guide_src/source/testing/response.rst | 2 + .../source/testing/session_testing.rst | 68 +++++++++++++++++++ .../source/testing/session_testing/001.php | 14 ++++ .../source/testing/session_testing/002.php | 10 +++ .../source/testing/session_testing/003.php | 38 +++++++++++ .../source/testing/session_testing/004.php | 16 +++++ 9 files changed, 173 insertions(+), 21 deletions(-) create mode 100644 user_guide_src/source/testing/session_testing.rst create mode 100644 user_guide_src/source/testing/session_testing/001.php create mode 100644 user_guide_src/source/testing/session_testing/002.php create mode 100644 user_guide_src/source/testing/session_testing/003.php create mode 100644 user_guide_src/source/testing/session_testing/004.php diff --git a/user_guide_src/source/libraries/sessions.rst b/user_guide_src/source/libraries/sessions.rst index a89e12df32c0..d98ece664243 100644 --- a/user_guide_src/source/libraries/sessions.rst +++ b/user_guide_src/source/libraries/sessions.rst @@ -514,7 +514,8 @@ Have it in mind though, every driver has different caveats, so be sure to get yourself familiar with them (below) before you make that choice. .. note:: The ArrayHandler is used during testing and stores all data within - a PHP array, while preventing the data from being persisted. + a PHP array, while preventing the data from being persisted. See + :doc:`Testing Sessions `. FileHandler Driver (the default) ================================ diff --git a/user_guide_src/source/testing/feature.rst b/user_guide_src/source/testing/feature.rst index 00100f8a7885..67337fa0464c 100644 --- a/user_guide_src/source/testing/feature.rst +++ b/user_guide_src/source/testing/feature.rst @@ -63,6 +63,8 @@ override any existing routes in the system: Each of the "routes" is a 3 element array containing the HTTP verb (or "add" for all), the URI to match, and the routing destination. +.. _feature-setting-session-values: + Setting Session Values ---------------------- diff --git a/user_guide_src/source/testing/index.rst b/user_guide_src/source/testing/index.rst index b6a71055698c..866ef30f5b6f 100644 --- a/user_guide_src/source/testing/index.rst +++ b/user_guide_src/source/testing/index.rst @@ -1,20 +1,21 @@ -####### -Testing -####### - -CodeIgniter ships with a number of tools to help you test and debug your application thoroughly. -The following sections should get you quickly testing your applications. - -.. toctree:: - :titlesonly: - - Getting Started - Database - Generating Data - Controller Testing - HTTP Testing - response - cli - Mocking - benchmark - debugging +####### +Testing +####### + +CodeIgniter ships with a number of tools to help you test and debug your application thoroughly. +The following sections should get you quickly testing your applications. + +.. toctree:: + :titlesonly: + + Getting Started + Database + Generating Data + Controller Testing + HTTP Testing + response + cli + Mocking + benchmark + debugging + Session Testing diff --git a/user_guide_src/source/testing/response.rst b/user_guide_src/source/testing/response.rst index 552133272a28..419df7620782 100644 --- a/user_guide_src/source/testing/response.rst +++ b/user_guide_src/source/testing/response.rst @@ -100,6 +100,8 @@ Asserts that the HTTP status code returned matches $code. .. literalinclude:: response/010.php :lines: 2- +.. _response-session-assertions: + Session Assertions ================== diff --git a/user_guide_src/source/testing/session_testing.rst b/user_guide_src/source/testing/session_testing.rst new file mode 100644 index 000000000000..17f787454fac --- /dev/null +++ b/user_guide_src/source/testing/session_testing.rst @@ -0,0 +1,68 @@ +############### +Session Testing +############### + +Testing session behavior in your application is made simple with the ArrayHandler session driver. +Unlike other session drivers, ArrayHandler does not persist data to disk, database, or external storage. +This allows you to simulate session interactions safely during unit or integration tests, without affecting real session data. + +Using this driver, you can set, retrieve, and assert session data entirely in memory, making your tests faster and more isolated. +While in most production scenarios you would use file, database, or cache-backed sessions, ArrayHandler exists specifically to support testing workflows and prevent side effects. + +.. contents:: + :local: + :depth: 2 + +Initializing Sessions +===================== + +You can initialize a session using the ArrayHandler driver for testing. This example shows how to create a session instance with a proper configuration: + +.. literalinclude:: session_testing/001.php + +Setting and Retrieving Data +=========================== + +Once initialized, you can set session values and retrieve them as usual: + +.. literalinclude:: session_testing/002.php + +.. note:: + + Session data is stored in memory and lasts as long as the ArrayHandler object exists; + after the object is destroyed (typically at the end of a request or test), the data is lost. + +Example Test Case +================= + +Here's a simple example demonstrating usage of the ArrayHandler in a PHPUnit test: + +.. literalinclude:: session_testing/003.php + +Session Assertions +================== + +Using PHPUnit Assertions with ArrayHandler +------------------------------------------ + +When testing sessions directly with Session and ArrayHandler in a unit test, use standard PHPUnit assertions. +``assertSessionHas()`` and ``assertSessionMissing()`` are not available in this context because you are interacting directly with the session object, +not a response object. + +.. literalinclude:: session_testing/004.php + +Session Assertions via TestResponse +----------------------------------- + +When testing controllers or HTTP responses, you can use CodeIgniter 4’s session +assertion helpers, such as ``assertSessionHas()`` and ``assertSessionMissing()``, +which are available on the ``TestResponse`` object. These helpers allow you to +assert the state of the session during the HTTP request/response lifecycle. +See more: :ref:`Session Assertions ` + +Custom Session Values +===================== + +In Feature Tests, you can provide custom session data for a single test using the ``withSession()`` method. +This allows you to simulate session states such as logged-in users or specific roles during the request. +For full details and examples, see: :ref:`Setting Session Values ` \ No newline at end of file diff --git a/user_guide_src/source/testing/session_testing/001.php b/user_guide_src/source/testing/session_testing/001.php new file mode 100644 index 000000000000..01ad520b7ed0 --- /dev/null +++ b/user_guide_src/source/testing/session_testing/001.php @@ -0,0 +1,14 @@ +set('framework', 'CodeIgniter4'); + +// Retrieve session data +echo $testSession->get('framework'); // outputs 'CodeIgniter4' + +// Remove session data +$testSession->remove('framework'); diff --git a/user_guide_src/source/testing/session_testing/003.php b/user_guide_src/source/testing/session_testing/003.php new file mode 100644 index 000000000000..8a9ecaf64b92 --- /dev/null +++ b/user_guide_src/source/testing/session_testing/003.php @@ -0,0 +1,38 @@ +testSession = new Session($arrayHandler, $config); + } + + public function testFrameworkNameInSession(): void + { + // Set a session value + $this->testSession->set('framework', 'CodeIgniter'); + + // Assert the value exists and is correct + $this->assertSame('CodeIgniter', $this->testSession->get('framework')); + + // Remove the session value + $this->testSession->remove('framework'); + $this->assertNull($this->testSession->get('framework')); + } +} diff --git a/user_guide_src/source/testing/session_testing/004.php b/user_guide_src/source/testing/session_testing/004.php new file mode 100644 index 000000000000..ef7ab12ba472 --- /dev/null +++ b/user_guide_src/source/testing/session_testing/004.php @@ -0,0 +1,16 @@ +set('framework', 'CodeIgniter4'); + +// Assert the state of the session using PHPUnit assertions +$this->assertSame('CodeIgniter4', $testSession->get('framework')); // Value exists + +// Not empty +$this->assertNotEmpty($testSession->get('framework')); + +// Remove the value and assert it's gone +$testSession->remove('framework'); + +// Should be null +$this->assertNull($testSession->get('framework')); From 6f5c1d77fdcc1a4ac5a8e0ec66c1166c3b70c1ec Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Tue, 30 Dec 2025 13:34:43 +0800 Subject: [PATCH 10/29] style: implement `CodeIgniterRuleCustomisationPolicy` (#9859) --- .php-cs-fixer.dist.php | 5 +- .php-cs-fixer.tests.php | 10 ++-- tests/_support/Commands/Foobar.php | 17 +++++-- .../Database/Live/PreparedQueryTest.php | 8 ++-- .../CodeIgniterRuleCustomisationPolicy.php | 46 +++++++++++++++++++ 5 files changed, 69 insertions(+), 17 deletions(-) create mode 100644 utils/src/PhpCsFixer/CodeIgniterRuleCustomisationPolicy.php diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index f256758deb2b..ad6ffacb8f0c 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -26,7 +26,6 @@ 'ThirdParty', 'Validation/Views', ]) - ->notName('#Foobar.php$#') ->append([ __FILE__, __DIR__ . '/.php-cs-fixer.no-header.php', @@ -37,9 +36,7 @@ __DIR__ . '/spark', ]); -$overrides = [ - 'modernize_strpos' => ['modernize_stripos' => true], -]; +$overrides = []; $options = [ 'cacheFile' => 'build/.php-cs-fixer.cache', diff --git a/.php-cs-fixer.tests.php b/.php-cs-fixer.tests.php index 412400aebaee..09ea5a62c55a 100644 --- a/.php-cs-fixer.tests.php +++ b/.php-cs-fixer.tests.php @@ -11,10 +11,11 @@ * the LICENSE file that was distributed with this source code. */ -use PhpCsFixer\ConfigInterface; +use PhpCsFixer\Config; use PhpCsFixer\Finder; +use Utils\PhpCsFixer\CodeIgniterRuleCustomisationPolicy; -/** @var ConfigInterface $config */ +/** @var Config $config */ $config = require __DIR__ . '/.php-cs-fixer.dist.php'; $finder = Finder::create() @@ -26,9 +27,7 @@ '_support/View/Cells/multiplier.php', '_support/View/Cells/colors.php', '_support/View/Cells/addition.php', - 'system/Database/Live/PreparedQueryTest.php', - ]) - ->notName('#Foobar.php$#'); + ]); $overrides = [ 'phpdoc_to_return_type' => true, @@ -36,6 +35,7 @@ ]; return $config + ->setRuleCustomisationPolicy(new CodeIgniterRuleCustomisationPolicy()) ->setFinder($finder) ->setCacheFile('build/.php-cs-fixer.tests.cache') ->setRules(array_merge($config->getRules(), $overrides)); diff --git a/tests/_support/Commands/Foobar.php b/tests/_support/Commands/Foobar.php index 1479c68ffa22..7ca79a9db7a5 100644 --- a/tests/_support/Commands/Foobar.php +++ b/tests/_support/Commands/Foobar.php @@ -1,11 +1,20 @@ + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + use Config\App; use CodeIgniter\CLI\CLI; return [ - 'foo' => 'The command will use this as foo.', - 'bar' => 'The command will use this as bar.', - 'baz' => 'The baz is here.', - 'bas' => CLI::color('bas', 'green') . (new App())->baseURL, + 'foo' => 'The command will use this as foo.', + 'bar' => 'The command will use this as bar.', + 'baz' => 'The baz is here.', + 'bas' => CLI::color('bas', 'green') . (new App())->baseURL, ]; diff --git a/tests/system/Database/Live/PreparedQueryTest.php b/tests/system/Database/Live/PreparedQueryTest.php index 7549e0b75ee0..b645c5992413 100644 --- a/tests/system/Database/Live/PreparedQueryTest.php +++ b/tests/system/Database/Live/PreparedQueryTest.php @@ -256,7 +256,7 @@ public function testDeallocatePreparedQueryThenTryToClose(): void 'name' => 'a', 'email' => 'b@example.com', 'country' => 'x', - ]) + ]), ); $this->query->close(); @@ -264,7 +264,7 @@ public function testDeallocatePreparedQueryThenTryToClose(): void // Try to close a non-existing prepared statement $this->expectException(BadMethodCallException::class); $this->expectExceptionMessage( - 'Cannot call close on a non-existing prepared statement.' + 'Cannot call close on a non-existing prepared statement.', ); $this->query->close(); @@ -325,7 +325,7 @@ public function testHandleTransStatusMarksTransactionFailedDuringTransaction(): $this->seeInDatabase($this->db->DBPrefix . 'without_auto_increment', [ 'key' => 'test_key', - 'value' => 'test_value' + 'value' => 'test_value', ]); $this->assertFalse($this->query->execute('test_key', 'different_value')); @@ -339,7 +339,7 @@ public function testHandleTransStatusMarksTransactionFailedDuringTransaction(): // Verify the first insert was rolled back $this->dontSeeInDatabase($this->db->DBPrefix . 'without_auto_increment', [ 'key' => 'test_key', - 'value' => 'test_value' + 'value' => 'test_value', ]); $this->db->resetTransStatus(); diff --git a/utils/src/PhpCsFixer/CodeIgniterRuleCustomisationPolicy.php b/utils/src/PhpCsFixer/CodeIgniterRuleCustomisationPolicy.php new file mode 100644 index 000000000000..8794f5dc787a --- /dev/null +++ b/utils/src/PhpCsFixer/CodeIgniterRuleCustomisationPolicy.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Utils\PhpCsFixer; + +use PhpCsFixer\Config\RuleCustomisationPolicyInterface; +use SplFileInfo; + +/** + * Rule customisation policy for CodeIgniter coding standard. + * + * @internal + */ +final class CodeIgniterRuleCustomisationPolicy implements RuleCustomisationPolicyInterface +{ + public function getPolicyVersionForCache(): string + { + return hash_file('sha256', __FILE__); + } + + public function getRuleCustomisers(): array + { + $normalisedStrEndsWith = static fn (string $haystack, string $needle): bool => str_ends_with(str_replace('\\', '/', $haystack), $needle); + + return [ + 'native_function_casing' => static fn (SplFileInfo $file): bool => ! $normalisedStrEndsWith( + $file->getPathname(), + '/tests/system/Database/Live/PreparedQueryTest.php', + ), + 'ordered_imports' => static fn (SplFileInfo $file): bool => ! $normalisedStrEndsWith( + $file->getPathname(), + '/tests/_support/Commands/Foobar.php', + ), + ]; + } +} From 0cb202fee5e9274f5e856f59161a90b74475a43f Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Sat, 3 Jan 2026 14:42:40 +0330 Subject: [PATCH 11/29] chore: remove redundant PHPStan baseline cleanup (#9866) * chore: add script to prepare PHPStan baseline * chore: use PHP script in phpstan:baseline command * chore: remove redundant cleanup from phpstan:baseline --- composer.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/composer.json b/composer.json index c1254bf5f583..4e2da479b0a4 100644 --- a/composer.json +++ b/composer.json @@ -112,8 +112,6 @@ ], "metrics": "utils/vendor/bin/phpmetrics --config=phpmetrics.json", "phpstan:baseline": [ - "bash -c \"rm -rf utils/phpstan-baseline/*.neon\"", - "bash -c \"touch utils/phpstan-baseline/loader.neon\"", "phpstan analyse --ansi --generate-baseline=utils/phpstan-baseline/loader.neon", "split-phpstan-baseline utils/phpstan-baseline/loader.neon" ], From 11f0130a5ecc447d5a4c21b2ba991ea381433949 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Sat, 3 Jan 2026 15:42:28 +0330 Subject: [PATCH 12/29] chore: add Environment field to bug report template (#9864) * chore: add Environment field to bug report template * chore: enable multi-select for environment in bug report form * chore: add self-env * chore: use Other(custom) for options * Apply suggestions from code review --- .github/ISSUE_TEMPLATE/bug_report.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 1f6dc36b395d..c00e70ba3291 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -83,6 +83,20 @@ body: validations: required: true + - type: dropdown + id: environment + attributes: + label: Environment + description: Which CI_ENVIRONMENT setting are you using? + multiple: true + options: + - production + - development + - testing + - other (custom) + validations: + required: true + - type: input id: database attributes: From c307c5f4a8a0aef6e36f0929476718caab9d7cbe Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Sat, 10 Jan 2026 16:50:30 +0100 Subject: [PATCH 13/29] refactor: debugbar time header not dependent on locale (#9880) Co-authored-by: John Paul E. Balandan, CPA Co-authored-by: neznaika0 --- system/Debug/Toolbar.php | 2 +- system/Debug/Toolbar/Collectors/History.php | 2 +- .../system/Debug/Toolbar/Collectors/HistoryTest.php | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index 7900c7c780c5..1a3ec63c9704 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -397,7 +397,7 @@ public function prepare(?RequestInterface $request = null, ?ResponseInterface $r helper('filesystem'); // Updated to microtime() so we can get history - $time = sprintf('%.6f', Time::now()->format('U.u')); + $time = sprintf('%.6F', Time::now()->format('U.u')); if (! is_dir(WRITEPATH . 'debugbar')) { mkdir(WRITEPATH . 'debugbar', 0777); diff --git a/system/Debug/Toolbar/Collectors/History.php b/system/Debug/Toolbar/Collectors/History.php index 4e5276b55b4e..583b350cb4b6 100644 --- a/system/Debug/Toolbar/Collectors/History.php +++ b/system/Debug/Toolbar/Collectors/History.php @@ -90,7 +90,7 @@ public function setFiles(string $current, int $limit = 20) $contents = @json_decode($contents); if (json_last_error() === JSON_ERROR_NONE) { preg_match('/debugbar_(.*)\.json$/s', $filename, $time); - $time = sprintf('%.6f', $time[1] ?? 0); + $time = sprintf('%.6F', $time[1] ?? 0); // Debugbar files shown in History Collector $files[] = [ diff --git a/tests/system/Debug/Toolbar/Collectors/HistoryTest.php b/tests/system/Debug/Toolbar/Collectors/HistoryTest.php index 6e72f2726ef6..dffa30ce67dc 100644 --- a/tests/system/Debug/Toolbar/Collectors/HistoryTest.php +++ b/tests/system/Debug/Toolbar/Collectors/HistoryTest.php @@ -34,7 +34,7 @@ protected function setUp(): void { parent::setUp(); - $this->time = (float) sprintf('%.6f', microtime(true)); + $this->time = (float) sprintf('%.6F', microtime(true)); } protected function tearDown(): void @@ -63,9 +63,9 @@ private function createDummyDebugbarJson(): void // create 20 dummy debugbar json files for ($i = 0; $i < 20; $i++) { - $path = str_replace((string) $time, sprintf('%.6f', $time - self::STEP), $path); + $path = str_replace((string) $time, sprintf('%.6F', $time - self::STEP), $path); file_put_contents($path, json_encode($dummyData)); - $time = sprintf('%.6f', $time - self::STEP); + $time = sprintf('%.6F', $time - self::STEP); } } @@ -76,7 +76,7 @@ public function testSetFiles(): void // test dir is now populated with json $this->createDummyDebugbarJson(); - $activeRowTime = $time = sprintf('%.6f', $time - self::STEP); + $activeRowTime = $time = sprintf('%.6F', $time - self::STEP); $history = new History(); $history->setFiles($time, 20); @@ -85,14 +85,14 @@ public function testSetFiles(): void $this->assertNotEmpty($history->display()['files'], 'Dummy Debugbar data is empty'); foreach ($history->display()['files'] as $request) { - $this->assertSame($request['time'], sprintf('%.6f', $time)); + $this->assertSame($request['time'], sprintf('%.6F', $time)); $this->assertSame( $request['datetime'], DateTime::createFromFormat('U.u', $time)->format('Y-m-d H:i:s.u'), ); $this->assertSame($request['active'], ($time === $activeRowTime)); - $time = sprintf('%.6f', $time - self::STEP); + $time = sprintf('%.6F', $time - self::STEP); } } } From a57f2f312183c2323a54578c568c1dc14c912078 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Sat, 10 Jan 2026 21:06:06 +0500 Subject: [PATCH 14/29] chore: set PHP ini-values from extra-ini-options input (#9879) --- .github/workflows/reusable-phpunit-test.yml | 5 +++++ .github/workflows/reusable-serviceless-phpunit-test.yml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/reusable-phpunit-test.yml b/.github/workflows/reusable-phpunit-test.yml index 16494d9f4c57..86a0d07bd228 100644 --- a/.github/workflows/reusable-phpunit-test.yml +++ b/.github/workflows/reusable-phpunit-test.yml @@ -43,6 +43,10 @@ on: description: Additional PHP extensions that are needed to be enabled type: string required: false + extra-ini-options: + description: Additional PHP configuration directives that should be appended to the php.ini + type: string + required: false extra-composer-options: description: Additional Composer options that should be appended to the `composer update` call type: string @@ -163,6 +167,7 @@ jobs: php-version: ${{ inputs.php-version }} tools: composer extensions: gd, ${{ inputs.extra-extensions }} + ini-values: ${{ inputs.extra-ini-options }} coverage: ${{ env.COVERAGE_DRIVER }} env: COVERAGE_DRIVER: ${{ inputs.enable-coverage && 'xdebug' || 'none' }} diff --git a/.github/workflows/reusable-serviceless-phpunit-test.yml b/.github/workflows/reusable-serviceless-phpunit-test.yml index 2bd34967cdf1..bbca4fe2acc8 100644 --- a/.github/workflows/reusable-serviceless-phpunit-test.yml +++ b/.github/workflows/reusable-serviceless-phpunit-test.yml @@ -37,6 +37,10 @@ on: description: Additional PHP extensions that are needed to be enabled type: string required: false + extra-ini-options: + description: Additional PHP configuration directives that should be appended to the php.ini + type: string + required: false extra-composer-options: description: Additional Composer options that should be appended to the `composer update` call type: string @@ -75,6 +79,7 @@ jobs: php-version: ${{ inputs.php-version }} tools: composer extensions: gd, ${{ inputs.extra-extensions }} + ini-values: ${{ inputs.extra-ini-options }} coverage: ${{ env.COVERAGE_DRIVER }} env: COVERAGE_DRIVER: ${{ inputs.enable-coverage && 'xdebug' || 'none' }} From 002a663eed9fb9ec84a14f1e09e57ba8afac52f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 15:26:02 +0000 Subject: [PATCH 15/29] chore(deps-dev): update rector/rector requirement from 2.3.0 to 2.3.1 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.3.0...2.3.1) --- updated-dependencies: - dependency-name: rector/rector dependency-version: 2.3.1 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 4e2da479b0a4..aaa0b9255ab5 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.3.0", + "rector/rector": "2.3.1", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { From 5ab52ae7438bc616b1b1f05d064a584d5825b462 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Fri, 16 Jan 2026 16:35:30 +0800 Subject: [PATCH 16/29] fix: `SUPPORTPATH` may be undefined in some contexts (#9882) * fix: `SUPPORTPATH` may be undefined in some contexts * Update system/Test/CIUnitTestCase.php Co-authored-by: Michal Sniatala --------- Co-authored-by: Michal Sniatala --- system/Test/CIUnitTestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Test/CIUnitTestCase.php b/system/Test/CIUnitTestCase.php index 81d669138422..edfa98df5c06 100644 --- a/system/Test/CIUnitTestCase.php +++ b/system/Test/CIUnitTestCase.php @@ -124,7 +124,7 @@ abstract class CIUnitTestCase extends TestCase * * @var string */ - protected $basePath = SUPPORTPATH . 'Database'; + protected $basePath = TESTPATH . '_support/Database'; /** * The namespace(s) to help us find the migration classes. From 5a65ee3b32227c74edb2327b6c48b61998327169 Mon Sep 17 00:00:00 2001 From: Kamil Tekiela Date: Sun, 18 Jan 2026 08:31:50 +0000 Subject: [PATCH 17/29] refactor: Remove dead code from MySQLi Connection related to PHP 5 (#9887) * Remove dead code * Update PHPStan baselines --- system/Database/MySQLi/Connection.php | 15 --------------- utils/phpstan-baseline/empty.notAllowed.neon | 4 ++-- utils/phpstan-baseline/loader.neon | 2 +- 3 files changed, 3 insertions(+), 18 deletions(-) diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index a1c0dcce8479..1a79a4d8ef41 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -207,21 +207,6 @@ public function connect(bool $persistent = false) $socket, $clientFlags, )) { - // Prior to version 5.7.3, MySQL silently downgrades to an unencrypted connection if SSL setup fails - if (($clientFlags & MYSQLI_CLIENT_SSL) !== 0 && version_compare($this->mysqli->client_info, 'mysqlnd 5.7.3', '<=') - && empty($this->mysqli->query("SHOW STATUS LIKE 'ssl_cipher'")->fetch_object()->Value) - ) { - $this->mysqli->close(); - $message = 'MySQLi was configured for an SSL connection, but got an unencrypted connection instead!'; - log_message('error', $message); - - if ($this->DBDebug) { - throw new DatabaseException($message); - } - - return false; - } - if (! $this->mysqli->set_charset($this->charset)) { log_message('error', "Database: Unable to set the configured connection charset ('{$this->charset}')."); diff --git a/utils/phpstan-baseline/empty.notAllowed.neon b/utils/phpstan-baseline/empty.notAllowed.neon index faf902d5ec10..a1a41e6588a9 100644 --- a/utils/phpstan-baseline/empty.notAllowed.neon +++ b/utils/phpstan-baseline/empty.notAllowed.neon @@ -1,4 +1,4 @@ -# total 237 errors +# total 236 errors parameters: ignoreErrors: @@ -74,7 +74,7 @@ parameters: - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 11 + count: 10 path: ../../system/Database/MySQLi/Connection.php - diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 4aee22070d3e..a0191547e773 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 2772 errors +# total 2771 errors includes: - argument.type.neon From 4ac1ab725602523c1eead41f2138e1f23f8a0a85 Mon Sep 17 00:00:00 2001 From: Kamil Tekiela Date: Sun, 18 Jan 2026 08:32:24 +0000 Subject: [PATCH 18/29] Clean up mysqli transactions (#9888) --- system/Database/MySQLi/Connection.php | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index 1a79a4d8ef41..f5700014cfb5 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -610,8 +610,6 @@ public function insertID(): int */ protected function _transBegin(): bool { - $this->connID->autocommit(false); - return $this->connID->begin_transaction(); } @@ -620,13 +618,7 @@ protected function _transBegin(): bool */ protected function _transCommit(): bool { - if ($this->connID->commit()) { - $this->connID->autocommit(true); - - return true; - } - - return false; + return $this->connID->commit(); } /** @@ -634,12 +626,6 @@ protected function _transCommit(): bool */ protected function _transRollback(): bool { - if ($this->connID->rollback()) { - $this->connID->autocommit(true); - - return true; - } - - return false; + return $this->connID->rollback(); } } From 04e12d1e00633bbd3c15eda7f1237603dd4a0903 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Sun, 18 Jan 2026 09:33:32 +0100 Subject: [PATCH 19/29] fix: make seeder to respect database group (#9886) --- system/Database/Seeder.php | 18 +++-- .../Database/Seeds/SeederWithDBGroup.php | 51 ++++++++++++++ .../Database/Seeds/SeederWithoutDBGroup.php | 49 +++++++++++++ tests/system/Database/DatabaseSeederTest.php | 68 ++++++++++++++++++- user_guide_src/source/changelogs/v4.6.5.rst | 2 + user_guide_src/source/dbmgmt/seeds.rst | 23 +++++++ user_guide_src/source/dbmgmt/seeds/005.php | 4 ++ user_guide_src/source/dbmgmt/seeds/006.php | 15 ++++ 8 files changed, 223 insertions(+), 7 deletions(-) create mode 100644 tests/_support/Database/Seeds/SeederWithDBGroup.php create mode 100644 tests/_support/Database/Seeds/SeederWithoutDBGroup.php create mode 100644 user_guide_src/source/dbmgmt/seeds/005.php create mode 100644 user_guide_src/source/dbmgmt/seeds/006.php diff --git a/system/Database/Seeder.php b/system/Database/Seeder.php index aacc8e9905ce..632008176031 100644 --- a/system/Database/Seeder.php +++ b/system/Database/Seeder.php @@ -27,7 +27,7 @@ class Seeder /** * The name of the database group to use. * - * @var non-empty-string + * @var non-empty-string|null */ protected $DBGroup; @@ -92,10 +92,16 @@ public function __construct(Database $config, ?BaseConnection $db = null) $this->config = &$config; - $db ??= Database::connect($this->DBGroup); - - $this->db = $db; - $this->forge = Database::forge($this->DBGroup); + if (isset($this->DBGroup)) { + $this->db = Database::connect($this->DBGroup); + $this->forge = Database::forge($this->DBGroup); + } elseif ($db instanceof BaseConnection) { + $this->db = $db; + $this->forge = Database::forge($db); + } else { + $this->db = Database::connect($config->defaultGroup); + $this->forge = Database::forge($config->defaultGroup); + } } /** @@ -145,7 +151,7 @@ public function call(string $class) } /** @var Seeder $seeder */ - $seeder = new $class($this->config); + $seeder = new $class($this->config, $this->db); $seeder->setSilent($this->silent)->run(); unset($seeder); diff --git a/tests/_support/Database/Seeds/SeederWithDBGroup.php b/tests/_support/Database/Seeds/SeederWithDBGroup.php new file mode 100644 index 000000000000..d25fb657bebf --- /dev/null +++ b/tests/_support/Database/Seeds/SeederWithDBGroup.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Tests\Support\Database\Seeds; + +use CodeIgniter\Database\BaseConnection; +use CodeIgniter\Database\Seeder; + +/** + * Test seeder with explicit DBGroup set. + */ +class SeederWithDBGroup extends Seeder +{ + protected $DBGroup = 'tests'; + + /** + * Store the connection used during run() for testing. + */ + public static ?BaseConnection $lastConnection = null; + + public function run(): void + { + self::$lastConnection = $this->db; + } + + /** + * Expose the db connection for testing. + */ + public function getDatabase(): BaseConnection + { + return $this->db; + } + + /** + * Reset static state for testing. + */ + public static function reset(): void + { + self::$lastConnection = null; + } +} diff --git a/tests/_support/Database/Seeds/SeederWithoutDBGroup.php b/tests/_support/Database/Seeds/SeederWithoutDBGroup.php new file mode 100644 index 000000000000..31ff72598eaf --- /dev/null +++ b/tests/_support/Database/Seeds/SeederWithoutDBGroup.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Tests\Support\Database\Seeds; + +use CodeIgniter\Database\BaseConnection; +use CodeIgniter\Database\Seeder; + +/** + * Test seeder without DBGroup set (should inherit connection). + */ +class SeederWithoutDBGroup extends Seeder +{ + /** + * Store the connection used during run() for testing. + */ + public static ?BaseConnection $lastConnection = null; + + public function run(): void + { + self::$lastConnection = $this->db; + } + + /** + * Expose the db connection for testing. + */ + public function getDatabase(): BaseConnection + { + return $this->db; + } + + /** + * Reset static state for testing. + */ + public static function reset(): void + { + self::$lastConnection = null; + } +} diff --git a/tests/system/Database/DatabaseSeederTest.php b/tests/system/Database/DatabaseSeederTest.php index 398052636246..e00599e26507 100644 --- a/tests/system/Database/DatabaseSeederTest.php +++ b/tests/system/Database/DatabaseSeederTest.php @@ -17,13 +17,23 @@ use Config\Database; use Faker\Generator; use PHPUnit\Framework\Attributes\Group; +use Tests\Support\Database\Seeds\SeederWithDBGroup; +use Tests\Support\Database\Seeds\SeederWithoutDBGroup; /** * @internal */ -#[Group('Others')] +#[Group('DatabaseLive')] final class DatabaseSeederTest extends CIUnitTestCase { + protected function tearDown(): void + { + parent::tearDown(); + + SeederWithDBGroup::reset(); + SeederWithoutDBGroup::reset(); + } + public function testInstantiateNoSeedPath(): void { $this->expectException('InvalidArgumentException'); @@ -57,4 +67,60 @@ public function testCallOnEmptySeeder(): void $seeder = new Seeder(new Database()); $seeder->call(''); } + + public function testSeederWithDBGroupUsesOwnConnection(): void + { + $config = new Database(); + $db = Database::connect('tests', false); + + $seeder = new SeederWithDBGroup($config, $db); + + $testsDb = Database::connect('tests'); + $this->assertSame($testsDb, $seeder->getDatabase()); + $this->assertNotSame($db, $seeder->getDatabase()); + } + + public function testSeederWithoutDBGroupUsesPassedConnection(): void + { + $config = new Database(); + $db = Database::connect('tests'); + + $seeder = new SeederWithoutDBGroup($config, $db); + + $this->assertSame($db, $seeder->getDatabase()); + } + + public function testSeederWithoutDBGroupAndNoConnectionUsesDefault(): void + { + $config = new Database(); + + $seeder = new SeederWithoutDBGroup($config); + + $defaultDb = Database::connect($config->defaultGroup); + $this->assertSame($defaultDb, $seeder->getDatabase()); + } + + public function testCallPassesConnectionToChildSeeder(): void + { + $config = new Database(); + $db = Database::connect('tests'); + + $seeder = new Seeder($config, $db); + $seeder->setSilent(true)->call(SeederWithoutDBGroup::class); + + $this->assertSame($db, SeederWithoutDBGroup::$lastConnection); + } + + public function testCallChildWithDBGroupUsesOwnConnection(): void + { + $config = new Database(); + $db = Database::connect('tests', false); + + $seeder = new Seeder($config, $db); + $seeder->setSilent(true)->call(SeederWithDBGroup::class); + + $testsDb = Database::connect('tests'); + $this->assertSame($testsDb, SeederWithDBGroup::$lastConnection); + $this->assertNotSame($db, SeederWithDBGroup::$lastConnection); + } } diff --git a/user_guide_src/source/changelogs/v4.6.5.rst b/user_guide_src/source/changelogs/v4.6.5.rst index 0ae6d47fe1be..98f3558dee92 100644 --- a/user_guide_src/source/changelogs/v4.6.5.rst +++ b/user_guide_src/source/changelogs/v4.6.5.rst @@ -30,6 +30,8 @@ Deprecations Bugs Fixed ********** +- **Database:** Fixed a bug where ``Seeder::call()`` did not pass the database connection to child seeders, causing them to use the default connection instead of the one specified via ``Database::seeder('group')``. + See the repo's `CHANGELOG.md `_ for a complete list of bugs fixed. diff --git a/user_guide_src/source/dbmgmt/seeds.rst b/user_guide_src/source/dbmgmt/seeds.rst index ded3836f36cd..dce9aa7cb6fc 100644 --- a/user_guide_src/source/dbmgmt/seeds.rst +++ b/user_guide_src/source/dbmgmt/seeds.rst @@ -44,6 +44,29 @@ You can grab a copy of the main seeder through the database config class: .. literalinclude:: seeds/004.php +Using a Different Database Group +================================ + +You can specify a different database group when obtaining a seeder instance by passing the group name +as the first parameter: + +.. literalinclude:: seeds/005.php + +When using ``call()`` to run child seeders, the database connection is automatically passed to them. +This means child seeders will use the same connection as the parent seeder, unless they explicitly +specify their own ``$DBGroup`` property. + +If a seeder needs to always use a specific database group regardless of the parent seeder's connection, +you can set the ``$DBGroup`` property in the seeder class: + +.. literalinclude:: seeds/006.php + +The connection priority is: + +1. If ``$DBGroup`` is set in the seeder class, that connection group is always used +2. Otherwise, if a connection was passed (from parent seeder via ``call()`` or from ``Database::seeder()``), it is used +3. Otherwise, the default connection group is used + Command Line Seeding ==================== diff --git a/user_guide_src/source/dbmgmt/seeds/005.php b/user_guide_src/source/dbmgmt/seeds/005.php new file mode 100644 index 000000000000..e709278fc4be --- /dev/null +++ b/user_guide_src/source/dbmgmt/seeds/005.php @@ -0,0 +1,4 @@ +call('TestSeeder'); diff --git a/user_guide_src/source/dbmgmt/seeds/006.php b/user_guide_src/source/dbmgmt/seeds/006.php new file mode 100644 index 000000000000..2ed2044c93c8 --- /dev/null +++ b/user_guide_src/source/dbmgmt/seeds/006.php @@ -0,0 +1,15 @@ + Date: Mon, 19 Jan 2026 23:10:05 +0800 Subject: [PATCH 20/29] tests: add sanity checks to some Redis tests (#9894) --- .../Handlers/AbstractHandlerTestCase.php | 2 +- .../Cache/Handlers/PredisHandlerTest.php | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/system/Cache/Handlers/AbstractHandlerTestCase.php b/tests/system/Cache/Handlers/AbstractHandlerTestCase.php index 5732663f99f9..c0343ff06da0 100644 --- a/tests/system/Cache/Handlers/AbstractHandlerTestCase.php +++ b/tests/system/Cache/Handlers/AbstractHandlerTestCase.php @@ -38,6 +38,7 @@ public function testGetMetaData(): void $this->handler->save(self::$key1, 'value'); $actual = $this->handler->getMetaData(self::$key1); + $this->assertIsArray($actual); // This test is time-dependent, and depending on the timing, // seconds in `$time` (e.g. 12:00:00.9999) and seconds of @@ -45,7 +46,6 @@ public function testGetMetaData(): void // may be off by one second. In that case, the following calculation // will result in maximum of (60 + 1). $this->assertLessThanOrEqual(60 + 1, $actual['expire'] - $time); - $this->assertLessThanOrEqual(1, $actual['mtime'] - $time); $this->assertSame('value', $actual['data']); } diff --git a/tests/system/Cache/Handlers/PredisHandlerTest.php b/tests/system/Cache/Handlers/PredisHandlerTest.php index 96857fc6a9f2..a906457b5c97 100644 --- a/tests/system/Cache/Handlers/PredisHandlerTest.php +++ b/tests/system/Cache/Handlers/PredisHandlerTest.php @@ -107,11 +107,12 @@ public function testSave(): void public function testSavePermanent(): void { $this->assertTrue($this->handler->save(self::$key1, 'value', 0)); - $metaData = $this->handler->getMetaData(self::$key1); - $this->assertNull($metaData['expire']); - $this->assertLessThanOrEqual(1, $metaData['mtime'] - Time::now()->getTimestamp()); - $this->assertSame('value', $metaData['data']); + $metadata = $this->handler->getMetaData(self::$key1); + $this->assertIsArray($metadata); + $this->assertNull($metadata['expire']); + $this->assertLessThanOrEqual(1, $metadata['mtime'] - Time::now()->getTimestamp()); + $this->assertSame('value', $metadata['data']); $this->assertTrue($this->handler->delete(self::$key1)); } @@ -131,8 +132,11 @@ public function testDeleteMatchingPrefix(): void $this->handler->save('key_' . $i, 'value' . $i); } + $cacheInfo = $this->handler->getCacheInfo(); + $this->assertIsArray($cacheInfo); + // check that there are 101 items is cache store - $this->assertSame('101', $this->handler->getCacheInfo()['Keyspace']['db0']['keys']); + $this->assertSame('101', $cacheInfo['Keyspace']['db0']['keys']); // Checking that given the prefix "key_1", deleteMatching deletes 13 keys: // (key_1, key_10, key_11, key_12, key_13, key_14, key_15, key_16, key_17, key_18, key_19, key_100, key_101) @@ -149,8 +153,11 @@ public function testDeleteMatchingSuffix(): void $this->handler->save('key_' . $i, 'value' . $i); } + $cacheInfo = $this->handler->getCacheInfo(); + $this->assertIsArray($cacheInfo); + // check that there are 101 items is cache store - $this->assertSame('101', $this->handler->getCacheInfo()['Keyspace']['db0']['keys']); + $this->assertSame('101', $cacheInfo['Keyspace']['db0']['keys']); // Checking that given the suffix "1", deleteMatching deletes 11 keys: // (key_1, key_11, key_21, key_31, key_41, key_51, key_61, key_71, key_81, key_91, key_101) From 02ea63c57e2b091b5a4ff663e9ea9b8649ec077f Mon Sep 17 00:00:00 2001 From: michalsn Date: Tue, 20 Jan 2026 08:17:47 +0100 Subject: [PATCH 21/29] chore: update rector/rector requirement from 2.3.1 to 2.3.2 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index aaa0b9255ab5..8d8010e5f8fe 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.3.1", + "rector/rector": "2.3.2", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { From 2768f810b05007c618b1250820c9c92c55b759fa Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 21 Jan 2026 12:56:47 +0700 Subject: [PATCH 22/29] chore: pin phpstan to 2.1.34 (#9897) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 8d8010e5f8fe..2662cd2a7671 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "mikey179/vfsstream": "^1.6.12", "nexusphp/tachycardia": "^2.0", "phpstan/extension-installer": "^1.4", - "phpstan/phpstan": "^2.0", + "phpstan/phpstan": "2.1.34", "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpcov": "^9.0.2 || ^10.0", "phpunit/phpunit": "^10.5.16 || ^11.2", From f749c9e55d8d264c432e5fd15d4a9e69b5b735e8 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 21 Jan 2026 23:16:08 +0700 Subject: [PATCH 23/29] chore: Bump PHPStan to ^2.1.36 and Rector to 2.3.4 (#9898) --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 2662cd2a7671..8e4d07be2ca4 100644 --- a/composer.json +++ b/composer.json @@ -23,12 +23,12 @@ "mikey179/vfsstream": "^1.6.12", "nexusphp/tachycardia": "^2.0", "phpstan/extension-installer": "^1.4", - "phpstan/phpstan": "2.1.34", + "phpstan/phpstan": "^2.1.36", "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpcov": "^9.0.2 || ^10.0", "phpunit/phpunit": "^10.5.16 || ^11.2", "predis/predis": "^3.0", - "rector/rector": "2.3.2", + "rector/rector": "2.3.4", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { From 3c8a61a9618a872d7a58405f5649653eaea7aaea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:22:29 +0000 Subject: [PATCH 24/29] chore(deps-dev): update rector/rector requirement from 2.3.4 to 2.3.5 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.3.4...2.3.5) --- updated-dependencies: - dependency-name: rector/rector dependency-version: 2.3.5 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 8e4d07be2ca4..f2fb1a6deebf 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.3.4", + "rector/rector": "2.3.5", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { From 4cdbee1a6489e380726fe8bb00cbdf29ee5fb849 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Sun, 1 Feb 2026 21:48:25 +0800 Subject: [PATCH 25/29] fix: ensure CSP nonces are Base64 encoded (#9907) --- system/HTTP/ContentSecurityPolicy.php | 4 ++-- tests/system/CommonFunctionsTest.php | 4 ++-- tests/system/HTTP/ContentSecurityPolicyTest.php | 14 ++++++++------ tests/system/Helpers/HTMLHelperTest.php | 2 +- tests/system/Helpers/URLHelper/MiscUrlTest.php | 2 +- tests/system/Honeypot/HoneypotTest.php | 2 +- tests/system/View/ParserPluginTest.php | 2 +- user_guide_src/source/changelogs/v4.6.5.rst | 1 + 8 files changed, 17 insertions(+), 14 deletions(-) diff --git a/system/HTTP/ContentSecurityPolicy.php b/system/HTTP/ContentSecurityPolicy.php index a6a2b26a71fc..4767face673e 100644 --- a/system/HTTP/ContentSecurityPolicy.php +++ b/system/HTTP/ContentSecurityPolicy.php @@ -303,7 +303,7 @@ public function enabled(): bool public function getStyleNonce(): string { if ($this->styleNonce === null) { - $this->styleNonce = bin2hex(random_bytes(12)); + $this->styleNonce = base64_encode(random_bytes(12)); $this->styleSrc[] = 'nonce-' . $this->styleNonce; } @@ -316,7 +316,7 @@ public function getStyleNonce(): string public function getScriptNonce(): string { if ($this->scriptNonce === null) { - $this->scriptNonce = bin2hex(random_bytes(12)); + $this->scriptNonce = base64_encode(random_bytes(12)); $this->scriptSrc[] = 'nonce-' . $this->scriptNonce; } diff --git a/tests/system/CommonFunctionsTest.php b/tests/system/CommonFunctionsTest.php index bc09ef6e9f57..d84378e01dc9 100644 --- a/tests/system/CommonFunctionsTest.php +++ b/tests/system/CommonFunctionsTest.php @@ -731,7 +731,7 @@ public function testDWithCSP(): void $cliDetection = Kint::$cli_detection; Kint::$cli_detection = false; - $this->expectOutputRegex('/