From ff4714946d8e956baf4bb7ee4d9604b7c9f3658d Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Wed, 25 Mar 2026 03:06:01 +0800 Subject: [PATCH 01/85] docs: add changelog and upgrade for v4.7.3 (#10068) --- system/CodeIgniter.php | 2 +- user_guide_src/source/changelogs/index.rst | 1 + user_guide_src/source/changelogs/v4.7.3.rst | 35 ++++++++++++ .../source/installation/upgrade_473.rst | 55 +++++++++++++++++++ .../source/installation/upgrading.rst | 1 + 5 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 user_guide_src/source/changelogs/v4.7.3.rst create mode 100644 user_guide_src/source/installation/upgrade_473.rst diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 3e60ebdfb49d..dca31618dd6f 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -55,7 +55,7 @@ class CodeIgniter /** * The current version of CodeIgniter Framework */ - public const CI_VERSION = '4.7.2'; + public const CI_VERSION = '4.7.3-dev'; /** * App startup time. diff --git a/user_guide_src/source/changelogs/index.rst b/user_guide_src/source/changelogs/index.rst index ca80566db7b3..b47fe1a7ffff 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.7.3 v4.7.2 v4.7.1 v4.7.0 diff --git a/user_guide_src/source/changelogs/v4.7.3.rst b/user_guide_src/source/changelogs/v4.7.3.rst new file mode 100644 index 000000000000..b0b0bf1b8c2d --- /dev/null +++ b/user_guide_src/source/changelogs/v4.7.3.rst @@ -0,0 +1,35 @@ +############# +Version 4.7.3 +############# + +Release Date: Unreleased + +**4.7.3 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_473.rst b/user_guide_src/source/installation/upgrade_473.rst new file mode 100644 index 000000000000..3a28c7db6824 --- /dev/null +++ b/user_guide_src/source/installation/upgrade_473.rst @@ -0,0 +1,55 @@ +############################# +Upgrading from 4.7.2 to 4.7.3 +############################# + +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 48daf40f482b..64dd632b1d1d 100644 --- a/user_guide_src/source/installation/upgrading.rst +++ b/user_guide_src/source/installation/upgrading.rst @@ -22,6 +22,7 @@ Alternatively, replace it with a new file and add your previous lines. backward_compatibility_notes + upgrade_473 upgrade_472 upgrade_471 upgrade_470 From 27a2d7291fb70fda4fc2dcb31d2c5948f101443b Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Wed, 25 Mar 2026 03:06:33 +0800 Subject: [PATCH 02/85] chore: migrate SCSS from deprecated `@import` usage (#10066) --- .github/workflows/test-scss.yml | 3 +- admin/css/debug-toolbar/_theme-dark.scss | 66 +++++++++++------------ admin/css/debug-toolbar/_theme-light.scss | 64 +++++++++++----------- admin/css/debug-toolbar/toolbar.scss | 39 +++++++------- system/Debug/Toolbar/Views/toolbar.css | 21 ++++++++ 5 files changed, 107 insertions(+), 86 deletions(-) diff --git a/.github/workflows/test-scss.yml b/.github/workflows/test-scss.yml index b1227e35e0d0..c544d51d3b48 100644 --- a/.github/workflows/test-scss.yml +++ b/.github/workflows/test-scss.yml @@ -38,8 +38,7 @@ jobs: - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: - # node version based on dart-sass test workflow - node-version: 16 + node-version: '24' - name: Install Dart Sass run: | diff --git a/admin/css/debug-toolbar/_theme-dark.scss b/admin/css/debug-toolbar/_theme-dark.scss index ead7d02a58e3..83176023a87e 100644 --- a/admin/css/debug-toolbar/_theme-dark.scss +++ b/admin/css/debug-toolbar/_theme-dark.scss @@ -2,23 +2,23 @@ // ========================================================================== */ // The "box-shadow" mixin uses colors -@import '_mixins'; +@use '_mixins'; // Graphic charter -@import '_graphic-charter'; +@use '_graphic-charter'; // DEBUG ICON // ========================================================================== */ #debug-icon { - background-color: $t-dark; - @include box-shadow(0, 0, 4px, $m-gray); + background-color: graphic-charter.$t-dark; + @include mixins.box-shadow(0, 0, 4px, graphic-charter.$m-gray); a:active, a:link, a:visited { - color: $g-orange; + color: graphic-charter.$g-orange; } } @@ -27,8 +27,8 @@ // ========================================================================== */ #debug-bar { - background-color: $t-dark; - color: $m-gray; + background-color: graphic-charter.$t-dark; + color: graphic-charter.$m-gray; // Reset to prevent conflict with other CSS files h1, @@ -44,35 +44,35 @@ button, .toolbar { background-color: transparent; - color: $m-gray; + color: graphic-charter.$m-gray; } // Buttons button { - background-color: $t-dark; + background-color: graphic-charter.$t-dark; } // Tables table { strong { - color: $g-orange; + color: graphic-charter.$g-orange; } tbody tr { &:hover { - background-color: $g-gray; + background-color: graphic-charter.$g-gray; } &.current { - background-color: $m-orange; + background-color: graphic-charter.$m-orange; td { - color: $t-dark; + color: graphic-charter.$t-dark; } &:hover td { - background-color: $g-red; - color: $t-light; + background-color: graphic-charter.$g-red; + color: graphic-charter.$t-light; } } } @@ -80,8 +80,8 @@ // The toolbar .toolbar { - background-color: $g-gray; - @include box-shadow(0, 0, 4px, $g-gray); + background-color: graphic-charter.$g-gray; + @include mixins.box-shadow(0, 0, 4px, graphic-charter.$g-gray); img { filter: brightness(0) invert(1); @@ -91,24 +91,24 @@ // Fixed top &.fixed-top { .toolbar { - @include box-shadow(0, 0, 4px, $g-gray); + @include mixins.box-shadow(0, 0, 4px, graphic-charter.$g-gray); } .tab { - @include box-shadow(0, 1px, 4px, $g-gray); + @include mixins.box-shadow(0, 1px, 4px, graphic-charter.$g-gray); } } // "Muted" elements .muted { - color: $m-gray; + color: graphic-charter.$m-gray; td { - color: $g-gray; + color: graphic-charter.$g-gray; } &:hover td { - color: $m-gray; + color: graphic-charter.$m-gray; } } @@ -121,34 +121,34 @@ // The toolbar menus .ci-label { &.active { - background-color: $t-dark; + background-color: graphic-charter.$t-dark; } &:hover { - background-color: $t-dark; + background-color: graphic-charter.$t-dark; } .badge { - background-color: $g-red; - color: $t-light; + background-color: graphic-charter.$g-red; + color: graphic-charter.$t-light; } } // The tabs container .tab { - background-color: $t-dark; - @include box-shadow(0, -1px, 4px, $g-gray); + background-color: graphic-charter.$t-dark; + @include mixins.box-shadow(0, -1px, 4px, graphic-charter.$g-gray); } // The "Timeline" tab .timeline { th, td { - border-color: $g-gray; + border-color: graphic-charter.$g-gray; } .timer { - background-color: $g-orange; + background-color: graphic-charter.$g-orange; } } } @@ -158,10 +158,10 @@ // ========================================================================== */ .debug-view.show-view { - border-color: $g-orange; + border-color: graphic-charter.$g-orange; } .debug-view-path { - background-color: $m-orange; - color: $g-gray; + background-color: graphic-charter.$m-orange; + color: graphic-charter.$g-gray; } diff --git a/admin/css/debug-toolbar/_theme-light.scss b/admin/css/debug-toolbar/_theme-light.scss index 4e4295ccd131..9aa0a5a2c9b4 100644 --- a/admin/css/debug-toolbar/_theme-light.scss +++ b/admin/css/debug-toolbar/_theme-light.scss @@ -2,23 +2,23 @@ // ========================================================================== */ // The "box-shadow" mixin uses colors -@import '_mixins'; +@use '_mixins'; // Graphic charter -@import '_graphic-charter'; +@use '_graphic-charter'; // DEBUG ICON // ========================================================================== */ #debug-icon { - background-color: $t-light; - @include box-shadow(0, 0, 4px, $m-gray); + background-color: graphic-charter.$t-light; + @include mixins.box-shadow(0, 0, 4px, graphic-charter.$m-gray); a:active, a:link, a:visited { - color: $g-orange; + color: graphic-charter.$g-orange; } } @@ -27,8 +27,8 @@ // ========================================================================== */ #debug-bar { - background-color: $t-light; - color: $g-gray; + background-color: graphic-charter.$t-light; + color: graphic-charter.$g-gray; // Reset to prevent conflict with other CSS files h1, @@ -44,31 +44,31 @@ button, .toolbar { background-color: transparent; - color: $g-gray; + color: graphic-charter.$g-gray; } // Buttons button { - background-color: $t-light; + background-color: graphic-charter.$t-light; } // Tables table { strong { - color: $g-orange; + color: graphic-charter.$g-orange; } tbody tr { &:hover { - background-color: $m-gray; + background-color: graphic-charter.$m-gray; } &.current { - background-color: $m-orange; + background-color: graphic-charter.$m-orange; &:hover td { - background-color: $g-red; - color: $t-light; + background-color: graphic-charter.$g-red; + color: graphic-charter.$t-light; } } } @@ -76,8 +76,8 @@ // The toolbar .toolbar { - background-color: $t-light; - @include box-shadow(0, 0, 4px, $m-gray); + background-color: graphic-charter.$t-light; + @include mixins.box-shadow(0, 0, 4px, graphic-charter.$m-gray); img { filter: brightness(0) invert(0.4); @@ -87,24 +87,24 @@ // Fixed top &.fixed-top { .toolbar { - @include box-shadow(0, 0, 4px, $m-gray); + @include mixins.box-shadow(0, 0, 4px, graphic-charter.$m-gray); } .tab { - @include box-shadow(0, 1px, 4px, $m-gray); + @include mixins.box-shadow(0, 1px, 4px, graphic-charter.$m-gray); } } // "Muted" elements .muted { - color: $g-gray; + color: graphic-charter.$g-gray; td { - color: $m-gray; + color: graphic-charter.$m-gray; } &:hover td { - color: $g-gray; + color: graphic-charter.$g-gray; } } @@ -117,34 +117,34 @@ // The toolbar menus .ci-label { &.active { - background-color: $m-gray; + background-color: graphic-charter.$m-gray; } &:hover { - background-color: $m-gray; + background-color: graphic-charter.$m-gray; } .badge { - background-color: $g-red; - color: $t-light; + background-color: graphic-charter.$g-red; + color: graphic-charter.$t-light; } } // The tabs container .tab { - background-color: $t-light; - @include box-shadow(0, -1px, 4px, $m-gray); + background-color: graphic-charter.$t-light; + @include mixins.box-shadow(0, -1px, 4px, graphic-charter.$m-gray); } // The "Timeline" tab .timeline { th, td { - border-color: $m-gray; + border-color: graphic-charter.$m-gray; } .timer { - background-color: $g-orange; + background-color: graphic-charter.$g-orange; } } } @@ -154,10 +154,10 @@ // ========================================================================== */ .debug-view.show-view { - border-color: $g-orange; + border-color: graphic-charter.$g-orange; } .debug-view-path { - background-color: $m-orange; - color: $g-gray; + background-color: graphic-charter.$m-orange; + color: graphic-charter.$g-gray; } diff --git a/admin/css/debug-toolbar/toolbar.scss b/admin/css/debug-toolbar/toolbar.scss index 65f4b802e22c..767c908bf176 100644 --- a/admin/css/debug-toolbar/toolbar.scss +++ b/admin/css/debug-toolbar/toolbar.scss @@ -10,8 +10,9 @@ // IMPORTS // ========================================================================== */ -@import '_mixins'; -@import '_settings'; +@use "sass:meta"; +@use '_mixins'; +@use '_settings'; // DEBUG ICON // ========================================================================== */ @@ -76,8 +77,8 @@ line-height: 36px; // Typography - font-family: $base-font; - font-size: $base-size; + font-family: settings.$base-font; + font-size: settings.$base-size; font-weight: 400; // General elements @@ -86,7 +87,7 @@ font-weight: normal; margin: 0 0 0 auto; padding: 0; - font-family: $base-font; + font-family: settings.$base-font; svg { width: 16px; @@ -96,7 +97,7 @@ h2 { font-weight: bold; - font-size: $base-size; + font-size: settings.$base-size; margin: 0; padding: 5px 0 10px 0; @@ -106,7 +107,7 @@ } h3 { - font-size: $base-size - 4; + font-size: settings.$base-size - 4; font-weight: 200; margin: 0 0 0 10px; padding: 0; @@ -114,7 +115,7 @@ } p { - font-size: $base-size - 4; + font-size: settings.$base-size - 4; margin: 0 0 0 15px; padding: 0; } @@ -129,7 +130,7 @@ button { border: 1px solid; - @include border-radius(4px); + @include mixins.border-radius(4px); cursor: pointer; line-height: 15px; @@ -140,7 +141,7 @@ table { border-collapse: collapse; - font-size: $base-size - 2; + font-size: settings.$base-size - 2; line-height: normal; // Tables indentation @@ -255,7 +256,7 @@ // The toolbar menus .ci-label { display: inline-flex; - font-size: $base-size - 2; + font-size: settings.$base-size - 2; &:hover { cursor: pointer; @@ -278,7 +279,7 @@ // The toolbar notification badges .badge { - @include border-radius(12px); + @include mixins.border-radius(12px); display: inline-block; font-size: 75%; font-weight: bold; @@ -316,7 +317,7 @@ th { border-left: 1px solid; - font-size: $base-size - 4; + font-size: settings.$base-size - 4; font-weight: 200; padding: 5px 5px 10px 5px; position: relative; @@ -355,7 +356,7 @@ } .timer { - @include border-radius(4px); + @include mixins.border-radius(4px); display: inline-block; padding: 5px; position: absolute; @@ -428,7 +429,7 @@ .debug-view-path { font-family: monospace; - font-size: $base-size - 4; + font-size: settings.$base-size - 4; letter-spacing: normal; min-height: 16px; padding: 2px; @@ -487,16 +488,16 @@ // ========================================================================== */ // Default theme is "Light" -@import '_theme-light'; +@include meta.load-css('_theme-light'); // If the browser supports "prefers-color-scheme" and the scheme is "Dark" @media (prefers-color-scheme: dark) { - @import '_theme-dark'; + @include meta.load-css('_theme-dark'); } // If we force the "Dark" theme #toolbarContainer.dark { - @import '_theme-dark'; + @include meta.load-css('_theme-dark'); td[data-debugbar-route] input[type=text] { background: #000; @@ -506,7 +507,7 @@ // If we force the "Light" theme #toolbarContainer.light { - @import '_theme-light'; + @include meta.load-css('_theme-light'); } // LAYOUT HELPERS diff --git a/system/Debug/Toolbar/Views/toolbar.css b/system/Debug/Toolbar/Views/toolbar.css index abf61e9cca93..69583597eed7 100644 --- a/system/Debug/Toolbar/Views/toolbar.css +++ b/system/Debug/Toolbar/Views/toolbar.css @@ -358,6 +358,7 @@ -moz-box-shadow: 0 0 4px #DFDFDF; -webkit-box-shadow: 0 0 4px #DFDFDF; } + #debug-icon a:active, #debug-icon a:link, #debug-icon a:visited { @@ -368,6 +369,7 @@ background-color: #FFFFFF; color: #434343; } + #debug-bar h1, #debug-bar h2, #debug-bar h3, @@ -383,74 +385,93 @@ background-color: transparent; color: #434343; } + #debug-bar button { background-color: #FFFFFF; } + #debug-bar table strong { color: #DD8615; } + #debug-bar table tbody tr:hover { background-color: #DFDFDF; } + #debug-bar table tbody tr.current { background-color: #FDC894; } + #debug-bar table tbody tr.current:hover td { background-color: #DD4814; color: #FFFFFF; } + #debug-bar .toolbar { background-color: #FFFFFF; box-shadow: 0 0 4px #DFDFDF; -moz-box-shadow: 0 0 4px #DFDFDF; -webkit-box-shadow: 0 0 4px #DFDFDF; } + #debug-bar .toolbar img { filter: brightness(0) invert(0.4); } + #debug-bar.fixed-top .toolbar { box-shadow: 0 0 4px #DFDFDF; -moz-box-shadow: 0 0 4px #DFDFDF; -webkit-box-shadow: 0 0 4px #DFDFDF; } + #debug-bar.fixed-top .tab { box-shadow: 0 1px 4px #DFDFDF; -moz-box-shadow: 0 1px 4px #DFDFDF; -webkit-box-shadow: 0 1px 4px #DFDFDF; } + #debug-bar .muted { color: #434343; } + #debug-bar .muted td { color: #DFDFDF; } + #debug-bar .muted:hover td { color: #434343; } + #debug-bar #toolbar-position, #debug-bar #toolbar-theme { filter: brightness(0) invert(0.6); } + #debug-bar .ci-label.active { background-color: #DFDFDF; } + #debug-bar .ci-label:hover { background-color: #DFDFDF; } + #debug-bar .ci-label .badge { background-color: #DD4814; color: #FFFFFF; } + #debug-bar .tab { background-color: #FFFFFF; box-shadow: 0 -1px 4px #DFDFDF; -moz-box-shadow: 0 -1px 4px #DFDFDF; -webkit-box-shadow: 0 -1px 4px #DFDFDF; } + #debug-bar .timeline th, #debug-bar .timeline td { border-color: #DFDFDF; } + #debug-bar .timeline .timer { background-color: #DD8615; } From 02a3a794a43177f41f55f8c88b181af0b2c9ed4c Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Sun, 29 Mar 2026 11:27:50 +0200 Subject: [PATCH 03/85] docs: clarify `Model::find()` note for null argument (#10072) --- user_guide_src/source/models/model.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/models/model.rst b/user_guide_src/source/models/model.rst index f1383b45f803..76feb2b7c7c1 100644 --- a/user_guide_src/source/models/model.rst +++ b/user_guide_src/source/models/model.rst @@ -492,8 +492,8 @@ of just one: .. literalinclude:: model/007.php -.. note:: If no parameters are passed in, ``find()`` will return all rows in that model's table, - effectively acting like ``findAll()``, though less explicit. +.. note:: If ``find()`` is called without parameters or with ``null``, it will return all rows in + that model's table, effectively acting like ``findAll()``, though less explicit. findColumn() ------------ From ba098b022c8eff2f05a178477fc66c9140f2acd5 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sun, 29 Mar 2026 17:29:14 +0800 Subject: [PATCH 04/85] chore: upload as artifacts the debug files of failing random execution tests (#10074) --- .github/workflows/test-random-execution.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/test-random-execution.yml b/.github/workflows/test-random-execution.yml index 91ccfc174148..33156cb48157 100644 --- a/.github/workflows/test-random-execution.yml +++ b/.github/workflows/test-random-execution.yml @@ -223,3 +223,12 @@ jobs: env: DB: ${{ matrix.db-platform }} TERM: xterm-256color + + - name: Upload random-test artifacts on failure + if: ${{ always() && failure() }} + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: random-tests-${{ matrix.db-platform }}-php${{ matrix.php-version }} + path: build/random-tests/ + retention-days: 1 + overwrite: true From 4838c2aab96238afecf24068080a8b01521a0353 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sun, 29 Mar 2026 21:21:43 +0800 Subject: [PATCH 05/85] test: indicate components that already pass random execution tests (#10073) --- .github/scripts/random-tests-config.txt | 44 ++++++++++++------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/scripts/random-tests-config.txt b/.github/scripts/random-tests-config.txt index bb08ccef669d..b5e00d270e23 100644 --- a/.github/scripts/random-tests-config.txt +++ b/.github/scripts/random-tests-config.txt @@ -9,41 +9,41 @@ # Reference: https://github.com/codeigniter4/CodeIgniter4/issues/9968 API -# AutoReview -# Autoloader +AutoReview +Autoloader # Cache CLI # Commands # Config -# Cookie -# DataCaster +Cookie +DataCaster # DataConverter # Database -# Debug -# Email -# Encryption -# Entity -# Events -# Files +Debug +Email +Encryption +Entity +Events +Files # Filters -# Format +Format # HTTP # Helpers # Honeypot -# HotReloader -# I18n +HotReloader +I18n # Images -# Language -# Log +Language +Log # Models -# Pager -# Publisher -# RESTful +Pager +Publisher +RESTful # Router -# Security +Security # Session # Test -# Throttle -# Typography +Throttle +Typography # Validation -# View +View From 057908ae5aff55828756de69d6953b25dc0d5e81 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Mon, 30 Mar 2026 01:26:46 +0800 Subject: [PATCH 06/85] chore: fix wrong trigger name for manually runnable workflow (#10077) --- .github/workflows/test-random-execution.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-random-execution.yml b/.github/workflows/test-random-execution.yml index 33156cb48157..49c81330186e 100644 --- a/.github/workflows/test-random-execution.yml +++ b/.github/workflows/test-random-execution.yml @@ -25,7 +25,7 @@ on: - 'system/**.php' - 'tests/**.php' - workflow_call: + workflow_dispatch: inputs: quiet: description: Suppress debug output From 409c317820aab2115a33f2bfd66bd99ae2684357 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Mon, 30 Mar 2026 02:12:06 +0800 Subject: [PATCH 07/85] chore: upgrade to `gvenzl/oracle-free` (#10075) --- .github/workflows/reusable-phpunit-test.yml | 2 +- .github/workflows/test-random-execution.yml | 2 +- app/Config/Database.php | 2 +- tests/_support/Config/Registrar.php | 2 +- tests/system/Database/Live/ExecuteLogMessageFormatTest.php | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/reusable-phpunit-test.yml b/.github/workflows/reusable-phpunit-test.yml index c71ab575a53d..2f4d59a3a166 100644 --- a/.github/workflows/reusable-phpunit-test.yml +++ b/.github/workflows/reusable-phpunit-test.yml @@ -114,7 +114,7 @@ jobs: --health-retries=3 oracle: - image: gvenzl/oracle-xe:21 + image: gvenzl/oracle-free:latest env: ORACLE_RANDOM_PASSWORD: true APP_USER: ORACLE diff --git a/.github/workflows/test-random-execution.yml b/.github/workflows/test-random-execution.yml index 49c81330186e..d1ef81937284 100644 --- a/.github/workflows/test-random-execution.yml +++ b/.github/workflows/test-random-execution.yml @@ -120,7 +120,7 @@ jobs: --health-retries=3 oracle: - image: gvenzl/oracle-xe:21 + image: gvenzl/oracle-free:latest env: ORACLE_RANDOM_PASSWORD: true APP_USER: ORACLE diff --git a/app/Config/Database.php b/app/Config/Database.php index 29df3641adf7..060781ea18a3 100644 --- a/app/Config/Database.php +++ b/app/Config/Database.php @@ -140,7 +140,7 @@ class Database extends Config // * @var array // */ // public array $default = [ - // 'DSN' => 'localhost:1521/XEPDB1', + // 'DSN' => 'localhost:1521/FREEPDB1', // 'username' => 'root', // 'password' => 'root', // 'DBDriver' => 'OCI8', diff --git a/tests/_support/Config/Registrar.php b/tests/_support/Config/Registrar.php index d69d5c83e463..058fec440b55 100644 --- a/tests/_support/Config/Registrar.php +++ b/tests/_support/Config/Registrar.php @@ -105,7 +105,7 @@ class Registrar 'port' => 1433, ], 'OCI8' => [ - 'DSN' => 'localhost:1521/XEPDB1', + 'DSN' => 'localhost:1521/FREEPDB1', 'hostname' => '', 'username' => 'ORACLE', 'password' => 'ORACLE', diff --git a/tests/system/Database/Live/ExecuteLogMessageFormatTest.php b/tests/system/Database/Live/ExecuteLogMessageFormatTest.php index ca79e4f46fcc..9913a2da05c0 100644 --- a/tests/system/Database/Live/ExecuteLogMessageFormatTest.php +++ b/tests/system/Database/Live/ExecuteLogMessageFormatTest.php @@ -50,7 +50,7 @@ public function testLogMessageWhenExecuteFailsShowFullStructuredBacktrace(): voi 'MySQLi' => '/Table \'test\.some_table\' doesn\'t exist/', 'Postgre' => '/pg_query\(\): Query failed: ERROR: relation "some_table" does not exist/', 'SQLite3' => '/Unable to prepare statement:\s(\d+,\s)?no such table: some_table/', - 'OCI8' => '/oci_execute\(\): ORA-00942: table or view does not exist/', + 'OCI8' => '/oci_execute\(\): ORA-00942: table or view "ORACLE"\."SOME_TABLE" does not exist/', 'SQLSRV' => '/\[Microsoft\]\[ODBC Driver \d+ for SQL Server\]\[SQL Server\]Invalid object name \'some_table\'/', default => '/Unknown DB error/', }; From 747966ad6062bbe375c93bec5b6c7d462ea918b8 Mon Sep 17 00:00:00 2001 From: Toto Date: Tue, 31 Mar 2026 20:30:19 +0700 Subject: [PATCH 08/85] docs: fix formatting in Time library guide (#10078) --- user_guide_src/source/libraries/time.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/user_guide_src/source/libraries/time.rst b/user_guide_src/source/libraries/time.rst index 53b869b1b12a..d9df6f545c51 100644 --- a/user_guide_src/source/libraries/time.rst +++ b/user_guide_src/source/libraries/time.rst @@ -328,16 +328,16 @@ modify the existing Time instance, but will return a new instance. addCalendarMonths() / subCalendarMonths() ----------------------------------------- -Modifies the current Time by adding or subtracting whole calendar months. These methods can be useful if you -require no calendar months are skipped in recurring dates. Refer to the table below for a comparison between +Modifies the current Time by adding or subtracting whole calendar months. These methods can be useful if you +require no calendar months are skipped in recurring dates. Refer to the table below for a comparison between ``addMonths()`` and ``addCalendarMonths()`` for an initial date of ``2025-01-31``. -======= =========== =================== +======= =========== =================== $months addMonths() addCalendarMonths() ======= =========== =================== -1 2025-03-03 2025-02-28 -2 2025-03-31 2025-03-31 -3 2025-05-01 2025-04-30 +1 2025-03-03 2025-02-28 +2 2025-03-31 2025-03-31 +3 2025-05-01 2025-04-30 4 2025-05-31 2025-05-31 5 2025-07-01 2025-06-30 6 2025-07-31 2025-07-31 @@ -401,7 +401,7 @@ isPast() .. versionadded:: 4.7.0 Determines if the current instance's time is in the past, relative to "now". -It returns a boolean true/false:: +It returns a boolean true/false: .. literalinclude:: time/043.php @@ -413,7 +413,7 @@ isFuture() .. versionadded:: 4.7.0 Determines if the current instance's time is in the future, relative to "now". -It returns a boolean true/false:: +It returns a boolean true/false: .. literalinclude:: time/044.php From 8e46f1f67140cd5817f96119302e65d2968e8f75 Mon Sep 17 00:00:00 2001 From: Robson Jonathas <68930311+robsonjonathas@users.noreply.github.com> Date: Thu, 2 Apr 2026 12:59:52 -0300 Subject: [PATCH 09/85] docs: update 014.php (#10083) --- user_guide_src/source/guides/api/code/014.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/guides/api/code/014.php b/user_guide_src/source/guides/api/code/014.php index 730c138039ee..04899e30a5b9 100644 --- a/user_guide_src/source/guides/api/code/014.php +++ b/user_guide_src/source/guides/api/code/014.php @@ -9,7 +9,7 @@ class BookModel extends Model public function withAuthorInfo() { return $this - ->select('book.*, author.name as author_name') - ->join('author', 'book.author_id = author.id'); + ->select('books.*, authors.name as author_name') + ->join('authors', 'books.author_id = authors.id'); } } From fc737a5be420c10f98a9754f027f5d6284bafbc4 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Sat, 4 Apr 2026 09:56:30 +0200 Subject: [PATCH 10/85] chore: resolve PHPStan nullCoalesce and isset errors on Config properties (#10081) * chore: resolve PHPStan nullCoalesce and isset errors on Config properties * fix tests --- system/BaseModel.php | 2 +- system/Boot.php | 2 +- system/Cache/CacheFactory.php | 6 +--- system/Cache/Handlers/PredisHandler.php | 4 +-- system/CodeIgniter.php | 10 +++---- system/Commands/Encryption/GenerateKey.php | 4 +-- system/Commands/Utilities/Environment.php | 4 +-- system/Commands/Utilities/Routes.php | 2 +- .../Utilities/Routes/FilterFinder.php | 4 +-- system/Database/BaseBuilder.php | 14 ++++----- system/Database/MigrationRunner.php | 6 ++-- system/Database/SQLSRV/Builder.php | 6 ++-- system/Database/Seeder.php | 2 +- system/Debug/Toolbar.php | 4 +-- system/Encryption/Encryption.php | 4 +-- system/Filters/Filters.php | 18 ++++-------- system/Filters/PageCache.php | 2 +- system/Format/JSONFormatter.php | 2 +- system/HTTP/CURLRequest.php | 4 +-- system/Honeypot/Honeypot.php | 2 -- system/Log/Logger.php | 4 +-- system/Model.php | 2 +- system/Router/Router.php | 6 ++-- system/Session/Session.php | 4 +-- system/Typography/Typography.php | 2 +- system/View/View.php | 2 +- system/View/ViewDecoratorTrait.php | 3 +- tests/system/Honeypot/HoneypotTest.php | 29 +++++++------------ utils/phpstan-baseline/argument.type.neon | 7 +---- utils/phpstan-baseline/loader.neon | 2 +- 30 files changed, 65 insertions(+), 98 deletions(-) diff --git a/system/BaseModel.php b/system/BaseModel.php index b9d21d5a974c..d40a662724d9 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -653,7 +653,7 @@ public function findColumn(string $columnName) */ public function findAll(?int $limit = null, int $offset = 0) { - $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; + $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; // @phpstan-ignore nullCoalesce.property if ($limitZeroAsAll) { $limit ??= 0; } diff --git a/system/Boot.php b/system/Boot.php index 4e0c94ddd19e..d3b25895093b 100644 --- a/system/Boot.php +++ b/system/Boot.php @@ -205,7 +205,7 @@ public static function preload(Paths $paths): void protected static function loadDotEnv(Paths $paths): void { require_once $paths->systemDirectory . '/Config/DotEnv.php'; - $envDirectory = $paths->envDirectory ?? $paths->appDirectory . '/../'; + $envDirectory = $paths->envDirectory ?? $paths->appDirectory . '/../'; // @phpstan-ignore nullCoalesce.property (new DotEnv($envDirectory))->load(); } diff --git a/system/Cache/CacheFactory.php b/system/Cache/CacheFactory.php index e84254142125..b0347ffa2a93 100644 --- a/system/Cache/CacheFactory.php +++ b/system/Cache/CacheFactory.php @@ -49,14 +49,10 @@ class CacheFactory */ public static function getHandler(Cache $config, ?string $handler = null, ?string $backup = null) { - if (! isset($config->validHandlers) || $config->validHandlers === []) { + if ($config->validHandlers === []) { throw CacheException::forInvalidHandlers(); } - if (! isset($config->handler) || ! isset($config->backupHandler)) { - throw CacheException::forNoBackup(); - } - $handler ??= $config->handler; $backup ??= $config->backupHandler; diff --git a/system/Cache/Handlers/PredisHandler.php b/system/Cache/Handlers/PredisHandler.php index 94e03773de88..b1dfb0e18bfd 100644 --- a/system/Cache/Handlers/PredisHandler.php +++ b/system/Cache/Handlers/PredisHandler.php @@ -65,9 +65,7 @@ public function __construct(Cache $config) { $this->prefix = $config->prefix; - if (isset($config->redis)) { - $this->config = array_merge($this->config, $config->redis); - } + $this->config = array_merge($this->config, $config->redis); } public function initialize(): void diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index dca31618dd6f..da3a438d0e25 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -186,10 +186,10 @@ public function __construct(App $config) public function initialize() { // Set default locale on the server - Locale::setDefault($this->config->defaultLocale ?? 'en'); + Locale::setDefault($this->config->defaultLocale); // Set default timezone on the server - date_default_timezone_set($this->config->appTimezone ?? 'UTC'); + date_default_timezone_set($this->config->appTimezone); } /** @@ -452,7 +452,7 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache if ($routeFilters !== null) { $filters->enableFilters($routeFilters, 'before'); - $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; + $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; // @phpstan-ignore nullCoalesce.property if (! $oldFilterOrder) { $routeFilters = array_reverse($routeFilters); } @@ -521,7 +521,7 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache } // Execute controller attributes' after() methods AFTER framework filters - if ((config('Routing')->useControllerAttributes ?? true) === true) { + if ((config('Routing')->useControllerAttributes ?? true) === true) { // @phpstan-ignore nullCoalesce.property $this->benchmark->start('route_attributes_after'); $this->response = $this->router->executeAfterAttributes($this->request, $this->response); $this->benchmark->stop('route_attributes_after'); @@ -887,7 +887,7 @@ protected function startController() // Execute route attributes' before() methods // This runs after routing/validation but BEFORE expensive controller instantiation - if ((config('Routing')->useControllerAttributes ?? true) === true) { + if ((config('Routing')->useControllerAttributes ?? true) === true) { // @phpstan-ignore nullCoalesce.property $this->benchmark->start('route_attributes_before'); $attributeResponse = $this->router->executeBeforeAttributes($this->request); $this->benchmark->stop('route_attributes_before'); diff --git a/system/Commands/Encryption/GenerateKey.php b/system/Commands/Encryption/GenerateKey.php index b34b422f7bfe..7a0d47d0d542 100644 --- a/system/Commands/Encryption/GenerateKey.php +++ b/system/Commands/Encryption/GenerateKey.php @@ -102,7 +102,7 @@ public function run(array $params) // force DotEnv to reload the new env vars putenv('encryption.key'); unset($_ENV['encryption.key'], $_SERVER['encryption.key']); - $dotenv = new DotEnv((new Paths())->envDirectory ?? ROOTPATH); + $dotenv = new DotEnv((new Paths())->envDirectory ?? ROOTPATH); // @phpstan-ignore nullCoalesce.property $dotenv->load(); CLI::write('Application\'s new encryption key was successfully set.', 'green'); @@ -156,7 +156,7 @@ protected function confirmOverwrite(array $params): bool protected function writeNewEncryptionKeyToFile(string $oldKey, string $newKey): bool { $baseEnv = ROOTPATH . 'env'; - $envFile = ((new Paths())->envDirectory ?? ROOTPATH) . '.env'; + $envFile = ((new Paths())->envDirectory ?? ROOTPATH) . '.env'; // @phpstan-ignore nullCoalesce.property if (! is_file($envFile)) { if (! is_file($baseEnv)) { diff --git a/system/Commands/Utilities/Environment.php b/system/Commands/Utilities/Environment.php index 5ab4c98bde84..44df09ad3bee 100644 --- a/system/Commands/Utilities/Environment.php +++ b/system/Commands/Utilities/Environment.php @@ -121,7 +121,7 @@ public function run(array $params) putenv('CI_ENVIRONMENT'); unset($_ENV['CI_ENVIRONMENT']); service('superglobals')->unsetServer('CI_ENVIRONMENT'); - (new DotEnv((new Paths())->envDirectory ?? ROOTPATH))->load(); + (new DotEnv((new Paths())->envDirectory ?? ROOTPATH))->load(); // @phpstan-ignore nullCoalesce.property CLI::write(sprintf('Environment is successfully changed to "%s".', $env), 'green'); CLI::write('The ENVIRONMENT constant will be changed in the next script execution.'); @@ -136,7 +136,7 @@ public function run(array $params) private function writeNewEnvironmentToEnvFile(string $newEnv): bool { $baseEnv = ROOTPATH . 'env'; - $envFile = ((new Paths())->envDirectory ?? ROOTPATH) . '.env'; + $envFile = ((new Paths())->envDirectory ?? ROOTPATH) . '.env'; // @phpstan-ignore nullCoalesce.property if (! is_file($envFile)) { if (! is_file($baseEnv)) { diff --git a/system/Commands/Utilities/Routes.php b/system/Commands/Utilities/Routes.php index 4f51c3c29437..074a8b50f5cf 100644 --- a/system/Commands/Utilities/Routes.php +++ b/system/Commands/Utilities/Routes.php @@ -122,7 +122,7 @@ public function run(array $params) } if ($collection->shouldAutoRoute()) { - $autoRoutesImproved = config(Feature::class)->autoRoutesImproved ?? false; + $autoRoutesImproved = config(Feature::class)->autoRoutesImproved; if ($autoRoutesImproved) { $autoRouteCollector = new AutoRouteCollectorImproved( diff --git a/system/Commands/Utilities/Routes/FilterFinder.php b/system/Commands/Utilities/Routes/FilterFinder.php index 849a1cdb59c9..98a1e3659eaf 100644 --- a/system/Commands/Utilities/Routes/FilterFinder.php +++ b/system/Commands/Utilities/Routes/FilterFinder.php @@ -56,7 +56,7 @@ public function find(string $uri): array // Add route filters $routeFilters = $this->getRouteFilters($uri); $this->filters->enableFilters($routeFilters, 'before'); - $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; + $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; // @phpstan-ignore nullCoalesce.property if (! $oldFilterOrder) { $routeFilters = array_reverse($routeFilters); } @@ -91,7 +91,7 @@ public function findClasses(string $uri): array // Add route filters $routeFilters = $this->getRouteFilters($uri); $this->filters->enableFilters($routeFilters, 'before'); - $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; + $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; // @phpstan-ignore nullCoalesce.property if (! $oldFilterOrder) { $routeFilters = array_reverse($routeFilters); } diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index f38d52dc9750..fe646bb1fa6d 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -1513,7 +1513,7 @@ public function orderBy(string $orderBy, string $direction = '', ?bool $escape = */ public function limit(?int $value = null, ?int $offset = 0) { - $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; + $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; // @phpstan-ignore nullCoalesce.property if ($limitZeroAsAll && $value === 0) { $value = null; } @@ -1635,7 +1635,7 @@ protected function compileFinalQuery(string $sql): string */ public function get(?int $limit = null, int $offset = 0, bool $reset = true) { - $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; + $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; // @phpstan-ignore nullCoalesce.property if ($limitZeroAsAll && $limit === 0) { $limit = null; } @@ -1773,7 +1773,7 @@ public function getWhere($where = null, ?int $limit = null, ?int $offset = 0, bo $this->where($where); } - $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; + $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; // @phpstan-ignore nullCoalesce.property if ($limitZeroAsAll && $limit === 0) { $limit = null; } @@ -2500,7 +2500,7 @@ public function update($set = null, $where = null, ?int $limit = null): bool $this->where($where); } - $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; + $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; // @phpstan-ignore nullCoalesce.property if ($limitZeroAsAll && $limit === 0) { $limit = null; } @@ -2547,7 +2547,7 @@ protected function _update(string $table, array $values): string $valStr[] = $key . ' = ' . $val; } - $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; + $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; // @phpstan-ignore nullCoalesce.property if ($limitZeroAsAll) { return 'UPDATE ' . $this->compileIgnore('update') . $table . ' SET ' . implode(', ', $valStr) . $this->compileWhereHaving('QBWhere') @@ -2824,7 +2824,7 @@ public function delete($where = '', ?int $limit = null, bool $resetData = true) $sql = $this->_delete($this->removeAlias($table)); - $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; + $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; // @phpstan-ignore nullCoalesce.property if ($limitZeroAsAll && $limit === 0) { $limit = null; } @@ -3099,7 +3099,7 @@ protected function compileSelect($selectOverride = false): string . $this->compileWhereHaving('QBHaving') . $this->compileOrderBy(); - $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; + $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; // @phpstan-ignore nullCoalesce.property if ($limitZeroAsAll) { if ($this->QBLimit) { $sql = $this->_limit($sql . "\n"); diff --git a/system/Database/MigrationRunner.php b/system/Database/MigrationRunner.php index 2a66a9cb512c..df5ee049f0b7 100644 --- a/system/Database/MigrationRunner.php +++ b/system/Database/MigrationRunner.php @@ -145,9 +145,9 @@ class MigrationRunner */ public function __construct(MigrationsConfig $config, $db = null) { - $this->enabled = $config->enabled ?? false; - $this->table = $config->table ?? 'migrations'; - $this->lock = $config->lock ?? false; + $this->enabled = $config->enabled; + $this->table = $config->table; + $this->lock = $config->lock ?? false; // @phpstan-ignore nullCoalesce.property // Even if a DB connection is passed, since it is a test, // it is assumed to use the default group name diff --git a/system/Database/SQLSRV/Builder.php b/system/Database/SQLSRV/Builder.php index 3c3e798c3e18..96890bf45cb2 100644 --- a/system/Database/SQLSRV/Builder.php +++ b/system/Database/SQLSRV/Builder.php @@ -337,7 +337,7 @@ protected function _limit(string $sql, bool $offsetIgnore = false): string // DatabaseException: // [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]The number of // rows provided for a FETCH clause must be greater then zero. - $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; + $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; // @phpstan-ignore nullCoalesce.property if (! $limitZeroAsAll && $this->QBLimit === 0) { return "SELECT * \nFROM " . $this->_fromTables() . ' WHERE 1=0 '; } @@ -624,7 +624,7 @@ protected function compileSelect($selectOverride = false): string . $this->compileOrderBy(); // ORDER BY // LIMIT - $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; + $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; // @phpstan-ignore nullCoalesce.property if ($limitZeroAsAll) { if ($this->QBLimit) { $sql = $this->_limit($sql . "\n"); @@ -644,7 +644,7 @@ protected function compileSelect($selectOverride = false): string */ public function get(?int $limit = null, int $offset = 0, bool $reset = true) { - $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; + $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; // @phpstan-ignore nullCoalesce.property if ($limitZeroAsAll && $limit === 0) { $limit = null; } diff --git a/system/Database/Seeder.php b/system/Database/Seeder.php index 632008176031..edbf3251cbf3 100644 --- a/system/Database/Seeder.php +++ b/system/Database/Seeder.php @@ -78,7 +78,7 @@ class Seeder */ public function __construct(Database $config, ?BaseConnection $db = null) { - $this->seedPath = $config->filesPath ?? APPPATH . 'Database/'; + $this->seedPath = $config->filesPath; if ($this->seedPath === '') { throw new InvalidArgumentException('Invalid filesPath set in the Config\Database.'); diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index c35003129764..cd868e860da0 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -336,7 +336,7 @@ protected function structureTimelineData(array $elements): array */ protected function collectVarData(): array { - if (! ($this->config->collectVarData ?? true)) { + if (! $this->config->collectVarData) { return []; } @@ -585,7 +585,7 @@ protected function hasNativeHeaderConflict(): bool private function shouldDisableToolbar(IncomingRequest $request): bool { // Fallback for older installations where the config option is missing (e.g. after upgrading from a previous version). - $headers = $this->config->disableOnHeaders ?? ['X-Requested-With' => 'xmlhttprequest']; + $headers = $this->config->disableOnHeaders ?? ['X-Requested-With' => 'xmlhttprequest']; // @phpstan-ignore nullCoalesce.property foreach ($headers as $headerName => $expectedValue) { if (! $request->hasHeader($headerName)) { diff --git a/system/Encryption/Encryption.php b/system/Encryption/Encryption.php index 17a5b4c547a2..73ec458b837a 100644 --- a/system/Encryption/Encryption.php +++ b/system/Encryption/Encryption.php @@ -93,7 +93,7 @@ public function __construct(?EncryptionConfig $config = null) $this->key = $config->key; $this->driver = $config->driver; - $this->digest = $config->digest ?? 'SHA512'; + $this->digest = $config->digest; $this->handlers = [ 'OpenSSL' => extension_loaded('openssl'), @@ -118,7 +118,7 @@ public function initialize(?EncryptionConfig $config = null) if ($config instanceof EncryptionConfig) { $this->key = $config->key; $this->driver = $config->driver; - $this->digest = $config->digest ?? 'SHA512'; + $this->digest = $config->digest; } if (empty($this->driver)) { diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 9a253a42b359..5ec5fc0fa074 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -465,7 +465,7 @@ public function initialize(?string $uri = null) // Decode URL-encoded string $uri = urldecode($uri ?? ''); - $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; + $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; // @phpstan-ignore nullCoalesce.property if ($oldFilterOrder) { $this->processGlobals($uri); $this->processMethods(); @@ -669,10 +669,6 @@ public function getArguments(?string $key = null) */ protected function processGlobals(?string $uri = null) { - if (! isset($this->config->globals) || ! is_array($this->config->globals)) { - return; - } - $uri = strtolower(trim($uri ?? '', '/ ')); // Add any global filters, unless they are excluded for this URI @@ -706,7 +702,7 @@ protected function processGlobals(?string $uri = null) } if (isset($filters['before'])) { - $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; + $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; // @phpstan-ignore nullCoalesce.property if ($oldFilterOrder) { $this->filters['before'] = array_merge($this->filters['before'], $filters['before']); } else { @@ -726,10 +722,6 @@ protected function processGlobals(?string $uri = null) */ protected function processMethods() { - if (! isset($this->config->methods) || ! is_array($this->config->methods)) { - return; - } - $method = $this->request->getMethod(); $found = false; @@ -752,7 +744,7 @@ protected function processMethods() } if ($found) { - $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; + $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; // @phpstan-ignore nullCoalesce.property if ($oldFilterOrder) { $this->filters['before'] = array_merge($this->filters['before'], $this->config->methods[$method]); } else { @@ -770,7 +762,7 @@ protected function processMethods() */ protected function processFilters(?string $uri = null) { - if (! isset($this->config->filters) || $this->config->filters === []) { + if ($this->config->filters === []) { return; } @@ -802,7 +794,7 @@ protected function processFilters(?string $uri = null) } } - $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; + $oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false; // @phpstan-ignore nullCoalesce.property if (isset($filters['before'])) { if ($oldFilterOrder) { diff --git a/system/Filters/PageCache.php b/system/Filters/PageCache.php index c170e0d46cac..ff4bd097f84c 100644 --- a/system/Filters/PageCache.php +++ b/system/Filters/PageCache.php @@ -39,7 +39,7 @@ public function __construct(?Cache $config = null) $config ??= config('Cache'); $this->pageCache = service('responsecache'); - $this->cacheStatusCodes = $config->cacheStatusCodes ?? []; + $this->cacheStatusCodes = $config->cacheStatusCodes ?? []; // @phpstan-ignore nullCoalesce.property } /** diff --git a/system/Format/JSONFormatter.php b/system/Format/JSONFormatter.php index 1ca6feb6d580..ac237fcc7af3 100644 --- a/system/Format/JSONFormatter.php +++ b/system/Format/JSONFormatter.php @@ -41,7 +41,7 @@ public function format($data) $options |= JSON_PRETTY_PRINT; } - $result = json_encode($data, $options, $config->jsonEncodeDepth ?? 512); + $result = json_encode($data, $options, $config->jsonEncodeDepth); if (! in_array(json_last_error(), [JSON_ERROR_NONE, JSON_ERROR_RECURSION], true)) { throw FormatException::forInvalidJSON(json_last_error_msg()); diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php index a1dc31dd7517..02aadfa8f97b 100644 --- a/system/HTTP/CURLRequest.php +++ b/system/HTTP/CURLRequest.php @@ -132,13 +132,13 @@ public function __construct(App $config, URI $uri, ?ResponseInterface $response $this->baseURI = $uri->useRawQueryString(); $this->defaultOptions = $options; - $this->shareOptions = config(ConfigCURLRequest::class)->shareOptions ?? true; + $this->shareOptions = config(ConfigCURLRequest::class)->shareOptions; $this->config = $this->defaultConfig; $this->parseOptions($options); // Share Connection - $optShareConnection = config(ConfigCURLRequest::class)->shareConnectionOptions ?? [ + $optShareConnection = config(ConfigCURLRequest::class)->shareConnectionOptions ?? [ // @phpstan-ignore nullCoalesce.property CURL_LOCK_DATA_CONNECT, CURL_LOCK_DATA_DNS, ]; diff --git a/system/Honeypot/Honeypot.php b/system/Honeypot/Honeypot.php index 1b09da4d1a68..a816bdd1ed60 100644 --- a/system/Honeypot/Honeypot.php +++ b/system/Honeypot/Honeypot.php @@ -46,8 +46,6 @@ public function __construct(HoneypotConfig $config) $this->config->container = '
{template}
'; } - $this->config->containerId ??= 'hpc'; - if ($this->config->template === '') { throw HoneypotException::forNoTemplate(); } diff --git a/system/Log/Logger.php b/system/Log/Logger.php index 8308ddaf94f7..011920e15bd1 100644 --- a/system/Log/Logger.php +++ b/system/Log/Logger.php @@ -138,9 +138,7 @@ public function __construct($config, bool $debug = CI_DEBUG) $this->loggableLevels[] = $stringLevel; } - if (isset($config->dateFormat)) { - $this->dateFormat = $config->dateFormat; - } + $this->dateFormat = $config->dateFormat; if ($config->handlers === []) { throw LogException::forNoHandlers('LoggerConfig'); diff --git a/system/Model.php b/system/Model.php index 2c9230a90ca0..d23cecad074a 100644 --- a/system/Model.php +++ b/system/Model.php @@ -232,7 +232,7 @@ protected function doFindColumn(string $columnName) */ protected function doFindAll(?int $limit = null, int $offset = 0) { - $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; + $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true; // @phpstan-ignore nullCoalesce.property if ($limitZeroAsAll) { $limit ??= 0; } diff --git a/system/Router/Router.php b/system/Router/Router.php index 0348daf10cdc..e6ca63acffdd 100644 --- a/system/Router/Router.php +++ b/system/Router/Router.php @@ -157,9 +157,7 @@ public function __construct(RouteCollectionInterface $routes, ?Request $request { $config = config(App::class); - if (isset($config->permittedURIChars)) { - $this->permittedURIChars = $config->permittedURIChars; - } + $this->permittedURIChars = $config->permittedURIChars; $this->collection = $routes; @@ -172,7 +170,7 @@ public function __construct(RouteCollectionInterface $routes, ?Request $request $this->translateURIDashes = $this->collection->shouldTranslateURIDashes(); if ($this->collection->shouldAutoRoute()) { - $autoRoutesImproved = config(Feature::class)->autoRoutesImproved ?? false; + $autoRoutesImproved = config(Feature::class)->autoRoutesImproved; if ($autoRoutesImproved) { assert($this->collection instanceof RouteCollection); diff --git a/system/Session/Session.php b/system/Session/Session.php index 70a2e4a419dd..1c3824928775 100644 --- a/system/Session/Session.php +++ b/system/Session/Session.php @@ -72,8 +72,8 @@ public function __construct(SessionHandlerInterface $driver, SessionConfig $conf 'domain' => $cookie->domain, 'secure' => $cookie->secure, 'httponly' => true, // for security - 'samesite' => $cookie->samesite ?? Cookie::SAMESITE_LAX, - 'raw' => $cookie->raw ?? false, + 'samesite' => $cookie->samesite, + 'raw' => $cookie->raw, ]))->withPrefix(''); // Cookie prefix should be ignored. helper('array'); diff --git a/system/Typography/Typography.php b/system/Typography/Typography.php index 1dfca68c5164..58bb10b0df5a 100644 --- a/system/Typography/Typography.php +++ b/system/Typography/Typography.php @@ -331,7 +331,7 @@ public function nl2brExceptPre(string $str): string $docTypes = new DocTypes(); for ($ex = explode('pre>', $str), $ct = count($ex), $i = 0; $i < $ct; $i++) { - $xhtml = ! ($docTypes->html5 ?? false); + $xhtml = ! $docTypes->html5; $newstr .= (($i % 2) === 0) ? nl2br($ex[$i], $xhtml) : $ex[$i]; if ($ct - 1 !== $i) { diff --git a/system/View/View.php b/system/View/View.php index 7ca89ac0c9ad..83b19a9313f6 100644 --- a/system/View/View.php +++ b/system/View/View.php @@ -202,7 +202,7 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n $this->renderVars['file'] = $this->viewPath . $this->renderVars['view']; if (str_contains($this->renderVars['view'], '\\')) { - $appOverridesFolder = $this->config->appOverridesFolder ?? 'overrides'; + $appOverridesFolder = $this->config->appOverridesFolder ?? 'overrides'; // @phpstan-ignore nullCoalesce.property $overrideFolder = $appOverridesFolder !== '' ? trim($appOverridesFolder, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR diff --git a/system/View/ViewDecoratorTrait.php b/system/View/ViewDecoratorTrait.php index 575c4f5d5a23..d617234deacb 100644 --- a/system/View/ViewDecoratorTrait.php +++ b/system/View/ViewDecoratorTrait.php @@ -14,7 +14,6 @@ namespace CodeIgniter\View; use CodeIgniter\View\Exceptions\ViewException; -use Config\View as ViewConfig; trait ViewDecoratorTrait { @@ -24,7 +23,7 @@ trait ViewDecoratorTrait */ protected function decorateOutput(string $html): string { - $decorators = $this->config->decorators ?? config(ViewConfig::class)->decorators; + $decorators = $this->config->decorators; foreach ($decorators as $decorator) { if (! is_subclass_of($decorator, ViewDecoratorInterface::class)) { diff --git a/tests/system/Honeypot/HoneypotTest.php b/tests/system/Honeypot/HoneypotTest.php index 958b61bbf43a..7389f7faea0b 100644 --- a/tests/system/Honeypot/HoneypotTest.php +++ b/tests/system/Honeypot/HoneypotTest.php @@ -24,6 +24,7 @@ use CodeIgniter\Superglobals; use CodeIgniter\Test\CIUnitTestCase; use Config\App; +use Config\Filters as FiltersConfig; use Config\Honeypot as HoneypotConfig; use PHPUnit\Framework\Attributes\BackupGlobals; use PHPUnit\Framework\Attributes\Group; @@ -162,15 +163,11 @@ public function testConfigName(): void public function testHoneypotFilterBefore(): void { - $config = [ - 'aliases' => ['trap' => \CodeIgniter\Filters\Honeypot::class], - 'globals' => [ - 'before' => ['trap'], - 'after' => [], - ], - ]; - - $filters = new Filters((object) $config, $this->request, $this->response); + $config = new FiltersConfig(); + $config->aliases = ['trap' => \CodeIgniter\Filters\Honeypot::class]; + $config->globals = ['before' => ['trap'], 'after' => []]; + + $filters = new Filters($config, $this->request, $this->response); $uri = 'admin/foo/bar'; $this->expectException(HoneypotException::class); @@ -179,15 +176,11 @@ public function testHoneypotFilterBefore(): void public function testHoneypotFilterAfter(): void { - $config = [ - 'aliases' => ['trap' => \CodeIgniter\Filters\Honeypot::class], - 'globals' => [ - 'before' => [], - 'after' => ['trap'], - ], - ]; - - $filters = new Filters((object) $config, $this->request, $this->response); + $config = new FiltersConfig(); + $config->aliases = ['trap' => \CodeIgniter\Filters\Honeypot::class]; + $config->globals = ['before' => [], 'after' => ['trap']]; + + $filters = new Filters($config, $this->request, $this->response); $uri = 'admin/foo/bar'; $this->response->setBody('
'); diff --git a/utils/phpstan-baseline/argument.type.neon b/utils/phpstan-baseline/argument.type.neon index b72e9130e480..3bb95bae65fb 100644 --- a/utils/phpstan-baseline/argument.type.neon +++ b/utils/phpstan-baseline/argument.type.neon @@ -1,4 +1,4 @@ -# total 84 errors +# total 82 errors parameters: ignoreErrors: @@ -162,11 +162,6 @@ parameters: count: 1 path: ../../tests/system/HomeTest.php - - - message: '#^Parameter \#1 \$config of class CodeIgniter\\Filters\\Filters constructor expects Config\\Filters, object\{aliases\: array\, globals\: array\\>\}&stdClass given\.$#' - count: 2 - path: ../../tests/system/Honeypot/HoneypotTest.php - - message: '#^Parameter \#1 \$data of method CodeIgniter\\HTTP\\Message\:\:setBody\(\) expects string, null given\.$#' count: 1 diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index c065fa299fd3..65e70e528efd 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 2062 errors +# total 2060 errors includes: - argument.type.neon From b281d3fcfab058a6ac7c9b92dade910b36b756a3 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Sat, 4 Apr 2026 10:34:33 +0200 Subject: [PATCH 11/85] fix: make Autoloader composer path injectable to fix parallel test race condition (#10082) --- .github/scripts/run-random-tests.sh | 1 + system/Autoloader/Autoloader.php | 14 +++++++++----- tests/system/Autoloader/AutoloaderTest.php | 7 +------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/scripts/run-random-tests.sh b/.github/scripts/run-random-tests.sh index 45376fb58fb2..23a90f955e2f 100755 --- a/.github/scripts/run-random-tests.sh +++ b/.github/scripts/run-random-tests.sh @@ -23,6 +23,7 @@ ################################################################################ set -u +export LC_NUMERIC=C trap 'kill "${bg_pids[@]:-}" 2>/dev/null; wait 2>/dev/null' EXIT INT TERM ################################################################################ diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index 65c535e8611e..69906d4e162a 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -93,6 +93,10 @@ class Autoloader */ protected $helpers = ['url']; + public function __construct(private readonly string $composerPath = COMPOSER_PATH) + { + } + /** * Reads in the configuration array (described above) and stores * the valid parts that we'll need. @@ -127,7 +131,7 @@ public function initialize(Autoload $config, Modules $modules) $this->helpers = [...$this->helpers, ...$config->helpers]; } - if (is_file(COMPOSER_PATH)) { + if (is_file($this->composerPath)) { $this->loadComposerAutoloader($modules); } @@ -139,11 +143,11 @@ private function loadComposerAutoloader(Modules $modules): void // The path to the vendor directory. // We do not want to enforce this, so set the constant if Composer was used. if (! defined('VENDORPATH')) { - define('VENDORPATH', dirname(COMPOSER_PATH) . DIRECTORY_SEPARATOR); + define('VENDORPATH', dirname($this->composerPath) . DIRECTORY_SEPARATOR); } /** @var ClassLoader $composer */ - $composer = include COMPOSER_PATH; + $composer = include $this->composerPath; // Should we load through Composer's namespaces, also? if ($modules->discoverInComposer) { @@ -451,14 +455,14 @@ private function loadComposerNamespaces(ClassLoader $composer, array $composerPa */ protected function discoverComposerNamespaces() { - if (! is_file(COMPOSER_PATH)) { + if (! is_file($this->composerPath)) { return; } /** * @var ClassLoader $composer */ - $composer = include COMPOSER_PATH; + $composer = include $this->composerPath; $paths = $composer->getPrefixesPsr4(); $classes = $composer->getClassMap(); diff --git a/tests/system/Autoloader/AutoloaderTest.php b/tests/system/Autoloader/AutoloaderTest.php index cd73356958fe..ef76091baa39 100644 --- a/tests/system/Autoloader/AutoloaderTest.php +++ b/tests/system/Autoloader/AutoloaderTest.php @@ -367,17 +367,12 @@ public function testComposerPackagesOnlyAndExclude(): void public function testFindsComposerRoutesWithComposerPathNotFound(): void { - $composerPath = COMPOSER_PATH; - $config = new Autoload(); $modules = new Modules(); $modules->discoverInComposer = true; - $loader = new Autoloader(); - - rename(COMPOSER_PATH, COMPOSER_PATH . '.backup'); + $loader = new Autoloader('/nonexistent/path/autoload.php'); $loader->initialize($config, $modules); - rename(COMPOSER_PATH . '.backup', $composerPath); $namespaces = $loader->getNamespace(); $this->assertArrayNotHasKey('Laminas\\Escaper', $namespaces); From 1cf8c93af0ad7ac14ec22af4cfd5b3e86bdb54d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 15:21:54 +0000 Subject: [PATCH 12/85] chore(deps-dev): update rector/rector requirement Updates the requirements on [rector/rector](https://github.com/rectorphp/rector) to permit the latest version. Updates `rector/rector` to 2.4.0 - [Release notes](https://github.com/rectorphp/rector/releases) - [Commits](https://github.com/rectorphp/rector/compare/2.3.9...2.4.0) --- updated-dependencies: - dependency-name: rector/rector dependency-version: 2.4.0 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 7ba9b8dd44e2..57f5a1b6ff9f 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.9", + "rector/rector": "2.4.0", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { From 16bcd78f32b7bf8dd61fbc06355061d29cf16ab3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:23:02 +0000 Subject: [PATCH 13/85] chore(deps-dev): update rector/rector requirement Updates the requirements on [rector/rector](https://github.com/rectorphp/rector) to permit the latest version. Updates `rector/rector` to 2.4.1 - [Release notes](https://github.com/rectorphp/rector/releases) - [Commits](https://github.com/rectorphp/rector/compare/2.4.0...2.4.1) --- updated-dependencies: - dependency-name: rector/rector dependency-version: 2.4.1 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 57f5a1b6ff9f..e1a7b6d6b2a4 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.4.0", + "rector/rector": "2.4.1", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { From 0bf518ebec0e7d314b6b17086fd02b04d66b8776 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 8 Apr 2026 22:29:08 +0700 Subject: [PATCH 14/85] chore: remove useless @var --- tests/system/Events/EventsTest.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/system/Events/EventsTest.php b/tests/system/Events/EventsTest.php index 5ec4ae679d3b..bfdef1e2bbe9 100644 --- a/tests/system/Events/EventsTest.php +++ b/tests/system/Events/EventsTest.php @@ -54,9 +54,6 @@ protected function tearDown(): void #[RunInSeparateProcess] public function testInitialize(): void { - /** - * @var Modules $config - */ $config = new Modules(); $config->aliases = []; From 35c48724f009fcdc45b0a95cc88e04699bbf11a4 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Thu, 9 Apr 2026 16:32:06 +0800 Subject: [PATCH 15/85] refactor: add full testing to `logs:clear` command (#10090) --- system/Commands/Housekeeping/ClearLogs.php | 19 ++- tests/system/Commands/ClearLogsTest.php | 128 +++++++++++++++++++-- 2 files changed, 125 insertions(+), 22 deletions(-) diff --git a/system/Commands/Housekeeping/ClearLogs.php b/system/Commands/Housekeeping/ClearLogs.php index ec4b700e07f3..71f299969055 100644 --- a/system/Commands/Housekeeping/ClearLogs.php +++ b/system/Commands/Housekeeping/ClearLogs.php @@ -67,27 +67,22 @@ public function run(array $params) $force = array_key_exists('force', $params) || CLI::getOption('force'); if (! $force && CLI::prompt('Are you sure you want to delete the logs?', ['n', 'y']) === 'n') { - // @codeCoverageIgnoreStart - CLI::error('Deleting logs aborted.', 'light_gray', 'red'); - CLI::error('If you want, use the "-force" option to force delete all log files.', 'light_gray', 'red'); - CLI::newLine(); + CLI::error('Deleting logs aborted.'); + CLI::error('If you want, use the "--force" option to force delete all log files.'); - return; - // @codeCoverageIgnoreEnd + return EXIT_ERROR; } helper('filesystem'); if (! delete_files(WRITEPATH . 'logs', false, true)) { - // @codeCoverageIgnoreStart - CLI::error('Error in deleting the logs files.', 'light_gray', 'red'); - CLI::newLine(); + CLI::error('Error in deleting the logs files.'); - return; - // @codeCoverageIgnoreEnd + return EXIT_ERROR; } CLI::write('Logs cleared.', 'green'); - CLI::newLine(); + + return EXIT_SUCCESS; } } diff --git a/tests/system/Commands/ClearLogsTest.php b/tests/system/Commands/ClearLogsTest.php index 28019d5d51f6..b2c4c8b0664c 100644 --- a/tests/system/Commands/ClearLogsTest.php +++ b/tests/system/Commands/ClearLogsTest.php @@ -13,9 +13,12 @@ namespace CodeIgniter\Commands; +use CodeIgniter\CLI\CLI; use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\Mock\MockInputOutput; use CodeIgniter\Test\StreamFilterTrait; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; /** * @internal @@ -34,9 +37,24 @@ protected function setUp(): void // test runs on other tests may log errors since default threshold // is now 4, so set this to a safe distance $this->date = date('Y-m-d', strtotime('+1 year')); + + command('logs:clear --force'); + $this->resetStreamFilterBuffer(); + + $this->createDummyLogFiles(); + } + + protected function tearDown(): void + { + command('logs:clear --force'); + $this->resetStreamFilterBuffer(); + + CLI::reset(); + + parent::tearDown(); } - protected function createDummyLogFiles(): void + private function createDummyLogFiles(): void { $date = $this->date; $path = WRITEPATH . 'logs' . DIRECTORY_SEPARATOR . "log-{$date}.log"; @@ -44,27 +62,117 @@ protected function createDummyLogFiles(): void // create 10 dummy log files for ($i = 0; $i < 10; $i++) { $newDate = date('Y-m-d', strtotime("+1 year -{$i} day")); - $path = str_replace($date, $newDate, $path); + + $path = str_replace($date, $newDate, $path); file_put_contents($path, 'Lorem ipsum'); $date = $newDate; } } - public function testClearLogsWorks(): void + public function testClearLogsUsingForce(): void { - // test clean logs dir + $this->assertFileExists(WRITEPATH . 'logs' . DIRECTORY_SEPARATOR . "log-{$this->date}.log"); + + command('logs:clear --force'); + $this->assertFileDoesNotExist(WRITEPATH . 'logs' . DIRECTORY_SEPARATOR . "log-{$this->date}.log"); + $this->assertFileExists(WRITEPATH . 'logs' . DIRECTORY_SEPARATOR . 'index.html'); + $this->assertSame("Logs cleared.\n", preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer())); + } - // test dir is now populated with logs - $this->createDummyLogFiles(); + public function testClearLogsAbortsClearWithoutForce(): void + { + $this->assertFileExists(WRITEPATH . 'logs' . DIRECTORY_SEPARATOR . "log-{$this->date}.log"); + + $io = new MockInputOutput(); + $io->setInputs(['n']); + CLI::setInputOutput($io); + + command('logs:clear'); + + $this->assertFileExists(WRITEPATH . 'logs' . DIRECTORY_SEPARATOR . "log-{$this->date}.log"); + $this->assertSame( + <<<'EOT' + Deleting logs aborted. + If you want, use the "--force" option to force delete all log files. + + EOT, + preg_replace('/\e\[[^m]+m/', '', $io->getOutput(2) . $io->getOutput(3)), + ); + } + + public function testClearLogsAbortsClearWithoutForceWithDefaultAnswer(): void + { + $this->assertFileExists(WRITEPATH . 'logs' . DIRECTORY_SEPARATOR . "log-{$this->date}.log"); + + $io = new MockInputOutput(); + $io->setInputs(['']); + CLI::setInputOutput($io); + + command('logs:clear'); + + $this->assertFileExists(WRITEPATH . 'logs' . DIRECTORY_SEPARATOR . "log-{$this->date}.log"); + $this->assertSame( + <<<'EOT' + Deleting logs aborted. + If you want, use the "--force" option to force delete all log files. + + EOT, + preg_replace('/\e\[[^m]+m/', '', $io->getOutput(2) . $io->getOutput(3)), + ); + } + + public function testClearLogsWithoutForceButWithConfirmation(): void + { $this->assertFileExists(WRITEPATH . 'logs' . DIRECTORY_SEPARATOR . "log-{$this->date}.log"); - command('logs:clear -force'); - $result = $this->getStreamFilterBuffer(); + $io = new MockInputOutput(); + $io->setInputs(['y']); + CLI::setInputOutput($io); + + command('logs:clear'); $this->assertFileDoesNotExist(WRITEPATH . 'logs' . DIRECTORY_SEPARATOR . "log-{$this->date}.log"); - $this->assertFileExists(WRITEPATH . 'logs' . DIRECTORY_SEPARATOR . 'index.html'); - $this->assertStringContainsString('Logs cleared.', $result); + $this->assertSame("Logs cleared.\n", preg_replace('/\e\[[^m]+m/', '', $io->getOutput(2))); + } + + #[RequiresOperatingSystem('Darwin|Linux')] + public function testClearLogsFailsOnChmodFailure(): void + { + $path = WRITEPATH . 'logs' . DIRECTORY_SEPARATOR . "log-{$this->date}.log"; + file_put_contents($path, 'Lorem ipsum'); + + // Attempt to make the file itself undeletable by setting the + // immutable/uchg flag on supported platforms. + $immutableSet = false; + if (str_starts_with(PHP_OS, 'Darwin')) { + @exec(sprintf('chflags uchg %s', escapeshellarg($path)), $output, $rc); + $immutableSet = $rc === 0; + } else { + // Try chattr on Linux with sudo (for containerized environments) + @exec('which chattr', $whichOut, $whichRc); + + if ($whichRc === 0) { + @exec(sprintf('sudo chattr +i %s', escapeshellarg($path)), $output, $rc); + $immutableSet = $rc === 0; + } + } + + if (! $immutableSet) { + $this->markTestSkipped('Cannot set file immutability in this environment'); + } + + command('logs:clear --force'); + + // Restore attributes so other tests are not affected. + if (str_starts_with(PHP_OS, 'Darwin')) { + @exec(sprintf('chflags nouchg %s', escapeshellarg($path))); + } else { + @exec(sprintf('sudo chattr -i %s', escapeshellarg($path))); + } + + $this->assertFileExists($path); + $this->assertSame("Error in deleting the logs files.\n", preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer())); } } From 2aaf6b6e4c978053d9f8c4b99a977d3147821356 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Thu, 9 Apr 2026 21:48:37 +0200 Subject: [PATCH 16/85] fix: store SPL closures in register() so unregister() can remove them (#10097) --- system/Autoloader/Autoloader.php | 29 ++++++++++++--- tests/system/Autoloader/AutoloaderTest.php | 39 +++++++++++++++++++++ user_guide_src/source/changelogs/v4.7.3.rst | 2 ++ 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index 69906d4e162a..29d119727f94 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Autoloader; +use Closure; use CodeIgniter\Exceptions\ConfigException; use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Exceptions\RuntimeException; @@ -93,6 +94,14 @@ class Autoloader */ protected $helpers = ['url']; + /** + * Stores the closures registered with spl_autoload_register() + * so that unregister() can remove the exact same instances. + * + * @var list + */ + private array $registeredClosures = []; + public function __construct(private readonly string $composerPath = COMPOSER_PATH) { } @@ -170,8 +179,17 @@ private function loadComposerAutoloader(Modules $modules): void */ public function register() { - spl_autoload_register($this->loadClassmap(...), true); - spl_autoload_register($this->loadClass(...), true); + // Store the exact Closure instances so unregister() can remove them. + // First-class callable syntax (e.g. $this->loadClass(...)) creates a + // new Closure object on every call, so we must reuse the same instances. + $loadClassmap = $this->loadClassmap(...); + $loadClass = $this->loadClass(...); + + $this->registeredClosures[] = $loadClassmap; + $this->registeredClosures[] = $loadClass; + + spl_autoload_register($loadClassmap, true); + spl_autoload_register($loadClass, true); foreach ($this->files as $file) { $this->includeFile($file); @@ -183,8 +201,11 @@ public function register() */ public function unregister(): void { - spl_autoload_unregister($this->loadClass(...)); - spl_autoload_unregister($this->loadClassmap(...)); + foreach ($this->registeredClosures as $closure) { + spl_autoload_unregister($closure); + } + + $this->registeredClosures = []; } /** diff --git a/tests/system/Autoloader/AutoloaderTest.php b/tests/system/Autoloader/AutoloaderTest.php index ef76091baa39..be22e4a403f1 100644 --- a/tests/system/Autoloader/AutoloaderTest.php +++ b/tests/system/Autoloader/AutoloaderTest.php @@ -123,6 +123,45 @@ public function testServiceAutoLoaderFromShareInstances(): void $this->assertSame($expected, $actual); } + public function testUnregisterRemovesClosuresFromSplStack(): void + { + $countBefore = count(spl_autoload_functions()); + + $config = new Autoload(); + $modules = new Modules(); + $modules->discoverInComposer = false; + $config->psr4 = ['CodeIgniter' => SYSTEMPATH]; + + $loader = new Autoloader(); + $loader->initialize($config, $modules)->register(); + + $this->assertCount($countBefore + 2, spl_autoload_functions()); + + $loader->unregister(); + + $this->assertCount($countBefore, spl_autoload_functions()); + } + + public function testUnregisterRemovesAllClosuresAfterMultipleRegistrations(): void + { + $countBefore = count(spl_autoload_functions()); + + $config = new Autoload(); + $modules = new Modules(); + $modules->discoverInComposer = false; + $config->psr4 = ['CodeIgniter' => SYSTEMPATH]; + + $loader = new Autoloader(); + $loader->initialize($config, $modules)->register(); + $loader->register(); + + $this->assertCount($countBefore + 4, spl_autoload_functions()); + + $loader->unregister(); + + $this->assertCount($countBefore, spl_autoload_functions()); + } + public function testServiceAutoLoader(): void { $autoloader = service('autoloader', false); diff --git a/user_guide_src/source/changelogs/v4.7.3.rst b/user_guide_src/source/changelogs/v4.7.3.rst index b0b0bf1b8c2d..1947048fe142 100644 --- a/user_guide_src/source/changelogs/v4.7.3.rst +++ b/user_guide_src/source/changelogs/v4.7.3.rst @@ -30,6 +30,8 @@ Deprecations Bugs Fixed ********** +- **Autoloader:** Fixed a bug where ``Autoloader::unregister()`` (used during tests) silently failed to remove handlers from the SPL autoload stack, causing closures to accumulate permanently. + See the repo's `CHANGELOG.md `_ for a complete list of bugs fixed. From 7fa1101646cbad1bbda6dc2a81ece1c4155c87b7 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Fri, 10 Apr 2026 04:07:04 +0800 Subject: [PATCH 17/85] refactor: add full testing for `debugbar:clear` command (#10093) --- .../Commands/Housekeeping/ClearDebugbar.php | 8 +-- tests/system/Commands/ClearDebugbarTest.php | 67 ++++++++++++++++--- 2 files changed, 62 insertions(+), 13 deletions(-) diff --git a/system/Commands/Housekeeping/ClearDebugbar.php b/system/Commands/Housekeeping/ClearDebugbar.php index dd49b24a7656..281a2c865d6b 100644 --- a/system/Commands/Housekeeping/ClearDebugbar.php +++ b/system/Commands/Housekeeping/ClearDebugbar.php @@ -58,15 +58,13 @@ public function run(array $params) helper('filesystem'); if (! delete_files(WRITEPATH . 'debugbar', false, true)) { - // @codeCoverageIgnoreStart CLI::error('Error deleting the debugbar JSON files.'); - CLI::newLine(); - return; - // @codeCoverageIgnoreEnd + return EXIT_ERROR; } CLI::write('Debugbar cleared.', 'green'); - CLI::newLine(); + + return EXIT_SUCCESS; } } diff --git a/tests/system/Commands/ClearDebugbarTest.php b/tests/system/Commands/ClearDebugbarTest.php index b3c2de18c394..dc1de4dc6f3b 100644 --- a/tests/system/Commands/ClearDebugbarTest.php +++ b/tests/system/Commands/ClearDebugbarTest.php @@ -16,6 +16,7 @@ use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; /** * @internal @@ -31,10 +32,22 @@ protected function setUp(): void { parent::setUp(); + command('debugbar:clear'); + $this->resetStreamFilterBuffer(); + $this->time = time(); + $this->createDummyDebugbarJson(); + } + + protected function tearDown(): void + { + command('debugbar:clear'); + $this->resetStreamFilterBuffer(); + + parent::tearDown(); } - protected function createDummyDebugbarJson(): void + private function createDummyDebugbarJson(): void { $time = $this->time; $path = WRITEPATH . 'debugbar' . DIRECTORY_SEPARATOR . "debugbar_{$time}.json"; @@ -50,18 +63,56 @@ protected function createDummyDebugbarJson(): void public function testClearDebugbarWorks(): void { - // test clean debugbar dir - $this->assertFileDoesNotExist(WRITEPATH . 'debugbar' . DIRECTORY_SEPARATOR . "debugbar_{$this->time}.json"); - - // test dir is now populated with json - $this->createDummyDebugbarJson(); $this->assertFileExists(WRITEPATH . 'debugbar' . DIRECTORY_SEPARATOR . "debugbar_{$this->time}.json"); command('debugbar:clear'); - $result = $this->getStreamFilterBuffer(); $this->assertFileDoesNotExist(WRITEPATH . 'debugbar' . DIRECTORY_SEPARATOR . "debugbar_{$this->time}.json"); $this->assertFileExists(WRITEPATH . 'debugbar' . DIRECTORY_SEPARATOR . 'index.html'); - $this->assertStringContainsString('Debugbar cleared.', $result); + $this->assertSame( + "Debugbar cleared.\n", + preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()), + ); + } + + #[RequiresOperatingSystem('Darwin|Linux')] + public function testClearDebugbarWithError(): void + { + $path = WRITEPATH . 'debugbar' . DIRECTORY_SEPARATOR . "debugbar_{$this->time}.json"; + + // Attempt to make the file itself undeletable by setting the + // immutable/uchg flag on supported platforms. + $immutableSet = false; + if (str_starts_with(PHP_OS, 'Darwin')) { + @exec(sprintf('chflags uchg %s', escapeshellarg($path)), $output, $rc); + $immutableSet = $rc === 0; + } else { + // Try chattr on Linux with sudo (for containerized environments) + @exec('which chattr', $whichOut, $whichRc); + + if ($whichRc === 0) { + @exec(sprintf('sudo chattr +i %s', escapeshellarg($path)), $output, $rc); + $immutableSet = $rc === 0; + } + } + + if (! $immutableSet) { + $this->markTestSkipped('Cannot set file immutability in this environment'); + } + + command('debugbar:clear'); + + // Restore attributes so other tests are not affected. + if (str_starts_with(PHP_OS, 'Darwin')) { + @exec(sprintf('chflags nouchg %s', escapeshellarg($path))); + } else { + @exec(sprintf('sudo chattr -i %s', escapeshellarg($path))); + } + + $this->assertFileExists($path); + $this->assertSame( + "Error deleting the debugbar JSON files.\n", + preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()), + ); } } From 0b330102d889e257c823e2e84fbd814329a9490a Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Thu, 9 Apr 2026 22:14:17 +0200 Subject: [PATCH 18/85] refactor: pass `--do-not-cache-result` to prevent shared cache corruption (#10098) Co-authored-by: John Paul E Balandan --- .github/scripts/run-random-tests.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/scripts/run-random-tests.sh b/.github/scripts/run-random-tests.sh index 23a90f955e2f..52568c2c2e07 100755 --- a/.github/scripts/run-random-tests.sh +++ b/.github/scripts/run-random-tests.sh @@ -486,6 +486,7 @@ run_component_tests() { "$test_dir" "--colors=never" "--no-coverage" + "--do-not-cache-result" "--order-by=random" "--random-order-seed=${random_seed}" "--log-events-text" @@ -611,7 +612,7 @@ run_component_tests() { fi { - echo "> ${phpunit_args[@]:0:6}" + echo "> ${phpunit_args[@]:0:7}" echo "" echo "$output" echo "$predecessor_info" From c67b0a73c3b474d4485856c94fb8e26e402f9c8f Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Fri, 10 Apr 2026 14:39:24 +0800 Subject: [PATCH 19/85] refactor: add full testing for `cache:clear` command (#10094) --- system/Commands/Cache/ClearCache.php | 14 ++++----- system/Language/en/Cache.php | 1 + tests/system/Commands/ClearCacheTest.php | 33 ++++++++++++++++++++- user_guide_src/source/changelogs/v4.7.3.rst | 2 ++ 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/system/Commands/Cache/ClearCache.php b/system/Commands/Cache/ClearCache.php index e1180c28c6bd..32f9466a4939 100644 --- a/system/Commands/Cache/ClearCache.php +++ b/system/Commands/Cache/ClearCache.php @@ -13,7 +13,6 @@ namespace CodeIgniter\Commands\Cache; -use CodeIgniter\Cache\CacheFactory; use CodeIgniter\CLI\BaseCommand; use CodeIgniter\CLI\CLI; use Config\Cache; @@ -69,22 +68,21 @@ public function run(array $params) $handler = $params[0] ?? $config->handler; if (! array_key_exists($handler, $config->validHandlers)) { - CLI::error($handler . ' is not a valid cache handler.'); + CLI::error(lang('Cache.invalidHandler', [$handler])); - return; + return EXIT_ERROR; } $config->handler = $handler; - $cache = CacheFactory::getHandler($config); - if (! $cache->clean()) { - // @codeCoverageIgnoreStart + if (! service('cache', $config)->clean()) { CLI::error('Error while clearing the cache.'); - return; - // @codeCoverageIgnoreEnd + return EXIT_ERROR; } CLI::write(CLI::color('Cache cleared.', 'green')); + + return EXIT_SUCCESS; } } diff --git a/system/Language/en/Cache.php b/system/Language/en/Cache.php index b877c9bbaabd..63512cd525d5 100644 --- a/system/Language/en/Cache.php +++ b/system/Language/en/Cache.php @@ -14,6 +14,7 @@ // Cache language settings return [ 'unableToWrite' => 'Cache unable to write to "{0}".', + 'invalidHandler' => 'Cache driver "{0}" is not a valid cache handler.', 'invalidHandlers' => 'Cache config must have an array of $validHandlers.', 'noBackup' => 'Cache config must have a handler and backupHandler set.', 'handlerNotFound' => 'Cache config has an invalid handler or backup handler specified.', diff --git a/tests/system/Commands/ClearCacheTest.php b/tests/system/Commands/ClearCacheTest.php index e27978e40919..caa37f74f7d9 100644 --- a/tests/system/Commands/ClearCacheTest.php +++ b/tests/system/Commands/ClearCacheTest.php @@ -14,6 +14,7 @@ namespace CodeIgniter\Commands; use CodeIgniter\Cache\CacheFactory; +use CodeIgniter\Cache\Handlers\FileHandler; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; use Config\Services; @@ -31,15 +32,27 @@ protected function setUp(): void { parent::setUp(); + $this->resetServices(); + // Make sure we are testing with the correct handler (override injections) Services::injectMock('cache', CacheFactory::getHandler(config('Cache'))); } + protected function tearDown(): void + { + parent::tearDown(); + + $this->resetServices(); + } + public function testClearCacheInvalidHandler(): void { command('cache:clear junk'); - $this->assertStringContainsString('junk is not a valid cache handler.', $this->getStreamFilterBuffer()); + $this->assertSame( + "Cache driver \"junk\" is not a valid cache handler.\n", + preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()), + ); } public function testClearCacheWorks(): void @@ -52,4 +65,22 @@ public function testClearCacheWorks(): void $this->assertNull(cache('foo')); $this->assertStringContainsString('Cache cleared.', $this->getStreamFilterBuffer()); } + + public function testClearCacheFails(): void + { + $cache = $this->getMockBuilder(FileHandler::class) + ->setConstructorArgs([config('Cache')]) + ->onlyMethods(['clean']) + ->getMock(); + $cache->expects($this->once())->method('clean')->willReturn(false); + + Services::injectMock('cache', $cache); + + command('cache:clear'); + + $this->assertSame( + "Error while clearing the cache.\n", + preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()), + ); + } } diff --git a/user_guide_src/source/changelogs/v4.7.3.rst b/user_guide_src/source/changelogs/v4.7.3.rst index 1947048fe142..be42d6379b8d 100644 --- a/user_guide_src/source/changelogs/v4.7.3.rst +++ b/user_guide_src/source/changelogs/v4.7.3.rst @@ -18,6 +18,8 @@ BREAKING Message Changes *************** +- The ``Cache.invalidHandler`` message string was added. + ******* Changes ******* From d277ac18927c5e8abf31ad1fe6186726ed458064 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Fri, 10 Apr 2026 21:28:23 +0800 Subject: [PATCH 20/85] chore: re-comment transiently failing component tests (#10095) --- .github/scripts/random-tests-config.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/scripts/random-tests-config.txt b/.github/scripts/random-tests-config.txt index b5e00d270e23..b5a05ade82ac 100644 --- a/.github/scripts/random-tests-config.txt +++ b/.github/scripts/random-tests-config.txt @@ -16,13 +16,13 @@ CLI # Commands # Config Cookie -DataCaster +# DataCaster # DataConverter # Database -Debug -Email -Encryption -Entity +# Debug +# Email +# Encryption +# Entity Events Files # Filters @@ -31,7 +31,7 @@ Format # Helpers # Honeypot HotReloader -I18n +# I18n # Images Language Log From ebd7b4258c6ff9418b495f4da41e0402306df46d Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sat, 11 Apr 2026 00:27:36 +0800 Subject: [PATCH 21/85] test: group commands tests similar to `system/Commands/` (#10096) --- tests/system/Commands/{ => Cache}/ClearCacheTest.php | 2 +- tests/system/Commands/{ => Cache}/InfoCacheTest.php | 2 +- tests/system/Commands/CreateDatabaseTest.php | 2 ++ tests/system/Commands/DatabaseCommandsTest.php | 2 ++ .../Commands/{ => Encryption}/GenerateKeyTest.php | 2 +- .../Commands/{ => Generators}/CellGeneratorTest.php | 2 +- .../Commands/{ => Generators}/CommandGeneratorTest.php | 2 +- .../Commands/{ => Generators}/ConfigGeneratorTest.php | 2 +- .../{ => Generators}/ControllerGeneratorTest.php | 2 +- .../Commands/{ => Generators}/EntityGeneratorTest.php | 2 +- .../Commands/{ => Generators}/FilterGeneratorTest.php | 2 +- .../Commands/{ => Generators}/GeneratorsTest.php | 2 +- .../{ => Generators}/MigrationGeneratorTest.php | 2 +- .../Commands/{ => Generators}/ModelGeneratorTest.php | 2 +- .../{ => Generators}/ScaffoldGeneratorTest.php | 2 +- .../Commands/{ => Generators}/SeederGeneratorTest.php | 2 +- .../Commands/{ => Generators}/TestGeneratorTest.php | 2 +- .../{ => Generators}/TransformerGeneratorTest.php | 2 +- .../{ => Generators}/ValidationGeneratorTest.php | 2 +- .../Commands/{ => Housekeeping}/ClearDebugbarTest.php | 2 +- .../Commands/{ => Housekeeping}/ClearLogsTest.php | 2 +- tests/system/Commands/MigrationIntegrationTest.php | 2 ++ .../{ => Utilities}/EnvironmentCommandTest.php | 2 +- .../Commands/{ => Utilities}/FilterCheckTest.php | 2 +- .../Commands/{ => Utilities}/PublishCommandTest.php | 2 +- tests/system/Commands/{ => Utilities}/RoutesTest.php | 2 +- .../Commands/{ => Worker}/WorkerCommandsTest.php | 2 +- utils/phpstan-baseline/argument.type.neon | 2 +- utils/phpstan-baseline/ternary.shortNotAllowed.neon | 10 +++++----- 29 files changed, 36 insertions(+), 30 deletions(-) rename tests/system/Commands/{ => Cache}/ClearCacheTest.php (98%) rename tests/system/Commands/{ => Cache}/InfoCacheTest.php (98%) rename tests/system/Commands/{ => Encryption}/GenerateKeyTest.php (99%) rename tests/system/Commands/{ => Generators}/CellGeneratorTest.php (98%) rename tests/system/Commands/{ => Generators}/CommandGeneratorTest.php (99%) rename tests/system/Commands/{ => Generators}/ConfigGeneratorTest.php (96%) rename tests/system/Commands/{ => Generators}/ControllerGeneratorTest.php (98%) rename tests/system/Commands/{ => Generators}/EntityGeneratorTest.php (96%) rename tests/system/Commands/{ => Generators}/FilterGeneratorTest.php (96%) rename tests/system/Commands/{ => Generators}/GeneratorsTest.php (98%) rename tests/system/Commands/{ => Generators}/MigrationGeneratorTest.php (97%) rename tests/system/Commands/{ => Generators}/ModelGeneratorTest.php (99%) rename tests/system/Commands/{ => Generators}/ScaffoldGeneratorTest.php (99%) rename tests/system/Commands/{ => Generators}/SeederGeneratorTest.php (97%) rename tests/system/Commands/{ => Generators}/TestGeneratorTest.php (98%) rename tests/system/Commands/{ => Generators}/TransformerGeneratorTest.php (98%) rename tests/system/Commands/{ => Generators}/ValidationGeneratorTest.php (96%) rename tests/system/Commands/{ => Housekeeping}/ClearDebugbarTest.php (98%) rename tests/system/Commands/{ => Housekeeping}/ClearLogsTest.php (99%) rename tests/system/Commands/{ => Utilities}/EnvironmentCommandTest.php (98%) rename tests/system/Commands/{ => Utilities}/FilterCheckTest.php (97%) rename tests/system/Commands/{ => Utilities}/PublishCommandTest.php (96%) rename tests/system/Commands/{ => Utilities}/RoutesTest.php (99%) rename tests/system/Commands/{ => Worker}/WorkerCommandsTest.php (99%) diff --git a/tests/system/Commands/ClearCacheTest.php b/tests/system/Commands/Cache/ClearCacheTest.php similarity index 98% rename from tests/system/Commands/ClearCacheTest.php rename to tests/system/Commands/Cache/ClearCacheTest.php index caa37f74f7d9..5b027e04f05a 100644 --- a/tests/system/Commands/ClearCacheTest.php +++ b/tests/system/Commands/Cache/ClearCacheTest.php @@ -11,7 +11,7 @@ * the LICENSE file that was distributed with this source code. */ -namespace CodeIgniter\Commands; +namespace CodeIgniter\Commands\Cache; use CodeIgniter\Cache\CacheFactory; use CodeIgniter\Cache\Handlers\FileHandler; diff --git a/tests/system/Commands/InfoCacheTest.php b/tests/system/Commands/Cache/InfoCacheTest.php similarity index 98% rename from tests/system/Commands/InfoCacheTest.php rename to tests/system/Commands/Cache/InfoCacheTest.php index e8cc409356f5..44aa389338cc 100644 --- a/tests/system/Commands/InfoCacheTest.php +++ b/tests/system/Commands/Cache/InfoCacheTest.php @@ -11,7 +11,7 @@ * the LICENSE file that was distributed with this source code. */ -namespace CodeIgniter\Commands; +namespace CodeIgniter\Commands\Cache; use CodeIgniter\Cache\CacheFactory; use CodeIgniter\Test\CIUnitTestCase; diff --git a/tests/system/Commands/CreateDatabaseTest.php b/tests/system/Commands/CreateDatabaseTest.php index a7e0fe11638f..5c78ba91470d 100644 --- a/tests/system/Commands/CreateDatabaseTest.php +++ b/tests/system/Commands/CreateDatabaseTest.php @@ -22,6 +22,8 @@ use PHPUnit\Framework\Attributes\Group; /** + * @todo To figure out how to transfer this test to `tests/system/Commands/Database/` without breaking DatabaseLive group. + * * @internal */ #[Group('DatabaseLive')] diff --git a/tests/system/Commands/DatabaseCommandsTest.php b/tests/system/Commands/DatabaseCommandsTest.php index 02d6e6b38963..7767b54f69f9 100644 --- a/tests/system/Commands/DatabaseCommandsTest.php +++ b/tests/system/Commands/DatabaseCommandsTest.php @@ -18,6 +18,8 @@ use PHPUnit\Framework\Attributes\Group; /** + * @todo To figure out how to transfer this test to `tests/system/Commands/Database/` without breaking DatabaseLive group. + * * @internal */ #[Group('DatabaseLive')] diff --git a/tests/system/Commands/GenerateKeyTest.php b/tests/system/Commands/Encryption/GenerateKeyTest.php similarity index 99% rename from tests/system/Commands/GenerateKeyTest.php rename to tests/system/Commands/Encryption/GenerateKeyTest.php index 6ed0d6c94b93..a4fb452fd5a2 100644 --- a/tests/system/Commands/GenerateKeyTest.php +++ b/tests/system/Commands/Encryption/GenerateKeyTest.php @@ -11,7 +11,7 @@ * the LICENSE file that was distributed with this source code. */ -namespace CodeIgniter\Commands; +namespace CodeIgniter\Commands\Encryption; use CodeIgniter\Config\Services; use CodeIgniter\Superglobals; diff --git a/tests/system/Commands/CellGeneratorTest.php b/tests/system/Commands/Generators/CellGeneratorTest.php similarity index 98% rename from tests/system/Commands/CellGeneratorTest.php rename to tests/system/Commands/Generators/CellGeneratorTest.php index 31ec6dc3921e..3412246966cd 100644 --- a/tests/system/Commands/CellGeneratorTest.php +++ b/tests/system/Commands/Generators/CellGeneratorTest.php @@ -11,7 +11,7 @@ * the LICENSE file that was distributed with this source code. */ -namespace CodeIgniter\Commands; +namespace CodeIgniter\Commands\Generators; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; diff --git a/tests/system/Commands/CommandGeneratorTest.php b/tests/system/Commands/Generators/CommandGeneratorTest.php similarity index 99% rename from tests/system/Commands/CommandGeneratorTest.php rename to tests/system/Commands/Generators/CommandGeneratorTest.php index d485d6e4f063..c2606051d57b 100644 --- a/tests/system/Commands/CommandGeneratorTest.php +++ b/tests/system/Commands/Generators/CommandGeneratorTest.php @@ -11,7 +11,7 @@ * the LICENSE file that was distributed with this source code. */ -namespace CodeIgniter\Commands; +namespace CodeIgniter\Commands\Generators; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; diff --git a/tests/system/Commands/ConfigGeneratorTest.php b/tests/system/Commands/Generators/ConfigGeneratorTest.php similarity index 96% rename from tests/system/Commands/ConfigGeneratorTest.php rename to tests/system/Commands/Generators/ConfigGeneratorTest.php index ab4914914719..473efff9a3a0 100644 --- a/tests/system/Commands/ConfigGeneratorTest.php +++ b/tests/system/Commands/Generators/ConfigGeneratorTest.php @@ -11,7 +11,7 @@ * the LICENSE file that was distributed with this source code. */ -namespace CodeIgniter\Commands; +namespace CodeIgniter\Commands\Generators; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; diff --git a/tests/system/Commands/ControllerGeneratorTest.php b/tests/system/Commands/Generators/ControllerGeneratorTest.php similarity index 98% rename from tests/system/Commands/ControllerGeneratorTest.php rename to tests/system/Commands/Generators/ControllerGeneratorTest.php index dc8293bb3997..e1c77bcb6d5d 100644 --- a/tests/system/Commands/ControllerGeneratorTest.php +++ b/tests/system/Commands/Generators/ControllerGeneratorTest.php @@ -11,7 +11,7 @@ * the LICENSE file that was distributed with this source code. */ -namespace CodeIgniter\Commands; +namespace CodeIgniter\Commands\Generators; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; diff --git a/tests/system/Commands/EntityGeneratorTest.php b/tests/system/Commands/Generators/EntityGeneratorTest.php similarity index 96% rename from tests/system/Commands/EntityGeneratorTest.php rename to tests/system/Commands/Generators/EntityGeneratorTest.php index 1e764add9cec..f8dce1c17608 100644 --- a/tests/system/Commands/EntityGeneratorTest.php +++ b/tests/system/Commands/Generators/EntityGeneratorTest.php @@ -11,7 +11,7 @@ * the LICENSE file that was distributed with this source code. */ -namespace CodeIgniter\Commands; +namespace CodeIgniter\Commands\Generators; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; diff --git a/tests/system/Commands/FilterGeneratorTest.php b/tests/system/Commands/Generators/FilterGeneratorTest.php similarity index 96% rename from tests/system/Commands/FilterGeneratorTest.php rename to tests/system/Commands/Generators/FilterGeneratorTest.php index b336387d3ab4..a1144c1bc31d 100644 --- a/tests/system/Commands/FilterGeneratorTest.php +++ b/tests/system/Commands/Generators/FilterGeneratorTest.php @@ -11,7 +11,7 @@ * the LICENSE file that was distributed with this source code. */ -namespace CodeIgniter\Commands; +namespace CodeIgniter\Commands\Generators; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; diff --git a/tests/system/Commands/GeneratorsTest.php b/tests/system/Commands/Generators/GeneratorsTest.php similarity index 98% rename from tests/system/Commands/GeneratorsTest.php rename to tests/system/Commands/Generators/GeneratorsTest.php index f72ceabc710c..bb031b286f94 100644 --- a/tests/system/Commands/GeneratorsTest.php +++ b/tests/system/Commands/Generators/GeneratorsTest.php @@ -11,7 +11,7 @@ * the LICENSE file that was distributed with this source code. */ -namespace CodeIgniter\Commands; +namespace CodeIgniter\Commands\Generators; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; diff --git a/tests/system/Commands/MigrationGeneratorTest.php b/tests/system/Commands/Generators/MigrationGeneratorTest.php similarity index 97% rename from tests/system/Commands/MigrationGeneratorTest.php rename to tests/system/Commands/Generators/MigrationGeneratorTest.php index 0f999d638789..c4670019a422 100644 --- a/tests/system/Commands/MigrationGeneratorTest.php +++ b/tests/system/Commands/Generators/MigrationGeneratorTest.php @@ -11,7 +11,7 @@ * the LICENSE file that was distributed with this source code. */ -namespace CodeIgniter\Commands; +namespace CodeIgniter\Commands\Generators; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; diff --git a/tests/system/Commands/ModelGeneratorTest.php b/tests/system/Commands/Generators/ModelGeneratorTest.php similarity index 99% rename from tests/system/Commands/ModelGeneratorTest.php rename to tests/system/Commands/Generators/ModelGeneratorTest.php index 65596958606c..520bbbdc7a3b 100644 --- a/tests/system/Commands/ModelGeneratorTest.php +++ b/tests/system/Commands/Generators/ModelGeneratorTest.php @@ -11,7 +11,7 @@ * the LICENSE file that was distributed with this source code. */ -namespace CodeIgniter\Commands; +namespace CodeIgniter\Commands\Generators; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; diff --git a/tests/system/Commands/ScaffoldGeneratorTest.php b/tests/system/Commands/Generators/ScaffoldGeneratorTest.php similarity index 99% rename from tests/system/Commands/ScaffoldGeneratorTest.php rename to tests/system/Commands/Generators/ScaffoldGeneratorTest.php index fe99f2c38ccd..341dac0f8573 100644 --- a/tests/system/Commands/ScaffoldGeneratorTest.php +++ b/tests/system/Commands/Generators/ScaffoldGeneratorTest.php @@ -11,7 +11,7 @@ * the LICENSE file that was distributed with this source code. */ -namespace CodeIgniter\Commands; +namespace CodeIgniter\Commands\Generators; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; diff --git a/tests/system/Commands/SeederGeneratorTest.php b/tests/system/Commands/Generators/SeederGeneratorTest.php similarity index 97% rename from tests/system/Commands/SeederGeneratorTest.php rename to tests/system/Commands/Generators/SeederGeneratorTest.php index b8f504489c5b..c455576876bb 100644 --- a/tests/system/Commands/SeederGeneratorTest.php +++ b/tests/system/Commands/Generators/SeederGeneratorTest.php @@ -11,7 +11,7 @@ * the LICENSE file that was distributed with this source code. */ -namespace CodeIgniter\Commands; +namespace CodeIgniter\Commands\Generators; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; diff --git a/tests/system/Commands/TestGeneratorTest.php b/tests/system/Commands/Generators/TestGeneratorTest.php similarity index 98% rename from tests/system/Commands/TestGeneratorTest.php rename to tests/system/Commands/Generators/TestGeneratorTest.php index b67c136a4009..71225847cfce 100644 --- a/tests/system/Commands/TestGeneratorTest.php +++ b/tests/system/Commands/Generators/TestGeneratorTest.php @@ -11,7 +11,7 @@ * the LICENSE file that was distributed with this source code. */ -namespace CodeIgniter\Commands; +namespace CodeIgniter\Commands\Generators; use CodeIgniter\CLI\CLI; use CodeIgniter\Test\CIUnitTestCase; diff --git a/tests/system/Commands/TransformerGeneratorTest.php b/tests/system/Commands/Generators/TransformerGeneratorTest.php similarity index 98% rename from tests/system/Commands/TransformerGeneratorTest.php rename to tests/system/Commands/Generators/TransformerGeneratorTest.php index 588bf3243d68..2b007d590aaa 100644 --- a/tests/system/Commands/TransformerGeneratorTest.php +++ b/tests/system/Commands/Generators/TransformerGeneratorTest.php @@ -11,7 +11,7 @@ * the LICENSE file that was distributed with this source code. */ -namespace CodeIgniter\Commands; +namespace CodeIgniter\Commands\Generators; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; diff --git a/tests/system/Commands/ValidationGeneratorTest.php b/tests/system/Commands/Generators/ValidationGeneratorTest.php similarity index 96% rename from tests/system/Commands/ValidationGeneratorTest.php rename to tests/system/Commands/Generators/ValidationGeneratorTest.php index 0bcbcdec12e7..6779e9a6d4d5 100644 --- a/tests/system/Commands/ValidationGeneratorTest.php +++ b/tests/system/Commands/Generators/ValidationGeneratorTest.php @@ -11,7 +11,7 @@ * the LICENSE file that was distributed with this source code. */ -namespace CodeIgniter\Commands; +namespace CodeIgniter\Commands\Generators; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; diff --git a/tests/system/Commands/ClearDebugbarTest.php b/tests/system/Commands/Housekeeping/ClearDebugbarTest.php similarity index 98% rename from tests/system/Commands/ClearDebugbarTest.php rename to tests/system/Commands/Housekeeping/ClearDebugbarTest.php index dc1de4dc6f3b..bb14906bcff9 100644 --- a/tests/system/Commands/ClearDebugbarTest.php +++ b/tests/system/Commands/Housekeeping/ClearDebugbarTest.php @@ -11,7 +11,7 @@ * the LICENSE file that was distributed with this source code. */ -namespace CodeIgniter\Commands; +namespace CodeIgniter\Commands\Housekeeping; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; diff --git a/tests/system/Commands/ClearLogsTest.php b/tests/system/Commands/Housekeeping/ClearLogsTest.php similarity index 99% rename from tests/system/Commands/ClearLogsTest.php rename to tests/system/Commands/Housekeeping/ClearLogsTest.php index b2c4c8b0664c..70406956d66e 100644 --- a/tests/system/Commands/ClearLogsTest.php +++ b/tests/system/Commands/Housekeeping/ClearLogsTest.php @@ -11,7 +11,7 @@ * the LICENSE file that was distributed with this source code. */ -namespace CodeIgniter\Commands; +namespace CodeIgniter\Commands\Housekeeping; use CodeIgniter\CLI\CLI; use CodeIgniter\Test\CIUnitTestCase; diff --git a/tests/system/Commands/MigrationIntegrationTest.php b/tests/system/Commands/MigrationIntegrationTest.php index 12869f9092c6..df924db690c3 100644 --- a/tests/system/Commands/MigrationIntegrationTest.php +++ b/tests/system/Commands/MigrationIntegrationTest.php @@ -18,6 +18,8 @@ use PHPUnit\Framework\Attributes\Group; /** + * @todo To figure out how to transfer this test to `tests/system/Commands/Database/` without breaking DatabaseLive group. + * * @internal */ #[Group('DatabaseLive')] diff --git a/tests/system/Commands/EnvironmentCommandTest.php b/tests/system/Commands/Utilities/EnvironmentCommandTest.php similarity index 98% rename from tests/system/Commands/EnvironmentCommandTest.php rename to tests/system/Commands/Utilities/EnvironmentCommandTest.php index 597805ee8a38..ca5236f5ff5b 100644 --- a/tests/system/Commands/EnvironmentCommandTest.php +++ b/tests/system/Commands/Utilities/EnvironmentCommandTest.php @@ -11,7 +11,7 @@ * the LICENSE file that was distributed with this source code. */ -namespace CodeIgniter\Commands; +namespace CodeIgniter\Commands\Utilities; use CodeIgniter\Config\Services; use CodeIgniter\Superglobals; diff --git a/tests/system/Commands/FilterCheckTest.php b/tests/system/Commands/Utilities/FilterCheckTest.php similarity index 97% rename from tests/system/Commands/FilterCheckTest.php rename to tests/system/Commands/Utilities/FilterCheckTest.php index c6644e02e94e..b2f0d3b8144d 100644 --- a/tests/system/Commands/FilterCheckTest.php +++ b/tests/system/Commands/Utilities/FilterCheckTest.php @@ -11,7 +11,7 @@ * the LICENSE file that was distributed with this source code. */ -namespace CodeIgniter\Commands; +namespace CodeIgniter\Commands\Utilities; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; diff --git a/tests/system/Commands/PublishCommandTest.php b/tests/system/Commands/Utilities/PublishCommandTest.php similarity index 96% rename from tests/system/Commands/PublishCommandTest.php rename to tests/system/Commands/Utilities/PublishCommandTest.php index 0cd2605aaa1e..ad35865367ec 100644 --- a/tests/system/Commands/PublishCommandTest.php +++ b/tests/system/Commands/Utilities/PublishCommandTest.php @@ -11,7 +11,7 @@ * the LICENSE file that was distributed with this source code. */ -namespace CodeIgniter\Commands; +namespace CodeIgniter\Commands\Utilities; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; diff --git a/tests/system/Commands/RoutesTest.php b/tests/system/Commands/Utilities/RoutesTest.php similarity index 99% rename from tests/system/Commands/RoutesTest.php rename to tests/system/Commands/Utilities/RoutesTest.php index a28c0cc02e1a..bf1789135387 100644 --- a/tests/system/Commands/RoutesTest.php +++ b/tests/system/Commands/Utilities/RoutesTest.php @@ -11,7 +11,7 @@ * the LICENSE file that was distributed with this source code. */ -namespace CodeIgniter\Commands; +namespace CodeIgniter\Commands\Utilities; use CodeIgniter\Router\RouteCollection; use CodeIgniter\Test\CIUnitTestCase; diff --git a/tests/system/Commands/WorkerCommandsTest.php b/tests/system/Commands/Worker/WorkerCommandsTest.php similarity index 99% rename from tests/system/Commands/WorkerCommandsTest.php rename to tests/system/Commands/Worker/WorkerCommandsTest.php index be0880f9157d..ff570356fdcf 100644 --- a/tests/system/Commands/WorkerCommandsTest.php +++ b/tests/system/Commands/Worker/WorkerCommandsTest.php @@ -11,7 +11,7 @@ * the LICENSE file that was distributed with this source code. */ -namespace CodeIgniter\Commands; +namespace CodeIgniter\Commands\Worker; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; diff --git a/utils/phpstan-baseline/argument.type.neon b/utils/phpstan-baseline/argument.type.neon index 3bb95bae65fb..4a1fe1aed2ef 100644 --- a/utils/phpstan-baseline/argument.type.neon +++ b/utils/phpstan-baseline/argument.type.neon @@ -50,7 +50,7 @@ parameters: - message: '#^Parameter \#2 \$mock of static method CodeIgniter\\Config\\BaseService\:\:injectMock\(\) expects object, null given\.$#' count: 4 - path: ../../tests/system/Commands/RoutesTest.php + path: ../../tests/system/Commands/Utilities/RoutesTest.php - message: '#^Parameter \#1 \$expected of method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) expects class\-string\, string given\.$#' diff --git a/utils/phpstan-baseline/ternary.shortNotAllowed.neon b/utils/phpstan-baseline/ternary.shortNotAllowed.neon index 0f831fde0a46..25ab1dce60e0 100644 --- a/utils/phpstan-baseline/ternary.shortNotAllowed.neon +++ b/utils/phpstan-baseline/ternary.shortNotAllowed.neon @@ -85,27 +85,27 @@ parameters: - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' count: 1 - path: ../../tests/system/Commands/CellGeneratorTest.php + path: ../../tests/system/Commands/Generators/CellGeneratorTest.php - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' count: 1 - path: ../../tests/system/Commands/CommandGeneratorTest.php + path: ../../tests/system/Commands/Generators/CommandGeneratorTest.php - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' count: 1 - path: ../../tests/system/Commands/ControllerGeneratorTest.php + path: ../../tests/system/Commands/Generators/ControllerGeneratorTest.php - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' count: 1 - path: ../../tests/system/Commands/ModelGeneratorTest.php + path: ../../tests/system/Commands/Generators/ModelGeneratorTest.php - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' count: 1 - path: ../../tests/system/Commands/ScaffoldGeneratorTest.php + path: ../../tests/system/Commands/Generators/ScaffoldGeneratorTest.php - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' From fbfc98228976338ee7ec788bbd02a48a866cad9b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 18:56:03 +0200 Subject: [PATCH 22/85] chore(deps): bump actions/github-script in / (#10100) Bumps [actions/github-script](https://github.com/actions/github-script) in `/` from 8.0.0 to 9.0.0. Updates `actions/github-script` from 8.0.0 to 9.0.0 - [Release notes](https://github.com/actions/github-script/releases) - [Commits](https://github.com/actions/github-script/compare/ed597411d8f924073f98dfc5c65a23a2325f34cd...3a2844b7e9c422d3c10d287c895573f7108da1b3) --- updated-dependencies: - dependency-name: actions/github-script dependency-version: 9.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github_actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy-distributables.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-distributables.yml b/.github/workflows/deploy-distributables.yml index cb3f2ca8194b..388829840852 100644 --- a/.github/workflows/deploy-distributables.yml +++ b/.github/workflows/deploy-distributables.yml @@ -72,7 +72,7 @@ jobs: run: ./source/.github/scripts/deploy-framework ${GITHUB_WORKSPACE}/source ${GITHUB_WORKSPACE}/framework ${GITHUB_REF##*/} - name: Release - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{secrets.ACCESS_TOKEN}} script: | @@ -126,7 +126,7 @@ jobs: run: ./source/.github/scripts/deploy-appstarter ${GITHUB_WORKSPACE}/source ${GITHUB_WORKSPACE}/appstarter ${GITHUB_REF##*/} - name: Release - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{secrets.ACCESS_TOKEN}} script: | @@ -190,7 +190,7 @@ jobs: run: ./source/.github/scripts/deploy-userguide ${GITHUB_WORKSPACE}/source ${GITHUB_WORKSPACE}/userguide ${GITHUB_REF##*/} - name: Release - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{secrets.ACCESS_TOKEN}} script: | From 8ca64a9142f0329f94b559777d0ec0af1a440812 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sat, 11 Apr 2026 02:14:49 +0800 Subject: [PATCH 23/85] fix: ensure output buffer is closed after use of `command()` (#10099) --- system/Common.php | 10 ++++-- .../_support/Commands/DestructiveCommand.php | 31 +++++++++++++++++++ tests/system/Commands/CommandTest.php | 8 +++++ .../Translation/LocalizationSyncTest.php | 2 -- user_guide_src/source/changelogs/v4.7.3.rst | 1 + 5 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 tests/_support/Commands/DestructiveCommand.php diff --git a/system/Common.php b/system/Common.php index bcf2a5c14db9..ea0c476423a0 100644 --- a/system/Common.php +++ b/system/Common.php @@ -185,10 +185,14 @@ function command(string $command) $params[$arg] = $value; } - ob_start(); - service('commands')->run($command, $params); + try { + ob_start(); + service('commands')->run($command, $params); - return ob_get_clean(); + return ob_get_contents(); + } finally { + ob_end_clean(); + } } } diff --git a/tests/_support/Commands/DestructiveCommand.php b/tests/_support/Commands/DestructiveCommand.php new file mode 100644 index 000000000000..723b880b0cd4 --- /dev/null +++ b/tests/_support/Commands/DestructiveCommand.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Tests\Support\Commands; + +use RuntimeException; + +/** + * @internal + */ +final class DestructiveCommand extends AbstractInfo +{ + protected $group = 'demo'; + protected $name = 'app:destructive'; + protected $description = 'This command is destructive.'; + + public function run(array $params): never + { + throw new RuntimeException('This command is destructive and should not be run.'); + } +} diff --git a/tests/system/Commands/CommandTest.php b/tests/system/Commands/CommandTest.php index 1c6e81033e63..6b668d2640c5 100644 --- a/tests/system/Commands/CommandTest.php +++ b/tests/system/Commands/CommandTest.php @@ -19,6 +19,7 @@ use CodeIgniter\Test\StreamFilterTrait; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use RuntimeException; use Tests\Support\Commands\AppInfo; use Tests\Support\Commands\ParamsReveal; @@ -136,6 +137,13 @@ public function testInexistentCommandsButWithManyAlternatives(): void $this->assertStringContainsString(':clear', $this->getBuffer()); } + public function testDestructiveCommandIsNotRisky(): void + { + $this->expectException(RuntimeException::class); + + command('app:destructive'); + } + /** * @param list $expected */ diff --git a/tests/system/Commands/Translation/LocalizationSyncTest.php b/tests/system/Commands/Translation/LocalizationSyncTest.php index f6dc00764eac..d485b19fd9b2 100644 --- a/tests/system/Commands/Translation/LocalizationSyncTest.php +++ b/tests/system/Commands/Translation/LocalizationSyncTest.php @@ -171,7 +171,6 @@ public function testSyncWithNullableOriginalLangValue(): void TEXT_WRAP; file_put_contents(self::$languageTestPath . self::$locale . '/SyncInvalid.php', $langWithNullValue); - ob_get_flush(); $this->expectException(LogicException::class); $this->expectExceptionMessageMatches('/Only "array" or "string" is allowed/'); @@ -192,7 +191,6 @@ public function testSyncWithIntegerOriginalLangValue(): void TEXT_WRAP; file_put_contents(self::$languageTestPath . self::$locale . '/SyncInvalid.php', $langWithIntegerValue); - ob_get_flush(); $this->expectException(LogicException::class); $this->expectExceptionMessageMatches('/Only "array" or "string" is allowed/'); diff --git a/user_guide_src/source/changelogs/v4.7.3.rst b/user_guide_src/source/changelogs/v4.7.3.rst index be42d6379b8d..50418133d15e 100644 --- a/user_guide_src/source/changelogs/v4.7.3.rst +++ b/user_guide_src/source/changelogs/v4.7.3.rst @@ -33,6 +33,7 @@ Bugs Fixed ********** - **Autoloader:** Fixed a bug where ``Autoloader::unregister()`` (used during tests) silently failed to remove handlers from the SPL autoload stack, causing closures to accumulate permanently. +- **Common:** Fixed a bug where the ``command()`` helper function did not properly clean up output buffers, which could lead to risky tests when exceptions were thrown. See the repo's `CHANGELOG.md `_ From f8deb60bb54b21b8aa3072f53183f786a53f1106 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sun, 12 Apr 2026 03:41:21 +0800 Subject: [PATCH 24/85] chore: fix labeler workflow (#10104) * chore: fix labeler workflow * revert now to use pull_request_target --- .github/labeler.yml | 8 +++---- .github/workflows/label-pr.yml | 42 ++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index 380ff441c04a..69cb0c51e123 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -10,14 +10,14 @@ - any-glob-to-any-file: - '.github/workflows/*' -# Add the `documentation` label to PRs that change any file in the `user_guide_src/source/` directory. +# Add the `documentation` label to PRs for documentation only. 'documentation': - changed-files: - any-glob-to-all-files: - - 'user_guide_src/source/*' + - 'user_guide_src/source/**' -# Add the `testing` label to PRs that change files in the `tests/` directory ONLY. +# Add the `testing` label to PRs that changes tests only. 'testing': - changed-files: - any-glob-to-all-files: - - 'tests/*' + - 'tests/**' diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml index 730c98f271c1..bc93f5e49dac 100644 --- a/.github/workflows/label-pr.yml +++ b/.github/workflows/label-pr.yml @@ -7,13 +7,55 @@ on: - pull_request_target jobs: + validate-source: + permissions: + contents: read + pull-requests: read + runs-on: ubuntu-24.04 + outputs: + valid: ${{ steps.check.outputs.valid }} + + steps: + - name: Check if PR is from the main repository + id: check + run: | + if [[ "$HEAD_REPO" == "codeigniter4/CodeIgniter4" ]]; then + echo "valid=true" >> $GITHUB_OUTPUT + else + echo "valid=false" >> $GITHUB_OUTPUT + fi + env: + HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} + add-labels: + needs: validate-source permissions: contents: read pull-requests: write runs-on: ubuntu-24.04 steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Verify PR source for workflow file changes + run: | + # Get changed files in this PR + git fetch origin "refs/pull/${{ github.event.pull_request.number }}/merge" + CHANGED_FILES=$(git diff --name-only origin/develop FETCH_HEAD 2>/dev/null || echo "") + + # Check if this workflow file is being modified + if echo "$CHANGED_FILES" | grep -q "\.github/workflows/label-pr\.yml"; then + if [[ "$IS_VALID" != "true" ]]; then + echo "::error::Changes to label-pr.yml can only be made from the main repository." + exit 1 + fi + fi + env: + IS_VALID: ${{ needs.validate-source.outputs.valid }} + - name: Add labels uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 with: From df914e2285823e4531b6806df1bde2179d8f6736 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sun, 12 Apr 2026 03:42:29 +0800 Subject: [PATCH 25/85] chore: refactor phpunit config file (#10102) --- .gitattributes | 2 +- .gitignore | 2 +- admin/framework/.gitignore | 2 +- .../{phpunit.xml.dist => phpunit.dist.xml} | 15 ++++++++------- admin/starter/.gitignore | 2 +- .../{phpunit.xml.dist => phpunit.dist.xml} | 15 ++++++++------- phpunit.xml.dist => phpunit.dist.xml | 11 ++++++----- tests/system/Files/FileTest.php | 2 +- 8 files changed, 27 insertions(+), 24 deletions(-) rename admin/framework/{phpunit.xml.dist => phpunit.dist.xml} (88%) rename admin/starter/{phpunit.xml.dist => phpunit.dist.xml} (88%) rename phpunit.xml.dist => phpunit.dist.xml (91%) diff --git a/.gitattributes b/.gitattributes index 3f67f2e35167..c2b4bb0759c9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -27,7 +27,7 @@ phpmetrics.json export-ignore phpstan-baseline.php export-ignore phpstan-bootstrap.php export-ignore phpstan.neon.dist export-ignore -phpunit.xml.dist export-ignore +phpunit.dist.xml export-ignore psalm-baseline.xml export-ignore psalm.xml export-ignore psalm_autoload.php export-ignore diff --git a/.gitignore b/.gitignore index e18328fc9b15..3e4a7c1d18f7 100644 --- a/.gitignore +++ b/.gitignore @@ -127,6 +127,6 @@ _modules/* .vscode/ /results/ -/phpunit*.xml +/phpunit.xml /.php-cs-fixer.php diff --git a/admin/framework/.gitignore b/admin/framework/.gitignore index d69ef2f7dbdb..56b9f10d9160 100644 --- a/admin/framework/.gitignore +++ b/admin/framework/.gitignore @@ -123,4 +123,4 @@ _modules/* .vscode/ /results/ -/phpunit*.xml +/phpunit.xml diff --git a/admin/framework/phpunit.xml.dist b/admin/framework/phpunit.dist.xml similarity index 88% rename from admin/framework/phpunit.xml.dist rename to admin/framework/phpunit.dist.xml index dea940878617..98e56e2141a5 100644 --- a/admin/framework/phpunit.xml.dist +++ b/admin/framework/phpunit.dist.xml @@ -1,7 +1,7 @@ - + cacheDirectory="build/.phpunit.cache" +> + @@ -22,16 +19,19 @@ + ./tests + + ./app @@ -41,6 +41,7 @@ ./app/Config/Routes.php + diff --git a/admin/starter/.gitignore b/admin/starter/.gitignore index d69ef2f7dbdb..56b9f10d9160 100644 --- a/admin/starter/.gitignore +++ b/admin/starter/.gitignore @@ -123,4 +123,4 @@ _modules/* .vscode/ /results/ -/phpunit*.xml +/phpunit.xml diff --git a/admin/starter/phpunit.xml.dist b/admin/starter/phpunit.dist.xml similarity index 88% rename from admin/starter/phpunit.xml.dist rename to admin/starter/phpunit.dist.xml index b408a99d988c..d9d2c6ade852 100644 --- a/admin/starter/phpunit.xml.dist +++ b/admin/starter/phpunit.dist.xml @@ -1,7 +1,7 @@ - + cacheDirectory="build/.phpunit.cache" +> + @@ -22,16 +19,19 @@ + ./tests + + ./app @@ -41,6 +41,7 @@ ./app/Config/Routes.php + diff --git a/phpunit.xml.dist b/phpunit.dist.xml similarity index 91% rename from phpunit.xml.dist rename to phpunit.dist.xml index a6dba0ff51b3..1de961c91f61 100644 --- a/phpunit.xml.dist +++ b/phpunit.dist.xml @@ -1,7 +1,7 @@ - + + + tests/system + system @@ -51,6 +51,7 @@ system/Test/FeatureTestCase.php + diff --git a/tests/system/Files/FileTest.php b/tests/system/Files/FileTest.php index 8975ba0a6fd2..b27c6b12c6da 100644 --- a/tests/system/Files/FileTest.php +++ b/tests/system/Files/FileTest.php @@ -54,7 +54,7 @@ public function testGuessExtension(): void $file = new File(SYSTEMPATH . 'index.html'); $this->assertSame('html', $file->guessExtension()); - $file = new File(ROOTPATH . 'phpunit.xml.dist'); + $file = new File(ROOTPATH . 'phpunit.dist.xml'); $this->assertSame('xml', $file->guessExtension()); $tmp = tempnam(SUPPORTPATH, 'foo'); From 05a02c9b126a3e1ab3fff93536c5b860f4f0ba20 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sun, 12 Apr 2026 21:19:00 +0800 Subject: [PATCH 26/85] chore: fixes for php-cs-fixer and psalm (#10105) --- app/Config/Routes.php | 4 +- app/Config/View.php | 10 +-- psalm-baseline.xml | 72 +------------------ system/Autoloader/Autoloader.php | 4 +- system/CLI/GeneratorTrait.php | 4 +- system/Cache/Handlers/PredisHandler.php | 7 +- system/Cache/Handlers/RedisHandler.php | 7 +- .../Commands/Translation/LocalizationSync.php | 4 +- system/Config/View.php | 24 ++----- system/Cookie/CookieStore.php | 4 +- system/DataCaster/Cast/DatetimeCast.php | 4 +- system/Database/BaseBuilder.php | 4 +- system/Database/BaseConnection.php | 4 +- system/Debug/Toolbar.php | 4 +- system/Helpers/filesystem_helper.php | 4 +- system/Helpers/kint_helper.php | 6 +- system/Pager/Pager.php | 4 +- system/View/Parser.php | 6 +- tests/_support/Config/Filters.php | 4 +- .../Commands/Utilities/ConfigCheckTest.php | 8 +-- .../ControllerMethodReaderTest.php | 3 +- .../Utilities/Routes/FilterCollectorTest.php | 6 +- .../Utilities/Routes/FilterFinderTest.php | 28 ++------ tests/system/Debug/ExceptionHandlerTest.php | 9 +-- tests/system/Debug/ExceptionsTest.php | 9 +-- tests/system/Filters/FiltersTest.php | 3 +- tests/system/Helpers/FormHelperTest.php | 3 +- .../system/Publisher/PublisherOutputTest.php | 4 +- .../SecurityCSRFSessionRandomizeTokenTest.php | 8 +-- .../Security/SecurityCSRFSessionTest.php | 8 +-- tests/system/Validation/RulesTest.php | 6 +- .../guides/first-app/static_pages/003.php | 4 +- .../installation/upgrade_routing/001.php | 4 +- 33 files changed, 59 insertions(+), 224 deletions(-) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index fc4914a6923b..4cf109ed3dd4 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -2,7 +2,5 @@ use CodeIgniter\Router\RouteCollection; -/** - * @var RouteCollection $routes - */ +/** @var RouteCollection $routes */ $routes->get('/', 'Home::index'); diff --git a/app/Config/View.php b/app/Config/View.php index 582ef73276b1..869df3cbce90 100644 --- a/app/Config/View.php +++ b/app/Config/View.php @@ -5,10 +5,6 @@ use CodeIgniter\Config\View as BaseView; use CodeIgniter\View\ViewDecoratorInterface; -/** - * @phpstan-type parser_callable (callable(mixed): mixed) - * @phpstan-type parser_callable_string (callable(mixed): mixed)&string - */ class View extends BaseView { /** @@ -34,8 +30,7 @@ class View extends BaseView * { title|esc(js) } * { created_on|date(Y-m-d)|esc(attr) } * - * @var array - * @phpstan-var array + * @var array */ public $filters = []; @@ -44,8 +39,7 @@ class View extends BaseView * by the core Parser by creating aliases that will be replaced with * any callable. Can be single or tag pair. * - * @var array|string> - * @phpstan-var array|parser_callable_string|parser_callable> + * @var array> */ public $plugins = []; diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 7a03f82c5e21..7285cab94899 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,22 +1,5 @@ - - - - |parser_callable_string|parser_callable>]]> - |parser_callable_string|parser_callable>]]> - ]]> - - - - - - - - |parser_callable_string|parser_callable>]]> - |parser_callable_string|parser_callable>]]> - ]]> - - + @@ -73,14 +56,6 @@ - - - |parser_callable_string|parser_callable>]]> - |parser_callable_string|parser_callable>]]> - - - - @@ -136,51 +111,6 @@ - - - country]]> - created_at]]> - deleted]]> - email]]> - name]]> - country]]> - created_at]]> - deleted]]> - email]]> - name]]> - - - - - country]]> - created_at]]> - created_at]]> - deleted]]> - email]]> - name]]> - name]]> - name]]> - name]]> - - - - - country]]> - deleted]]> - email]]> - id]]> - name]]> - country]]> - country]]> - deleted]]> - id]]> - name]]> - country]]> - deleted]]> - id]]> - name]]> - - diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index 29d119727f94..4666149f27e9 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -480,9 +480,7 @@ protected function discoverComposerNamespaces() return; } - /** - * @var ClassLoader $composer - */ + /** @var ClassLoader $composer */ $composer = include $this->composerPath; $paths = $composer->getPrefixesPsr4(); $classes = $composer->getClassMap(); diff --git a/system/CLI/GeneratorTrait.php b/system/CLI/GeneratorTrait.php index 62f5ceec09ae..7d66b8b290af 100644 --- a/system/CLI/GeneratorTrait.php +++ b/system/CLI/GeneratorTrait.php @@ -303,9 +303,7 @@ private function normalizeInputClassName(): string $component = singular($this->component); - /** - * @see https://regex101.com/r/a5KNCR/2 - */ + /** @see https://regex101.com/r/a5KNCR/2 */ $pattern = sprintf('/([a-z][a-z0-9_\/\\\\]+)(%s)$/i', $component); if (preg_match($pattern, $class, $matches) === 1) { diff --git a/system/Cache/Handlers/PredisHandler.php b/system/Cache/Handlers/PredisHandler.php index b1dfb0e18bfd..c868f34550e9 100644 --- a/system/Cache/Handlers/PredisHandler.php +++ b/system/Cache/Handlers/PredisHandler.php @@ -92,10 +92,9 @@ public function get(string $key): mixed } return match ($data['__ci_type']) { - 'array', 'object' => unserialize($data['__ci_value']), - // Yes, 'double' is returned and NOT 'float' + 'array', 'object' => unserialize($data['__ci_value']), 'boolean', 'integer', 'double', 'string', 'NULL' => settype($data['__ci_value'], $data['__ci_type']) ? $data['__ci_value'] : null, - default => null, + default => null, }; } @@ -111,7 +110,7 @@ public function save(string $key, mixed $value, int $ttl = 60): bool case 'boolean': case 'integer': - case 'double': // Yes, 'double' is returned and NOT 'float' + case 'double': case 'string': case 'NULL': break; diff --git a/system/Cache/Handlers/RedisHandler.php b/system/Cache/Handlers/RedisHandler.php index 7ab6e392bfbc..05cae32da440 100644 --- a/system/Cache/Handlers/RedisHandler.php +++ b/system/Cache/Handlers/RedisHandler.php @@ -108,10 +108,9 @@ public function get(string $key): mixed } return match ($data['__ci_type']) { - 'array', 'object' => unserialize($data['__ci_value']), - // Yes, 'double' is returned and NOT 'float' + 'array', 'object' => unserialize($data['__ci_value']), 'boolean', 'integer', 'double', 'string', 'NULL' => settype($data['__ci_value'], $data['__ci_type']) ? $data['__ci_value'] : null, - default => null, + default => null, }; } @@ -127,7 +126,7 @@ public function save(string $key, mixed $value, int $ttl = 60): bool case 'boolean': case 'integer': - case 'double': // Yes, 'double' is returned and NOT 'float' + case 'double': case 'string': case 'NULL': break; diff --git a/system/Commands/Translation/LocalizationSync.php b/system/Commands/Translation/LocalizationSync.php index 6381ab33ccad..f54df45c364c 100644 --- a/system/Commands/Translation/LocalizationSync.php +++ b/system/Commands/Translation/LocalizationSync.php @@ -131,9 +131,7 @@ private function process(string $originalLocale, string $targetLocale): int ), ); - /** - * @var array $files - */ + /** @var array $files */ $files = iterator_to_array($iterator, true); ksort($files); diff --git a/system/Config/View.php b/system/Config/View.php index 038c6fa774dc..c790a40e4ee2 100644 --- a/system/Config/View.php +++ b/system/Config/View.php @@ -16,16 +16,12 @@ use CodeIgniter\View\ViewDecoratorInterface; /** - * View configuration - * - * @phpstan-type parser_callable (callable(mixed): mixed) - * @phpstan-type parser_callable_string (callable(mixed): mixed)&string + * View configuration. */ class View extends BaseConfig { /** - * When false, the view method will clear the data between each - * call. + * When false, the view method will clear the data between each call. * * @var bool */ @@ -41,8 +37,7 @@ class View extends BaseConfig * * @psalm-suppress UndefinedDocblockClass * - * @var array - * @phpstan-var array + * @var array */ public $filters = []; @@ -53,18 +48,14 @@ class View extends BaseConfig * * @psalm-suppress UndefinedDocblockClass * - * @var array|string> - * @phpstan-var array|parser_callable_string|parser_callable> + * @var array> */ public $plugins = []; /** * Built-in View filters. * - * @psalm-suppress UndefinedDocblockClass - * - * @var array - * @phpstan-var array + * @var array */ protected $coreFilters = [ 'abs' => '\abs', @@ -93,10 +84,7 @@ class View extends BaseConfig /** * Built-in View plugins. * - * @psalm-suppress UndefinedDocblockClass - * - * @var array|string> - * @phpstan-var array|parser_callable_string|parser_callable> + * @var array> */ protected $corePlugins = [ 'csp_script_nonce' => '\CodeIgniter\View\Plugins::cspScriptNonce', diff --git a/system/Cookie/CookieStore.php b/system/Cookie/CookieStore.php index 6d5caa2aa5e0..c4b2994d9cf3 100644 --- a/system/Cookie/CookieStore.php +++ b/system/Cookie/CookieStore.php @@ -45,9 +45,7 @@ class CookieStore implements Countable, IteratorAggregate */ public static function fromCookieHeaders(array $headers, bool $raw = false) { - /** - * @var list $cookies - */ + /** @var list $cookies */ $cookies = array_filter(array_map(static function (string $header) use ($raw) { try { return Cookie::fromHeaderString($header, $raw); diff --git a/system/DataCaster/Cast/DatetimeCast.php b/system/DataCaster/Cast/DatetimeCast.php index b5f346c9c419..398fdc7beed7 100644 --- a/system/DataCaster/Cast/DatetimeCast.php +++ b/system/DataCaster/Cast/DatetimeCast.php @@ -40,9 +40,7 @@ public static function get( throw new InvalidArgumentException($message); } - /** - * @see https://www.php.net/manual/en/datetimeimmutable.createfromformat.php#datetimeimmutable.createfromformat.parameters - */ + /** @see https://www.php.net/manual/en/datetimeimmutable.createfromformat.php#datetimeimmutable.createfromformat.parameters */ $format = self::getDateTimeFormat($params, $helper); return Time::createFromFormat($format, $value); diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index fe646bb1fa6d..d891b3d61954 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -310,9 +310,7 @@ public function __construct($tableName, ConnectionInterface $db, ?array $options throw new DatabaseException('A table must be specified when creating a new Query Builder.'); } - /** - * @var BaseConnection $db - */ + /** @var BaseConnection $db */ $this->db = $db; if ($tableName instanceof TableName) { diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index bd2cb62053dd..dea7536de984 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -795,9 +795,7 @@ public function query(string $sql, $binds = null, bool $setEscapeFlags = true, s $this->initialize(); } - /** - * @var Query $query - */ + /** @var Query $query */ $query = new $queryClass($this); $query->setQuery($sql, $binds, $setEscapeFlags); diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index cd868e860da0..fdd0b0f3f54c 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -368,9 +368,7 @@ protected function roundTo(float $number, int $increments = 5): float */ public function prepare(?RequestInterface $request = null, ?ResponseInterface $response = null): void { - /** - * @var IncomingRequest|null $request - */ + /** @var IncomingRequest|null $request */ if (CI_DEBUG && ! is_cli()) { if ($this->hasNativeHeaderConflict()) { return; diff --git a/system/Helpers/filesystem_helper.php b/system/Helpers/filesystem_helper.php index e7bd35441959..da6acabb039e 100644 --- a/system/Helpers/filesystem_helper.php +++ b/system/Helpers/filesystem_helper.php @@ -84,9 +84,7 @@ function directory_mirror(string $originDir, string $targetDir, bool $overwrite $dirLen = strlen($originDir); - /** - * @var SplFileInfo $file - */ + /** @var SplFileInfo $file */ foreach (new RecursiveIteratorIterator( new RecursiveDirectoryIterator($originDir, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST, diff --git a/system/Helpers/kint_helper.php b/system/Helpers/kint_helper.php index 1f89dc78c55b..8d8e73bfc92f 100644 --- a/system/Helpers/kint_helper.php +++ b/system/Helpers/kint_helper.php @@ -69,18 +69,14 @@ function d(...$vars) /** * Provides a backtrace to the current execution point, from Kint. */ - /** - * trace function - */ function trace(): void { Kint::$aliases[] = 'trace'; Kint::trace(); } } else { - // In case that Kint is not loaded. /** - * trace function + * Generic trace function in case that Kint is not loaded. * * @return int */ diff --git a/system/Pager/Pager.php b/system/Pager/Pager.php index e1c38edf3594..a4883f3809ae 100644 --- a/system/Pager/Pager.php +++ b/system/Pager/Pager.php @@ -265,9 +265,7 @@ public function getPageURI(?int $page = null, string $group = 'default', bool $r { $this->ensureGroup($group); - /** - * @var URI $uri - */ + /** @var URI $uri */ $uri = $this->groups[$group]['uri']; $segment = $this->segment[$group] ?? 0; diff --git a/system/View/Parser.php b/system/View/Parser.php index 85d0ff8468db..e0e53812c046 100644 --- a/system/View/Parser.php +++ b/system/View/Parser.php @@ -20,9 +20,6 @@ /** * Class for parsing pseudo-vars * - * @phpstan-type parser_callable (callable(mixed): mixed) - * @phpstan-type parser_callable_string (callable(mixed): mixed)&string - * * @see \CodeIgniter\View\ParserTest */ class Parser extends View @@ -63,8 +60,7 @@ class Parser extends View /** * Stores any plugins registered at run-time. * - * @var array|string> - * @phpstan-var array|parser_callable_string|parser_callable> + * @var array> */ protected $plugins = []; diff --git a/tests/_support/Config/Filters.php b/tests/_support/Config/Filters.php index f3960544f294..f46bf6f6beec 100644 --- a/tests/_support/Config/Filters.php +++ b/tests/_support/Config/Filters.php @@ -16,8 +16,6 @@ use Tests\Support\Filters\Customfilter; use Tests\Support\Filters\RedirectFilter; -/** - * @psalm-suppress UndefinedGlobalVariable - */ +/** @psalm-suppress UndefinedGlobalVariable */ $filters->aliases['test-customfilter'] = Customfilter::class; $filters->aliases['test-redirectfilter'] = RedirectFilter::class; diff --git a/tests/system/Commands/Utilities/ConfigCheckTest.php b/tests/system/Commands/Utilities/ConfigCheckTest.php index 22d0e3d1f1d3..9ac1c84fa51f 100644 --- a/tests/system/Commands/Utilities/ConfigCheckTest.php +++ b/tests/system/Commands/Utilities/ConfigCheckTest.php @@ -89,9 +89,7 @@ public function testCommandConfigCheckNonexistentClass(): void public function testConfigCheckWithKintEnabledUsesKintD(): void { - /** - * @var Closure(mixed...): string - */ + /** @var Closure(mixed...): string */ $command = self::getPrivateMethodInvoker( new ConfigCheck(service('logger'), service('commands')), 'getKintD', @@ -107,9 +105,7 @@ public function testConfigCheckWithKintEnabledUsesKintD(): void public function testConfigCheckWithKintDisabledUsesVarDump(): void { - /** - * @var Closure(mixed...): string - */ + /** @var Closure(mixed...): string */ $command = self::getPrivateMethodInvoker( new ConfigCheck(service('logger'), service('commands')), 'getVarDump', diff --git a/tests/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReaderTest.php b/tests/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReaderTest.php index 1619dd668b31..2586f4400031 100644 --- a/tests/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReaderTest.php +++ b/tests/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReaderTest.php @@ -128,8 +128,7 @@ public function testReadTranslateUriToCamelCase(): void 'route' => 'sub-dir/blog-controller', 'route_params' => '', 'handler' => '\CodeIgniter\Commands\Utilities\Routes\AutoRouterImproved\Controllers\SubDir\BlogController::getIndex', - 'params' => [ - ], + 'params' => [], ], [ 'method' => 'get', diff --git a/tests/system/Commands/Utilities/Routes/FilterCollectorTest.php b/tests/system/Commands/Utilities/Routes/FilterCollectorTest.php index fb6c0e5e8ebd..3e716fb71edf 100644 --- a/tests/system/Commands/Utilities/Routes/FilterCollectorTest.php +++ b/tests/system/Commands/Utilities/Routes/FilterCollectorTest.php @@ -35,10 +35,8 @@ public function testGet(): void $filters = $collector->get(Method::GET, '/'); $expected = [ - 'before' => [ - ], - 'after' => [ - ], + 'before' => [], + 'after' => [], ]; $this->assertSame($expected, $filters); } diff --git a/tests/system/Commands/Utilities/Routes/FilterFinderTest.php b/tests/system/Commands/Utilities/Routes/FilterFinderTest.php index 8d6db5c20620..ce813ec3631e 100644 --- a/tests/system/Commands/Utilities/Routes/FilterFinderTest.php +++ b/tests/system/Commands/Utilities/Routes/FilterFinderTest.php @@ -102,9 +102,7 @@ private function createFilters(array $config = []): Filters public function testFindGlobalsFilters(): void { - /** - * @var RouteCollection $collection - */ + /** @var RouteCollection $collection */ $collection = $this->createRouteCollection(); $router = $this->createRouter($collection); $filters = $this->createFilters(); @@ -122,9 +120,7 @@ public function testFindGlobalsFilters(): void public function testFindGlobalsFiltersWithRedirectRoute(): void { - /** - * @var RouteCollection $collection - */ + /** @var RouteCollection $collection */ $collection = $this->createRouteCollection(); $collection->addRedirect('users/about', 'profile'); @@ -144,9 +140,7 @@ public function testFindGlobalsFiltersWithRedirectRoute(): void public function testFindGlobalsAndRouteFilters(): void { - /** - * @var RouteCollection $collection - */ + /** @var RouteCollection $collection */ $collection = $this->createRouteCollection(); $collection->get('admin', ' AdminController::index', ['filter' => 'honeypot']); $router = $this->createRouter($collection); @@ -201,9 +195,7 @@ public function testFindClassesGlobalsAndRouteFiltersWithArguments(): void public function testFindGlobalsAndRouteClassnameFilters(): void { - /** - * @var RouteCollection $collection - */ + /** @var RouteCollection $collection */ $collection = $this->createRouteCollection(); $collection->get('admin', ' AdminController::index', ['filter' => InvalidChars::class]); $router = $this->createRouter($collection); @@ -222,9 +214,7 @@ public function testFindGlobalsAndRouteClassnameFilters(): void public function testFindGlobalsAndRouteMultipleFilters(): void { - /** - * @var RouteCollection $collection - */ + /** @var RouteCollection $collection */ $collection = $this->createRouteCollection(); $collection->get('admin', ' AdminController::index', ['filter' => ['honeypot', InvalidChars::class]]); $router = $this->createRouter($collection); @@ -243,9 +233,7 @@ public function testFindGlobalsAndRouteMultipleFilters(): void public function testFilterOrder(): void { - /** - * @var RouteCollection $collection - */ + /** @var RouteCollection $collection */ $collection = $this->createRouteCollection([]); $collection->get('/', ' Home::index', ['filter' => ['route1', 'route2']]); $router = $this->createRouter($collection); @@ -311,9 +299,7 @@ public function testFilterOrderWithOldFilterOrder(): void $feature = config(Feature::class); $feature->oldFilterOrder = true; - /** - * @var RouteCollection $collection - */ + /** @var RouteCollection $collection */ $collection = $this->createRouteCollection([]); $collection->get('/', ' Home::index', ['filter' => ['route1', 'route2']]); $router = $this->createRouter($collection); diff --git a/tests/system/Debug/ExceptionHandlerTest.php b/tests/system/Debug/ExceptionHandlerTest.php index 7bd7bdb35465..43086860b0d6 100644 --- a/tests/system/Debug/ExceptionHandlerTest.php +++ b/tests/system/Debug/ExceptionHandlerTest.php @@ -198,8 +198,7 @@ public function testMaskSensitiveData(): void 'function' => 'index', 'class' => Home::class, 'type' => '->', - 'args' => [ - ], + 'args' => [], ], ]; $keysToMask = ['password']; @@ -224,8 +223,7 @@ public function testMaskSensitiveDataTraceDataKey(): void 'function' => 'f', 'class' => Home::class, 'type' => '->', - 'args' => [ - ], + 'args' => [], ], 1 => [ 'file' => '/var/www/CodeIgniter4/system/CodeIgniter.php', @@ -233,8 +231,7 @@ public function testMaskSensitiveDataTraceDataKey(): void 'function' => 'index', 'class' => Home::class, 'type' => '->', - 'args' => [ - ], + 'args' => [], ], ]; $keysToMask = ['file']; diff --git a/tests/system/Debug/ExceptionsTest.php b/tests/system/Debug/ExceptionsTest.php index 2de1d0d7290f..415b6772c509 100644 --- a/tests/system/Debug/ExceptionsTest.php +++ b/tests/system/Debug/ExceptionsTest.php @@ -160,8 +160,7 @@ public function testMaskSensitiveData(): void 'function' => 'index', 'class' => Home::class, 'type' => '->', - 'args' => [ - ], + 'args' => [], ], ]; $keysToMask = ['password']; @@ -186,8 +185,7 @@ public function testMaskSensitiveDataTraceDataKey(): void 'function' => 'f', 'class' => Home::class, 'type' => '->', - 'args' => [ - ], + 'args' => [], ], 1 => [ 'file' => '/var/www/CodeIgniter4/system/CodeIgniter.php', @@ -195,8 +193,7 @@ public function testMaskSensitiveDataTraceDataKey(): void 'function' => 'index', 'class' => Home::class, 'type' => '->', - 'args' => [ - ], + 'args' => [], ], ]; $keysToMask = ['file']; diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php index bf65d43dd0de..cdf57c8b9211 100644 --- a/tests/system/Filters/FiltersTest.php +++ b/tests/system/Filters/FiltersTest.php @@ -854,8 +854,7 @@ public function testFiltersWithArguments(): void $config = [ 'aliases' => ['role' => Role::class], - 'globals' => [ - ], + 'globals' => [], 'filters' => [ 'role:admin,super' => [ 'before' => ['admin/*'], diff --git a/tests/system/Helpers/FormHelperTest.php b/tests/system/Helpers/FormHelperTest.php index 58c1cb2a94cf..04c35458cd01 100644 --- a/tests/system/Helpers/FormHelperTest.php +++ b/tests/system/Helpers/FormHelperTest.php @@ -945,8 +945,7 @@ public function testSetCheckboxWithUnchecked(): void { $_SESSION = [ '_ci_old_input' => [ - 'post' => [ - ], + 'post' => [], ], ]; diff --git a/tests/system/Publisher/PublisherOutputTest.php b/tests/system/Publisher/PublisherOutputTest.php index 0b9b744e01fd..73571cd81a98 100644 --- a/tests/system/Publisher/PublisherOutputTest.php +++ b/tests/system/Publisher/PublisherOutputTest.php @@ -55,9 +55,7 @@ protected function setUp(): void { parent::setUp(); - /** - * Files to seed to VFS - */ + /** Files to seed to VFS */ $structure = [ 'able' => [ 'apple.php' => 'Once upon a midnight dreary', diff --git a/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php b/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php index 50ee40e061fd..f5f1f437c83d 100644 --- a/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php +++ b/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php @@ -299,9 +299,7 @@ public function testRegenerateWithFalseSecurityRegenerateProperty(): void service('superglobals')->setServer('REQUEST_METHOD', 'POST'); service('superglobals')->setPost('csrf_test_name', $this->randomizedToken); - /** - * @var SecurityConfig - */ + /** @var SecurityConfig */ $config = Factories::config('Security'); $config->tokenRandomize = true; $config->regenerate = false; @@ -322,9 +320,7 @@ public function testRegenerateWithTrueSecurityRegenerateProperty(): void service('superglobals')->setServer('REQUEST_METHOD', 'POST'); service('superglobals')->setPost('csrf_test_name', $this->randomizedToken); - /** - * @var SecurityConfig - */ + /** @var SecurityConfig */ $config = Factories::config('Security'); $config->tokenRandomize = true; $config->regenerate = true; diff --git a/tests/system/Security/SecurityCSRFSessionTest.php b/tests/system/Security/SecurityCSRFSessionTest.php index 5c7aaf336e1f..fb410ac71ac3 100644 --- a/tests/system/Security/SecurityCSRFSessionTest.php +++ b/tests/system/Security/SecurityCSRFSessionTest.php @@ -288,9 +288,7 @@ public function testRegenerateWithFalseSecurityRegenerateProperty(): void ->setServer('REQUEST_METHOD', 'POST') ->setPost('csrf_test_name', '8b9218a55906f9dcc1dc263dce7f005a'); - /** - * @var SecurityConfig - */ + /** @var SecurityConfig */ $config = Factories::config('Security'); $config->regenerate = false; Factories::injectMock('config', 'Security', $config); @@ -311,9 +309,7 @@ public function testRegenerateWithTrueSecurityRegenerateProperty(): void ->setServer('REQUEST_METHOD', 'POST') ->setPost('csrf_test_name', '8b9218a55906f9dcc1dc263dce7f005a'); - /** - * @var SecurityConfig - */ + /** @var SecurityConfig */ $config = Factories::config('Security'); $config->regenerate = true; Factories::injectMock('config', 'Security', $config); diff --git a/tests/system/Validation/RulesTest.php b/tests/system/Validation/RulesTest.php index 37f7eebdfaa1..288a6d247e50 100644 --- a/tests/system/Validation/RulesTest.php +++ b/tests/system/Validation/RulesTest.php @@ -748,8 +748,8 @@ public static function provideRequiredWithAndOtherRules(): iterable [true, ['mustBeADate' => []]], // `otherField` and `mustBeADate` exist [true, ['mustBeADate' => '', 'otherField' => '']], - [true, ['mustBeADate' => '2023-06-12', 'otherField' => 'exists']], - [true, ['mustBeADate' => '2023-06-12', 'otherField' => '']], + [true, ['mustBeADate' => '2023-06-12', 'otherField' => 'exists']], + [true, ['mustBeADate' => '2023-06-12', 'otherField' => '']], [false, ['mustBeADate' => '', 'otherField' => 'exists']], [false, ['mustBeADate' => [], 'otherField' => 'exists']], [false, ['mustBeADate' => null, 'otherField' => 'exists']], @@ -773,7 +773,7 @@ public static function provideRequiredWithAndOtherRuleWithValueZero(): iterable { yield from [ [true, ['married' => '0', 'partner_name' => '']], - [true, ['married' => '1', 'partner_name' => 'Foo']], + [true, ['married' => '1', 'partner_name' => 'Foo']], [false, ['married' => '1', 'partner_name' => '']], ]; } diff --git a/user_guide_src/source/guides/first-app/static_pages/003.php b/user_guide_src/source/guides/first-app/static_pages/003.php index fc4914a6923b..4cf109ed3dd4 100644 --- a/user_guide_src/source/guides/first-app/static_pages/003.php +++ b/user_guide_src/source/guides/first-app/static_pages/003.php @@ -2,7 +2,5 @@ use CodeIgniter\Router\RouteCollection; -/** - * @var RouteCollection $routes - */ +/** @var RouteCollection $routes */ $routes->get('/', 'Home::index'); diff --git a/user_guide_src/source/installation/upgrade_routing/001.php b/user_guide_src/source/installation/upgrade_routing/001.php index b2cb2ee5d2d5..59450f4863e2 100644 --- a/user_guide_src/source/installation/upgrade_routing/001.php +++ b/user_guide_src/source/installation/upgrade_routing/001.php @@ -2,9 +2,7 @@ use CodeIgniter\Router\RouteCollection; -/** - * @var RouteCollection $routes - */ +/** @var RouteCollection $routes */ $routes->get('/', 'Home::index'); $routes->add('posts/index', 'Posts::index'); From 1af911c896444bcbc477659f0b77338886a86667 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Sun, 12 Apr 2026 15:59:14 +0200 Subject: [PATCH 27/85] fix: preserve null values in Validation::getValidated() (#10101) --- system/Validation/DotArrayFilter.php | 9 ++-- .../system/Validation/DotArrayFilterTest.php | 11 +++++ tests/system/Validation/ValidationTest.php | 43 +++++++++++++++++++ user_guide_src/source/changelogs/v4.7.3.rst | 1 + 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/system/Validation/DotArrayFilter.php b/system/Validation/DotArrayFilter.php index 62da95cb61c7..673fe67ed138 100644 --- a/system/Validation/DotArrayFilter.php +++ b/system/Validation/DotArrayFilter.php @@ -62,8 +62,9 @@ private static function filter(array $indexes, array $array): array // Get the current index $currentIndex = array_shift($indexes); - // If the current index doesn't exist and is not a wildcard, return an empty array - if (! isset($array[$currentIndex]) && $currentIndex !== '*') { + // If the current index doesn't exist and is not a wildcard, return an empty array. + // Use array_key_exists() so explicit null values are preserved. + if ($currentIndex !== '*' && ! array_key_exists($currentIndex, $array)) { return []; } @@ -88,9 +89,9 @@ private static function filter(array $indexes, array $array): array return $result; } - // If this is the last index, return the value + // If this is the last index, return the value as-is, including null. if ($indexes === []) { - return [$currentIndex => $array[$currentIndex] ?? []]; + return [$currentIndex => $array[$currentIndex]]; } // If the current value is an array, recursively filter it diff --git a/tests/system/Validation/DotArrayFilterTest.php b/tests/system/Validation/DotArrayFilterTest.php index e83b4fe9e566..84e4131102a0 100644 --- a/tests/system/Validation/DotArrayFilterTest.php +++ b/tests/system/Validation/DotArrayFilterTest.php @@ -197,4 +197,15 @@ public function testRunReturnOrderedIndices(): void $this->assertSame($data, $result); } + + public function testRunPreservesNullValue(): void + { + $data = [ + 'foo' => null, + ]; + + $result = DotArrayFilter::run(['foo'], $data); + + $this->assertSame(['foo' => null], $result); + } } diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php index 830296b75b97..7e0f4411f079 100644 --- a/tests/system/Validation/ValidationTest.php +++ b/tests/system/Validation/ValidationTest.php @@ -901,6 +901,49 @@ public function testJsonInput(): void service('superglobals')->unsetServer('CONTENT_TYPE'); } + /** + * @param array $rules + * @param array $expectedValidated + */ + #[DataProvider('provideGetValidatedWithNullValue')] + public function testGetValidatedWithNullValue( + array $rules, + bool $expectedResult, + array $expectedValidated, + ): void { + $data = [ + 'role' => null, + ]; + + $result = $this->validation->setRules($rules)->run($data); + + $this->assertSame($expectedResult, $result); + $this->assertSame($expectedValidated, $this->validation->getValidated()); + $this->assertSame($expectedResult ? [] : ['role'], array_keys($this->validation->getErrors())); + } + + /** + * @return iterable, + * 1: bool, + * 2: array + * }> + */ + public static function provideGetValidatedWithNullValue(): iterable + { + yield 'permit_empty preserves null' => [ + ['role' => 'permit_empty|string'], + true, + ['role' => null], + ]; + + yield 'string fails on null' => [ + ['role' => 'string'], + false, + [], + ]; + } + public function testJsonInputInvalid(): void { $this->expectException(HTTPException::class); diff --git a/user_guide_src/source/changelogs/v4.7.3.rst b/user_guide_src/source/changelogs/v4.7.3.rst index 50418133d15e..268f49ecd227 100644 --- a/user_guide_src/source/changelogs/v4.7.3.rst +++ b/user_guide_src/source/changelogs/v4.7.3.rst @@ -34,6 +34,7 @@ Bugs Fixed - **Autoloader:** Fixed a bug where ``Autoloader::unregister()`` (used during tests) silently failed to remove handlers from the SPL autoload stack, causing closures to accumulate permanently. - **Common:** Fixed a bug where the ``command()`` helper function did not properly clean up output buffers, which could lead to risky tests when exceptions were thrown. +- **Validation:** Fixed a bug where ``Validation::getValidated()`` dropped fields whose validated value was explicitly ``null``. See the repo's `CHANGELOG.md `_ From 246cbd4db6953031551635d9269ad9f8f2071baf Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sun, 12 Apr 2026 22:04:16 +0800 Subject: [PATCH 28/85] test: refactor tests on `BaseCommand` and `Commands` (#10103) --- system/CLI/Commands.php | 25 +-- tests/_support/Commands/AppInfo.php | 25 ++- tests/_support/Commands/ParamsReveal.php | 30 --- tests/_support/_command/ListCommands.php | 16 +- tests/system/CLI/BaseCommandTest.php | 104 ++++++++++ tests/system/CLI/CommandsTest.php | 176 ++++++++++++++++ tests/system/Commands/BaseCommandTest.php | 62 ------ tests/system/Commands/CommandOverrideTest.php | 64 ------ tests/system/Commands/CommandTest.php | 192 ------------------ tests/system/Commands/ListCommandsTest.php | 55 +++++ utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 7 +- 12 files changed, 369 insertions(+), 389 deletions(-) delete mode 100644 tests/_support/Commands/ParamsReveal.php create mode 100644 tests/system/CLI/BaseCommandTest.php create mode 100644 tests/system/CLI/CommandsTest.php delete mode 100644 tests/system/Commands/BaseCommandTest.php delete mode 100644 tests/system/Commands/CommandOverrideTest.php delete mode 100644 tests/system/Commands/CommandTest.php create mode 100644 tests/system/Commands/ListCommandsTest.php diff --git a/system/CLI/Commands.php b/system/CLI/Commands.php index 5630ee0f7b69..8f623d509c0a 100644 --- a/system/CLI/Commands.php +++ b/system/CLI/Commands.php @@ -64,8 +64,6 @@ public function run(string $command, array $params) return EXIT_ERROR; } - // The file would have already been loaded during the - // createCommandList function... $className = $this->commands[$command]['class']; $class = new $className($this->logger, $this); @@ -104,14 +102,10 @@ public function discoverCommands() $locator = service('locator'); $files = $locator->listFiles('Commands/'); - // If no matching command files were found, bail - // This should never happen in unit testing. if ($files === []) { - return; // @codeCoverageIgnore + return; } - // Loop over each file checking to see if a command with that - // alias exists in the class. foreach ($files as $file) { /** @var class-string|false */ $className = $locator->findQualifiedNameFromPath($file); @@ -159,21 +153,20 @@ public function verifyCommand(string $command, array $commands): bool return true; } - $message = lang('CLI.commandNotFound', [$command]); + $message = lang('CLI.commandNotFound', [$command]); + $alternatives = $this->getCommandAlternatives($command, $commands); if ($alternatives !== []) { - if (count($alternatives) === 1) { - $message .= "\n\n" . lang('CLI.altCommandSingular') . "\n "; - } else { - $message .= "\n\n" . lang('CLI.altCommandPlural') . "\n "; - } - - $message .= implode("\n ", $alternatives); + $message = sprintf( + "%s\n\n%s\n %s", + $message, + count($alternatives) === 1 ? lang('CLI.altCommandSingular') : lang('CLI.altCommandPlural'), + implode("\n ", $alternatives), + ); } CLI::error($message); - CLI::newLine(); return false; } diff --git a/tests/_support/Commands/AppInfo.php b/tests/_support/Commands/AppInfo.php index 767523071466..94502e0706f5 100644 --- a/tests/_support/Commands/AppInfo.php +++ b/tests/_support/Commands/AppInfo.php @@ -18,29 +18,40 @@ use CodeIgniter\CodeIgniter; use CodeIgniter\Exceptions\RuntimeException; -class AppInfo extends BaseCommand +/** + * @internal + */ +final class AppInfo extends BaseCommand { protected $group = 'demo'; protected $name = 'app:info'; protected $arguments = ['draft' => 'unused']; protected $description = 'Displays basic application information.'; - public function run(array $params): void + public function run(array $params): int { - CLI::write('CI Version: ' . CLI::color(CodeIgniter::CI_VERSION, 'red')); + CLI::write(sprintf('CodeIgniter Version: %s', CodeIgniter::CI_VERSION)); + + return 0; } - public function bomb(): void + public function bomb(): int { try { CLI::color('test', 'white', 'Background'); - } catch (RuntimeException $oops) { - $this->showError($oops); + } catch (RuntimeException $e) { + $this->showError($e); + + return 1; } + + return 0; } - public function helpme(): void + public function helpMe(): int { $this->call('help'); + + return 0; } } diff --git a/tests/_support/Commands/ParamsReveal.php b/tests/_support/Commands/ParamsReveal.php deleted file mode 100644 index 2feb04de29aa..000000000000 --- a/tests/_support/Commands/ParamsReveal.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace Tests\Support\Commands; - -use CodeIgniter\CLI\BaseCommand; - -class ParamsReveal extends BaseCommand -{ - protected $group = 'demo'; - protected $name = 'reveal'; - protected $usage = 'reveal [options] [arguments]'; - protected $description = 'Reveal params'; - public static $args; - - public function run(array $params): void - { - static::$args = $params; - } -} diff --git a/tests/_support/_command/ListCommands.php b/tests/_support/_command/ListCommands.php index fe1c9611a222..5b73548d89de 100644 --- a/tests/_support/_command/ListCommands.php +++ b/tests/_support/_command/ListCommands.php @@ -13,36 +13,30 @@ namespace App\Commands; +use CodeIgniter\CLI\BaseCommand; use CodeIgniter\CLI\CLI; -use CodeIgniter\Commands\ListCommands as BaseListCommands; -class ListCommands extends BaseListCommands +/** + * @internal + */ +final class ListCommands extends BaseCommand { /** - * The group the command is lumped under - * when listing commands. - * * @var string */ protected $group = 'App'; /** - * The Command's name - * * @var string */ protected $name = 'list'; /** - * the Command's short description - * * @var string */ protected $description = 'This is testing to override `list` command.'; /** - * the Command's usage - * * @var string */ protected $usage = 'list'; diff --git a/tests/system/CLI/BaseCommandTest.php b/tests/system/CLI/BaseCommandTest.php new file mode 100644 index 000000000000..143851dee9ce --- /dev/null +++ b/tests/system/CLI/BaseCommandTest.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\CLI; + +use CodeIgniter\CLI\Exceptions\CLIException; +use CodeIgniter\CodeIgniter; +use CodeIgniter\Log\Logger; +use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\StreamFilterTrait; +use PHPUnit\Framework\Attributes\After; +use PHPUnit\Framework\Attributes\Before; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; +use Tests\Support\Commands\AppInfo; + +/** + * @internal + */ +#[CoversClass(BaseCommand::class)] +#[CoversClass(CLIException::class)] +#[Group('Others')] +final class BaseCommandTest extends CIUnitTestCase +{ + use StreamFilterTrait; + + #[After] + #[Before] + protected function resetCli(): void + { + CLI::reset(); + } + + public function testRunCommand(): void + { + $command = new AppInfo(single_service('logger'), single_service('commands')); + + $this->assertSame(0, $command->run([])); + $this->assertSame( + sprintf("\nCodeIgniter Version: %s\n", CodeIgniter::CI_VERSION), + preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()), + ); + } + + public function testCallingOtherCommands(): void + { + $command = new AppInfo(single_service('logger'), single_service('commands')); + + $this->assertSame(0, $command->helpMe()); + $this->assertStringContainsString('Displays basic usage information.', $this->getStreamFilterBuffer()); + } + + public function testShowError(): void + { + $command = new AppInfo(single_service('logger'), single_service('commands')); + + $this->assertSame(1, $command->bomb()); + $this->assertStringContainsString('[CodeIgniter\CLI\Exceptions\CLIException]', $this->getStreamFilterBuffer()); + $this->assertStringContainsString('Invalid "background" color: "Background".', $this->getStreamFilterBuffer()); + } + + public function testShowHelp(): void + { + $command = new AppInfo(single_service('logger'), single_service('commands')); + $command->showHelp(); + + $this->assertSame( + <<<'EOT' + + Usage: + app:info [arguments] + + Description: + Displays basic application information. + + Arguments: + draft unused + + EOT, + preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()), + ); + } + + public function testMagicGetAndIsset(): void + { + $command = new AppInfo(single_service('logger'), single_service('commands')); + + $this->assertInstanceOf(Logger::class, $command->logger); + $this->assertInstanceOf(Commands::class, $command->commands); + $this->assertSame('demo', $command->group); + $this->assertSame('app:info', $command->name); + $this->assertNull($command->foo); // @phpstan-ignore property.notFound + } +} diff --git a/tests/system/CLI/CommandsTest.php b/tests/system/CLI/CommandsTest.php new file mode 100644 index 000000000000..f6a9dc1c0ff5 --- /dev/null +++ b/tests/system/CLI/CommandsTest.php @@ -0,0 +1,176 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\CLI; + +use CodeIgniter\Autoloader\FileLocatorInterface; +use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\StreamFilterTrait; +use Config\Services; +use PHPUnit\Framework\Attributes\After; +use PHPUnit\Framework\Attributes\Before; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; +use RuntimeException; +use Tests\Support\Commands\AppInfo; + +/** + * @internal + */ +#[CoversClass(Commands::class)] +#[Group('Others')] +final class CommandsTest extends CIUnitTestCase +{ + use StreamFilterTrait; + + #[After] + #[Before] + protected function resetAll(): void + { + $this->resetServices(); + + CLI::reset(); + } + + private function copyAppListCommands(): void + { + if (! is_dir(APPPATH . 'Commands')) { + mkdir(APPPATH . 'Commands'); + } + + copy(SUPPORTPATH . '_command/ListCommands.php', APPPATH . 'Commands/ListCommands.php'); + } + + private function deleteAppListCommands(): void + { + if (is_file(APPPATH . 'Commands/ListCommands.php')) { + unlink(APPPATH . 'Commands/ListCommands.php'); + } + } + + public function testRunOnUnknownCommand(): void + { + $commands = new Commands(); + + $this->assertSame(EXIT_ERROR, $commands->run('app:unknown', [])); + $this->assertArrayNotHasKey('app:unknown', $commands->getCommands()); + $this->assertStringContainsString('Command "app:unknown" not found', $this->getStreamFilterBuffer()); + } + + public function testRunOnUnknownCommandButWithOneAlternative(): void + { + $commands = new Commands(); + + $this->assertSame(EXIT_ERROR, $commands->run('app:inf', [])); + $this->assertSame( + <<<'EOT' + Command "app:inf" not found. + + Did you mean this? + app:info + + EOT, + preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()), + ); + } + + public function testRunOnUnknownCommandButWithMultipleAlternatives(): void + { + $commands = new Commands(); + + $this->assertSame(EXIT_ERROR, $commands->run('app:', [])); + $this->assertSame( + <<<'EOT' + Command "app:" not found. + + Did you mean one of these? + app:destructive + app:info + + EOT, + preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()), + ); + } + + public function testRunOnAbstractCommandCannotBeRun(): void + { + $commands = new Commands(); + + $this->assertSame(EXIT_ERROR, $commands->run('app:pablo', [])); + $this->assertArrayNotHasKey('app:pablo', $commands->getCommands()); + $this->assertStringContainsString('Command "app:pablo" not found', $this->getStreamFilterBuffer()); + } + + public function testRunOnKnownCommand(): void + { + $commands = new Commands(); + + $this->assertSame(EXIT_SUCCESS, $commands->run('app:info', [])); + $this->assertArrayHasKey('app:info', $commands->getCommands()); + $this->assertStringContainsString('CodeIgniter Version', $this->getStreamFilterBuffer()); + } + + public function testDestructiveCommandIsNotRisky(): void + { + $this->expectException(RuntimeException::class); + + command('app:destructive'); + } + + public function testDiscoverCommandsDoNotRunTwice(): void + { + $locator = $this->createMock(FileLocatorInterface::class); + $locator + ->expects($this->once()) + ->method('listFiles') + ->with('Commands/') + ->willReturn([SUPPORTPATH . 'Commands/AppInfo.php']); + $locator + ->expects($this->once()) + ->method('findQualifiedNameFromPath') + ->with(SUPPORTPATH . 'Commands/AppInfo.php') + ->willReturn(AppInfo::class); + Services::injectMock('locator', $locator); + + $commands = new Commands(); // discoverCommands will be called in the constructor + $commands->discoverCommands(); + } + + public function testDiscoverCommandsWithNoFiles(): void + { + $locator = $this->createMock(FileLocatorInterface::class); + $locator + ->expects($this->once()) + ->method('listFiles') + ->with('Commands/') + ->willReturn([]); + $locator + ->expects($this->never()) + ->method('findQualifiedNameFromPath'); + Services::injectMock('locator', $locator); + + new Commands(); + } + + public function testDiscoveredCommandsCanBeOverridden(): void + { + $this->copyAppListCommands(); + + command('list'); + + $this->assertStringContainsString('This is App\Commands\ListCommands', $this->getStreamFilterBuffer()); + $this->assertStringNotContainsString('Displays basic usage information.', $this->getStreamFilterBuffer()); + + $this->deleteAppListCommands(); + } +} diff --git a/tests/system/Commands/BaseCommandTest.php b/tests/system/Commands/BaseCommandTest.php deleted file mode 100644 index 47ca2a6f6a1b..000000000000 --- a/tests/system/Commands/BaseCommandTest.php +++ /dev/null @@ -1,62 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace CodeIgniter\Commands; - -use CodeIgniter\Log\Logger; -use CodeIgniter\Test\CIUnitTestCase; -use PHPUnit\Framework\Attributes\Group; -use Tests\Support\Commands\AppInfo; - -/** - * @internal - */ -#[Group('Others')] -final class BaseCommandTest extends CIUnitTestCase -{ - private Logger $logger; - - protected function setUp(): void - { - parent::setUp(); - $this->logger = service('logger'); - } - - public function testMagicIssetTrue(): void - { - $command = new AppInfo($this->logger, service('commands')); - - $this->assertSame($command->group !== null, isset($command->group)); // @phpstan-ignore isset.property - } - - public function testMagicIssetFalse(): void - { - $command = new AppInfo($this->logger, service('commands')); - - $this->assertFalse(isset($command->foobar)); // @phpstan-ignore property.notFound - } - - public function testMagicGet(): void - { - $command = new AppInfo($this->logger, service('commands')); - - $this->assertSame('demo', $command->group); - } - - public function testMagicGetMissing(): void - { - $command = new AppInfo($this->logger, service('commands')); - - $this->assertNull($command->foobar); // @phpstan-ignore property.notFound - } -} diff --git a/tests/system/Commands/CommandOverrideTest.php b/tests/system/Commands/CommandOverrideTest.php deleted file mode 100644 index b21b7e0c6faa..000000000000 --- a/tests/system/Commands/CommandOverrideTest.php +++ /dev/null @@ -1,64 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace CodeIgniter\Commands; - -use CodeIgniter\Test\CIUnitTestCase; -use CodeIgniter\Test\StreamFilterTrait; -use PHPUnit\Framework\Attributes\Group; - -/** - * @internal - */ -#[Group('Others')] -final class CommandOverrideTest extends CIUnitTestCase -{ - use StreamFilterTrait; - - protected function setUp(): void - { - $this->resetServices(); - - parent::setUp(); - } - - protected function getBuffer(): string - { - return $this->getStreamFilterBuffer(); - } - - public function testOverrideListCommands(): void - { - $this->copyListCommands(); - - command('list'); - - $this->assertStringContainsString('This is App\Commands\ListCommands', $this->getBuffer()); - $this->assertStringNotContainsString('Displays basic usage information.', $this->getBuffer()); - - $this->deleteListCommands(); - } - - private function copyListCommands(): void - { - if (! is_dir(APPPATH . 'Commands')) { - mkdir(APPPATH . 'Commands'); - } - copy(SUPPORTPATH . '_command/ListCommands.php', APPPATH . 'Commands/ListCommands.php'); - } - - private function deleteListCommands(): void - { - unlink(APPPATH . 'Commands/ListCommands.php'); - } -} diff --git a/tests/system/Commands/CommandTest.php b/tests/system/Commands/CommandTest.php deleted file mode 100644 index 6b668d2640c5..000000000000 --- a/tests/system/Commands/CommandTest.php +++ /dev/null @@ -1,192 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace CodeIgniter\Commands; - -use CodeIgniter\CLI\Commands; -use CodeIgniter\Log\Logger; -use CodeIgniter\Test\CIUnitTestCase; -use CodeIgniter\Test\StreamFilterTrait; -use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\Attributes\Group; -use RuntimeException; -use Tests\Support\Commands\AppInfo; -use Tests\Support\Commands\ParamsReveal; - -/** - * @internal - */ -#[Group('Others')] -final class CommandTest extends CIUnitTestCase -{ - use StreamFilterTrait; - - private Logger $logger; - private Commands $commands; - - protected function setUp(): void - { - $this->resetServices(); - - parent::setUp(); - - $this->logger = service('logger'); - $this->commands = service('commands'); - } - - protected function getBuffer(): string - { - return $this->getStreamFilterBuffer(); - } - - public function testListCommands(): void - { - command('list'); - - // make sure the result looks like a command list - $this->assertStringContainsString('Lists the available commands.', $this->getBuffer()); - $this->assertStringContainsString('Displays basic usage information.', $this->getBuffer()); - } - - public function testListCommandsSimple(): void - { - command('list --simple'); - - $this->assertStringContainsString('db:seed', $this->getBuffer()); - $this->assertStringNotContainsString('Lists the available commands.', $this->getBuffer()); - } - - public function testCustomCommand(): void - { - command('app:info'); - $this->assertStringContainsString('CI Version:', $this->getBuffer()); - } - - public function testShowError(): void - { - command('app:info'); - $commands = $this->commands->getCommands(); - - /** @var AppInfo */ - $command = new $commands['app:info']['class']($this->logger, $this->commands); - - $command->helpme(); - - $this->assertStringContainsString('Displays basic usage information.', $this->getBuffer()); - } - - public function testCommandCall(): void - { - command('app:info'); - $commands = $this->commands->getCommands(); - - /** @var AppInfo */ - $command = new $commands['app:info']['class']($this->logger, $this->commands); - - $command->bomb(); - - $this->assertStringContainsString('Invalid "background" color:', $this->getBuffer()); - } - - public function testAbstractCommand(): void - { - command('app:pablo'); - $this->assertStringContainsString('not found', $this->getBuffer()); - } - - public function testNamespacesCommand(): void - { - command('namespaces'); - - $this->assertStringContainsString('| Namespace', $this->getBuffer()); - $this->assertStringContainsString('| Config', $this->getBuffer()); - $this->assertStringContainsString('| Yes', $this->getBuffer()); - } - - public function testInexistentCommandWithNoAlternatives(): void - { - command('app:oops'); - $this->assertStringContainsString('Command "app:oops" not found', $this->getBuffer()); - } - - public function testInexistentCommandsButWithOneAlternative(): void - { - command('namespace'); - - $this->assertStringContainsString('Command "namespace" not found.', $this->getBuffer()); - $this->assertStringContainsString('Did you mean this?', $this->getBuffer()); - $this->assertStringContainsString('namespaces', $this->getBuffer()); - } - - public function testInexistentCommandsButWithManyAlternatives(): void - { - command('clear'); - - $this->assertStringContainsString('Command "clear" not found.', $this->getBuffer()); - $this->assertStringContainsString('Did you mean one of these?', $this->getBuffer()); - $this->assertStringContainsString(':clear', $this->getBuffer()); - } - - public function testDestructiveCommandIsNotRisky(): void - { - $this->expectException(RuntimeException::class); - - command('app:destructive'); - } - - /** - * @param list $expected - */ - #[DataProvider('provideCommandParsesArgsCorrectly')] - public function testCommandParsesArgsCorrectly(string $input, array $expected): void - { - ParamsReveal::$args = null; - command($input); - - $this->assertSame($expected, ParamsReveal::$args); - } - - public static function provideCommandParsesArgsCorrectly(): iterable - { - return [ - [ - 'reveal as df', - ['as', 'df'], - ], - [ - 'reveal', - [], - ], - [ - 'reveal seg1 seg2 -opt1 -opt2', - ['seg1', 'seg2', 'opt1' => null, 'opt2' => null], - ], - [ - 'reveal seg1 seg2 -opt1 val1 seg3', - ['seg1', 'seg2', 'opt1' => 'val1', 'seg3'], - ], - [ - 'reveal as df -gh -jk -qw 12 zx cv', - ['as', 'df', 'gh' => null, 'jk' => null, 'qw' => '12', 'zx', 'cv'], - ], - [ - 'reveal as -df "some stuff" -jk 12 -sd "Some longer stuff" -fg \'using single quotes\'', - ['as', 'df' => 'some stuff', 'jk' => '12', 'sd' => 'Some longer stuff', 'fg' => 'using single quotes'], - ], - [ - 'reveal as -df "using mixed \'quotes\'\" here\""', - ['as', 'df' => 'using mixed \'quotes\'" here"'], - ], - ]; - } -} diff --git a/tests/system/Commands/ListCommandsTest.php b/tests/system/Commands/ListCommandsTest.php new file mode 100644 index 000000000000..9d86b1e0dd39 --- /dev/null +++ b/tests/system/Commands/ListCommandsTest.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Commands; + +use CodeIgniter\CLI\CLI; +use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\StreamFilterTrait; +use PHPUnit\Framework\Attributes\After; +use PHPUnit\Framework\Attributes\Before; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; + +/** + * @internal + */ +#[CoversClass(ListCommands::class)] +#[Group('Others')] +final class ListCommandsTest extends CIUnitTestCase +{ + use StreamFilterTrait; + + #[After] + #[Before] + protected function resetCli(): void + { + CLI::reset(); + } + + public function testRunCommand(): void + { + command('list'); + + $this->assertStringContainsString('cache:clear', $this->getStreamFilterBuffer()); + $this->assertStringContainsString('Clears the current system caches.', $this->getStreamFilterBuffer()); + } + + public function testRunCommandWithSimpleOption(): void + { + command('list --simple'); + + $this->assertStringContainsString('cache:clear', $this->getStreamFilterBuffer()); + $this->assertStringNotContainsString('Clears the current system caches.', $this->getStreamFilterBuffer()); + } +} diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 65e70e528efd..bcdfa61b4264 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 2060 errors +# total 2059 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 94f900209bdd..fb692f3ac52f 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 1259 errors +# total 1258 errors parameters: ignoreErrors: @@ -4827,11 +4827,6 @@ parameters: count: 1 path: ../../tests/system/CodeIgniterTest.php - - - message: '#^Method CodeIgniter\\Commands\\CommandTest\:\:provideCommandParsesArgsCorrectly\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Commands/CommandTest.php - - message: '#^Method CodeIgniter\\Commands\\Translation\\LocalizationFinderTest\:\:getActualTranslationFourKeys\(\) return type has no value type specified in iterable type array\.$#' count: 1 From 875f8fece6be248cf2e5e87c3eb6964189d25b76 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Mon, 13 Apr 2026 08:01:23 +0300 Subject: [PATCH 29/85] fix: Rename phpunit.xml.dist (#10111) --- .github/workflows/test-phpunit.yml | 4 ++-- .github/workflows/test-random-execution.yml | 4 ++-- admin/starter/builds | 2 +- admin/starter/tests/README.md | 4 ++-- admin/starter/tests/unit/HealthTest.php | 4 ++-- tests/README.md | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test-phpunit.yml b/.github/workflows/test-phpunit.yml index 00dee9b9e43d..8c17cae53de6 100644 --- a/.github/workflows/test-phpunit.yml +++ b/.github/workflows/test-phpunit.yml @@ -11,7 +11,7 @@ on: - 'tests/**.php' - 'spark' - composer.json - - phpunit.xml.dist + - phpunit.dist.xml - .github/workflows/test-phpunit.yml - .github/workflows/reusable-phpunit-test.yml @@ -25,7 +25,7 @@ on: - 'tests/**.php' - 'spark' - composer.json - - phpunit.xml.dist + - phpunit.dist.xml - .github/workflows/test-phpunit.yml - .github/workflows/reusable-phpunit-test.yml diff --git a/.github/workflows/test-random-execution.yml b/.github/workflows/test-random-execution.yml index d1ef81937284..4e304e84338a 100644 --- a/.github/workflows/test-random-execution.yml +++ b/.github/workflows/test-random-execution.yml @@ -9,7 +9,7 @@ on: - '.github/scripts/run-random-tests.sh' - '.github/scripts/random-tests-config.txt' - '.github/workflows/test-random-execution.yml' - - 'phpunit.xml.dist' + - 'phpunit.dist.xml' - 'system/**.php' - 'tests/**.php' @@ -21,7 +21,7 @@ on: - '.github/scripts/run-random-tests.sh' - '.github/scripts/random-tests-config.txt' - '.github/workflows/test-random-execution.yml' - - 'phpunit.xml.dist' + - 'phpunit.dist.xml' - 'system/**.php' - 'tests/**.php' diff --git a/admin/starter/builds b/admin/starter/builds index f156b6944cfd..c68c0894e93b 100755 --- a/admin/starter/builds +++ b/admin/starter/builds @@ -94,7 +94,7 @@ if (is_file($file)) { $files = [ __DIR__ . DIRECTORY_SEPARATOR . 'app/Config/Paths.php', - __DIR__ . DIRECTORY_SEPARATOR . 'phpunit.xml.dist', + __DIR__ . DIRECTORY_SEPARATOR . 'phpunit.dist.xml', __DIR__ . DIRECTORY_SEPARATOR . 'phpunit.xml', ]; diff --git a/admin/starter/tests/README.md b/admin/starter/tests/README.md index fc40e447301e..d5b12eea54f7 100644 --- a/admin/starter/tests/README.md +++ b/admin/starter/tests/README.md @@ -80,11 +80,11 @@ The HTML files can be viewed by opening **tests/coverage/index.html** in your fa ## PHPUnit XML Configuration -The repository has a ``phpunit.xml.dist`` file in the project root that's used for +The repository has a ``phpunit.dist.xml`` file in the project root that's used for PHPUnit configuration. This is used to provide a default configuration if you do not have your own configuration file in the project root. -The normal practice would be to copy ``phpunit.xml.dist`` to ``phpunit.xml`` +The normal practice would be to copy ``phpunit.dist.xml`` to ``phpunit.xml`` (which is git ignored), and to tailor it as you see fit. For instance, you might wish to exclude database tests, or automatically generate HTML code coverage reports. diff --git a/admin/starter/tests/unit/HealthTest.php b/admin/starter/tests/unit/HealthTest.php index b3e480f4b0bf..1df24df823a7 100644 --- a/admin/starter/tests/unit/HealthTest.php +++ b/admin/starter/tests/unit/HealthTest.php @@ -27,7 +27,7 @@ public function testBaseUrlHasBeenSet(): void if ($env) { // BaseURL in .env is a valid URL? - // phpunit.xml.dist sets app.baseURL in $_SERVER + // phpunit.dist.xml sets app.baseURL in $_SERVER // So if you set app.baseURL in .env, it takes precedence $config = new App(); $this->assertTrue( @@ -37,7 +37,7 @@ public function testBaseUrlHasBeenSet(): void } // Get the baseURL in app/Config/App.php - // You can't use Config\App, because phpunit.xml.dist sets app.baseURL + // You can't use Config\App, because phpunit.dist.xml sets app.baseURL $reader = new ConfigReader(); // BaseURL in app/Config/App.php is a valid URL? diff --git a/tests/README.md b/tests/README.md index 2ce023d37119..4e35d026e838 100644 --- a/tests/README.md +++ b/tests/README.md @@ -172,11 +172,11 @@ as a comprehensive collection of HTML files that show the status of every line o ## PHPUnit XML Configuration -The repository has a ``phpunit.xml.dist`` file in the project root that's used for +The repository has a ``phpunit.dist.xml`` file in the project root that's used for PHPUnit configuration. This is used to provide a default configuration if you do not have your own configuration file in the project root. -The normal practice would be to copy ``phpunit.xml.dist`` to ``phpunit.xml`` +The normal practice would be to copy ``phpunit.dist.xml`` to ``phpunit.xml`` (which is git ignored), and to tailor it as you see fit. For instance, you might wish to exclude database tests, or automatically generate HTML code coverage reports. From 2010172f01635fe5d5f9d39fe76f7e9d3171720f Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Mon, 13 Apr 2026 13:16:39 +0800 Subject: [PATCH 30/85] fix: refactor inconsistent behavior on `CLI::write()` and `CLI::error()` (#10106) --- system/CLI/CLI.php | 13 +++++-- tests/system/CLI/CLITest.php | 26 +++++++++++--- tests/system/CLI/CommandsTest.php | 2 ++ .../system/Commands/Cache/ClearCacheTest.php | 7 ++-- .../Database/ShowTableInfoMockIOTest.php | 36 ++++++++++++------- .../Commands/Generators/TestGeneratorTest.php | 16 ++++----- .../Housekeeping/ClearDebugbarTest.php | 9 +++-- .../Commands/Housekeeping/ClearLogsTest.php | 28 +++++++++++---- .../Commands/Utilities/ConfigCheckTest.php | 33 ++++++++--------- 9 files changed, 118 insertions(+), 52 deletions(-) diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index d5980bb9bc19..f01124ec9075 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -255,6 +255,7 @@ public static function prompt(string $field, $options = null, $validation = null } static::fwrite(STDOUT, $field . (trim($field) !== '' ? ' ' : '') . $extraOutput . ': '); + static::$lastWrite = 'write'; // Read the input from keyboard. $input = trim(static::$io->input()); @@ -458,7 +459,8 @@ public static function write(string $text = '', ?string $foreground = null, ?str } if (static::$lastWrite !== 'write') { - $text = PHP_EOL . $text; + $text = PHP_EOL . $text; + static::$lastWrite = 'write'; } @@ -473,13 +475,20 @@ public static function write(string $text = '', ?string $foreground = null, ?str public static function error(string $text, string $foreground = 'light_red', ?string $background = null) { // Check color support for STDERR - $stdout = static::$isColored; + $stdout = static::$isColored; + static::$isColored = static::hasColorSupport(STDERR); if ($foreground !== '' || (string) $background !== '') { $text = static::color($text, $foreground, $background); } + if (static::$lastWrite !== 'write') { + $text = PHP_EOL . $text; + + static::$lastWrite = 'write'; + } + static::fwrite(STDERR, $text . PHP_EOL); // return STDOUT color support diff --git a/tests/system/CLI/CLITest.php b/tests/system/CLI/CLITest.php index 3dc3a20c43fe..6a69cf7e69ab 100644 --- a/tests/system/CLI/CLITest.php +++ b/tests/system/CLI/CLITest.php @@ -37,6 +37,7 @@ protected function setUp(): void Services::injectMock('superglobals', new Superglobals()); + CLI::reset(); CLI::init(); } @@ -393,16 +394,33 @@ public function testError(): void { CLI::error('test'); - // red expected cuz stderr - $expected = "\033[1;31mtest\033[0m" . PHP_EOL; + $expected = PHP_EOL . "\033[1;31mtest\033[0m" . PHP_EOL; $this->assertSame($expected, $this->getStreamFilterBuffer()); } + public function testMixedWriteError(): void + { + CLI::write('test 1'); + CLI::error('test 2'); + CLI::write('test 3'); + + $this->assertSame( + <<<'EOT' + + test 1 + test 2 + test 3 + + EOT, + preg_replace('/\e\[[^m]+m/u', '', $this->getStreamFilterBuffer()), + ); + } + public function testErrorForeground(): void { CLI::error('test', 'purple'); - $expected = "\033[0;35mtest\033[0m" . PHP_EOL; + $expected = PHP_EOL . "\033[0;35mtest\033[0m" . PHP_EOL; $this->assertSame($expected, $this->getStreamFilterBuffer()); } @@ -410,7 +428,7 @@ public function testErrorBackground(): void { CLI::error('test', 'purple', 'green'); - $expected = "\033[0;35m\033[42mtest\033[0m" . PHP_EOL; + $expected = PHP_EOL . "\033[0;35m\033[42mtest\033[0m" . PHP_EOL; $this->assertSame($expected, $this->getStreamFilterBuffer()); } diff --git a/tests/system/CLI/CommandsTest.php b/tests/system/CLI/CommandsTest.php index f6a9dc1c0ff5..34bb0601da65 100644 --- a/tests/system/CLI/CommandsTest.php +++ b/tests/system/CLI/CommandsTest.php @@ -74,6 +74,7 @@ public function testRunOnUnknownCommandButWithOneAlternative(): void $this->assertSame(EXIT_ERROR, $commands->run('app:inf', [])); $this->assertSame( <<<'EOT' + Command "app:inf" not found. Did you mean this? @@ -91,6 +92,7 @@ public function testRunOnUnknownCommandButWithMultipleAlternatives(): void $this->assertSame(EXIT_ERROR, $commands->run('app:', [])); $this->assertSame( <<<'EOT' + Command "app:" not found. Did you mean one of these? diff --git a/tests/system/Commands/Cache/ClearCacheTest.php b/tests/system/Commands/Cache/ClearCacheTest.php index 5b027e04f05a..a845eccda139 100644 --- a/tests/system/Commands/Cache/ClearCacheTest.php +++ b/tests/system/Commands/Cache/ClearCacheTest.php @@ -15,6 +15,7 @@ use CodeIgniter\Cache\CacheFactory; use CodeIgniter\Cache\Handlers\FileHandler; +use CodeIgniter\CLI\CLI; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; use Config\Services; @@ -32,6 +33,7 @@ protected function setUp(): void { parent::setUp(); + CLI::reset(); $this->resetServices(); // Make sure we are testing with the correct handler (override injections) @@ -42,6 +44,7 @@ protected function tearDown(): void { parent::tearDown(); + CLI::reset(); $this->resetServices(); } @@ -50,7 +53,7 @@ public function testClearCacheInvalidHandler(): void command('cache:clear junk'); $this->assertSame( - "Cache driver \"junk\" is not a valid cache handler.\n", + "\nCache driver \"junk\" is not a valid cache handler.\n", preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()), ); } @@ -79,7 +82,7 @@ public function testClearCacheFails(): void command('cache:clear'); $this->assertSame( - "Error while clearing the cache.\n", + "\nError while clearing the cache.\n", preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()), ); } diff --git a/tests/system/Commands/Database/ShowTableInfoMockIOTest.php b/tests/system/Commands/Database/ShowTableInfoMockIOTest.php index 0842c28ae12a..0ad012816eef 100644 --- a/tests/system/Commands/Database/ShowTableInfoMockIOTest.php +++ b/tests/system/Commands/Database/ShowTableInfoMockIOTest.php @@ -33,6 +33,8 @@ protected function setUp(): void { parent::setUp(); + CLI::reset(); + putenv('NO_COLOR=1'); CLI::init(); } @@ -41,6 +43,8 @@ protected function tearDown(): void { parent::tearDown(); + CLI::reset(); + putenv('NO_COLOR'); CLI::init(); } @@ -58,17 +62,25 @@ public function testDbTableWithInputs(): void $result = $io->getOutput(); - $expectedPattern = '/Which table do you want to see\? \[0, 1, 2, 3, 4, 5, 6, 7, 8, 9.*?\]: a -The "Which table do you want to see\?" field must be one of: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9.*?./'; - $this->assertMatchesRegularExpression($expectedPattern, $result); - - $expected = 'Data of Table "db_migrations":'; - $this->assertStringContainsString($expected, $result); - - $expectedPattern = '/\| id[[:blank:]]+\| version[[:blank:]]+\| class[[:blank:]]+\| group[[:blank:]]+\| namespace[[:blank:]]+\| time[[:blank:]]+\| batch \|/'; - $this->assertMatchesRegularExpression($expectedPattern, $result); - - // Remove MockInputOutput. - CLI::resetInputOutput(); + $this->assertMatchesRegularExpression( + '/Which table do you want to see\? \[[\d,\s]+\]\: a/', + $result, + ); + $this->assertMatchesRegularExpression( + '/The "Which table do you want to see\?" field must be one of: [\d,\s]+./', + $result, + ); + $this->assertMatchesRegularExpression( + '/Which table do you want to see\? \[[\d,\s]+\]\: 0/', + $result, + ); + $this->assertMatchesRegularExpression( + '/Data of Table "db_migrations"\:/', + $result, + ); + $this->assertMatchesRegularExpression( + '/\| id[[:blank:]]+\| version[[:blank:]]+\| class[[:blank:]]+\| group[[:blank:]]+\| namespace[[:blank:]]+\| time[[:blank:]]+\| batch \|/', + $result, + ); } } diff --git a/tests/system/Commands/Generators/TestGeneratorTest.php b/tests/system/Commands/Generators/TestGeneratorTest.php index 71225847cfce..3cad6b214307 100644 --- a/tests/system/Commands/Generators/TestGeneratorTest.php +++ b/tests/system/Commands/Generators/TestGeneratorTest.php @@ -33,9 +33,6 @@ protected function setUp(): void parent::setUp(); $this->resetStreamFilterBuffer(); - - putenv('NO_COLOR=1'); - CLI::init(); } protected function tearDown(): void @@ -44,14 +41,16 @@ protected function tearDown(): void $this->clearTestFiles(); $this->resetStreamFilterBuffer(); + } - putenv('NO_COLOR'); - CLI::init(); + private function getUndecoratedBuffer(): string + { + return preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()); } private function clearTestFiles(): void { - preg_match('/File created: (.*)/', $this->getStreamFilterBuffer(), $result); + preg_match('/File created: (.*)/', $this->getUndecoratedBuffer(), $result); $file = str_replace('ROOTPATH' . DIRECTORY_SEPARATOR, ROOTPATH, $result[1] ?? ''); if (is_file($file)) { @@ -71,7 +70,7 @@ public function testGenerateTestFiles(string $name, string $expectedClass): void $expectedTestFile = str_replace('/', DIRECTORY_SEPARATOR, sprintf('%stests/%s.php', ROOTPATH, $expectedClass)); $expectedMessage = sprintf('File created: %s', str_replace(ROOTPATH, 'ROOTPATH' . DIRECTORY_SEPARATOR, $expectedTestFile)); - $this->assertStringContainsString($expectedMessage, $this->getStreamFilterBuffer()); + $this->assertStringContainsString($expectedMessage, $this->getUndecoratedBuffer()); $this->assertFileExists($expectedTestFile); } @@ -93,6 +92,7 @@ public static function provideGenerateTestFiles(): iterable public function testGenerateTestWithEmptyClassName(): void { $expectedFile = ROOTPATH . 'tests/FooTest.php'; + CLI::reset(); try { $io = new MockInputOutput(); @@ -106,7 +106,7 @@ public function testGenerateTestWithEmptyClassName(): void $expectedOutput .= 'The "Test class name" field is required.' . PHP_EOL; $expectedOutput .= 'Test class name : Foo' . PHP_EOL . PHP_EOL; $expectedOutput .= 'File created: ROOTPATH/tests/FooTest.php' . PHP_EOL . PHP_EOL; - $this->assertSame($expectedOutput, $io->getOutput()); + $this->assertSame($expectedOutput, preg_replace('/\e\[[^m]+m/', '', $io->getOutput())); $this->assertFileExists($expectedFile); } finally { if (is_file($expectedFile)) { diff --git a/tests/system/Commands/Housekeeping/ClearDebugbarTest.php b/tests/system/Commands/Housekeeping/ClearDebugbarTest.php index bb14906bcff9..3925eae6a033 100644 --- a/tests/system/Commands/Housekeeping/ClearDebugbarTest.php +++ b/tests/system/Commands/Housekeeping/ClearDebugbarTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Commands\Housekeeping; +use CodeIgniter\CLI\CLI; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; use PHPUnit\Framework\Attributes\Group; @@ -35,6 +36,8 @@ protected function setUp(): void command('debugbar:clear'); $this->resetStreamFilterBuffer(); + CLI::reset(); + $this->time = time(); $this->createDummyDebugbarJson(); } @@ -44,6 +47,8 @@ protected function tearDown(): void command('debugbar:clear'); $this->resetStreamFilterBuffer(); + CLI::reset(); + parent::tearDown(); } @@ -70,7 +75,7 @@ public function testClearDebugbarWorks(): void $this->assertFileDoesNotExist(WRITEPATH . 'debugbar' . DIRECTORY_SEPARATOR . "debugbar_{$this->time}.json"); $this->assertFileExists(WRITEPATH . 'debugbar' . DIRECTORY_SEPARATOR . 'index.html'); $this->assertSame( - "Debugbar cleared.\n", + "\nDebugbar cleared.\n", preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()), ); } @@ -111,7 +116,7 @@ public function testClearDebugbarWithError(): void $this->assertFileExists($path); $this->assertSame( - "Error deleting the debugbar JSON files.\n", + "\nError deleting the debugbar JSON files.\n", preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()), ); } diff --git a/tests/system/Commands/Housekeeping/ClearLogsTest.php b/tests/system/Commands/Housekeeping/ClearLogsTest.php index 70406956d66e..38c247ec8d90 100644 --- a/tests/system/Commands/Housekeeping/ClearLogsTest.php +++ b/tests/system/Commands/Housekeeping/ClearLogsTest.php @@ -41,6 +41,8 @@ protected function setUp(): void command('logs:clear --force'); $this->resetStreamFilterBuffer(); + CLI::reset(); + $this->createDummyLogFiles(); } @@ -78,7 +80,7 @@ public function testClearLogsUsingForce(): void $this->assertFileDoesNotExist(WRITEPATH . 'logs' . DIRECTORY_SEPARATOR . "log-{$this->date}.log"); $this->assertFileExists(WRITEPATH . 'logs' . DIRECTORY_SEPARATOR . 'index.html'); - $this->assertSame("Logs cleared.\n", preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer())); + $this->assertSame("\nLogs cleared.\n", preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer())); } public function testClearLogsAbortsClearWithoutForce(): void @@ -94,11 +96,12 @@ public function testClearLogsAbortsClearWithoutForce(): void $this->assertFileExists(WRITEPATH . 'logs' . DIRECTORY_SEPARATOR . "log-{$this->date}.log"); $this->assertSame( <<<'EOT' + Are you sure you want to delete the logs? [n, y]: n Deleting logs aborted. If you want, use the "--force" option to force delete all log files. EOT, - preg_replace('/\e\[[^m]+m/', '', $io->getOutput(2) . $io->getOutput(3)), + preg_replace('/\e\[[^m]+m/', '', $io->getOutput()), ); } @@ -110,16 +113,19 @@ public function testClearLogsAbortsClearWithoutForceWithDefaultAnswer(): void $io->setInputs(['']); CLI::setInputOutput($io); + $space = ' '; + command('logs:clear'); $this->assertFileExists(WRITEPATH . 'logs' . DIRECTORY_SEPARATOR . "log-{$this->date}.log"); $this->assertSame( - <<<'EOT' + <<getOutput(2) . $io->getOutput(3)), + preg_replace('/\e\[[^m]+m/', '', $io->getOutput()), ); } @@ -134,7 +140,14 @@ public function testClearLogsWithoutForceButWithConfirmation(): void command('logs:clear'); $this->assertFileDoesNotExist(WRITEPATH . 'logs' . DIRECTORY_SEPARATOR . "log-{$this->date}.log"); - $this->assertSame("Logs cleared.\n", preg_replace('/\e\[[^m]+m/', '', $io->getOutput(2))); + $this->assertSame( + <<<'EOT' + Are you sure you want to delete the logs? [n, y]: y + Logs cleared. + + EOT, + preg_replace('/\e\[[^m]+m/', '', $io->getOutput()), + ); } #[RequiresOperatingSystem('Darwin|Linux')] @@ -173,6 +186,9 @@ public function testClearLogsFailsOnChmodFailure(): void } $this->assertFileExists($path); - $this->assertSame("Error in deleting the logs files.\n", preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer())); + $this->assertSame( + "\nError in deleting the logs files.\n", + preg_replace('/\e\[[^m]+m/', '', $this->getStreamFilterBuffer()), + ); } } diff --git a/tests/system/Commands/Utilities/ConfigCheckTest.php b/tests/system/Commands/Utilities/ConfigCheckTest.php index 9ac1c84fa51f..d6aeba15e664 100644 --- a/tests/system/Commands/Utilities/ConfigCheckTest.php +++ b/tests/system/Commands/Utilities/ConfigCheckTest.php @@ -14,6 +14,7 @@ namespace CodeIgniter\Commands\Utilities; use Closure; +use CodeIgniter\CLI\CLI; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; use Config\App; @@ -31,34 +32,38 @@ final class ConfigCheckTest extends CIUnitTestCase public static function setUpBeforeClass(): void { + parent::setUpBeforeClass(); + App::$override = false; putenv('NO_COLOR=1'); CliRenderer::$cli_colors = false; - - parent::setUpBeforeClass(); } public static function tearDownAfterClass(): void { + parent::tearDownAfterClass(); + App::$override = true; putenv('NO_COLOR'); CliRenderer::$cli_colors = true; - - parent::tearDownAfterClass(); } protected function setUp(): void { - $this->resetServices(); parent::setUp(); + + $this->resetServices(); + CLI::reset(); } protected function tearDown(): void { - $this->resetServices(); parent::tearDown(); + + $this->resetServices(); + CLI::reset(); } public function testCommandConfigCheckWithNoArgumentPassed(): void @@ -67,13 +72,14 @@ public function testCommandConfigCheckWithNoArgumentPassed(): void $this->assertSame( <<<'EOF' + You must specify a Config classname. Usage: config:check Example: config:check App config:check 'CodeIgniter\Shield\Config\Auth' EOF, - str_replace("\n\n", "\n", $this->getStreamFilterBuffer()), + $this->getStreamFilterBuffer(), ); } @@ -82,7 +88,7 @@ public function testCommandConfigCheckNonexistentClass(): void command('config:check Nonexistent'); $this->assertSame( - "No such Config class: Nonexistent\n", + "\nNo such Config class: Nonexistent\n", $this->getStreamFilterBuffer(), ); } @@ -98,7 +104,7 @@ public function testConfigCheckWithKintEnabledUsesKintD(): void command('config:check App'); $this->assertSame( - $command(config('App')) . "\n", + "\n" . $command(config('App')) . "\n", preg_replace('/\s+Config Caching: \S+/', '', $this->getStreamFilterBuffer()), ); } @@ -110,19 +116,14 @@ public function testConfigCheckWithKintDisabledUsesVarDump(): void new ConfigCheck(service('logger'), service('commands')), 'getVarDump', ); - $clean = static fn (string $input): string => trim(preg_replace( - '/(\033\[[0-9;]+m)|(\035\[[0-9;]+m)/u', - '', - $input, - )); try { Kint::$enabled_mode = false; command('config:check App'); $this->assertSame( - $clean($command(config('App'))), - $clean(preg_replace('/\s+Config Caching: \S+/', '', $this->getStreamFilterBuffer())), + "\n" . $command(config('App')), + preg_replace('/\s+Config Caching: \S+/', '', $this->getStreamFilterBuffer()), ); } finally { Kint::$enabled_mode = true; From c8ae390ac6d2bb79160755ab86869165434dd608 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Mon, 13 Apr 2026 18:43:26 +0800 Subject: [PATCH 31/85] test: fix command tests that may hang on linux due to sudo (#10107) --- .../Housekeeping/ClearDebugbarTest.php | 27 +++---------------- .../Commands/Housekeeping/ClearLogsTest.php | 27 +++---------------- 2 files changed, 6 insertions(+), 48 deletions(-) diff --git a/tests/system/Commands/Housekeeping/ClearDebugbarTest.php b/tests/system/Commands/Housekeeping/ClearDebugbarTest.php index 3925eae6a033..e380e0aaaf68 100644 --- a/tests/system/Commands/Housekeeping/ClearDebugbarTest.php +++ b/tests/system/Commands/Housekeeping/ClearDebugbarTest.php @@ -85,34 +85,13 @@ public function testClearDebugbarWithError(): void { $path = WRITEPATH . 'debugbar' . DIRECTORY_SEPARATOR . "debugbar_{$this->time}.json"; - // Attempt to make the file itself undeletable by setting the - // immutable/uchg flag on supported platforms. - $immutableSet = false; - if (str_starts_with(PHP_OS, 'Darwin')) { - @exec(sprintf('chflags uchg %s', escapeshellarg($path)), $output, $rc); - $immutableSet = $rc === 0; - } else { - // Try chattr on Linux with sudo (for containerized environments) - @exec('which chattr', $whichOut, $whichRc); - - if ($whichRc === 0) { - @exec(sprintf('sudo chattr +i %s', escapeshellarg($path)), $output, $rc); - $immutableSet = $rc === 0; - } - } - - if (! $immutableSet) { - $this->markTestSkipped('Cannot set file immutability in this environment'); - } + // Attempt to make the file itself undeletable + chmod(dirname($path), 0555); command('debugbar:clear'); // Restore attributes so other tests are not affected. - if (str_starts_with(PHP_OS, 'Darwin')) { - @exec(sprintf('chflags nouchg %s', escapeshellarg($path))); - } else { - @exec(sprintf('sudo chattr -i %s', escapeshellarg($path))); - } + chmod(dirname($path), 0755); $this->assertFileExists($path); $this->assertSame( diff --git a/tests/system/Commands/Housekeeping/ClearLogsTest.php b/tests/system/Commands/Housekeeping/ClearLogsTest.php index 38c247ec8d90..09c889a6e573 100644 --- a/tests/system/Commands/Housekeeping/ClearLogsTest.php +++ b/tests/system/Commands/Housekeeping/ClearLogsTest.php @@ -156,34 +156,13 @@ public function testClearLogsFailsOnChmodFailure(): void $path = WRITEPATH . 'logs' . DIRECTORY_SEPARATOR . "log-{$this->date}.log"; file_put_contents($path, 'Lorem ipsum'); - // Attempt to make the file itself undeletable by setting the - // immutable/uchg flag on supported platforms. - $immutableSet = false; - if (str_starts_with(PHP_OS, 'Darwin')) { - @exec(sprintf('chflags uchg %s', escapeshellarg($path)), $output, $rc); - $immutableSet = $rc === 0; - } else { - // Try chattr on Linux with sudo (for containerized environments) - @exec('which chattr', $whichOut, $whichRc); - - if ($whichRc === 0) { - @exec(sprintf('sudo chattr +i %s', escapeshellarg($path)), $output, $rc); - $immutableSet = $rc === 0; - } - } - - if (! $immutableSet) { - $this->markTestSkipped('Cannot set file immutability in this environment'); - } + // Attempt to make the file itself undeletable + chmod(dirname($path), 0555); command('logs:clear --force'); // Restore attributes so other tests are not affected. - if (str_starts_with(PHP_OS, 'Darwin')) { - @exec(sprintf('chflags nouchg %s', escapeshellarg($path))); - } else { - @exec(sprintf('sudo chattr -i %s', escapeshellarg($path))); - } + chmod(dirname($path), 0755); $this->assertFileExists($path); $this->assertSame( From 855d60a4d7179eafc5045e8a990c552aba77b724 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Mon, 13 Apr 2026 19:51:05 +0800 Subject: [PATCH 32/85] refactor: rename `-h` option of `routes` command as `--handler` (#10113) --- system/Commands/Utilities/Routes.php | 17 +++++-- .../system/Commands/Utilities/RoutesTest.php | 46 ++++++++++++++++--- user_guide_src/source/changelogs/v4.7.3.rst | 4 ++ utils/phpstan-baseline/argument.type.neon | 7 +-- utils/phpstan-baseline/loader.neon | 2 +- 5 files changed, 58 insertions(+), 18 deletions(-) diff --git a/system/Commands/Utilities/Routes.php b/system/Commands/Utilities/Routes.php index 074a8b50f5cf..86f7bd74158f 100644 --- a/system/Commands/Utilities/Routes.php +++ b/system/Commands/Utilities/Routes.php @@ -73,8 +73,8 @@ class Routes extends BaseCommand * @var array */ protected $options = [ - '-h' => 'Sort by Handler.', - '--host' => 'Specify hostname in request URI.', + '--handler' => 'Sort by Handler.', + '--host' => 'Specify hostname in request URI.', ]; /** @@ -82,8 +82,17 @@ class Routes extends BaseCommand */ public function run(array $params) { - $sortByHandler = array_key_exists('h', $params); - $host = $params['host'] ?? null; + $sortByHandler = array_key_exists('handler', $params); + + if (! $sortByHandler && array_key_exists('h', $params)) { + // Support -h as a shortcut but print a warning that it is not the intended use of -h. + CLI::write('Warning: -h will be used as shortcut for --help in v4.8.0. Please use --handler to sort by handler.', 'yellow'); + CLI::newLine(); + + $sortByHandler = true; + } + + $host = $params['host'] ?? null; // Set HTTP_HOST if ($host !== null) { diff --git a/tests/system/Commands/Utilities/RoutesTest.php b/tests/system/Commands/Utilities/RoutesTest.php index bf1789135387..f2dc10bfc5bd 100644 --- a/tests/system/Commands/Utilities/RoutesTest.php +++ b/tests/system/Commands/Utilities/RoutesTest.php @@ -56,7 +56,7 @@ private function getCleanRoutes(): RouteCollection public function testRoutesCommand(): void { - Services::injectMock('routes', null); + Services::resetSingle('routes'); command('routes'); @@ -92,11 +92,43 @@ public function testRoutesCommand(): void public function testRoutesCommandSortByHandler(): void { - Services::injectMock('routes', null); + Services::resetSingle('routes'); + + command('routes --handler'); + + $expected = <<<'EOL' + +---------+---------+---------------+----------------------------------------+----------------+---------------+ + | Method | Route | Name | Handler ↓ | Before Filters | After Filters | + +---------+---------+---------------+----------------------------------------+----------------+---------------+ + | GET | closure | » | (Closure) | | | + | GET | / | » | \App\Controllers\Home::index | | | + | GET | testing | testing-index | \App\Controllers\TestController::index | | | + | HEAD | testing | testing-index | \App\Controllers\TestController::index | | | + | POST | testing | testing-index | \App\Controllers\TestController::index | | | + | PATCH | testing | testing-index | \App\Controllers\TestController::index | | | + | PUT | testing | testing-index | \App\Controllers\TestController::index | | | + | DELETE | testing | testing-index | \App\Controllers\TestController::index | | | + | OPTIONS | testing | testing-index | \App\Controllers\TestController::index | | | + | TRACE | testing | testing-index | \App\Controllers\TestController::index | | | + | CONNECT | testing | testing-index | \App\Controllers\TestController::index | | | + | CLI | testing | testing-index | \App\Controllers\TestController::index | | | + +---------+---------+---------------+----------------------------------------+----------------+---------------+ + EOL; + $this->assertStringContainsString($expected, $this->getBuffer()); + } + + /** + * @todo To remove this test and the backward compatibility for -h in v4.8.0. + */ + public function testRoutesCommandSortByHandlerUsingShortcutForBc(): void + { + Services::resetSingle('routes'); command('routes -h'); $expected = <<<'EOL' + Warning: -h will be used as shortcut for --help in v4.8.0. Please use --handler to sort by handler. + +---------+---------+---------------+----------------------------------------+----------------+---------------+ | Method | Route | Name | Handler ↓ | Before Filters | After Filters | +---------+---------+---------------+----------------------------------------+----------------+---------------+ @@ -119,7 +151,7 @@ public function testRoutesCommandSortByHandler(): void public function testRoutesCommandHostHostname(): void { - Services::injectMock('routes', null); + Services::resetSingle('routes'); command('routes --host blog.example.com'); @@ -148,7 +180,7 @@ public function testRoutesCommandHostHostname(): void public function testRoutesCommandHostSubdomain(): void { - Services::injectMock('routes', null); + Services::resetSingle('routes'); command('routes --host sub.example.com'); @@ -181,8 +213,7 @@ public function testRoutesCommandAutoRouteImproved(): void $routes->setAutoRoute(true); config('Feature')->autoRoutesImproved = true; - $namespace = 'Tests\Support\Controllers'; - $routes->setDefaultNamespace($namespace); + $routes->setDefaultNamespace('Tests\Support\Controllers'); command('routes'); @@ -214,7 +245,8 @@ public function testRoutesCommandRouteLegacy(): void $routes = $this->getCleanRoutes(); $routes->loadRoutes(); - $featureConfig = config(Feature::class); + $featureConfig = config(Feature::class); + $featureConfig->autoRoutesImproved = false; $routes->setAutoRoute(true); diff --git a/user_guide_src/source/changelogs/v4.7.3.rst b/user_guide_src/source/changelogs/v4.7.3.rst index 268f49ecd227..1d2e45d05ed9 100644 --- a/user_guide_src/source/changelogs/v4.7.3.rst +++ b/user_guide_src/source/changelogs/v4.7.3.rst @@ -24,6 +24,10 @@ Message Changes Changes ******* +- **Commands:** The ``-h`` option for the ``routes`` command is renamed to ``--handler`` to avoid conflict with the common use of ``-h`` as a shortcut for ``--help``. + The old ``-h`` option will continue to work until v4.8.0, at which point it will be removed and repurposed as a shortcut for ``--help``. + A warning message is displayed when using the old ``-h`` option to encourage users to switch to the new ``--handler`` option. + ************ Deprecations ************ diff --git a/utils/phpstan-baseline/argument.type.neon b/utils/phpstan-baseline/argument.type.neon index 4a1fe1aed2ef..9697915545a3 100644 --- a/utils/phpstan-baseline/argument.type.neon +++ b/utils/phpstan-baseline/argument.type.neon @@ -1,4 +1,4 @@ -# total 82 errors +# total 78 errors parameters: ignoreErrors: @@ -47,11 +47,6 @@ parameters: count: 1 path: ../../tests/system/CodeIgniterTest.php - - - message: '#^Parameter \#2 \$mock of static method CodeIgniter\\Config\\BaseService\:\:injectMock\(\) expects object, null given\.$#' - count: 4 - path: ../../tests/system/Commands/Utilities/RoutesTest.php - - message: '#^Parameter \#1 \$expected of method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) expects class\-string\, string given\.$#' count: 1 diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index bcdfa61b4264..57d40a379ca7 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 2059 errors +# total 2055 errors includes: - argument.type.neon From 554be61c166d9815357a3b7770987219da2f295b Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Mon, 13 Apr 2026 19:53:59 +0800 Subject: [PATCH 33/85] fix: ensure calling `env` command with options only would not throw (#10114) --- system/Commands/Utilities/Environment.php | 2 +- tests/system/Commands/Utilities/EnvironmentCommandTest.php | 7 +++++++ user_guide_src/source/changelogs/v4.7.3.rst | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/system/Commands/Utilities/Environment.php b/system/Commands/Utilities/Environment.php index 44df09ad3bee..778e1833becb 100644 --- a/system/Commands/Utilities/Environment.php +++ b/system/Commands/Utilities/Environment.php @@ -85,7 +85,7 @@ final class Environment extends BaseCommand */ public function run(array $params) { - if ($params === []) { + if (! isset($params[0])) { CLI::write(sprintf('Your environment is currently set as %s.', CLI::color(service('superglobals')->server('CI_ENVIRONMENT', ENVIRONMENT), 'green'))); CLI::newLine(); diff --git a/tests/system/Commands/Utilities/EnvironmentCommandTest.php b/tests/system/Commands/Utilities/EnvironmentCommandTest.php index ca5236f5ff5b..3513436fa9f9 100644 --- a/tests/system/Commands/Utilities/EnvironmentCommandTest.php +++ b/tests/system/Commands/Utilities/EnvironmentCommandTest.php @@ -64,6 +64,13 @@ public function testUsingCommandWithNoArgumentsGivesCurrentEnvironment(): void $this->assertStringContainsString(ENVIRONMENT, $this->getStreamFilterBuffer()); } + public function testUsingCommandWithOptionsOnlyGivesCurrentEnvironment(): void + { + command('env --foo'); + $this->assertStringContainsString('testing', $this->getStreamFilterBuffer()); + $this->assertStringContainsString(ENVIRONMENT, $this->getStreamFilterBuffer()); + } + public function testProvidingTestingAsEnvGivesErrorMessage(): void { command('env testing'); diff --git a/user_guide_src/source/changelogs/v4.7.3.rst b/user_guide_src/source/changelogs/v4.7.3.rst index 1d2e45d05ed9..ea3b37f579f7 100644 --- a/user_guide_src/source/changelogs/v4.7.3.rst +++ b/user_guide_src/source/changelogs/v4.7.3.rst @@ -37,6 +37,7 @@ Bugs Fixed ********** - **Autoloader:** Fixed a bug where ``Autoloader::unregister()`` (used during tests) silently failed to remove handlers from the SPL autoload stack, causing closures to accumulate permanently. +- **Commands:** Fixed a bug in the ``env`` command where passing options only would cause the command to throw a ``TypeError`` instead of showing the current environment. - **Common:** Fixed a bug where the ``command()`` helper function did not properly clean up output buffers, which could lead to risky tests when exceptions were thrown. - **Validation:** Fixed a bug where ``Validation::getValidated()`` dropped fields whose validated value was explicitly ``null``. From 3573f5d59eb2558b84da4787c0292c10709a8a09 Mon Sep 17 00:00:00 2001 From: neznaika0 Date: Mon, 13 Apr 2026 17:37:59 +0300 Subject: [PATCH 34/85] docs: Improve guide (#10109) * docs: Update "Managing your Applications" * docs: Update "Composer Installation" * docs: Update "Worker Mode" * docs: Update "Testing" * fix: Move next line --- .../source/general/managing_apps.rst | 25 ++++++++++--------- .../source/general/managing_apps/001.php | 2 +- .../installation/installing_composer.rst | 7 +++++- .../source/installation/worker_mode.rst | 2 ++ user_guide_src/source/testing/overview.rst | 2 +- 5 files changed, 23 insertions(+), 15 deletions(-) diff --git a/user_guide_src/source/general/managing_apps.rst b/user_guide_src/source/general/managing_apps.rst index 5e71840a98bf..823caf5a45cb 100644 --- a/user_guide_src/source/general/managing_apps.rst +++ b/user_guide_src/source/general/managing_apps.rst @@ -20,7 +20,7 @@ Renaming or Relocating the Application Directory If you would like to rename your application directory or even move it to a different location on your server, other than your project root, open your main **app/Config/Paths.php** and set a *full server path* in the -``$appDirectory`` variable (at about line 44): +``$appDirectory`` variable (at about line 45): .. literalinclude:: managing_apps/001.php @@ -46,26 +46,26 @@ If you would like to share a common CodeIgniter framework installation, to manag several different applications, simply put all of the directories located inside your application directory into their own (sub)-directory. -For example, let's say you want to create two applications, named **foo** -and **bar**. You could structure your application project directories like this: +For example, let's say you want to create two applications, named **blog** +and **shop**. You could structure your application project directories like this: .. code-block:: text - foo/ + blog/ app/ public/ tests/ writable/ env - phpunit.xml.dist + phpunit.dist.xml spark - bar/ + shop/ app/ public/ tests/ writable/ env - phpunit.xml.dist + phpunit.dist.xml spark vendor/ autoload.php @@ -77,11 +77,11 @@ and **bar**. You could structure your application project directories like this: .. code-block:: text - foo/ - bar/ + blog/ + shop/ codeigniter4/system/ -This would have two apps, **foo** and **bar**, both having standard application directories +This would have two apps, **blog** and **shop**, both having standard application directories and a **public** folder, and sharing a common **codeigniter4/framework**. The ``$systemDirectory`` variable in **app/Config/Paths.php** inside each @@ -89,13 +89,14 @@ of those would be set to refer to the shared common **codeigniter4/framework** f .. literalinclude:: managing_apps/005.php -.. note:: If you install CodeIgniter from the Zip file, the ``$systemDirectory`` would be ``__DIR__ . '/../../../codeigniter4/system'``. - And modify the ``COMPOSER_PATH`` constant in **app/Config/Constants.php** inside each of those: .. literalinclude:: managing_apps/004.php +.. note:: If you install CodeIgniter from the Zip file, the ``$systemDirectory`` would be ``__DIR__ . '/../../../codeigniter4/system'``. + If you don't use Composer, you don't have to edit the ``COMPOSER_PATH``. + Only when you change the Application Directory, see :ref:`renaming-app-directory` and modify the paths in the **index.php** and **spark**. Changing the Location of the .env File diff --git a/user_guide_src/source/general/managing_apps/001.php b/user_guide_src/source/general/managing_apps/001.php index da4991fd738d..612d8b5886f5 100644 --- a/user_guide_src/source/general/managing_apps/001.php +++ b/user_guide_src/source/general/managing_apps/001.php @@ -6,7 +6,7 @@ class Paths { // ... - public $appDirectory = '/path/to/your/app'; + public string $appDirectory = '/path/to/your/app'; // ... } diff --git a/user_guide_src/source/installation/installing_composer.rst b/user_guide_src/source/installation/installing_composer.rst index ab24aeffadea..bcf141c122e6 100644 --- a/user_guide_src/source/installation/installing_composer.rst +++ b/user_guide_src/source/installation/installing_composer.rst @@ -166,11 +166,15 @@ Latest Dev The App Starter repo comes with a ``builds`` scripts to switch Composer sources between the current stable release and the latest development branch of the framework. Use this script for a developer who is willing to live with the latest unreleased changes, which may be unstable. +This way you can help find bugs and improve new features. The `development user guide `_ is accessible online. Note that this differs from the released user guide, and will pertain to the develop branch explicitly. +Before running the script, check ``$files``, which is the list of files to be changed. +This is useful if you have additional places in the code that mention the directory with the framework. + .. note:: You should not rely on the version of the framework in your project - the development code may contain an incorrect number. @@ -247,7 +251,7 @@ Setting Up ---------- 1. Copy the **app**, **public**, **tests** and **writable** folders from **vendor/codeigniter4/framework** to your project root - 2. Copy the **env**, **phpunit.xml.dist** and **spark** files, from **vendor/codeigniter4/framework** to your project root + 2. Copy the **env**, **phpunit.dist.xml** and **spark** files, from **vendor/codeigniter4/framework** to your project root 3. You will have to adjust the ``$systemDirectory`` property in **app/Config/Paths.php** to refer to the vendor one, e.g., ``__DIR__ . '/../../vendor/codeigniter4/framework/system'``. Initial Configuration @@ -322,4 +326,5 @@ From the command line inside your project root: composer require codeigniter4/translations +Copy the **vendor/codeigniter4/translations/Language** folder contents in it to your **app/Language** folder. These will be updated along with the framework whenever you do a ``composer update``. diff --git a/user_guide_src/source/installation/worker_mode.rst b/user_guide_src/source/installation/worker_mode.rst index 6f71af593ee9..d3e0cc87b13e 100644 --- a/user_guide_src/source/installation/worker_mode.rst +++ b/user_guide_src/source/installation/worker_mode.rst @@ -110,6 +110,8 @@ Installation 3. Configure your worker settings in **app/Config/WorkerMode.php** if needed. The defaults are recommended for most applications. +.. note:: When changing the project directory, see the section :ref:`renaming-app-directory` and update your **public/frankenphp-worker.php**. + Running the Worker ================== diff --git a/user_guide_src/source/testing/overview.rst b/user_guide_src/source/testing/overview.rst index 8a94d6bc382c..4d49c5569355 100644 --- a/user_guide_src/source/testing/overview.rst +++ b/user_guide_src/source/testing/overview.rst @@ -60,7 +60,7 @@ Testing Your Application PHPUnit Configuration ===================== -In your CodeIgniter project root, there is the ``phpunit.xml.dist`` file. This +In your CodeIgniter project root, there is the ``phpunit.dist.xml`` file. This controls unit testing of your application. If you provide your own ``phpunit.xml``, it will over-ride this. From 0366b77a14957b801351cf6bbcefe430fcab0253 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Mon, 13 Apr 2026 19:03:14 +0200 Subject: [PATCH 35/85] refactor: start only required services (#10115) --- .github/workflows/reusable-phpunit-test.yml | 15 +++++++-------- .github/workflows/test-random-execution.yml | 8 ++++---- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.github/workflows/reusable-phpunit-test.yml b/.github/workflows/reusable-phpunit-test.yml index 2f4d59a3a166..97bbf5c7c2dc 100644 --- a/.github/workflows/reusable-phpunit-test.yml +++ b/.github/workflows/reusable-phpunit-test.yml @@ -73,7 +73,7 @@ jobs: # Service containers cannot be extracted to caller workflows yet services: mysql: - image: mysql:${{ inputs.mysql-version || '8.0' }} + image: ${{ inputs.db-platform == 'MySQLi' && format('mysql:{0}', inputs.mysql-version || '8.0') || '' }} env: MYSQL_ALLOW_EMPTY_PASSWORD: yes MYSQL_DATABASE: test @@ -86,7 +86,7 @@ jobs: --health-retries=3 postgres: - image: postgres + image: ${{ inputs.db-platform == 'Postgre' && 'postgres' || '' }} env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres @@ -100,7 +100,7 @@ jobs: --health-retries=3 mssql: - image: mcr.microsoft.com/mssql/server:2025-CU2-ubuntu-24.04 + image: ${{ inputs.db-platform == 'SQLSRV' && 'mcr.microsoft.com/mssql/server:2025-CU3-ubuntu-24.04' || '' }} env: MSSQL_SA_PASSWORD: 1Secure*Password1 ACCEPT_EULA: Y @@ -114,7 +114,7 @@ jobs: --health-retries=3 oracle: - image: gvenzl/oracle-free:latest + image: ${{ inputs.db-platform == 'OCI8' && 'gvenzl/oracle-free:latest' || '' }} env: ORACLE_RANDOM_PASSWORD: true APP_USER: ORACLE @@ -146,10 +146,9 @@ jobs: - name: Install mssql-tools on runner if: ${{ inputs.db-platform == 'SQLSRV' }} run: | - # Detect Ubuntu version used by the runner (fallback to 24.04) - DISTRO=$(lsb_release -rs 2>/dev/null || echo '24.04') - curl -sSL https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add - - curl -sSL https://packages.microsoft.com/config/ubuntu/${DISTRO}/prod.list | sudo tee /etc/apt/sources.list.d/mssql-release.list + source /etc/os-release + curl -sSL https://packages.microsoft.com/keys/microsoft.asc | sudo gpg --dearmor --batch --yes -o /usr/share/keyrings/microsoft-prod.gpg + echo "deb [arch=amd64 signed-by=/usr/share/keyrings/microsoft-prod.gpg] https://packages.microsoft.com/ubuntu/${VERSION_ID}/prod ${UBUNTU_CODENAME} main" | sudo tee /etc/apt/sources.list.d/mssql-release.list sudo apt-get update sudo ACCEPT_EULA=Y apt-get install -y msodbcsql18 mssql-tools18 unixodbc-dev diff --git a/.github/workflows/test-random-execution.yml b/.github/workflows/test-random-execution.yml index 4e304e84338a..bb7124526bd2 100644 --- a/.github/workflows/test-random-execution.yml +++ b/.github/workflows/test-random-execution.yml @@ -79,7 +79,7 @@ jobs: services: mysql: - image: mysql:8.0 + image: ${{ matrix.db-platform == 'MySQLi' && 'mysql:8.0' || '' }} env: MYSQL_ALLOW_EMPTY_PASSWORD: yes MYSQL_DATABASE: test @@ -92,7 +92,7 @@ jobs: --health-retries=3 postgres: - image: postgres + image: ${{ matrix.db-platform == 'Postgre' && 'postgres' || '' }} env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres @@ -106,7 +106,7 @@ jobs: --health-retries=3 mssql: - image: mcr.microsoft.com/mssql/server:2025-CU2-ubuntu-24.04 + image: ${{ matrix.db-platform == 'SQLSRV' && 'mcr.microsoft.com/mssql/server:2025-CU3-ubuntu-24.04' || '' }} env: MSSQL_SA_PASSWORD: 1Secure*Password1 ACCEPT_EULA: Y @@ -120,7 +120,7 @@ jobs: --health-retries=3 oracle: - image: gvenzl/oracle-free:latest + image: ${{ matrix.db-platform == 'Oracle' && 'gvenzl/oracle-free:latest' || '' }} env: ORACLE_RANDOM_PASSWORD: true APP_USER: ORACLE From b7a64229cf67f11ff2e97535f1114957080ac90b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 19:27:40 +0200 Subject: [PATCH 36/85] chore(deps): bump actions/upload-artifact in / (#10116) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) in `/` from 7.0.0 to 7.0.1. Updates `actions/upload-artifact` from 7.0.0 to 7.0.1 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/bbbca2ddaa5d8feaa63e36b76fdaad77386f024f...043fb46d1a93c77aae656e7c1c64a875d1fc6a0a) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github_actions ... 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 +- .github/workflows/test-random-execution.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy-userguide-latest.yml b/.github/workflows/deploy-userguide-latest.yml index 196c764587e6..48c69e126f88 100644 --- a/.github/workflows/deploy-userguide-latest.yml +++ b/.github/workflows/deploy-userguide-latest.yml @@ -59,7 +59,7 @@ jobs: # Create an artifact of the html output - name: Upload artifact - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 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 97bbf5c7c2dc..574154d0320c 100644 --- a/.github/workflows/reusable-phpunit-test.yml +++ b/.github/workflows/reusable-phpunit-test.yml @@ -240,7 +240,7 @@ jobs: - name: Upload coverage results as artifact if: ${{ inputs.enable-artifact-upload }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ steps.setup-env.outputs.ARTIFACT_NAME }} path: build/cov/coverage-${{ steps.setup-env.outputs.ARTIFACT_NAME }}.cov diff --git a/.github/workflows/reusable-serviceless-phpunit-test.yml b/.github/workflows/reusable-serviceless-phpunit-test.yml index 98bda6f81922..e81934a419e2 100644 --- a/.github/workflows/reusable-serviceless-phpunit-test.yml +++ b/.github/workflows/reusable-serviceless-phpunit-test.yml @@ -133,7 +133,7 @@ jobs: - name: Upload coverage results as artifact if: ${{ inputs.enable-artifact-upload }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ steps.setup-env.outputs.ARTIFACT_NAME }} path: build/cov/coverage-${{ steps.setup-env.outputs.ARTIFACT_NAME }}.cov diff --git a/.github/workflows/test-random-execution.yml b/.github/workflows/test-random-execution.yml index bb7124526bd2..a6f4e9547a28 100644 --- a/.github/workflows/test-random-execution.yml +++ b/.github/workflows/test-random-execution.yml @@ -226,7 +226,7 @@ jobs: - name: Upload random-test artifacts on failure if: ${{ always() && failure() }} - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: random-tests-${{ matrix.db-platform }}-php${{ matrix.php-version }} path: build/random-tests/ From b8351b409f0f70be15512ba349a7d097bc040962 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 19:28:02 +0200 Subject: [PATCH 37/85] chore(deps): bump actions/cache in / (#10117) Bumps [actions/cache](https://github.com/actions/cache) in `/` from 5.0.4 to 5.0.5. Updates `actions/cache` from 5.0.4 to 5.0.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/668228422ae6a00e4ad889ee87cd7109ec5666a7...27d5ce7f107fe9357f9df03efb73ab90386fccae) --- updated-dependencies: - dependency-name: actions/cache dependency-version: 5.0.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github_actions ... 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-random-execution.yml | 2 +- .github/workflows/test-rector.yml | 4 ++-- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/reusable-coveralls.yml b/.github/workflows/reusable-coveralls.yml index aca808161485..9ac42b22c408 100644 --- a/.github/workflows/reusable-coveralls.yml +++ b/.github/workflows/reusable-coveralls.yml @@ -50,7 +50,7 @@ jobs: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ${{ steps.composer-cache.outputs.COMPOSER_CACHE_FILES_DIR }} key: ${{ github.job }}-php-${{ inputs.php-version }}-${{ hashFiles('**/composer.*') }} @@ -59,7 +59,7 @@ jobs: ${{ github.job }}- - name: Cache PHPUnit's static analysis cache - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 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 574154d0320c..a809b55c4407 100644 --- a/.github/workflows/reusable-phpunit-test.yml +++ b/.github/workflows/reusable-phpunit-test.yml @@ -199,7 +199,7 @@ jobs: echo "ARTIFACT_NAME=${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}-db-${{ inputs.db-platform || 'none' }}${{ inputs.mysql-version || '' }}" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ${{ steps.setup-env.outputs.COMPOSER_CACHE_FILES_DIR }} key: ${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}-db-${{ inputs.db-platform || 'none' }}-${{ hashFiles('**/composer.*') }} @@ -210,7 +210,7 @@ jobs: - name: Cache PHPUnit's static analysis cache if: ${{ inputs.enable-artifact-upload }} - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 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 e81934a419e2..af205cda26f0 100644 --- a/.github/workflows/reusable-serviceless-phpunit-test.yml +++ b/.github/workflows/reusable-serviceless-phpunit-test.yml @@ -95,7 +95,7 @@ jobs: echo "ARTIFACT_NAME=${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}" >> $GITHUB_OUTPUT - name: Cache Composer dependencies - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ${{ steps.setup-env.outputs.COMPOSER_CACHE_FILES_DIR }} key: ${{ inputs.job-id || github.job }}-php-${{ inputs.php-version }}-${{ hashFiles('**/composer.*') }} @@ -105,7 +105,7 @@ jobs: - name: Cache PHPUnit's static analysis cache if: ${{ inputs.enable-artifact-upload }} - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 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 dcef94f5f0fa..ef6a106f8561 100644 --- a/.github/workflows/test-coding-standards.yml +++ b/.github/workflows/test-coding-standards.yml @@ -55,7 +55,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ${{ steps.composer-cache.outputs.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 de0da7242503..4be5e35aeeb5 100644 --- a/.github/workflows/test-deptrac.yml +++ b/.github/workflows/test-deptrac.yml @@ -60,7 +60,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ${{ steps.composer-cache.outputs.COMPOSER_CACHE_FILES_DIR }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} @@ -70,7 +70,7 @@ jobs: run: mkdir -p build/ - name: Cache Deptrac results - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: build key: ${{ runner.os }}-deptrac-${{ github.sha }} diff --git a/.github/workflows/test-phpstan.yml b/.github/workflows/test-phpstan.yml index 00b858923ed8..4d9e6a4f2303 100644 --- a/.github/workflows/test-phpstan.yml +++ b/.github/workflows/test-phpstan.yml @@ -72,7 +72,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ${{ steps.composer-cache.outputs.COMPOSER_CACHE_FILES_DIR }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} @@ -82,7 +82,7 @@ jobs: run: mkdir -p build/phpstan - name: Cache PHPStan result cache directory - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 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 c165dc259adc..7df95e400e96 100644 --- a/.github/workflows/test-psalm.yml +++ b/.github/workflows/test-psalm.yml @@ -61,7 +61,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ${{ steps.composer-cache.outputs.COMPOSER_CACHE_FILES_DIR }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}-${{ hashFiles('**/composer.lock') }} @@ -71,7 +71,7 @@ jobs: run: mkdir -p build/psalm - name: Cache Psalm results - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: build/psalm key: ${{ runner.os }}-psalm-${{ github.sha }} diff --git a/.github/workflows/test-random-execution.yml b/.github/workflows/test-random-execution.yml index a6f4e9547a28..953357e6f82b 100644 --- a/.github/workflows/test-random-execution.yml +++ b/.github/workflows/test-random-execution.yml @@ -182,7 +182,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ${{ steps.composer-cache.outputs.COMPOSER_CACHE_FILES_DIR }} key: PHP_${{ matrix.php-version }}-${{ hashFiles('**/composer.*') }} diff --git a/.github/workflows/test-rector.yml b/.github/workflows/test-rector.yml index c3462d8fba09..441175eeeb73 100644 --- a/.github/workflows/test-rector.yml +++ b/.github/workflows/test-rector.yml @@ -78,7 +78,7 @@ jobs: run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ${{ steps.composer-cache.outputs.COMPOSER_CACHE_FILES_DIR }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} @@ -88,7 +88,7 @@ jobs: run: composer update --ansi --no-interaction ${{ matrix.composer-option }} - name: Rector Cache - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: /tmp/rector key: ${{ runner.os }}-rector-${{ github.run_id }} From 5f0e0932b73136105424a1fe4599f9b23acce424 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Tue, 14 Apr 2026 01:45:55 +0800 Subject: [PATCH 38/85] chore: fix label-pr verification step (#10118) * chore: fix label-pr verification step * revert to pull request target --- .github/workflows/label-pr.yml | 55 +++++++++++++--------------------- 1 file changed, 20 insertions(+), 35 deletions(-) diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml index bc93f5e49dac..b8425dd9cc3a 100644 --- a/.github/workflows/label-pr.yml +++ b/.github/workflows/label-pr.yml @@ -7,28 +7,7 @@ on: - pull_request_target jobs: - validate-source: - permissions: - contents: read - pull-requests: read - runs-on: ubuntu-24.04 - outputs: - valid: ${{ steps.check.outputs.valid }} - - steps: - - name: Check if PR is from the main repository - id: check - run: | - if [[ "$HEAD_REPO" == "codeigniter4/CodeIgniter4" ]]; then - echo "valid=true" >> $GITHUB_OUTPUT - else - echo "valid=false" >> $GITHUB_OUTPUT - fi - env: - HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} - add-labels: - needs: validate-source permissions: contents: read pull-requests: write @@ -41,20 +20,26 @@ jobs: persist-credentials: false - name: Verify PR source for workflow file changes - run: | - # Get changed files in this PR - git fetch origin "refs/pull/${{ github.event.pull_request.number }}/merge" - CHANGED_FILES=$(git diff --name-only origin/develop FETCH_HEAD 2>/dev/null || echo "") - - # Check if this workflow file is being modified - if echo "$CHANGED_FILES" | grep -q "\.github/workflows/label-pr\.yml"; then - if [[ "$IS_VALID" != "true" ]]; then - echo "::error::Changes to label-pr.yml can only be made from the main repository." - exit 1 - fi - fi - env: - IS_VALID: ${{ needs.validate-source.outputs.valid }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const prFiles = await github.paginate(github.rest.pulls.listFiles.endpoint.merge({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + })); + const workflowFileChanged = prFiles.some(file => file.filename === '.github/workflows/label-pr.yml'); + + if (workflowFileChanged) { + if (context.payload.pull_request.head.repo.full_name !== 'codeigniter4/CodeIgniter4') { + throw new Error('Changes to label-pr.yml are not allowed from forks.'); + } + + console.log('Workflow file changed, but PR is from the main repository. Proceeding with label addition.'); + return; + } + + console.log('No changes to workflow file detected, proceeding with label addition.'); - name: Add labels uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 From a6bc922d8d947dac2b390fc4b6cbc9a532ebe328 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 15:22:57 +0000 Subject: [PATCH 39/85] chore(deps-dev): update rector/rector requirement Updates the requirements on [rector/rector](https://github.com/rectorphp/rector) to permit the latest version. Updates `rector/rector` to 2.4.2 - [Release notes](https://github.com/rectorphp/rector/releases) - [Commits](https://github.com/rectorphp/rector/compare/2.4.1...2.4.2) --- updated-dependencies: - dependency-name: rector/rector dependency-version: 2.4.2 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] --- composer.json | 2 +- system/Config/Services.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/composer.json b/composer.json index e1a7b6d6b2a4..ea3bf5391bad 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.4.1", + "rector/rector": "2.4.2", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { diff --git a/system/Config/Services.php b/system/Config/Services.php index 3878911c8cbf..cef65a8895a9 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -698,7 +698,6 @@ public static function session(?SessionConfig $config = null, bool $getShared = )); } - /** @var SessionBaseHandler $driver */ $driver = new $driverName($config, AppServices::get('request')->getIPAddress()); $driver->setLogger($logger); From e6f3c35b28913189eae327b98de5ec33676ae107 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Tue, 21 Apr 2026 02:01:30 +0800 Subject: [PATCH 40/85] chore: fix transient random test failures (#10122) --- .github/scripts/run-random-tests.sh | 151 ++++++++++++-------- .github/workflows/test-random-execution.yml | 1 + 2 files changed, 90 insertions(+), 62 deletions(-) diff --git a/.github/scripts/run-random-tests.sh b/.github/scripts/run-random-tests.sh index 52568c2c2e07..fe321bef0492 100755 --- a/.github/scripts/run-random-tests.sh +++ b/.github/scripts/run-random-tests.sh @@ -477,78 +477,105 @@ run_component_tests() { local output_file="$results_dir/random_test_output_${component}_$$.log" local events_file="$results_dir/random_test_events_${component}_$$.log" - local random_seed=$(generate_phpunit_random_seed) local exit_code=0 - - # Security: Use array to avoid eval and prevent command injection - local -a phpunit_args=( - "vendor/bin/phpunit" - "$test_dir" - "--colors=never" - "--no-coverage" - "--do-not-cache-result" - "--order-by=random" - "--random-order-seed=${random_seed}" - "--log-events-text" - "$events_file" - ) - - if [[ $timeout_seconds -gt 0 ]] && command -v timeout >/dev/null 2>&1; then - (cd "$project_root" && timeout --kill-after=2s "${timeout_seconds}s" "${phpunit_args[@]}") > "$output_file" 2>&1 - exit_code=$? - elif [[ $timeout_seconds -gt 0 ]] && command -v gtimeout >/dev/null 2>&1; then - (cd "$project_root" && gtimeout --kill-after=2s "${timeout_seconds}s" "${phpunit_args[@]}") > "$output_file" 2>&1 - exit_code=$? - else - local timeout_marker="$output_file.timeout" - (cd "$project_root" && "${phpunit_args[@]}") > "$output_file" 2>&1 & - local test_pid=$! - - if [[ $timeout_seconds -gt 0 ]]; then - # Watchdog: monitors test process and kills it after timeout - # Uses 1-second sleep intervals to respond quickly when test finishes early - ( - local elapsed=0 - while [[ $elapsed -lt $timeout_seconds ]]; do - sleep 1 - elapsed=$((elapsed + 1)) - kill -0 "$test_pid" 2>/dev/null || exit 0 - done - - if kill -0 "$test_pid" 2>/dev/null; then - touch "$timeout_marker" - local pids_to_kill=$(pgrep -P "$test_pid" 2>/dev/null) - - kill -TERM "$test_pid" 2>/dev/null || true - if [[ -n "$pids_to_kill" ]]; then - echo "$pids_to_kill" | xargs kill -TERM 2>/dev/null || true - fi - - sleep 2 + local attempt=1 + local -r max_attempts=2 + local random_seed + local -a phpunit_args + + # Retry loop: the Composer classmap autoloader occasionally fails to load + # CodeIgniter\CodeIgniter under parallel CI load — a transient infra race, + # not a real test failure. Retry once on that signature with a fresh random + # seed; a second miss is reported as genuine failure. + while true; do + random_seed=$(generate_phpunit_random_seed) + + # Security: Use array to avoid eval and prevent command injection + phpunit_args=( + "vendor/bin/phpunit" + "$test_dir" + "--colors=never" + "--no-coverage" + "--do-not-cache-result" + "--order-by=random" + "--random-order-seed=${random_seed}" + "--log-events-text" + "$events_file" + ) + + if [[ $timeout_seconds -gt 0 ]] && command -v timeout >/dev/null 2>&1; then + (cd "$project_root" && timeout --kill-after=2s "${timeout_seconds}s" "${phpunit_args[@]}") > "$output_file" 2>&1 + exit_code=$? + elif [[ $timeout_seconds -gt 0 ]] && command -v gtimeout >/dev/null 2>&1; then + (cd "$project_root" && gtimeout --kill-after=2s "${timeout_seconds}s" "${phpunit_args[@]}") > "$output_file" 2>&1 + exit_code=$? + else + local timeout_marker="$output_file.timeout" + (cd "$project_root" && "${phpunit_args[@]}") > "$output_file" 2>&1 & + local test_pid=$! + + if [[ $timeout_seconds -gt 0 ]]; then + # Watchdog: monitors test process and kills it after timeout + # Uses 1-second sleep intervals to respond quickly when test finishes early + ( + local elapsed=0 + while [[ $elapsed -lt $timeout_seconds ]]; do + sleep 1 + elapsed=$((elapsed + 1)) + kill -0 "$test_pid" 2>/dev/null || exit 0 + done if kill -0 "$test_pid" 2>/dev/null; then - kill -KILL "$test_pid" 2>/dev/null || true + touch "$timeout_marker" + local pids_to_kill=$(pgrep -P "$test_pid" 2>/dev/null) + + kill -TERM "$test_pid" 2>/dev/null || true if [[ -n "$pids_to_kill" ]]; then - echo "$pids_to_kill" | xargs kill -KILL 2>/dev/null || true + echo "$pids_to_kill" | xargs kill -TERM 2>/dev/null || true + fi + + sleep 2 + + if kill -0 "$test_pid" 2>/dev/null; then + kill -KILL "$test_pid" 2>/dev/null || true + if [[ -n "$pids_to_kill" ]]; then + echo "$pids_to_kill" | xargs kill -KILL 2>/dev/null || true + fi + # Security: Quote and escape test_dir for safe pattern matching + pkill -KILL -f "phpunit.*${test_dir//\//\\/}" 2>/dev/null || true fi - # Security: Quote and escape test_dir for safe pattern matching - pkill -KILL -f "phpunit.*${test_dir//\//\\/}" 2>/dev/null || true fi - fi - ) & - disown $! 2>/dev/null || true + ) & + disown $! 2>/dev/null || true + fi + + wait "$test_pid" 2>/dev/null + exit_code=$? + + if [[ -f "$timeout_marker" ]]; then + exit_code=124 + rm -f "$timeout_marker" + elif [[ $exit_code -eq 143 || $exit_code -eq 137 ]]; then + exit_code=124 + fi fi - wait "$test_pid" 2>/dev/null - exit_code=$? + # Success, exhausted attempts, or a non-infra failure: stop retrying. + if [[ $exit_code -eq 0 ]] || [[ $attempt -ge $max_attempts ]]; then + break + fi - if [[ -f "$timeout_marker" ]]; then - exit_code=124 - rm -f "$timeout_marker" - elif [[ $exit_code -eq 143 || $exit_code -eq 137 ]]; then - exit_code=124 + # Only retry on the known transient autoload race signatures. + # Matching on error messages (not line numbers) so the pattern survives + # unrelated edits to MockCodeIgniter/CIUnitTestCase. + if ! grep -qE 'Failed to open stream: No such file or directory|Class "CodeIgniter.CodeIgniter" not found' "$output_file" 2>/dev/null; then + break fi - fi + + print_debug "Transient autoload failure detected in $component; retrying (attempt $((attempt + 1))/${max_attempts})" + ((attempt++)) + rm -f "$events_file" + done local elapsed=$((($(date +%s%N) - $start_time) / 1000000)) local result_file="$results_dir/random_test_result_${elapsed}_${component}.txt" diff --git a/.github/workflows/test-random-execution.yml b/.github/workflows/test-random-execution.yml index 953357e6f82b..9785ee9efe35 100644 --- a/.github/workflows/test-random-execution.yml +++ b/.github/workflows/test-random-execution.yml @@ -175,6 +175,7 @@ jobs: with: php-version: ${{ matrix.php-version }} extensions: gd, curl, iconv, json, mbstring, openssl, sodium + ini-values: opcache.enable_cli=0 coverage: none - name: Get composer cache directory From a442b12e724c6dc27a74e85b7468aacd70b47dd1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Apr 2026 02:01:51 +0800 Subject: [PATCH 41/85] chore(deps): bump actions/setup-node in / (#10123) Bumps [actions/setup-node](https://github.com/actions/setup-node) in `/` from 6.3.0 to 6.4.0. Updates `actions/setup-node` from 6.3.0 to 6.4.0 - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/53b83947a5a98c8d113130e565377fae1a50d02f...48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: 6.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github_actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test-scss.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-scss.yml b/.github/workflows/test-scss.yml index c544d51d3b48..919fae137812 100644 --- a/.github/workflows/test-scss.yml +++ b/.github/workflows/test-scss.yml @@ -36,7 +36,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Node - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: '24' From 279eae27466b7b2118e26a0ca7b4586f6f019a68 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Tue, 21 Apr 2026 15:41:55 +0800 Subject: [PATCH 42/85] fix: suppress stty stderr leak in `CLI::generateDimensions()` when stdin is not a TTY (#10124) --- system/CLI/CLI.php | 2 +- tests/system/CLI/CLITest.php | 28 +++++++++++++++++++++ user_guide_src/source/changelogs/v4.7.3.rst | 1 + 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index f01124ec9075..68bd0e22433d 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -778,7 +778,7 @@ public static function generateDimensions() static::$width = (int) $matches[2]; } } - } elseif (($size = exec('stty size')) && preg_match('/(\d+)\s+(\d+)/', $size, $matches)) { + } elseif (($size = exec('stty size 2>/dev/null')) && preg_match('/(\d+)\s+(\d+)/', $size, $matches)) { static::$height = (int) $matches[1]; static::$width = (int) $matches[2]; } else { diff --git a/tests/system/CLI/CLITest.php b/tests/system/CLI/CLITest.php index 6a69cf7e69ab..c084d067ad6b 100644 --- a/tests/system/CLI/CLITest.php +++ b/tests/system/CLI/CLITest.php @@ -21,6 +21,7 @@ use CodeIgniter\Test\StreamFilterTrait; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; use ReflectionProperty; /** @@ -594,6 +595,33 @@ public function testWindow(): void $this->assertIsInt(CLI::getWidth()); } + #[RequiresOperatingSystem('Darwin|Linux')] + public function testGenerateDimensionsDoesNotLeakSttyErrorToStderr(): void + { + $code = <<<'PHP' + require __DIR__ . '/system/Test/bootstrap.php'; + CodeIgniter\CLI\CLI::generateDimensions(); + PHP; + + $cmd = sprintf('%s -r %s < /dev/null', PHP_BINARY, escapeshellarg($code)); + + $proc = proc_open( + $cmd, + [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], + $pipes, + ROOTPATH, + ); + $this->assertIsResource($proc); + + stream_get_contents($pipes[1]); + $stderr = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($proc); + + $this->assertSame('', $stderr); + } + /** * @param array $tbody * @param array $thead diff --git a/user_guide_src/source/changelogs/v4.7.3.rst b/user_guide_src/source/changelogs/v4.7.3.rst index ea3b37f579f7..3b5c2ce38555 100644 --- a/user_guide_src/source/changelogs/v4.7.3.rst +++ b/user_guide_src/source/changelogs/v4.7.3.rst @@ -37,6 +37,7 @@ Bugs Fixed ********** - **Autoloader:** Fixed a bug where ``Autoloader::unregister()`` (used during tests) silently failed to remove handlers from the SPL autoload stack, causing closures to accumulate permanently. +- **CLI:** Fixed a bug where ``CLI::generateDimensions()`` leaked ``stty`` error output (e.g., ``stty: 'standard input': Inappropriate ioctl for device``) to stderr when stdin was not a TTY. - **Commands:** Fixed a bug in the ``env`` command where passing options only would cause the command to throw a ``TypeError`` instead of showing the current environment. - **Common:** Fixed a bug where the ``command()`` helper function did not properly clean up output buffers, which could lead to risky tests when exceptions were thrown. - **Validation:** Fixed a bug where ``Validation::getValidated()`` dropped fields whose validated value was explicitly ``null``. From e8ee92b44e892a4ac79b630ff3ca2276cae66254 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Wed, 22 Apr 2026 03:13:43 +0800 Subject: [PATCH 43/85] refactor: further rename `--handler` to `--sort-by-handler` for `routes` (#10125) --- system/Commands/Utilities/Routes.php | 9 +++++---- tests/system/Commands/Utilities/RoutesTest.php | 9 ++++++--- user_guide_src/source/changelogs/v4.7.3.rst | 4 ++-- user_guide_src/source/incoming/routing.rst | 5 ++--- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/system/Commands/Utilities/Routes.php b/system/Commands/Utilities/Routes.php index 86f7bd74158f..a42ff0197525 100644 --- a/system/Commands/Utilities/Routes.php +++ b/system/Commands/Utilities/Routes.php @@ -73,8 +73,8 @@ class Routes extends BaseCommand * @var array */ protected $options = [ - '--handler' => 'Sort by Handler.', - '--host' => 'Specify hostname in request URI.', + '--sort-by-handler' => 'Sort by handler.', + '--host' => 'Specify hostname in request URI.', ]; /** @@ -82,11 +82,12 @@ class Routes extends BaseCommand */ public function run(array $params) { - $sortByHandler = array_key_exists('handler', $params); + $sortByHandler = array_key_exists('sort-by-handler', $params); if (! $sortByHandler && array_key_exists('h', $params)) { + // @todo to remove support in v4.8.0 // Support -h as a shortcut but print a warning that it is not the intended use of -h. - CLI::write('Warning: -h will be used as shortcut for --help in v4.8.0. Please use --handler to sort by handler.', 'yellow'); + CLI::write('Warning: -h will be used as shortcut for --help in v4.8.0. Please use --sort-by-handler to sort by handler.', 'yellow'); CLI::newLine(); $sortByHandler = true; diff --git a/tests/system/Commands/Utilities/RoutesTest.php b/tests/system/Commands/Utilities/RoutesTest.php index f2dc10bfc5bd..1d433242846b 100644 --- a/tests/system/Commands/Utilities/RoutesTest.php +++ b/tests/system/Commands/Utilities/RoutesTest.php @@ -94,7 +94,7 @@ public function testRoutesCommandSortByHandler(): void { Services::resetSingle('routes'); - command('routes --handler'); + command('routes --sort-by-handler'); $expected = <<<'EOL' +---------+---------+---------------+----------------------------------------+----------------+---------------+ @@ -127,7 +127,7 @@ public function testRoutesCommandSortByHandlerUsingShortcutForBc(): void command('routes -h'); $expected = <<<'EOL' - Warning: -h will be used as shortcut for --help in v4.8.0. Please use --handler to sort by handler. + Warning: -h will be used as shortcut for --help in v4.8.0. Please use --sort-by-handler to sort by handler. +---------+---------+---------------+----------------------------------------+----------------+---------------+ | Method | Route | Name | Handler ↓ | Before Filters | After Filters | @@ -146,7 +146,10 @@ public function testRoutesCommandSortByHandlerUsingShortcutForBc(): void | CLI | testing | testing-index | \App\Controllers\TestController::index | | | +---------+---------+---------------+----------------------------------------+----------------+---------------+ EOL; - $this->assertStringContainsString($expected, $this->getBuffer()); + $this->assertStringContainsString( + $expected, + (string) preg_replace('/\e\[[^m]+m/u', '', $this->getBuffer()), + ); } public function testRoutesCommandHostHostname(): void diff --git a/user_guide_src/source/changelogs/v4.7.3.rst b/user_guide_src/source/changelogs/v4.7.3.rst index 3b5c2ce38555..7b8ba18a1892 100644 --- a/user_guide_src/source/changelogs/v4.7.3.rst +++ b/user_guide_src/source/changelogs/v4.7.3.rst @@ -24,9 +24,9 @@ Message Changes Changes ******* -- **Commands:** The ``-h`` option for the ``routes`` command is renamed to ``--handler`` to avoid conflict with the common use of ``-h`` as a shortcut for ``--help``. +- **Commands:** The ``-h`` option for the ``routes`` command is renamed to ``--sort-by-handler`` to avoid conflict with the common use of ``-h`` as a shortcut for ``--help``. The old ``-h`` option will continue to work until v4.8.0, at which point it will be removed and repurposed as a shortcut for ``--help``. - A warning message is displayed when using the old ``-h`` option to encourage users to switch to the new ``--handler`` option. + A warning message is displayed when using the old ``-h`` option to encourage users to switch to the new ``--sort-by-handler`` option. ************ Deprecations diff --git a/user_guide_src/source/incoming/routing.rst b/user_guide_src/source/incoming/routing.rst index 4f4afce136f5..7dda8b687131 100644 --- a/user_guide_src/source/incoming/routing.rst +++ b/user_guide_src/source/incoming/routing.rst @@ -1001,11 +1001,11 @@ Sort by Handler .. versionadded:: 4.3.0 -You can sort the routes by *Handler*: +You can sort the routes by *handler*: .. code-block:: console - php spark routes -h + php spark routes --sort-by-handler .. _routing-spark-routes-specify-host: @@ -1060,4 +1060,3 @@ Additionally, if we use ``addRedirect()`` we can also expect the ``redirect`` ke To access the values of these parameters, we can call ``Router::getMatchedRouteOptions()``. Here is an example of the returned array: .. literalinclude:: routing/074.php - From 74982b140f9f36cb7e6596297e728836ecea7020 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Wed, 22 Apr 2026 03:28:58 +0800 Subject: [PATCH 44/85] test: optimize AutoReview tests (#10127) --- .../reusable-serviceless-phpunit-test.yml | 1 + .github/workflows/test-random-execution.yml | 3 +++ .../AutoReview/CreateNewChangelogTest.php | 17 -------------- tests/system/AutoReview/FrameworkCodeTest.php | 22 +++++-------------- 4 files changed, 10 insertions(+), 33 deletions(-) diff --git a/.github/workflows/reusable-serviceless-phpunit-test.yml b/.github/workflows/reusable-serviceless-phpunit-test.yml index af205cda26f0..c59592f481ad 100644 --- a/.github/workflows/reusable-serviceless-phpunit-test.yml +++ b/.github/workflows/reusable-serviceless-phpunit-test.yml @@ -76,6 +76,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false + fetch-depth: 0 - name: Setup PHP uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0 diff --git a/.github/workflows/test-random-execution.yml b/.github/workflows/test-random-execution.yml index 9785ee9efe35..5a1f9609a613 100644 --- a/.github/workflows/test-random-execution.yml +++ b/.github/workflows/test-random-execution.yml @@ -169,6 +169,9 @@ jobs: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + fetch-depth: 0 - name: Setup PHP ${{ matrix.php-version }} uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0 diff --git a/tests/system/AutoReview/CreateNewChangelogTest.php b/tests/system/AutoReview/CreateNewChangelogTest.php index b6a89e047f88..21911a0e7293 100644 --- a/tests/system/AutoReview/CreateNewChangelogTest.php +++ b/tests/system/AutoReview/CreateNewChangelogTest.php @@ -27,23 +27,6 @@ final class CreateNewChangelogTest extends TestCase { private string $currentVersion; - public static function setUpBeforeClass(): void - { - parent::setUpBeforeClass(); - - if (getenv('GITHUB_ACTIONS') !== false) { - exec('git fetch --unshallow 2>&1', $output, $exitCode); - exec('git fetch --tags 2>&1', $output, $exitCode); - - if ($exitCode !== 0) { - self::fail(sprintf( - "Failed to fetch git history and tags.\nOutput: %s", - implode("\n", $output), - )); - } - } - } - protected function setUp(): void { parent::setUp(); diff --git a/tests/system/AutoReview/FrameworkCodeTest.php b/tests/system/AutoReview/FrameworkCodeTest.php index 137839b024e5..de515fffebf6 100644 --- a/tests/system/AutoReview/FrameworkCodeTest.php +++ b/tests/system/AutoReview/FrameworkCodeTest.php @@ -107,17 +107,9 @@ private static function getTestClasses(): array $testClasses = array_map( static function (SplFileInfo $file) use ($directory): string { - $relativePath = substr_replace( - $file->getPathname(), - '', - 0, - strlen($directory), - ); - $relativePath = substr_replace( - $relativePath, - '', - strlen($relativePath) - strlen(DIRECTORY_SEPARATOR . $file->getBasename()), - ); + $relativePath = substr($file->getPathname(), strlen($directory)); + $separatorPos = strrpos($relativePath, DIRECTORY_SEPARATOR); + $relativePath = $separatorPos === false ? '' : substr($relativePath, 0, $separatorPos); return sprintf( 'CodeIgniter\\%s%s%s', @@ -128,17 +120,15 @@ static function (SplFileInfo $file) use ($directory): string { }, array_filter( iterator_to_array($iterator, false), + // Filename-based heuristic: avoids the is_subclass_of() cold-autoload issue + // by only considering files that end with "Test.php" or "TestCase.php". static fn (SplFileInfo $file): bool => $file->isFile() + && (str_ends_with($file->getBasename(), 'Test.php') || str_ends_with($file->getBasename(), 'TestCase.php')) && ! str_contains($file->getPathname(), DIRECTORY_SEPARATOR . 'fixtures' . DIRECTORY_SEPARATOR) && ! str_contains($file->getPathname(), DIRECTORY_SEPARATOR . 'Views' . DIRECTORY_SEPARATOR), ), ); - $testClasses = array_filter( - $testClasses, - static fn (string $class): bool => is_subclass_of($class, TestCase::class), - ); - sort($testClasses); self::$testClasses = $testClasses; From 1e9a2b58cd9d6c48469a4dd956606755a984fb81 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Wed, 22 Apr 2026 10:35:00 +0800 Subject: [PATCH 45/85] refactor: UX: `ClearLogs::execute()` error message is misleading after interactive `'n'` (#10126) --- system/Commands/Housekeeping/ClearLogs.php | 4 +++- tests/system/Commands/Housekeeping/ClearLogsTest.php | 2 -- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/system/Commands/Housekeeping/ClearLogs.php b/system/Commands/Housekeeping/ClearLogs.php index 71f299969055..e416a383cc48 100644 --- a/system/Commands/Housekeeping/ClearLogs.php +++ b/system/Commands/Housekeeping/ClearLogs.php @@ -68,7 +68,9 @@ public function run(array $params) if (! $force && CLI::prompt('Are you sure you want to delete the logs?', ['n', 'y']) === 'n') { CLI::error('Deleting logs aborted.'); - CLI::error('If you want, use the "--force" option to force delete all log files.'); + + // @todo to re-add under non-interactive mode + // CLI::error('If you want, use the "--force" option to force delete all log files.'); return EXIT_ERROR; } diff --git a/tests/system/Commands/Housekeeping/ClearLogsTest.php b/tests/system/Commands/Housekeeping/ClearLogsTest.php index 09c889a6e573..a8469873e84d 100644 --- a/tests/system/Commands/Housekeeping/ClearLogsTest.php +++ b/tests/system/Commands/Housekeeping/ClearLogsTest.php @@ -98,7 +98,6 @@ public function testClearLogsAbortsClearWithoutForce(): void <<<'EOT' Are you sure you want to delete the logs? [n, y]: n Deleting logs aborted. - If you want, use the "--force" option to force delete all log files. EOT, preg_replace('/\e\[[^m]+m/', '', $io->getOutput()), @@ -122,7 +121,6 @@ public function testClearLogsAbortsClearWithoutForceWithDefaultAnswer(): void <<getOutput()), From e66d0256cc249d748bb0c47ac9f1936aff5e82ba Mon Sep 17 00:00:00 2001 From: Asad Date: Wed, 22 Apr 2026 18:37:53 +0500 Subject: [PATCH 46/85] docs: document Axios header configuration for AJAX (#10069) Added Axios information regarding the X-Requested-With header. --- user_guide_src/source/general/ajax.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/user_guide_src/source/general/ajax.rst b/user_guide_src/source/general/ajax.rst index ddc276fdf39a..be674ad52dd5 100644 --- a/user_guide_src/source/general/ajax.rst +++ b/user_guide_src/source/general/ajax.rst @@ -25,6 +25,17 @@ Fetch API } }); +Axios +===== + +If you are using Axios, it also does not include the ``X-Requested-With`` header by default. +You can add it globally as follows: + +.. code-block:: javascript + + axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; + + jQuery ====== From 5925c3a32d31aaa00157827a7038a3ed29c5222f Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Thu, 23 Apr 2026 15:04:42 +0200 Subject: [PATCH 47/85] docs: refactor AJAX request and clarify framework examples (#10129) --- user_guide_src/source/general/ajax.rst | 60 ++++++++++++++++++-------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/user_guide_src/source/general/ajax.rst b/user_guide_src/source/general/ajax.rst index be674ad52dd5..72e75c5d0429 100644 --- a/user_guide_src/source/general/ajax.rst +++ b/user_guide_src/source/general/ajax.rst @@ -2,11 +2,11 @@ AJAX Requests ############## -The ``IncomingRequest::isAJAX()`` method uses the ``X-Requested-With`` header to define whether the request is XHR or normal. However, the most recent JavaScript implementations (i.e., fetch) no longer send this header along with the request, thus the use of ``IncomingRequest::isAJAX()`` becomes less reliable, because without this header it is not possible to define whether the request is or not XHR. +The ``IncomingRequest::isAJAX()`` method uses the ``X-Requested-With`` header to define whether the request is XHR or normal. However, modern JavaScript APIs such as ``fetch`` no longer send this header by default, so ``IncomingRequest::isAJAX()`` becomes less reliable without additional configuration. -To get around this problem, the most efficient solution (so far) is to manually define the request header, forcing the information to be sent to the server, which will then be able to identify that the request is XHR. +To work around this problem, manually define the request header so the server can identify the request as XHR. -Here's how to force the ``X-Requested-With`` header to be sent in the Fetch API and other JavaScript libraries. +Here are common ways to send the ``X-Requested-With`` header in the Fetch API and other JavaScript libraries. .. contents:: :local: @@ -28,7 +28,7 @@ Fetch API Axios ===== -If you are using Axios, it also does not include the ``X-Requested-With`` header by default. +Axios does not include the ``X-Requested-With`` header by default. You can add it globally as follows: .. code-block:: javascript @@ -36,33 +36,46 @@ You can add it globally as follows: axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; -jQuery -====== - -For libraries like jQuery for example, it is not necessary to make explicit the sending of this header, because according to the `official documentation `_ it is a standard header for all requests ``$.ajax()``. But if you still want to force the shipment to not take risks, just do it as follows: +If you prefer to avoid global defaults, create an Axios instance instead: .. code-block:: javascript - $.ajax({ - url: "your url", - headers: {'X-Requested-With': 'XMLHttpRequest'} + const api = axios.create({ + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } }); -VueJS -===== -In VueJS you just need to add the following code to the ``created`` function, as long as you are using Axios for this type of request. +Vue.js +------- + +Vue does not require a specific HTTP client. If your Vue app uses Axios, configure Axios once during application bootstrap or in a shared API module, and reuse that configuration throughout the app. + +React +----- + +React also does not provide a built-in HTTP client. If your React app uses Axios, reuse the shared Axios configuration above, or set the header for an individual request when needed: .. code-block:: javascript - axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; + axios.get('your url', { + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } + }) -React -===== +jQuery +====== + +For libraries like jQuery for example, it is not necessary to make explicit the sending of this header, because according to the `official documentation `_ it is a standard header for all requests ``$.ajax()``. But if you still want to force the shipment to not take risks, just do it as follows: .. code-block:: javascript - axios.get("your url", {headers: {'Content-Type': 'application/json'}}) + $.ajax({ + url: "your url", + headers: {'X-Requested-With': 'XMLHttpRequest'} + }); htmx ==== @@ -74,3 +87,14 @@ You can use `ajax-header ... + + +Or you can set the header manually with ``hx-headers``: + +.. code-block:: html + + From 91f2cb0db6f95e5664452bb6a7edb7aa312e5678 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Thu, 23 Apr 2026 22:38:32 +0800 Subject: [PATCH 48/85] docs: fix indentation on `4.7.2` and `4.7.3` changelogs (#10131) --- user_guide_src/source/changelogs/v4.7.2.rst | 4 ++-- user_guide_src/source/changelogs/v4.7.3.rst | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.7.2.rst b/user_guide_src/source/changelogs/v4.7.2.rst index c202c52637eb..d8bd9f47aeb4 100644 --- a/user_guide_src/source/changelogs/v4.7.2.rst +++ b/user_guide_src/source/changelogs/v4.7.2.rst @@ -15,8 +15,8 @@ Bugs Fixed ********** - **Security:** Fixed a bug where the CSRF filter could corrupt JSON request bodies after successful - verification when the CSRF token was provided via the ``X-CSRF-TOKEN`` header. - This caused ``IncomingRequest::getJSON()`` to fail on valid ``application/json`` requests. + verification when the CSRF token was provided via the ``X-CSRF-TOKEN`` header. + This caused ``IncomingRequest::getJSON()`` to fail on valid ``application/json`` requests. See the repo's `CHANGELOG.md `_ diff --git a/user_guide_src/source/changelogs/v4.7.3.rst b/user_guide_src/source/changelogs/v4.7.3.rst index 7b8ba18a1892..677e1072500b 100644 --- a/user_guide_src/source/changelogs/v4.7.3.rst +++ b/user_guide_src/source/changelogs/v4.7.3.rst @@ -25,8 +25,8 @@ Changes ******* - **Commands:** The ``-h`` option for the ``routes`` command is renamed to ``--sort-by-handler`` to avoid conflict with the common use of ``-h`` as a shortcut for ``--help``. - The old ``-h`` option will continue to work until v4.8.0, at which point it will be removed and repurposed as a shortcut for ``--help``. - A warning message is displayed when using the old ``-h`` option to encourage users to switch to the new ``--sort-by-handler`` option. + The old ``-h`` option will continue to work until v4.8.0, at which point it will be removed and repurposed as a shortcut for ``--help``. + A warning message is displayed when using the old ``-h`` option to encourage users to switch to the new ``--sort-by-handler`` option. ************ Deprecations From 4291460b72468057bfb9e3d354092ec87d66fe03 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sat, 25 Apr 2026 17:04:03 +0800 Subject: [PATCH 49/85] docs: add version switcher to docs page (#10135) --- .../source/_static/js/version_switcher.js | 119 ++++++++++++++++++ user_guide_src/source/conf.py | 3 +- 2 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 user_guide_src/source/_static/js/version_switcher.js diff --git a/user_guide_src/source/_static/js/version_switcher.js b/user_guide_src/source/_static/js/version_switcher.js new file mode 100644 index 000000000000..a201bb4ddc7a --- /dev/null +++ b/user_guide_src/source/_static/js/version_switcher.js @@ -0,0 +1,119 @@ +/* + * Version switcher for the user guide sidebar. + * + * Injects the sphinx_rtd_theme's native ".switch-menus > .version-switch" + * scaffolding above the search box in the left sidebar, containing a