From ca0ca4d3cc5100eda99dfbb3ea7b0af68af67817 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Tue, 7 Jul 2026 18:09:34 +0800 Subject: [PATCH 001/163] docs: add changelog and upgrade for v4.7.5 (#10381) --- system/CodeIgniter.php | 2 +- user_guide_src/source/changelogs/index.rst | 1 + user_guide_src/source/changelogs/v4.7.5.rst | 35 ++++++++++++ .../source/installation/upgrade_475.rst | 55 +++++++++++++++++++ .../source/installation/upgrading.rst | 1 + 5 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 user_guide_src/source/changelogs/v4.7.5.rst create mode 100644 user_guide_src/source/installation/upgrade_475.rst diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index b806fe00a740..a291809938e5 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.4'; + public const CI_VERSION = '4.7.5-dev'; /** * App startup time. diff --git a/user_guide_src/source/changelogs/index.rst b/user_guide_src/source/changelogs/index.rst index ca8f8c2cb715..9b6030c49735 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.5 v4.7.4 v4.7.3 v4.7.2 diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst new file mode 100644 index 000000000000..e379b7b0cfd1 --- /dev/null +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -0,0 +1,35 @@ +############# +Version 4.7.5 +############# + +Release Date: Unreleased + +**4.7.5 release of CodeIgniter4** + +.. contents:: + :local: + :depth: 3 + +******** +BREAKING +******** + +*************** +Message Changes +*************** + +******* +Changes +******* + +************ +Deprecations +************ + +********** +Bugs Fixed +********** + +See the repo's +`CHANGELOG.md `_ +for a complete list of bugs fixed. diff --git a/user_guide_src/source/installation/upgrade_475.rst b/user_guide_src/source/installation/upgrade_475.rst new file mode 100644 index 000000000000..7c82e487e46b --- /dev/null +++ b/user_guide_src/source/installation/upgrade_475.rst @@ -0,0 +1,55 @@ +############################# +Upgrading from 4.7.4 to 4.7.5 +############################# + +Please refer to the upgrade instructions corresponding to your installation method. + +- :ref:`Composer Installation App Starter Upgrading ` +- :ref:`Composer Installation Adding CodeIgniter4 to an Existing Project Upgrading ` +- :ref:`Manual Installation Upgrading ` + +.. contents:: + :local: + :depth: 2 + +********************** +Mandatory File Changes +********************** + +**************** +Breaking Changes +**************** + +********************* +Breaking Enhancements +********************* + +************* +Project Files +************* + +Some files in the **project space** (root, app, public, writable) received updates. Due to +these files being outside of the **system** scope they will not be changed without your intervention. + +.. note:: There are some third-party CodeIgniter modules available to assist + with merging changes to the project space: + `Explore on Packagist `_. + +Content Changes +=============== + +The following files received significant changes (including deprecations or visual adjustments) +and it is recommended that you merge the updated versions with your application: + +Config +------ + +- @TODO + +All Changes +=========== + +This is a list of all files in the **project space** that received changes; +many will be simple comments or formatting that have no effect on the runtime: + +- @TODO diff --git a/user_guide_src/source/installation/upgrading.rst b/user_guide_src/source/installation/upgrading.rst index dc9199da84ca..fbe8095a8f68 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_475 upgrade_474 upgrade_473 upgrade_472 From f42352a88a89c3dd21022dbc4ad0a0854db72df3 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Wed, 8 Jul 2026 01:25:22 +0800 Subject: [PATCH 002/163] docs: update release docs and remove legacy release scripts (#10382) --- admin/README.md | 39 +++------- admin/RELEASE.md | 56 ++++++++------ admin/release | 145 ----------------------------------- admin/release-config | 26 ------- admin/release-deploy | 31 -------- admin/release-notes.bb | 43 ----------- admin/release-revert | 26 ------- admin/release-userguide | 29 ------- admin/workflow.md | 92 ---------------------- utils/check_permission_x.php | 5 -- 10 files changed, 43 insertions(+), 449 deletions(-) delete mode 100755 admin/release delete mode 100644 admin/release-config delete mode 100755 admin/release-deploy delete mode 100755 admin/release-notes.bb delete mode 100755 admin/release-revert delete mode 100755 admin/release-userguide delete mode 100644 admin/workflow.md diff --git a/admin/README.md b/admin/README.md index 1f5dd89bfcd1..8f06a6282e22 100644 --- a/admin/README.md +++ b/admin/README.md @@ -43,45 +43,30 @@ This folder contains tools or docs useful for project maintainers. ## Maintainer Scripts -- **release-config** holds variables used for the maintainer & release building - **docbot** re-builds the user guide from the RST source for it, and optionally deploys it to the `gh-pages` branch of the main repository (if the user running it has maintainer rights on that repo). See the [writeup](./docbot.md). -## Release Building Scripts +## Release Scripts -*Do not use these scripts! They are left here for reference only.* - -The release workflow is detailed in its own writeup; these are the main -scripts used by the release manager: - -- **release** builds a new release branch in the main repo, for vetting. - This includes updating version dependencies or constants, - generating version(s) of the user guide; and possibly - moving or ignoring stuff, distinguishing release from development. - If successful, it will update the `config` file, with the version number - in it, and it will run the related scripts following, to revise - the release distributions. - Usage: `admin/release version qualifier` -- **release-userguide** builds the distributable userguide repo. - It could be used on its own, but is normally part of `release`. -- **release-deploy** pushes the release changes to the appropriate github - repositories. Tag & create releases on GitHub. This is not easily reversible! - Usage: `admin/release-deploy version qualifier` -- **release-revert** can be used to restore your repositories to the state they - were in before you started a release. **IF** you haven't deployed. - This is in case you decide not to proceed with the release, for any reason. - Remember to be polite when running it. +The release process is detailed in [RELEASE.md](./RELEASE.md). These scripts +are used as part of that process: +- **prepare-release.php** creates the `release-4.x.x` branch and updates + version references in the framework source, the user guide, and the + distribution build script. + Usage: `php admin/prepare-release.php 4.x.x` +- **create-new-changelog.php** creates the changelog and upgrade guide + stubs for the next version and adds them to their index files. + Usage: `php admin/create-new-changelog.php ` ## Other Stuff -- **release-notes.bb** is a boilerplate for forum announcements of a new release. - It is marked up using [BBcode](https://en.wikipedia.org/wiki/BBCode). - The **framework** and **starter** subfolders contain files that will over-ride those from the development repository, when the distribution repositories are built. - The subfolders inside `admin` contain "next release" files in the case of `codeigniter4` and over-written distribution files in the other cases. -- The CHANGELOG.md file is auto-generated using the [GitHub Changelog Generator](https://github.com/github-changelog-generator/github-changelog-generator) +- The CHANGELOG.md file is generated from GitHub's auto-generated release + notes, as described in [RELEASE.md](./RELEASE.md). diff --git a/admin/RELEASE.md b/admin/RELEASE.md index 75b4e26b2b31..a6b3f680b6ad 100644 --- a/admin/RELEASE.md +++ b/admin/RELEASE.md @@ -18,17 +18,18 @@ ## Merge `develop` branch into next minor version branch `4.y` -Before starting release process, if there are commits in `develop` branch that +Before starting the release process, if there are commits in `develop` branch that are not merged into `4.y` branch, merge them. This is because if conflicts occur, merging will take time. -```console -git fetch upstream -git switch 4.y -git merge upstream/4.y -git merge upstream/develop -git push upstream HEAD -``` +* [ ] Merge `develop` into `4.y`: + ```console + git fetch upstream + git switch 4.y + git merge upstream/4.y + git merge upstream/develop + git push upstream HEAD + ``` ## [Minor version only] Merge minor version branch into `develop` @@ -118,16 +119,16 @@ the existing content. * Create a new branch `release-4.x.x` * Update **system/CodeIgniter.php** with the new version number: `const CI_VERSION = '4.x.x';` - * Update **user_guide_src/source/conf.py** with the new `version = '4.x'` (if releasing - the minor version) and `release = '4.x.x'`. + * Update **user_guide_src/source/conf.py** with the new `version = '4.x'` + and `release = '4.x.x'`. * Update **user_guide_src/source/changelogs/{version}.rst** * Set the date to format `Release Date: January 31, 2021` * Update **phpdoc.dist.xml** with the new `CodeIgniter v4.x API` and `` * Update **admin/starter/builds**: * Set `define('LATEST_RELEASE', '^4.x')` - * Set `define('NEXT_MINOR', '4.y-dev')`. - * If the major version changes, you need to manually change to `define('NEXT_MINOR', '5.0-dev')`. + * Set `define('NEXT_MINOR', '^4.y-dev')`. + * If the major version changes, you need to manually change to `define('NEXT_MINOR', '^5.0-dev')`. * Commit the changes with `Prep for 4.x.x release` * [ ] Create a new PR from `release-4.x.x` to `develop`: * Title: `Prep for 4.x.x release` @@ -234,26 +235,31 @@ sudo pip3 install sphinx_rtd_theme ### Manual User Guide Process -* Still in the **CodeIgniter4** repo enter the **user_guide_src** directory -* Clear out any old build files: `rm -rf build/` -* Build the HTML version of the User Guide: `make html` -* Build the ePub version of the User Guide: `make epub` -* Switch to the **userguide** repo and create a new branch `release-4.x.x` -* Replace **docs/** with **CodeIgniter4/user_guide_src/build/html** -* Ensure the file **docs/.nojekyll** exists or GitHub Pages will ignore folders +> [!NOTE] +> This process is normally not needed. The "Deploy Distributable Repos" action +> builds and deploys the User Guide automatically when a release is published. +> Follow these steps only if the automatic deployment fails. + +* [ ] Still in the **CodeIgniter4** repo enter the **user_guide_src** directory +* [ ] Clear out any old build files: `rm -rf build/` +* [ ] Build the HTML version of the User Guide: `make html` +* [ ] Build the ePub version of the User Guide: `make epub` +* [ ] Switch to the **userguide** repo and create a new branch `release-4.x.x` +* [ ] Replace **docs/** with **CodeIgniter4/user_guide_src/build/html** +* [ ] Ensure the file **docs/.nojekyll** exists or GitHub Pages will ignore folders with an underscore prefix -* Copy **CodeIgniter4/user_guide_src/build/epub/CodeIgniter.epub** to +* [ ] Copy **CodeIgniter4/user_guide_src/build/epub/CodeIgniter.epub** to **./CodeIgniter4.x.x.epub** -* Commit the changes with "Update for 4.x.x" and push to origin -* Create a new PR from `release-4.x.x` to `develop`: +* [ ] Commit the changes with "Update for 4.x.x" and push to origin +* [ ] Create a new PR from `release-4.x.x` to `develop`: * Title: "Update for 4.x.x" * Description: blank -* Merge the PR -* Create a new Release: +* [ ] Merge the PR +* [ ] Create a new Release: * Version: "v4.x.x" * Title: "CodeIgniter 4.x.x User Guide" * Description: "CodeIgniter 4.x.x User Guide" -* Watch for the "github pages" Environment to make sure the deployment succeeds +* [ ] Watch for the "github pages" Environment to make sure the deployment succeeds The User Guide website should update itself via the deploy GitHub Action. Should this fail the server must be updated manually. See repo and hosting details in diff --git a/admin/release b/admin/release deleted file mode 100755 index d14167f0502e..000000000000 --- a/admin/release +++ /dev/null @@ -1,145 +0,0 @@ -#!/bin/bash - -## Build framework release branch & distributables - -# Setup variables -. admin/release-config - -echo -e "${BOLD}${COLOR}CodeIgniter4 release builder${NORMAL}" -echo '----------------------------' - -#--------------------------------------------------- -# Check arguments -echo -e "${BOLD}Checking arguments...${NORMAL}" - -if [ $# -lt 1 ]; then - echo -e "${BOLD}Usage: admin/release version# pre-release-qualifier${NORMAL}" - exit 1 -fi - -#--------------------------------------------------- -# Create the release branch -echo -e "${BOLD}Creating $which $branch to $action ${NORMAL}" - -git checkout develop -git branch -d $branch &>/dev/null # remove the branch if there -git checkout -b $branch -composer update - -#--------------------------------------------------- -# Update version dependencies -echo -e "${BOLD}Updating version dependencies${NORMAL}" - -function check_unique { - count=`grep -c '$1' < $2 | wc -l` - if [ $count -ne 1 ]; then - echo -e "${BOLD}${COLOR}$2 has ${count} occurrences of '$1'${NORMAL}" - exit 1 - fi -} - -# Make sure there is only one line to affect in each file -check_unique "const CI_VERSION" 'system/CodeIgniter.php' -check_unique "release =" 'user_guide_src/source/conf.py' -check_unique "|release|" 'user_guide_src/source/changelogs/index.rst' -check_unique "Release Date.*Not Released" 'user_guide_src/source/changelogs/index.rst' - -# CI_VERSION definition in system/CodeIgniter.php -sed -i "/const CI_VERSION/s/'.*'/'${RELEASE}'/" system/CodeIgniter.php - -# release substitution variable in user_guide_src/source/conf.py -sed -i "/release =/s/'.*'/'${RELEASE}'/" user_guide_src/source/conf.py - -# version & date in user_guide_src/source/index.rst -sed -i "/|release|/s/|.*|/${RELEASE}/" user_guide_src/source/changelogs/index.rst -sed -i "/Release Date/s/Not Released/$(date +'%B %d, %Y')/" user_guide_src/source/changelogs/index.rst -sed -i "/|version|/s/|version|/${RELEASE}/" user_guide_src/source/changelogs/index.rst - -# version & date in user_guide_src/source/next.rst -sed -i "/Release Date/s/Not Released/$(date +'%B %d, %Y')/" user_guide_src/source/changelogs/next.rst -sed -i "/|version|/s/|version|/${RELEASE}/" user_guide_src/source/changelogs/next.rst - -# establish version-specific changelog -sed -i "|changelogs/next|s|changeslog/next|changelogs/v{$RELEASE}|" user_guide_src/source/changelogs/index.rst -mv user_guide_src/source/changelogs/next.rst user_guide_src/source/changelogs/v${RELEASE}.rst -touch user_guide_src/source/changelogs/next.rst -cp admin/next.rst user_guide_src/source/changelogs/next.rst - -#--------------------------------------------------- -# Setup the distribution folders -echo -e "${BOLD}Building distribution folders${NORMAL}" - -function setup_repo { - echo -e "${BOLD}... $1${NORMAL}" - if [ -d dist/$1 ]; then - rm -rf dist/$1 - fi - mkdir dist/$1 - cd dist/$1 - git init - git remote add origin ${CI_ORG}/$1.git - git fetch - git checkout master - git checkout -b $branch - cd $CI_DIR -} - -if [ -d dist ]; then - rm -rf dist/ -fi -mkdir dist - -setup_repo userguide - -#--------------------------------------------------- -# Housekeeping - make sure writable is flushed of test files -# at least, test files that crop up on my system :-/ -rm -f writable/cache/H* -rm -f writable/cache/d* -rm -f writable/cache/s* -rm -f writable/debugbar/debug* -rm -f writable/logs/log* - -#--------------------------------------------------- -# Generate the user guide -echo -e "${BOLD}Generate the user guide${NORMAL}" - -cd user_guide_src - -# make the UG -rm -rf build/* -echo -e "${BOLD}... HTML version${NORMAL}" -make html -touch build/html/.nojekyll -echo -e "${BOLD}... epub version${NORMAL}" -make epub - -cd ${CI_DIR} - -# add changelog preamble -file=user_guide_src/source/changelogs/index.rst -sed -i "4 a Version |version|" $file -sed -i "5 a ====================================================" $file -sed -i "6 G" $file -sed -i "7 a Release Date: Not Released" $file -sed -i "8 G" $file -sed -i "9 a **Next release of CodeIgniter4**" $file -sed -i "10 G" $file -sed -i "11 G" $file -sed -i "12 a :doc:\`See all the changes. \`" $file -sed -i "13 G" $file - -#--------------------------------------------------- -echo -e "${BOLD}Commit the release branch${NORMAL}" -git add . -git commit -S -m "Release ${RELEASE}" - -#--------------------------------------------------- -# Build the distributables - -. admin/release-userguide - -#--------------------------------------------------- -# Done for now -echo -e "${BOLD}Your $branch branch is ready to inspect.${NORMAL}" -echo -e "${BOLD}Follow the directions in workflow.md to continue.${NORMAL}" diff --git a/admin/release-config b/admin/release-config deleted file mode 100644 index 8a969af81c68..000000000000 --- a/admin/release-config +++ /dev/null @@ -1,26 +0,0 @@ -# Variables used for release building - -if [ -z "$CI_ORG" ]; then - - # Initialize variables - CI_ORG=https://github.com/codeigniter4 - CI_DIR=`pwd` - - BOLD='\033[1m' - NORMAL='\033[0m' - COLOR='\033[1;31m' - ERROR='\033[0;31m' - - qualifier= - which=release - - version=$1 - if [ $# -gt 1 ]; then - qualifier="-${2}" - which='pre-release' - fi - - RELEASE=$version$qualifier - branch="release-$RELEASE" - -fi diff --git a/admin/release-deploy b/admin/release-deploy deleted file mode 100755 index fe70284024fc..000000000000 --- a/admin/release-deploy +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash - -## Push local changes to github - -echo -e "${BOLD}${COLOR}CodeIgniter4 release deployment${NORMAL}" -echo '-------------------------------' - -. admin/release-config - -echo -e "${BOLD}Merge release into develop${NORMAL}" -git checkout develop -git merge $branch -git push origin develop -git push ${CI_ORG}/CodeIgniter4 develop - -echo -e "${BOLD}Merge develop into master${NORMAL}" -git checkout master -git merge develop -git push origin master -git push ${CI_ORG}/CodeIgniter4 master - -echo -e "${BOLD}Pushing to the user guide repository${NORMAL}" -cd ${CI_DIR}/dist/userguide -git push origin master - -cd ${CI_DIR} - -#--------------------------------------------------- -# Phew! -echo -e "${BOLD}Congratulations - we have liftoff${NORMAL}" -echo "Don't forget to announce this release on the forum and on twitter!" diff --git a/admin/release-notes.bb b/admin/release-notes.bb deleted file mode 100755 index 6147a011cde1..000000000000 --- a/admin/release-notes.bb +++ /dev/null @@ -1,43 +0,0 @@ -CAUTION: THIS FILE IS A MIX OF BBCODE & MARKDOWN... NEEDS PROOFING - -CodeIgniter-4.0.0-alpha.1 launches today, after a lengthy build-up :) - -Huge shoutout to Lonnie Ezell for all of his hard work getting the vision -and the core implementation in place! - -This is an early pre-release of 4.0.0. It is not suitable for production! - -There are several possible downloads, that you can see on the -[url=https://github.com/codeigniter4/CodeIgniter4/releases/tag/v4.0.0-alpha.1]release page[/url]) - -- the runnable versions as a -[zip](https://github.com/codeigniter4/CodeIgniter4/releases/download/v4.0.0-alpha.1/CodeIgniter-4.0.0-alpha.1.zip) or a -[tarball](https://github.com/codeigniter4/CodeIgniter4/releases/download/v4.0.0-alpha.1/CodeIgniter-4.0.0-alpha.1.tar.gz)/ -- the developer versions of the framework (with contributor components) as -a [zip](https://github.com/codeigniter4/CodeIgniter4/archive/v4.0.0-alpha.1.zip) or a -[tarball](https://github.com/codeigniter4/CodeIgniter4/archive/v4.0.0-alpha.1.tar.gz)/ -- and finally the [epub](https://github.com/codeigniter4/CodeIgniter4/releases/download/v4.0.0-alpha.1/CodeIgniter-4.0.0-alpha.1.epub) version of the user guide for this release. - -The release has all the major features in place, but there are still gaps -and issues. See ... - -- [Bugs in the 4.0.0-alpha.1](https://github.com/codeigniter4/CodeIgniter4/issues?q=is%3Aopen+is%3Aissue+milestone%3A4.0.0-alpha) -- [Things that we definitely want to see fixed before first proper release](https://github.com/codeigniter4/CodeIgniter4/issues?q=is%3Aopen+is%3Aissue+milestone%3A4.0.0) -- [Features we would like to see in the initial proper release](), if possible, else the next update to it -- [Features that we are consciously deferring to later](https://github.com/codeigniter4/CodeIgniter4/issues?q=is%3Aopen+is%3Aissue+milestone%3A4.1.0) - -What we need now is feedback from the community ... - -- What doesn't work well (raise a post in [url=https://forum.codeigniter.com/forum-27.html]CodeIgniter 4 Development[/url]). -If you have found a bug, then by all means create a new issue on the repo. -- What you think is missing (raise a post in [url=https://forum.codeigniter.com/forum-29.html]CodeIgniter 4 Feature Requests[/url]) -We will soon start the [url=https://forum.codeigniter.com/forum-33.html]candidate new feature[/url] posts, -for those items in the feature requests forum that appear to have traction -- Problems that you are having using the release (raise a post in [url=https://forum.codeigniter.com/forum-30.html]CodeIgniter 4 Support[/url]) -Do NOT create repo issues with support questions - we are using github for bug and work package tracking - -Do NOT post support questions or feature requests in response to this thread - those -will be deleted. We are trying to make the best of the -limited resources that we have! - -Thank you, and ENJOY! \ No newline at end of file diff --git a/admin/release-revert b/admin/release-revert deleted file mode 100755 index 1c8a4eacd504..000000000000 --- a/admin/release-revert +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -## Revert local repos to pre-release state -echo -e "${BOLD}${COLOR}CodeIgniter4 release revert${NORMAL}" -echo '---------------------------' - -if [ $# -lt 2 ]; then - echo "You forgot the magic word" - exit 1 -fi -if [ $1 != 'please' ]; then - echo "What do you say?" - exit 1 -fi - -. admin/release-config - -echo -e "${BOLD}Reverting the main repository${NORMAL}" -git checkout master -git pull -f ${CI_ORG}/CodeIgniter4 master -git checkout develop -git pull -f${CI_ORG}/CodeIgniter4 develop - -#--------------------------------------------------- -# Phew! -echo -e "${BOLD}Congratulations - we have aborted liftoff${NORMAL}" diff --git a/admin/release-userguide b/admin/release-userguide deleted file mode 100755 index 6a8beb7bcbaf..000000000000 --- a/admin/release-userguide +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash - -## Build user guide distributable - -# Setup variables -. admin/release-config -TARGET=dist/userguide -cd $TARGET -git checkout $branch - -#--------------------------------------------------- -echo -e "${BOLD}Build the user guide distributable${NORMAL}" - -cp -rf ${CI_DIR}/user_guide_src/build/html/* docs -cp -rf ${CI_DIR}/user_guide_src/build/epub/CodeIgniter4.epub ./CodeIgniter${RELEASE}.epub - -#--------------------------------------------------- -# And finally, get ready for merging -echo -e "${BOLD}Assemble the pieces...${NORMAL}" -git add . -git commit -S -m "Release ${RELEASE}" -git checkout master -git merge $branch - -cd $CI_DIR - -#--------------------------------------------------- -# Done for now -echo -e "${BOLD}Distributable user guide ready..${NORMAL}" diff --git a/admin/workflow.md b/admin/workflow.md deleted file mode 100644 index 85e38e018ec2..000000000000 --- a/admin/workflow.md +++ /dev/null @@ -1,92 +0,0 @@ -# Workflow - -The main repo has two branches of interest: "master" (stable) and "develop" (in progress). -There might be other feature branches, but they are not relevant to this process. - -Once "develop" is ready for a new release, the general workflow is to - -release prep (`admin/release `)... -- create a "release" branch from develop -- update version dependencies or constants -- generate version(s) of the user guide -- move or ignore stuff, distinguishing release from development -- test that all is as it should be -- merge the release branch into "master" & "develop" - -After these have been vetted ... -- push the release(s) to github (`admin/release-deploy `) -- **manually** create the release & tag it on GitHub, based on master - Include any supplementary binaries as part of releases. -- Confirm the GitHub release action successfully deploys `appstarter` and `framework` -- **manually** post a sticky announcement thread on the forum -- **manually** tweet the release announcement - -## Assumptions - -You (a maintainer) have forked the main CodeIgniter4 repo, -and the git alias `origin`, in your local clone, refers to your fork. -The `config` script defines an additional alias, `CI_ORG`, which refers to the -CodeIgniter 4 organization on github. -This separation keeps the release branch isolated for any testing you want to do. - -The `develop` branch of the main repo should be "clean", and ready for -a release. This means that the changelog and upgrading instructions -have been suitably edited. - -This script is not intended to deal with hotfixes, i.e. PRs against -`master` that need to also be merged into `develop`, probably -as part of a bug fix minor release. - -## GitHub Action - -There is an action defined to run on any release publish event: -**.github/workflows/deploy.yml**. This will cascade any release made from -the main repo to the distribution repos, `appstarter` and `framework`. In order -for the action to authenticate you must create a Personal Access Token and add it -as a repo secret `ACCESS_TOKEN`. It is recommended that the PAT be to a secure bot -account with organization access that is dedicated for this purpose. - -If for some reason a release needs to be made that should not cascade the easiest -solution is to disable repo actions temporarily. - -## Usage - -Inside a shell prompt, in the project root: - - `admin/release version [qualifier]` - -Nothing is pushed to the repo at this point. You have a chance to vet -the repository changes. - -The "version" should follow semantic versioning, e.g. `4.0.6`, and the -version number should be higher than the current released one. - -The "qualifier" argument is a suffix to add to the version -for a pre-release, e.g. `beta.2` or `rc.41`. - -Examples: -- `admin/release 4.0.0 alpha.1` would prepare the "4.0.0-alpha.1" pre-release PR -- `admin/release 4.0.0` would prepare the "4.0.0" release PR -- `admin/release peanut butter banana` would complain and tell you to read these directions - -Once you have vetted the `dist` folder inside your local repo, you -can merge & push everything with - - `admin/release-deploy please` - -On github.com, create an appropriate release (or pre-release), -with any optional files from the root of `dist`. - -## Release notes - -On launch of a new release, a release notes post should be made in the -announcements subforum. The planned text for it (so it can be previewed -by admins) is in `admin/release_notes.bb`. - -## Audience - -These scripts are intended for use by framework maintainers, -i.e. someone with commit rights on the CI4 repositories. - -You will be prompted for your github credentials and -GPG-signing key as appropriate. diff --git a/utils/check_permission_x.php b/utils/check_permission_x.php index 606a5f9982ca..441a875adcbe 100644 --- a/utils/check_permission_x.php +++ b/utils/check_permission_x.php @@ -32,14 +32,9 @@ function findExecutableFiles(string $dir, array $excludeDirs = []): array static $execFileList = [ '.github/scripts/deploy-userguide', '.github/scripts/secure-git-push', - 'admin/release-userguide', - 'admin/release-deploy', 'admin/apibot', 'admin/alldocs', - 'admin/release', 'admin/docbot', - 'admin/release-notes.bb', - 'admin/release-revert', 'admin/starter/builds', 'user_guide_src/add-edit-this-page', ]; From bafa1b62183e134c87eddcf40e11b4505b63d5e6 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Wed, 8 Jul 2026 03:57:19 +0800 Subject: [PATCH 003/163] chore: remove legacy docbot and apibot scripts (#10383) --- admin/README.md | 7 ------ admin/alldocs | 7 ------ admin/apibot | 42 ------------------------------------ admin/apibot.md | 36 ------------------------------- admin/docbot | 42 ------------------------------------ admin/docbot.md | 39 --------------------------------- utils/check_permission_x.php | 3 --- 7 files changed, 176 deletions(-) delete mode 100755 admin/alldocs delete mode 100755 admin/apibot delete mode 100644 admin/apibot.md delete mode 100755 admin/docbot delete mode 100644 admin/docbot.md diff --git a/admin/README.md b/admin/README.md index 8f06a6282e22..e12b4bea0af0 100644 --- a/admin/README.md +++ b/admin/README.md @@ -41,13 +41,6 @@ This folder contains tools or docs useful for project maintainers. to be added as part of a git commit, ensuring that they conform to the framework coding style standards, and automatically fixing what can be. -## Maintainer Scripts - -- **docbot** re-builds the user guide from the RST source for it, - and optionally deploys it to the `gh-pages` branch of the main - repository (if the user running it has maintainer rights on that repo). - See the [writeup](./docbot.md). - ## Release Scripts The release process is detailed in [RELEASE.md](./RELEASE.md). These scripts diff --git a/admin/alldocs b/admin/alldocs deleted file mode 100755 index 12d0b410ff84..000000000000 --- a/admin/alldocs +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -# Rebuild and deploy all CodeIgniter4 docs -# - -. admin/docbot $1 -. admin/apibot $1 \ No newline at end of file diff --git a/admin/apibot b/admin/apibot deleted file mode 100755 index 08b451a04bbb..000000000000 --- a/admin/apibot +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash - -# Rebuild and deploy CodeIgniter4 under-development user guide -# -# This is a quick way to test user guide changes, and if they -# look good, to push them to the gh-pages branch of the -# development repository. -# -# This is not meant for updating the "stable" user guide. - -UPSTREAM=https://github.com/codeigniter4/api.git - -# Prepare the nested repo clone folder -rm -rf build/api* -mkdir -p build/api/docs - -# Get ready for git -cd build/api -git init -git remote add origin $UPSTREAM -git fetch -git checkout master -git reset --hard origin/master -rm -r docs/* - -# Make the new user guide -cd ../.. -phpdoc -cp -R api/build/* build/api/docs - -# All done? -if [ $# -lt 1 ]; then - exit 0 -fi - -# Optionally update the remote repo -if [ $1 = "deploy" ]; then - cd build/api - git add . - git commit -S -m "APIbot synching" - git push -f origin master -fi diff --git a/admin/apibot.md b/admin/apibot.md deleted file mode 100644 index ba63aaf269fa..000000000000 --- a/admin/apibot.md +++ /dev/null @@ -1,36 +0,0 @@ -# apibot - -Builds & deploys API docs. - -The in-progress CI4 API docs, warts & all, are rebuilt and -then copied to a nested -repository clone (`build/api`), with the result -optionally pushed to the `master` branch of the `api` repo. -That would then be publicly visible as the in-progress -version of the [API](https://codeigniter4.github.io/api/). - -## Requirements - -You must have phpDocumentor installed, with a `phpdoc` alias installed globally. - -## Audience - -This script is intended for use by framework maintainers, -i.e. someone with commit rights on the CI4 repository. - -You will be prompted for your github credentials and -GPG-signing key as appropriate. - -## Usage - -Inside a shell prompt, in the project root: - - `admin/apibot [deploy]` - -If "deploy" is not added, the script execution is considered -a trial run, and nothing is pushed to the repo. - -Whether or not deployed, the results are left inside -`build/api` (which is git ignored). - -Generate these and the userguide together with the 'alldocs' script. diff --git a/admin/docbot b/admin/docbot deleted file mode 100755 index c4dd8229c9ac..000000000000 --- a/admin/docbot +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash - -# Rebuild and deploy CodeIgniter4 under-development user guide -# -# This is a quick way to test user guide changes, and if they -# look good, to push them to the gh-pages branch of the -# development repository. -# -# This is not meant for updating the "stable" user guide. - -UPSTREAM=https://github.com/codeigniter4/CodeIgniter4.git - -# Prepare the nested repo clone folder -cd user_guide_src -rm -rf build/* -mkdir build/html - -# Get ready for git -cd build/html -git init -git remote add origin $UPSTREAM -git fetch -git checkout gh-pages -git reset --hard origin/gh-pages -rm -r * - -# Make the new user guide -cd ../.. -make html - -# All done? -if [ $# -lt 1 ]; then - exit 0 -fi - -# Optionally update the remote repo -if [ $1 = "deploy" ]; then - cd build/html - git add . - git commit -S -m "Docbot synching" - git push -f origin gh-pages -fi \ No newline at end of file diff --git a/admin/docbot.md b/admin/docbot.md deleted file mode 100644 index a3577b2e5d36..000000000000 --- a/admin/docbot.md +++ /dev/null @@ -1,39 +0,0 @@ -# docbot - -Builds & deploys user guide. - -The in-progress CI4 user guide, warts & all, is rebuilt in a nested -repository clone (`user_guide_src/build/html`), with the result -optionally pushed to the `gh-pages` branch of the repo. -That would then be publicly visible as the in-progress -version of the [User Guide](https://codeigniter4.github.io/CodeIgniter4/). - -## Requirements - -You must have python & sphinx installed. - -## Audience - -This script is intended for use by framework maintainers, -i.e. someone with commit rights on the CI4 repository. - -This script wraps the conventional user guide building, -i.e. `user_guide_src/make html`, with additional -steps. - -You will be prompted for your github credentials and -GPG-signing key as appropriate. - -## Usage - -Inside a shell prompt, in the project root: - - `admin/docbot [deploy]` - -If "deploy" is not added, the script execution is considered -a trial run, and nothing is pushed to the repo. - -Whether or not deployed, the results are left inside -user_guide_src/build (which is git ignored). - -Generate these and the API docs together with the 'alldocs' script. diff --git a/utils/check_permission_x.php b/utils/check_permission_x.php index 441a875adcbe..c6155ea23a8c 100644 --- a/utils/check_permission_x.php +++ b/utils/check_permission_x.php @@ -32,9 +32,6 @@ function findExecutableFiles(string $dir, array $excludeDirs = []): array static $execFileList = [ '.github/scripts/deploy-userguide', '.github/scripts/secure-git-push', - 'admin/apibot', - 'admin/alldocs', - 'admin/docbot', 'admin/starter/builds', 'user_guide_src/add-edit-this-page', ]; From d43762f71eee6439cac8fae2697dd0cc0ed0147f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 03:58:03 +0800 Subject: [PATCH 004/163] chore(deps-dev): update boundwize/structarmed requirement (#10386) Updates the requirements on [boundwize/structarmed](https://github.com/boundwize/structarmed) to permit the latest version. Updates `boundwize/structarmed` to 0.14.11 - [Release notes](https://github.com/boundwize/structarmed/releases) - [Commits](https://github.com/boundwize/structarmed/compare/0.14.10...0.14.11) --- updated-dependencies: - dependency-name: boundwize/structarmed dependency-version: 0.14.11 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 8e0990839d9f..f0f8767776c6 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "0.14.10", + "boundwize/structarmed": "0.14.11", "codeigniter/phpstan-codeigniter": "^2.1", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", From 192c22fac7f8d0504507783ee9e34f8264f49130 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 8 Jul 2026 12:18:58 +0700 Subject: [PATCH 005/163] chore: enable RemoveDuplicatedReturnSelfDocblockRector (#10389) --- rector.php | 2 -- system/DataCaster/DataCaster.php | 2 -- system/Database/BaseBuilder.php | 4 ---- system/HTTP/MessageTrait.php | 17 ----------------- system/HTTP/RedirectResponse.php | 2 -- system/Images/Handlers/ImageMagickHandler.php | 2 -- system/Images/ImageHandlerInterface.php | 2 -- system/Traits/ConditionalTrait.php | 4 ---- system/Traits/PropertiesTrait.php | 7 +++++-- utils/phpstan-baseline/empty.notAllowed.neon | 2 +- utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 7 +------ utils/phpstan-baseline/return.type.neon | 2 +- 13 files changed, 9 insertions(+), 46 deletions(-) diff --git a/rector.php b/rector.php index c3a1b22ad716..ba14babedae0 100644 --- a/rector.php +++ b/rector.php @@ -23,7 +23,6 @@ use Rector\CodingStyle\Rector\FuncCall\CountArrayToEmptyArrayComparisonRector; use Rector\CodingStyle\Rector\FuncCall\VersionCompareFuncCallToConstantRector; use Rector\Config\RectorConfig; -use Rector\DeadCode\Rector\ClassMethod\RemoveDuplicatedReturnSelfDocblockRector; use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedConstructorParamRector; use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodRector; use Rector\DeadCode\Rector\MethodCall\RemoveNullArgOnNullDefaultParamRector; @@ -178,7 +177,6 @@ // to be applied in separate PRs to ease review NegatedAndsToPositiveOrsRector::class, - RemoveDuplicatedReturnSelfDocblockRector::class, ]) // auto import fully qualified class names ->withImportNames() diff --git a/system/DataCaster/DataCaster.php b/system/DataCaster/DataCaster.php index e19e42bb7f0c..9d9d3c77e268 100644 --- a/system/DataCaster/DataCaster.php +++ b/system/DataCaster/DataCaster.php @@ -105,8 +105,6 @@ public function __construct( * * @param array $types [field => type] * - * @return $this - * * @internal */ public function setTypes(array $types): static diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index f1a9428c13eb..3989b9a2d011 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -577,8 +577,6 @@ public function distinct(bool $val = true) * Generates the FROM portion of the query * * @param array|string $from - * - * @return $this */ public function from($from, bool $overwrite = false): self { @@ -608,8 +606,6 @@ public function from($from, bool $overwrite = false): self /** * @param BaseBuilder $from Expected subquery * @param string $alias Subquery alias - * - * @return $this */ public function fromSubquery(BaseBuilder $from, string $alias): self { diff --git a/system/HTTP/MessageTrait.php b/system/HTTP/MessageTrait.php index aed6987f34d7..044594bcfcdd 100644 --- a/system/HTTP/MessageTrait.php +++ b/system/HTTP/MessageTrait.php @@ -48,13 +48,10 @@ trait MessageTrait // -------------------------------------------------------------------- // Body // -------------------------------------------------------------------- - /** * Sets the body of the current message. * * @param string $data - * - * @return $this */ public function setBody($data): self { @@ -67,8 +64,6 @@ public function setBody($data): self * Appends data to the body of the current message. * * @param string $data - * - * @return $this */ public function appendBody($data): self { @@ -144,8 +139,6 @@ public function header($name) * Sets a header and it's value. * * @param array|string|null $value - * - * @return $this */ public function setHeader(string $name, $value): self { @@ -191,8 +184,6 @@ private function checkMultipleHeaders(string $name): void /** * Removes a header from the list of headers we track. - * - * @return $this */ public function removeHeader(string $name): self { @@ -205,8 +196,6 @@ public function removeHeader(string $name): self /** * Adds an additional header value to any headers that accept * multiple values (i.e. are an array or implement ArrayAccess) - * - * @return $this */ public function appendHeader(string $name, ?string $value): self { @@ -225,8 +214,6 @@ public function appendHeader(string $name, ?string $value): self * Adds a header (not a header value) with the same name. * Use this only when you set multiple headers with the same name, * typically, for `Set-Cookie`. - * - * @return $this */ public function addHeader(string $name, string $value): static { @@ -251,8 +238,6 @@ public function addHeader(string $name, string $value): static /** * Adds an additional header value to any headers that accept * multiple values (i.e. are an array or implement ArrayAccess) - * - * @return $this */ public function prependHeader(string $name, string $value): self { @@ -277,8 +262,6 @@ protected function getHeaderName(string $name): string /** * Sets the HTTP protocol version. * - * @return $this - * * @throws HTTPException For invalid protocols */ public function setProtocolVersion(string $version): self diff --git a/system/HTTP/RedirectResponse.php b/system/HTTP/RedirectResponse.php index 50c6b649373e..d8c8f00e868d 100644 --- a/system/HTTP/RedirectResponse.php +++ b/system/HTTP/RedirectResponse.php @@ -108,8 +108,6 @@ public function withInput() * If the validation has any errors, transmit those back * so they can be displayed when the validation is handled * within a method different than displaying the form. - * - * @return $this */ private function withErrors(): self { diff --git a/system/Images/Handlers/ImageMagickHandler.php b/system/Images/Handlers/ImageMagickHandler.php index 1472936d071b..c2b24c81f272 100644 --- a/system/Images/Handlers/ImageMagickHandler.php +++ b/system/Images/Handlers/ImageMagickHandler.php @@ -537,8 +537,6 @@ public function reorient(bool $silent = false) /** * Clears metadata from the image. * - * @return $this - * * @throws ImagickException */ public function clearMetadata(): static diff --git a/system/Images/ImageHandlerInterface.php b/system/Images/ImageHandlerInterface.php index a96977214ec8..1bb1e2b20ee0 100644 --- a/system/Images/ImageHandlerInterface.php +++ b/system/Images/ImageHandlerInterface.php @@ -152,8 +152,6 @@ public function save(?string $target = null, int $quality = 90); /** * Clear metadata before saving image as a new file. - * - * @return $this */ public function clearMetadata(): static; } diff --git a/system/Traits/ConditionalTrait.php b/system/Traits/ConditionalTrait.php index b24fbdcd1b0c..37703189615d 100644 --- a/system/Traits/ConditionalTrait.php +++ b/system/Traits/ConditionalTrait.php @@ -23,8 +23,6 @@ trait ConditionalTrait * @param TWhen $condition * @param callable(self, TWhen): mixed $callback * @param (callable(self): mixed)|null $defaultCallback - * - * @return $this */ public function when($condition, callable $callback, ?callable $defaultCallback = null): self { @@ -45,8 +43,6 @@ public function when($condition, callable $callback, ?callable $defaultCallback * @param TWhenNot $condition * @param callable(self, TWhenNot): mixed $callback * @param (callable(self): mixed)|null $defaultCallback - * - * @return $this */ public function whenNot($condition, callable $callback, ?callable $defaultCallback = null): self { diff --git a/system/Traits/PropertiesTrait.php b/system/Traits/PropertiesTrait.php index 30b3f86690c0..bf0571b2f05b 100644 --- a/system/Traits/PropertiesTrait.php +++ b/system/Traits/PropertiesTrait.php @@ -27,8 +27,6 @@ trait PropertiesTrait { /** * Attempts to set the values of public class properties. - * - * @return $this */ final public function fill(array $params): self { @@ -43,10 +41,15 @@ final public function fill(array $params): self /** * Get the public properties of the class and return as an array. + * + * @return array */ final public function getPublicProperties(): array { $worker = new class () { + /** + * @return array + */ public function getProperties(object $obj): array { return get_object_vars($obj); diff --git a/utils/phpstan-baseline/empty.notAllowed.neon b/utils/phpstan-baseline/empty.notAllowed.neon index 1d8d1796b5c2..3172047e0c1e 100644 --- a/utils/phpstan-baseline/empty.notAllowed.neon +++ b/utils/phpstan-baseline/empty.notAllowed.neon @@ -1,4 +1,4 @@ -# total 213 errors +# total 212 errors parameters: ignoreErrors: diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index bf2719972413..2e67047327fc 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 1827 errors +# total 1825 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 1627bbb4a52a..677e7101b2c3 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 1141 errors +# total 1140 errors parameters: ignoreErrors: @@ -4132,11 +4132,6 @@ parameters: count: 1 path: ../../system/View/Cells/Cell.php - - - message: '#^Method CodeIgniter\\View\\Cells\\Cell\:\:getPublicProperties\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/View/Cells/Cell.php - - message: '#^Method CodeIgniter\\View\\Cells\\Cell\:\:includeComputedProperties\(\) has parameter \$properties with no value type specified in iterable type array\.$#' count: 1 diff --git a/utils/phpstan-baseline/return.type.neon b/utils/phpstan-baseline/return.type.neon index cd3da83ccce5..258159a982c6 100644 --- a/utils/phpstan-baseline/return.type.neon +++ b/utils/phpstan-baseline/return.type.neon @@ -3,7 +3,7 @@ parameters: ignoreErrors: - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:cleanClone\(\) should return \$this\(CodeIgniter\\Database\\BaseBuilder\) but returns static\(CodeIgniter\\Database\\BaseBuilder\)\.$#' + message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:cleanClone\(\) should return \$this\(CodeIgniter\\Database\\BaseBuilder\) but returns CodeIgniter\\Database\\BaseBuilder\.$#' count: 1 path: ../../system/Database/BaseBuilder.php From 4a7d1449e18030b57db153660a09afb7f4a1199d Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Wed, 8 Jul 2026 18:38:29 +0200 Subject: [PATCH 006/163] fix: safely interpolate logger context values (#10384) Co-authored-by: John Paul E Balandan --- system/Log/Logger.php | 53 ++++++- tests/system/Log/LoggerTest.php | 158 ++++++++++++++++++++ user_guide_src/source/changelogs/v4.7.5.rst | 2 + 3 files changed, 208 insertions(+), 5 deletions(-) diff --git a/system/Log/Logger.php b/system/Log/Logger.php index 011920e15bd1..6b61837fa22b 100644 --- a/system/Log/Logger.php +++ b/system/Log/Logger.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Log; +use CodeIgniter\Entity\Entity; use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Log\Exceptions\LogException; use CodeIgniter\Log\Handlers\HandlerInterface; @@ -298,14 +299,14 @@ protected function interpolate($message, array $context = []) $replace = []; foreach ($context as $key => $val) { - // Verify that the 'exception' key is actually an exception - // or error, both of which implement the 'Throwable' interface. - if ($key === 'exception' && $val instanceof Throwable) { - $val = $val->getMessage() . ' ' . clean_path($val->getFile()) . ':' . $val->getLine(); + $placeholder = '{' . $key . '}'; + + if (! str_contains($message, $placeholder)) { + continue; } // todo - sanitize input before writing to file? - $replace['{' . $key . '}'] = $val; + $replace[$placeholder] = $this->stringifyContextValue($key, $val); } $replace['{post_vars}'] = '$_POST: ' . print_r(service('superglobals')->getPostArray(), true); @@ -337,6 +338,48 @@ protected function interpolate($message, array $context = []) return strtr($message, $replace); } + /** + * Converts context values to strings without raising PHP errors. + */ + protected function stringifyContextValue(int|string $key, mixed $value): string + { + // Verify that the 'exception' key is actually an exception + // or error, both of which implement the 'Throwable' interface. + if ($key === 'exception' && $value instanceof Throwable) { + return $value->getMessage() . ' ' . clean_path($value->getFile()) . ':' . $value->getLine(); + } + + if ($value === null || is_scalar($value)) { + return (string) $value; + } + + if ($value instanceof Stringable) { + try { + return (string) $value; + } catch (Throwable) { + return '[object ' . $value::class . ']'; + } + } + + if (is_array($value)) { + return print_r($value, true); + } + + if ($value instanceof Entity) { + try { + return print_r($value->toArray(), true); + } catch (Throwable) { + return '[object ' . $value::class . ']'; + } + } + + if (is_object($value)) { + return '[object ' . $value::class . ']'; + } + + return '[' . get_debug_type($value) . ']'; + } + /** * Determines the file and line that the logging call * was made from by analyzing the backtrace. diff --git a/tests/system/Log/LoggerTest.php b/tests/system/Log/LoggerTest.php index 80d42d6c83b3..bbdf304acf8c 100644 --- a/tests/system/Log/LoggerTest.php +++ b/tests/system/Log/LoggerTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Log; +use CodeIgniter\Entity\Entity; use CodeIgniter\Exceptions\FrameworkException; use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\I18n\Time; @@ -22,6 +23,8 @@ use PHPUnit\Framework\Attributes\Group; use ReflectionMethod; use ReflectionNamedType; +use stdClass; +use Stringable; use Tests\Support\Log\Handlers\TestHandler; /** @@ -123,6 +126,161 @@ public function testLogInterpolatesMessage(): void $this->assertSame($expected, $logs[0]); } + public function testLogInterpolatesArrayContextValue(): void + { + $config = new LoggerConfig(); + $logger = new Logger($config); + + Time::setTestNow('2023-11-25 12:00:00'); + + $payload = ['id' => 123, 'status' => 'created']; + $expected = 'DEBUG - ' . Time::now()->format('Y-m-d') . ' --> Test message ' . print_r($payload, true); + + $logger->log('debug', 'Test message {payload}', ['payload' => $payload]); + + $logs = TestHandler::getLogs(); + + $this->assertCount(1, $logs); + $this->assertSame($expected, $logs[0]); + } + + public function testLogInterpolatesOnlyExactDottedContextKey(): void + { + $config = new LoggerConfig(); + $logger = new Logger($config); + + Time::setTestNow('2023-11-25 12:00:00'); + + $expected = 'DEBUG - ' . Time::now()->format('Y-m-d') . ' --> Test message literal'; + + $logger->log('debug', 'Test message {foo.bar}', [ + 'foo.bar' => 'literal', + 'foo' => ['bar' => 'nested'], + ]); + + $logs = TestHandler::getLogs(); + + $this->assertCount(1, $logs); + $this->assertSame($expected, $logs[0]); + } + + public function testLogIgnoresUnusedArrayContextValueDuringInterpolation(): void + { + $config = new LoggerConfig(); + $logger = new Logger($config); + + Time::setTestNow('2023-11-25 12:00:00'); + + $expected = 'DEBUG - ' . Time::now()->format('Y-m-d') . ' --> Test message value'; + + $logger->log('debug', 'Test message {name}', [ + 'name' => 'value', + 'payload' => ['id' => 123], + ]); + + $logs = TestHandler::getLogs(); + + $this->assertCount(1, $logs); + $this->assertSame($expected, $logs[0]); + } + + public function testLogInterpolatesStringableContextValue(): void + { + $config = new LoggerConfig(); + $logger = new Logger($config); + + Time::setTestNow('2023-11-25 12:00:00'); + + $expected = 'DEBUG - ' . Time::now()->format('Y-m-d') . ' --> Test message stringable'; + + $logger->log('debug', 'Test message {value}', [ + 'value' => new class () implements Stringable { + public function __toString(): string + { + return 'stringable'; + } + }, + ]); + + $logs = TestHandler::getLogs(); + + $this->assertCount(1, $logs); + $this->assertSame($expected, $logs[0]); + } + + public function testLogInterpolatesTimeContextValue(): void + { + $config = new LoggerConfig(); + $logger = new Logger($config); + + Time::setTestNow('2023-11-25 12:00:00'); + + $time = Time::parse('2024-01-02 03:04:05', 'UTC'); + $expected = 'DEBUG - ' . Time::now()->format('Y-m-d') . ' --> Test message 2024-01-02 03:04:05'; + + $logger->log('debug', 'Test message {time}', ['time' => $time]); + + $logs = TestHandler::getLogs(); + + $this->assertCount(1, $logs); + $this->assertSame($expected, $logs[0]); + } + + public function testLogInterpolatesNonStringableObjectContextValue(): void + { + $config = new LoggerConfig(); + $logger = new Logger($config); + + Time::setTestNow('2023-11-25 12:00:00'); + + $expected = 'DEBUG - ' . Time::now()->format('Y-m-d') . ' --> Test message [object stdClass]'; + + $logger->log('debug', 'Test message {value}', ['value' => new stdClass()]); + + $logs = TestHandler::getLogs(); + + $this->assertCount(1, $logs); + $this->assertSame($expected, $logs[0]); + } + + public function testLogInterpolatesResourceContextValue(): void + { + $config = new LoggerConfig(); + $logger = new Logger($config); + + Time::setTestNow('2023-11-25 12:00:00'); + + $resource = fopen('php://memory', 'rb'); + $expected = 'DEBUG - ' . Time::now()->format('Y-m-d') . ' --> Test message [resource (stream)]'; + + $logger->log('debug', 'Test message {value}', ['value' => $resource]); + + fclose($resource); + + $logs = TestHandler::getLogs(); + + $this->assertCount(1, $logs); + $this->assertSame($expected, $logs[0]); + } + + public function testLogInterpolatesEntityContextValue(): void + { + $config = new LoggerConfig(); + $logger = new Logger($config); + + Time::setTestNow('2023-11-25 12:00:00'); + + $entity = new Entity(['id' => 123, 'name' => 'Ada']); + $expected = 'DEBUG - ' . Time::now()->format('Y-m-d') . ' --> Test message ' . print_r($entity->toArray(), true); + + $logger->log('debug', 'Test message {entity}', ['entity' => $entity]); + + $logs = TestHandler::getLogs(); + + $this->assertCount(1, $logs); + $this->assertSame($expected, $logs[0]); + } + public function testLogInterpolatesPost(): void { $config = new LoggerConfig(); diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index e379b7b0cfd1..c4bb7900e07a 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -30,6 +30,8 @@ Deprecations Bugs Fixed ********** +- **Logger:** Fixed a bug where interpolating a log message with array or non-stringable context values could raise PHP warnings or errors. + See the repo's `CHANGELOG.md `_ for a complete list of bugs fixed. From d99d257f5bf4138b772616a7adc6a95a637fe4c7 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Thu, 9 Jul 2026 02:35:56 +0800 Subject: [PATCH 007/163] chore: automate CHANGELOG.md generation for releases (#10390) --- .github/release.yml | 1 + admin/README.md | 4 + admin/RELEASE.md | 37 ++-- admin/generate-changelog.php | 153 +++++++++++++++ .../AutoReview/GenerateChangelogTest.php | 183 ++++++++++++++++++ 5 files changed, 362 insertions(+), 16 deletions(-) create mode 100644 admin/generate-changelog.php create mode 100644 tests/system/AutoReview/GenerateChangelogTest.php diff --git a/.github/release.yml b/.github/release.yml index de6e982d91a6..09ead7ca89d2 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -2,6 +2,7 @@ changelog: exclude: authors: - dependabot + - dependabot[bot] categories: - title: Breaking Changes labels: diff --git a/admin/README.md b/admin/README.md index e12b4bea0af0..f070ca58b459 100644 --- a/admin/README.md +++ b/admin/README.md @@ -46,6 +46,10 @@ This folder contains tools or docs useful for project maintainers. The release process is detailed in [RELEASE.md](./RELEASE.md). These scripts are used as part of that process: +- **generate-changelog.php** prepends the CHANGELOG.md entry for a new + release, built from GitHub's auto-generated release notes and the + SECURITY section of the version's detailed changelog. + Usage: `php admin/generate-changelog.php 4.x.x [--dry-run]` - **prepare-release.php** creates the `release-4.x.x` branch and updates version references in the framework source, the user guide, and the distribution build script. diff --git a/admin/RELEASE.md b/admin/RELEASE.md index a6b3f680b6ad..cd2dbcababd3 100644 --- a/admin/RELEASE.md +++ b/admin/RELEASE.md @@ -76,21 +76,26 @@ PRs with breaking changes must have the following additional label: ### Generate Changelog -To auto-generate the changelog, navigate to the -[Releases](https://github.com/codeigniter4/CodeIgniter4/releases) page, -click the "Draft a new release" button. - -* Choose a tag: `v4.x.x` (Create new tag: v4.x.x on publish) -* Target: `develop` - -Click the "Generate release notes" button. - -Check the resulting content. If there are items in the *Others* section which -should be included in the changelog, add a label to the PR and regenerate -the changelog. - -Copy the resulting contents into **CHANGELOG.md** and adjust the format to match -the existing content. +The changelog is generated from GitHub's auto-generated release notes with a +script. It requires the authenticated [GitHub CLI](https://cli.github.com/). + +The script also copies the security fixes from the SECURITY section of +**user_guide_src/source/changelogs/v4.x.x.rst**, so any security advisory PRs +must be merged and documented there before generating the changelog. + +* [ ] Run `php admin/generate-changelog.php 4.x.x --dry-run` and check the output. + * The script lists the PRs that have none of the changelog labels. If any of + them should be included in the changelog, add a label to the PR and run + the script again. +* [ ] Run `php admin/generate-changelog.php 4.x.x` to prepend the new entry to + **CHANGELOG.md**. + +If the script cannot be used, the same notes can be generated from the +[Releases](https://github.com/codeigniter4/CodeIgniter4/releases) page: +click "Draft a new release", choose the tag `v4.x.x` (create new tag on +publish) with target `develop`, and click "Generate release notes". Copy the +resulting contents into **CHANGELOG.md** and adjust the format to match the +existing content. ## Process @@ -100,7 +105,7 @@ the existing content. > generating much new content. * [ ] Merge any security advisory PRs in private forks. -* [ ] Add the current version to **CHANGELOG.md** with the contents generated above. +* [ ] Check that **CHANGELOG.md** contains the entry for the new version, generated above. * [ ] Update **user_guide_src/source/changelogs/v4.x.x.rst** * Remove the section titles that have no items * [ ] Update **user_guide_src/source/installation/upgrade_4xx.rst** diff --git a/admin/generate-changelog.php b/admin/generate-changelog.php new file mode 100644 index 000000000000..f6578cb1372f --- /dev/null +++ b/admin/generate-changelog.php @@ -0,0 +1,153 @@ + [--dry-run] + */ +chdir(__DIR__ . '/..'); + +$args = array_slice($argv, 1); +$options = array_values(array_filter($args, static fn (string $arg): bool => str_starts_with($arg, '--'))); +$params = array_values(array_diff($args, $options)); + +if (count($params) !== 1 || preg_match('/\A\d+\.\d+\.\d+\z/', $params[0]) !== 1) { + echo "Usage: php {$argv[0]} [--dry-run]\n"; + echo "E.g.,: php {$argv[0]} 4.7.5\n"; + + exit(1); +} + +$version = $params[0]; +$dryRun = in_array('--dry-run', $options, true); +$repo = 'codeigniter4/CodeIgniter4'; + +$changelogPath = './CHANGELOG.md'; +$changelog = file_get_contents($changelogPath); + +if (! $dryRun && str_contains($changelog, "## [v{$version}]")) { + echo "CHANGELOG.md already contains an entry for v{$version}.\n"; + + exit(1); +} + +// Fetches the auto-generated release notes. +$command = sprintf( + 'gh api repos/%s/releases/generate-notes -f tag_name=v%s -f target_commitish=develop 2>&1', + $repo, + $version, +); +exec($command, $output, $exitCode); + +if ($exitCode !== 0) { + echo "Failed to fetch the release notes from GitHub:\n"; + echo implode("\n", $output) . "\n"; + + exit(1); +} + +$response = json_decode(implode("\n", $output), true); +$body = $response['body'] ?? ''; + +// Parses the release notes into categories. See ".github/release.yml". +$sections = []; +$current = null; +$previousTag = null; + +foreach (preg_split('/\R/', $body) as $line) { + if (preg_match('/^### (.+)$/', $line, $matches) === 1) { + $current = $matches[1]; + $sections[$current] = []; + + continue; + } + + if (str_starts_with($line, '## ')) { + $current = null; + + continue; + } + + if (preg_match('~^\*\*Full Changelog\*\*: https://github\.com/[^/]+/[^/]+/compare/(.+?)\.\.\.~', $line, $matches) === 1) { + $previousTag = $matches[1]; + + continue; + } + + if ($current !== null && str_starts_with($line, '* ')) { + $sections[$current][] = $line; + } +} + +if ($previousTag === null) { + echo "Could not determine the previous tag from the release notes.\n"; + + exit(1); +} + +// PRs in the catch-all category have none of the changelog labels. They are +// listed for checking only and are not part of the generated entry. +$othersTitle = 'Others (Only for checking. Remove this category)'; +$others = $sections[$othersTitle] ?? []; +unset($sections[$othersTitle]); + +// Extracts the security fixes from the SECURITY section of the detailed +// changelog, if any. These come from security advisories, not from PRs, so +// they are not part of the generated release notes. +$rstPath = "./user_guide_src/source/changelogs/v{$version}.rst"; +$security = ''; + +if (is_file($rstPath)) { + $rst = file_get_contents($rstPath); + + if (preg_match('/^\*+\nSECURITY\n\*+\n(.*?)(?=^\*+$|\z)/msu', $rst, $matches) === 1) { + $security = trim($matches[1], "\n"); + $security = preg_replace('/^- /mu', '* ', $security); + } +} + +$date = date('Y-m-d'); +$entry = "## [v{$version}](https://github.com/{$repo}/tree/v{$version}) ({$date})\n"; +$entry .= "[Full Changelog](https://github.com/{$repo}/compare/{$previousTag}...v{$version})\n"; + +if ($security !== '') { + $entry .= "\n### Security\n\n{$security}\n"; +} + +foreach ($sections as $title => $items) { + if ($items === []) { + continue; + } + + $entry .= "\n### {$title}\n\n"; + $entry .= implode("\n", $items) . "\n"; +} + +if ($others !== []) { + echo "The following PRs have no changelog label and were NOT included.\n"; + echo "If any of them belong in the changelog, label the PR and run this script again:\n"; + echo implode("\n", $others) . "\n\n"; +} + +if ($dryRun) { + echo $entry; + + exit(0); +} + +$updated = preg_replace('/\A# Changelog\n\n/', "# Changelog\n\n{$entry}\n", $changelog, 1, $count); + +if ($count !== 1) { + echo 'Could not find the "# Changelog" header in CHANGELOG.md.' . "\n"; + + exit(1); +} + +file_put_contents($changelogPath, $updated); + +echo "Added the v{$version} entry to CHANGELOG.md.\n"; diff --git a/tests/system/AutoReview/GenerateChangelogTest.php b/tests/system/AutoReview/GenerateChangelogTest.php new file mode 100644 index 000000000000..cbb7e87e1e5c --- /dev/null +++ b/tests/system/AutoReview/GenerateChangelogTest.php @@ -0,0 +1,183 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\AutoReview; + +use Nexus\PHPUnit\Tachycardia\Attribute\TimeLimit; +use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\TestCase; + +/** + * @internal + */ +#[CoversNothing] +#[Group('AutoReview')] +final class GenerateChangelogTest extends TestCase +{ + public function testUsageErrorWithoutArguments(): void + { + exec('php ./admin/generate-changelog.php 2>&1', $output, $exitCode); + + $this->assertSame(1, $exitCode); + $this->assertStringContainsString('Usage:', implode("\n", $output)); + } + + public function testUsageErrorWithInvalidVersion(): void + { + exec('php ./admin/generate-changelog.php 4.7 2>&1', $output, $exitCode); + + $this->assertSame(1, $exitCode); + $this->assertStringContainsString('Usage:', implode("\n", $output)); + } + + public function testRejectsVersionAlreadyInChangelog(): void + { + $version = $this->latestChangelogVersion(); + + exec("php ./admin/generate-changelog.php {$version} 2>&1", $output, $exitCode); + + $this->assertSame(1, $exitCode); + $this->assertStringContainsString( + "CHANGELOG.md already contains an entry for v{$version}.", + implode("\n", $output), + ); + } + + #[TimeLimit(10.0)] + public function testDryRunGeneratesEntryWithoutWriting(): void + { + $this->skipUnlessGhIsAuthenticated(); + + $version = $this->incrementPatch($this->latestChangelogVersion()); + + exec("php ./admin/generate-changelog.php {$version} --dry-run 2>&1", $output, $exitCode); + $outputString = implode("\n", $output); + + $this->assertSame(0, $exitCode, "Script exited with code {$exitCode}. Output: {$outputString}"); + $this->assertStringContainsString( + "## [v{$version}](https://github.com/codeigniter4/CodeIgniter4/tree/v{$version})", + $outputString, + ); + $this->assertMatchesRegularExpression( + '~\[Full Changelog\]\(https://github\.com/codeigniter4/CodeIgniter4/compare/v[\d.]+\.\.\.v' . preg_quote($version, '~') . '\)~', + $outputString, + ); + $this->assertSame('', trim((string) exec('git status --porcelain -- CHANGELOG.md'))); + } + + #[TimeLimit(10.0)] + public function testDryRunIncludesSecurityFixesFromDetailedChangelog(): void + { + $this->skipUnlessGhIsAuthenticated(); + + $version = $this->latestVersionWithSecuritySection(); + + if ($version === null) { + $this->markTestSkipped('No detailed changelog with a SECURITY section found.'); + } + + exec("php ./admin/generate-changelog.php {$version} --dry-run 2>&1", $output, $exitCode); + $outputString = implode("\n", $output); + + $this->assertSame(0, $exitCode, "Script exited with code {$exitCode}. Output: {$outputString}"); + $this->assertMatchesRegularExpression('/### Security\n\n\* /', $outputString); + } + + #[TimeLimit(10.0)] + public function testAddsEntryToChangelog(): void + { + $this->skipUnlessGhIsAuthenticated(); + + if (trim((string) exec('git status --porcelain -- CHANGELOG.md')) !== '') { + $this->markTestSkipped('You have uncommitted changes to CHANGELOG.md that will be erased by this test.'); + } + + $version = $this->incrementPatch($this->latestChangelogVersion()); + + try { + exec("php ./admin/generate-changelog.php {$version} 2>&1", $output, $exitCode); + $outputString = implode("\n", $output); + + $this->assertSame(0, $exitCode, "Script exited with code {$exitCode}. Output: {$outputString}"); + $this->assertStringContainsString("Added the v{$version} entry to CHANGELOG.md.", $outputString); + + $changelog = (string) file_get_contents('./CHANGELOG.md'); + $this->assertStringStartsWith( + "# Changelog\n\n## [v{$version}](https://github.com/codeigniter4/CodeIgniter4/tree/v{$version})", + $changelog, + ); + } finally { + exec('git restore -- CHANGELOG.md'); + } + } + + private function skipUnlessGhIsAuthenticated(): void + { + exec('gh auth status 2>&1', $output, $exitCode); + + if ($exitCode !== 0) { + $this->markTestSkipped( + 'The GitHub CLI is not available or not authenticated. This test is expected ' + . 'to be skipped in CI and runs only where `gh` is authenticated, such as on ' + . 'a maintainer\'s machine.', + ); + } + } + + private function latestChangelogVersion(): string + { + $changelog = (string) file_get_contents('./CHANGELOG.md'); + + if (preg_match('/^## \[v(\d+\.\d+\.\d+)\]/m', $changelog, $matches) !== 1) { + $this->fail('Could not find a version entry in CHANGELOG.md.'); + } + + return $matches[1]; + } + + private function latestVersionWithSecuritySection(): ?string + { + $versions = []; + $paths = glob('./user_guide_src/source/changelogs/v*.rst'); + + if ($paths === false) { + return null; + } + + foreach ($paths as $path) { + if (preg_match('/v(\d+\.\d+\.\d+)\.rst$/', $path, $matches) !== 1) { + continue; + } + + if (str_contains((string) file_get_contents($path), "\nSECURITY\n")) { + $versions[] = $matches[1]; + } + } + + if ($versions === []) { + return null; + } + + usort($versions, version_compare(...)); + + return end($versions); + } + + private function incrementPatch(string $version): string + { + $parts = explode('.', $version); + + return sprintf('%d.%d.%d', $parts[0], $parts[1], ++$parts[2]); + } +} From 1533c8591b900ca31bba6ee868a2383fc488f32e Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Thu, 9 Jul 2026 02:59:35 +0800 Subject: [PATCH 008/163] chore: add script to audit changelog labels on merged PRs (#10391) --- admin/README.md | 44 ++-- admin/RELEASE.md | 5 + admin/check-pr-labels.php | 203 ++++++++++++++++++ tests/system/AutoReview/CheckPrLabelsTest.php | 95 ++++++++ 4 files changed, 327 insertions(+), 20 deletions(-) create mode 100644 admin/check-pr-labels.php create mode 100644 tests/system/AutoReview/CheckPrLabelsTest.php diff --git a/admin/README.md b/admin/README.md index f070ca58b459..46973c1f71ce 100644 --- a/admin/README.md +++ b/admin/README.md @@ -4,33 +4,33 @@ This folder contains tools or docs useful for project maintainers. ## Repositories inside https://github.com/codeigniter4 -- **CodeIgniter4** is the main development repository. - It supports issues and pull requests, and has a rule to enforce GPG-signed commits. - In addition to the framework source, it includes unit testing and documentation source. - The three repositories following are built from this one as part of the release workflow. +- **CodeIgniter4** is the main development repository. + It supports issues and pull requests, and has a rule to enforce GPG-signed commits. + In addition to the framework source, it includes unit testing and documentation source. + The three repositories following are built from this one as part of the release workflow. This repo is meant to be forked by contributors. -- **framework** is the released developer repository. +- **framework** is the released developer repository. It contains all the main pieces of the framework that developers would use to - build their apps, but not the framework unit testing or the user guide source. - It is meant to be downloaded by developers, or composer-installed. + build their apps, but not the framework unit testing or the user guide source. + It is meant to be downloaded by developers, or composer-installed. This is a read-only repository. -- **appstarter** is the released application starter repository. +- **appstarter** is the released application starter repository. It is derived from the framework's `app` and `public` folders, with - a composer requirement dependency to pull in the framework itself. - It is meant to be downloaded or composer-installed. + a composer requirement dependency to pull in the framework itself. + It is meant to be downloaded or composer-installed. This is a read-only repository. -- **userguide** is released documentation publishing repository. +- **userguide** is released documentation publishing repository. It contains built versions of the user guide, corresponding to the - framework releases. - It could be downloaded, forked or potentially composer-installed. + framework releases. + It could be downloaded, forked or potentially composer-installed. This is a read-only repository. -- **coding-standard** is the coding style standards repository. +- **coding-standard** is the coding style standards repository. It contains PHP-CS-Fixer rules to ensure consistent code style - within the framework itself. + within the framework itself. It is meant to be composer-installed. - **translations** is the repository holding official translations of - the locale-dependent system messages. - It is community-maintained, and accepts issues and pull requests. + the locale-dependent system messages. + It is community-maintained, and accepts issues and pull requests. It could be downloaded, forked or composer-installed. ## Contributor Scripts @@ -52,18 +52,22 @@ are used as part of that process: Usage: `php admin/generate-changelog.php 4.x.x [--dry-run]` - **prepare-release.php** creates the `release-4.x.x` branch and updates version references in the framework source, the user guide, and the - distribution build script. + distribution build script. Usage: `php admin/prepare-release.php 4.x.x` - **create-new-changelog.php** creates the changelog and upgrade guide - stubs for the next version and adds them to their index files. + stubs for the next version and adds them to their index files. Usage: `php admin/create-new-changelog.php ` +- **check-pr-labels.php** flags PRs merged after a release (default: the + latest) that appear to be missing the labels used to generate the + changelog. + Usage: `php admin/check-pr-labels.php []` ## Other Stuff - The **framework** and **starter** subfolders contain files that will over-ride those from the development repository, when the distribution repositories are built. -- The subfolders inside `admin` contain "next release" files in the case of +- The subfolders inside `admin` contain "next release" files in the case of `codeigniter4` and over-written distribution files in the other cases. - The CHANGELOG.md file is generated from GitHub's auto-generated release notes, as described in [RELEASE.md](./RELEASE.md). diff --git a/admin/RELEASE.md b/admin/RELEASE.md index cd2dbcababd3..405e9af10a6a 100644 --- a/admin/RELEASE.md +++ b/admin/RELEASE.md @@ -74,6 +74,11 @@ the following [labels](https://github.com/codeigniter4/CodeIgniter4/labels): PRs with breaking changes must have the following additional label: - **breaking change** ... PRs that may break existing functionalities +At any time, missing labels can be checked for with +`php admin/check-pr-labels.php`, which flags PRs merged since the last release +that appear to lack a changelog label. It requires the authenticated +[GitHub CLI](https://cli.github.com/). + ### Generate Changelog The changelog is generated from GitHub's auto-generated release notes with a diff --git a/admin/check-pr-labels.php b/admin/check-pr-labels.php new file mode 100644 index 000000000000..a1a2f7d9c33e --- /dev/null +++ b/admin/check-pr-labels.php @@ -0,0 +1,203 @@ +&1", $repo, $number), $files, $exitCode); + + if ($exitCode !== 0) { + return true; + } + + foreach ($files as $file) { + if (str_starts_with($file, 'system/')) { + return true; + } + } + + return false; +} + +chdir(__DIR__ . '/..'); + +$ansi = stream_isatty(STDOUT); +$repo = 'codeigniter4/CodeIgniter4'; + +// Labels used by the changelog categories. See ".github/release.yml". +$changelogLabels = ['breaking change', 'bug', 'enhancement', 'new feature', 'refactor']; + +// PR title types that map to a changelog label. +$typeToLabel = [ + 'fix' => 'bug', + 'feat' => 'new feature', + 'perf' => 'enhancement', + 'refactor' => 'refactor', +]; + +// PR title types that need no changelog label. +$typesWithoutLabel = ['chore', 'ci', 'docs', 'style', 'test']; + +// Release process PRs carry no labels. +$releaseTitles = '/\A(?:Prep for \d+\.\d+\.\d+ release|\d+\.\d+\.\d+ (?:Ready|Merge) code)\z/'; + +$tag = null; + +foreach (array_slice($argv, 1) as $arg) { + if (preg_match('/\Av?(\d+\.\d+\.\d+)\z/', $arg, $matches) === 1) { + $tag = "v{$matches[1]}"; + + continue; + } + + echo sprintf("Usage: php %s [version]\n", $argv[0]); + echo sprintf("E.g.,: php %s 4.7.3\n", $argv[0]); + echo "Checks the PRs merged after the given release. Defaults to the latest release.\n"; + + exit(1); +} + +$endpoint = $tag === null ? sprintf('repos/%s/releases/latest', $repo) : sprintf('repos/%s/releases/tags/%s', $repo, $tag); +exec(sprintf('gh api %s 2>&1', $endpoint), $output, $exitCode); + +if ($exitCode !== 0) { + echo sprintf("Failed to fetch the release %s from GitHub:\n", $tag ?? '(latest)'); + echo implode("\n", $output) . "\n"; + + exit(1); +} + +$release = json_decode(implode("\n", $output), true); +$since = $release['published_at'] ?? ''; + +echo sprintf("Checking PRs merged since %s (%s).\n", $release['tag_name'], $since); + +$command = sprintf( + 'gh pr list --repo %s --search %s --json number,title,labels,baseRefName,url --limit 300 2>&1', + $repo, + escapeshellarg(sprintf('is:merged -base:master merged:>%s', $since)), +); +exec($command, $prOutput, $exitCode); + +if ($exitCode !== 0) { + echo "Failed to fetch the merged PRs from GitHub:\n"; + echo implode("\n", $prOutput) . "\n"; + + exit(1); +} + +$pulls = json_decode(implode("\n", $prOutput), true); + +if (! is_array($pulls)) { + echo "Unexpected response from GitHub.\n"; + + exit(1); +} + +echo sprintf("Found %d merged PRs.\n\n", count($pulls)); + +$missingLabel = []; +$needManualLook = []; + +foreach ($pulls as $pull) { + $labels = array_column($pull['labels'], 'name'); + $hasLabel = array_intersect($changelogLabels, $labels) !== []; + $type = null; + + if (preg_match('/\A(\w+)(?:\([^)]*\))?:/', $pull['title'], $matches) === 1) { + $type = $matches[1]; + } + + if ($hasLabel || in_array($type, $typesWithoutLabel, true)) { + continue; + } + + if (preg_match($releaseTitles, $pull['title']) === 1) { + continue; + } + + if ($type !== null && isset($typeToLabel[$type])) { + // The refactor label applies only to refactoring in system/. Test-only + // refactors may use the testing label, which is not in the changelog. + if ($type === 'refactor' && ! touches_system_dir($repo, (int) $pull['number'])) { + continue; + } + + $missingLabel[] = [ + 'number' => $pull['number'], + 'title' => $pull['title'], + 'base' => $pull['baseRefName'], + 'url' => $pull['url'], + 'label' => $typeToLabel[$type], + ]; + } else { + $needManualLook[] = [ + 'number' => $pull['number'], + 'title' => $pull['title'], + 'base' => $pull['baseRefName'], + 'url' => $pull['url'], + ]; + } +} + +if ($missingLabel !== []) { + echo color('PRs that appear to be missing a changelog label:', '1', $ansi) . "\n"; + + foreach ($missingLabel as $pull) { + echo sprintf("%s [suggested: %s]\n", format_pull_line($pull, $ansi), color($pull['label'], '33', $ansi)); + } + + echo "\nTo add the suggested labels, run the following commands (drop any that are not warranted):\n"; + + foreach ($missingLabel as $pull) { + echo sprintf("gh pr edit %d --repo %s --add-label \"%s\"\n", $pull['number'], $repo, $pull['label']); + } + + echo "\n"; +} + +if ($needManualLook !== []) { + echo color('PRs with no changelog label and no recognized title type (check manually):', '1', $ansi) . "\n"; + + foreach ($needManualLook as $pull) { + echo format_pull_line($pull, $ansi) . "\n"; + } + + echo "\n"; +} + +if ($missingLabel === []) { + echo color('No PRs are missing changelog labels.', '32', $ansi) . "\n"; + + exit(0); +} + +exit(1); diff --git a/tests/system/AutoReview/CheckPrLabelsTest.php b/tests/system/AutoReview/CheckPrLabelsTest.php new file mode 100644 index 000000000000..b220f6b548eb --- /dev/null +++ b/tests/system/AutoReview/CheckPrLabelsTest.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\AutoReview; + +use Nexus\PHPUnit\Tachycardia\Attribute\TimeLimit; +use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\TestCase; + +/** + * @internal + */ +#[CoversNothing] +#[Group('AutoReview')] +final class CheckPrLabelsTest extends TestCase +{ + public function testUsageErrorWithUnknownArgument(): void + { + exec('php ./admin/check-pr-labels.php foo 2>&1', $output, $exitCode); + + $this->assertSame(1, $exitCode); + $this->assertStringContainsString('Usage:', implode("\n", $output)); + } + + public function testUsageErrorWithInvalidVersion(): void + { + exec('php ./admin/check-pr-labels.php 4.7 2>&1', $output, $exitCode); + + $this->assertSame(1, $exitCode); + $this->assertStringContainsString('Usage:', implode("\n", $output)); + } + + #[TimeLimit(10.0)] + public function testChecksAgainstLatestRelease(): void + { + $this->skipUnlessGhIsAuthenticated(); + + exec('php ./admin/check-pr-labels.php 2>&1', $output, $exitCode); + $outputString = implode("\n", $output); + + $this->assertContains($exitCode, [0, 1], "Script exited with code {$exitCode}. Output: {$outputString}"); + $this->assertMatchesRegularExpression('/Checking PRs merged since v\d+\.\d+\.\d+/', $outputString); + $this->assertMatchesRegularExpression('/Found \d+ merged PRs\./', $outputString); + } + + #[TimeLimit(10.0)] + public function testChecksSinceGivenRelease(): void + { + $this->skipUnlessGhIsAuthenticated(); + + $version = $this->latestChangelogVersion(); + + exec("php ./admin/check-pr-labels.php {$version} 2>&1", $output, $exitCode); + $outputString = implode("\n", $output); + + $this->assertContains($exitCode, [0, 1], "Script exited with code {$exitCode}. Output: {$outputString}"); + $this->assertStringContainsString("Checking PRs merged since v{$version} (", $outputString); + $this->assertMatchesRegularExpression('/Found \d+ merged PRs\./', $outputString); + } + + private function latestChangelogVersion(): string + { + $changelog = (string) file_get_contents('./CHANGELOG.md'); + + if (preg_match('/^## \[v(\d+\.\d+\.\d+)\]/m', $changelog, $matches) !== 1) { + $this->fail('Could not find a version entry in CHANGELOG.md.'); + } + + return $matches[1]; + } + + private function skipUnlessGhIsAuthenticated(): void + { + exec('gh auth status 2>&1', $output, $exitCode); + + if ($exitCode !== 0) { + $this->markTestSkipped( + 'The GitHub CLI is not available or not authenticated. This test is expected ' + . 'to be skipped in CI and runs only where `gh` is authenticated, such as on ' + . 'a maintainer\'s machine.', + ); + } + } +} From cd312f3f000dc2c50cbe876bf016f23ed4f96a53 Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Wed, 8 Jul 2026 21:04:04 +0200 Subject: [PATCH 009/163] refactor(HTTP): split setCURLOptions into category helper methods to reduce complexity (#10339) * refactor(HTTP): split setCURLOptions into category helper methods to reduce complexity * fix: lazy-load CURL_HTTP_VERSION_3 constant only for HTTP/3.0 * fix: replace empty() with strict isset/bool check to satisfy PHPStan --- system/HTTP/CURLRequest.php | 223 ++++++++++++++----- tests/system/HTTP/CURLRequestTest.php | 99 ++++++++ utils/phpstan-baseline/empty.notAllowed.neon | 2 +- 3 files changed, 272 insertions(+), 52 deletions(-) diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php index ac05f9ae903b..76ded55c20a6 100644 --- a/system/HTTP/CURLRequest.php +++ b/system/HTTP/CURLRequest.php @@ -526,21 +526,50 @@ protected function setResponseHeaders(array $headers = []) */ protected function setCURLOptions(array $curlOptions = [], array $config = []) { - // Auth Headers - if (! empty($config['auth'])) { - $curlOptions[CURLOPT_USERPWD] = $config['auth'][0] . ':' . $config['auth'][1]; + $curlOptions = $this->applyAuthOptions($curlOptions, $config); + $curlOptions = $this->applySslOptions($curlOptions, $config); + $curlOptions = $this->applyProxyOptions($curlOptions, $config); + $curlOptions = $this->applyDebugOptions($curlOptions, $config); + $curlOptions = $this->applyRedirectOptions($curlOptions, $config); + $curlOptions = $this->applyConnectionOptions($curlOptions, $config); + $curlOptions = $this->applyBodyOptions($curlOptions, $config); + $curlOptions = $this->applyResponseOptions($curlOptions, $config); + $curlOptions = $this->applyProtocolOptions($curlOptions, $config); + + return $this->applyClientOptions($curlOptions, $config); + } - if (! empty($config['auth'][2]) && strtolower($config['auth'][2]) === 'digest') { - $curlOptions[CURLOPT_HTTPAUTH] = CURLAUTH_DIGEST; - } else { - $curlOptions[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC; - } + /** + * @param array $curlOptions + * @param array $config + * + * @return array + */ + private function applyAuthOptions(array $curlOptions, array $config): array + { + // Auth Headers + if (isset($config['auth']) && is_array($config['auth']) && count($config['auth']) >= 2) { + $curlOptions[CURLOPT_USERPWD] = $config['auth'][0] . ':' . $config['auth'][1]; + $curlOptions[CURLOPT_HTTPAUTH] = (isset($config['auth'][2]) && $config['auth'][2] !== '' && strtolower($config['auth'][2]) === 'digest') + ? CURLAUTH_DIGEST + : CURLAUTH_BASIC; } + return $curlOptions; + } + + /** + * @param array $curlOptions + * @param array $config + * + * @return array + */ + private function applySslOptions(array $curlOptions, array $config): array + { // Certificate - if (! empty($config['cert'])) { - $cert = $config['cert']; + $cert = $config['cert'] ?? null; + if ((bool) $cert) { if (is_array($cert)) { $curlOptions[CURLOPT_SSLCERTPASSWD] = $cert[1]; $cert = $cert[0]; @@ -571,30 +600,51 @@ protected function setCURLOptions(array $curlOptions = [], array $config = []) } } + return $curlOptions; + } + + /** + * @param array $curlOptions + * @param array $config + * + * @return array + */ + private function applyProxyOptions(array $curlOptions, array $config): array + { // Proxy if (isset($config['proxy'])) { $curlOptions[CURLOPT_HTTPPROXYTUNNEL] = true; $curlOptions[CURLOPT_PROXY] = $config['proxy']; } + return $curlOptions; + } + + /** + * @param array $curlOptions + * @param array $config + * + * @return array + */ + private function applyDebugOptions(array $curlOptions, array $config): array + { // Debug - if ($config['debug']) { + if ((bool) ($config['debug'] ?? false)) { $curlOptions[CURLOPT_VERBOSE] = 1; $curlOptions[CURLOPT_STDERR] = is_string($config['debug']) ? fopen($config['debug'], 'a+b') : fopen('php://stderr', 'wb'); } - // Decode Content - if (! empty($config['decode_content'])) { - $accept = $this->getHeaderLine('Accept-Encoding'); - - if ($accept !== '') { - $curlOptions[CURLOPT_ENCODING] = $accept; - } else { - $curlOptions[CURLOPT_ENCODING] = ''; - $curlOptions[CURLOPT_HTTPHEADER] = 'Accept-Encoding'; - } - } + return $curlOptions; + } + /** + * @param array $curlOptions + * @param array $config + * + * @return array + */ + private function applyRedirectOptions(array $curlOptions, array $config): array + { // Allow Redirects if (array_key_exists('allow_redirects', $config)) { $settings = $this->redirectDefaults; @@ -623,6 +673,17 @@ protected function setCURLOptions(array $curlOptions = [], array $config = []) } } + return $curlOptions; + } + + /** + * @param array $curlOptions + * @param array $config + * + * @return array + */ + private function applyConnectionOptions(array $curlOptions, array $config): array + { // DNS Cache Timeout if (isset($config['dns_cache_timeout']) && is_numeric($config['dns_cache_timeout']) && $config['dns_cache_timeout'] >= -1) { $curlOptions[CURLOPT_DNS_CACHE_TIMEOUT] = (int) $config['dns_cache_timeout']; @@ -634,13 +695,33 @@ protected function setCURLOptions(array $curlOptions = [], array $config = []) : true; // Timeout - $curlOptions[CURLOPT_TIMEOUT_MS] = (float) $config['timeout'] * 1000; + $curlOptions[CURLOPT_TIMEOUT_MS] = (float) ($config['timeout'] ?? 0) * 1000; // Connection Timeout - $curlOptions[CURLOPT_CONNECTTIMEOUT_MS] = (float) $config['connect_timeout'] * 1000; + $curlOptions[CURLOPT_CONNECTTIMEOUT_MS] = (float) ($config['connect_timeout'] ?? 150) * 1000; + + // Resolve IP + if (array_key_exists('force_ip_resolve', $config)) { + $curlOptions[CURLOPT_IPRESOLVE] = match ($config['force_ip_resolve']) { + 'v4' => CURL_IPRESOLVE_V4, + 'v6' => CURL_IPRESOLVE_V6, + default => CURL_IPRESOLVE_WHATEVER, + }; + } + + return $curlOptions; + } + /** + * @param array $curlOptions + * @param array $config + * + * @return array + */ + private function applyBodyOptions(array $curlOptions, array $config): array + { // Post Data - application/x-www-form-urlencoded - if (! empty($config['form_params']) && is_array($config['form_params'])) { + if (isset($config['form_params']) && is_array($config['form_params']) && $config['form_params'] !== []) { $postFields = http_build_query($config['form_params']); $curlOptions[CURLOPT_POSTFIELDS] = $postFields; @@ -651,14 +732,11 @@ protected function setCURLOptions(array $curlOptions = [], array $config = []) } // Post Data - multipart/form-data - if (! empty($config['multipart']) && is_array($config['multipart'])) { + if (isset($config['multipart']) && is_array($config['multipart']) && $config['multipart'] !== []) { // setting the POSTFIELDS option automatically sets multipart $curlOptions[CURLOPT_POSTFIELDS] = $config['multipart']; } - // HTTP Errors - $curlOptions[CURLOPT_FAILONERROR] = array_key_exists('http_errors', $config) ? (bool) $config['http_errors'] : true; - // JSON if (isset($config['json'])) { // Will be set as the body in `applyBody()` @@ -668,33 +746,76 @@ protected function setCURLOptions(array $curlOptions = [], array $config = []) $this->setHeader('Content-Length', (string) strlen($json)); } - // Resolve IP - if (array_key_exists('force_ip_resolve', $config)) { - $curlOptions[CURLOPT_IPRESOLVE] = match ($config['force_ip_resolve']) { - 'v4' => CURL_IPRESOLVE_V4, - 'v6' => CURL_IPRESOLVE_V6, - default => CURL_IPRESOLVE_WHATEVER, - }; - } + return $curlOptions; + } - // version - if (! empty($config['version'])) { - $version = sprintf('%.1F', $config['version']); - if ($version === '1.0') { - $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0; - } elseif ($version === '1.1') { - $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1; - } elseif ($version === '2.0') { - $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0; - } elseif ($version === '3.0') { - if (! defined('CURL_HTTP_VERSION_3')) { - define('CURL_HTTP_VERSION_3', 30); - } + /** + * @param array $curlOptions + * @param array $config + * + * @return array + */ + private function applyResponseOptions(array $curlOptions, array $config): array + { + // Decode Content + if ((bool) ($config['decode_content'] ?? false)) { + $accept = $this->getHeaderLine('Accept-Encoding'); - $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_3; + if ($accept !== '') { + $curlOptions[CURLOPT_ENCODING] = $accept; + } else { + $curlOptions[CURLOPT_ENCODING] = ''; + $curlOptions[CURLOPT_HTTPHEADER] = 'Accept-Encoding'; } } + // HTTP Errors + $curlOptions[CURLOPT_FAILONERROR] = array_key_exists('http_errors', $config) ? (bool) $config['http_errors'] : true; + + return $curlOptions; + } + + /** + * @param array $curlOptions + * @param array $config + * + * @return array + */ + private function applyProtocolOptions(array $curlOptions, array $config): array + { + if (! isset($config['version']) || (bool) $config['version'] === false) { + return $curlOptions; + } + + $version = sprintf('%.1F', (float) $config['version']); + + if ($version === '3.0' && ! defined('CURL_HTTP_VERSION_3')) { + define('CURL_HTTP_VERSION_3', 30); + } + + $curlVersion = match ($version) { + '1.0' => CURL_HTTP_VERSION_1_0, + '1.1' => CURL_HTTP_VERSION_1_1, + '2.0' => CURL_HTTP_VERSION_2_0, + '3.0' => CURL_HTTP_VERSION_3, + default => null, + }; + + if ($curlVersion !== null) { + $curlOptions[CURLOPT_HTTP_VERSION] = $curlVersion; + } + + return $curlOptions; + } + + /** + * @param array $curlOptions + * @param array $config + * + * @return array + */ + private function applyClientOptions(array $curlOptions, array $config): array + { // Cookie if (isset($config['cookie'])) { $curlOptions[CURLOPT_COOKIEJAR] = $config['cookie']; diff --git a/tests/system/HTTP/CURLRequestTest.php b/tests/system/HTTP/CURLRequestTest.php index 9fac71493b39..54cadb91bf9c 100644 --- a/tests/system/HTTP/CURLRequestTest.php +++ b/tests/system/HTTP/CURLRequestTest.php @@ -1566,4 +1566,103 @@ public function testProxyAndContinueResponses(): void $this->assertSame($testBody, $response->getBody()); } + + public function testSetCURLOptions(): void + { + $invoker = self::getPrivateMethodInvoker($this->request, 'setCURLOptions'); + + // Test Auth Options + $options = $invoker([], ['auth' => ['user', 'pass', 'digest']]); + $this->assertSame('user:pass', $options[CURLOPT_USERPWD]); + $this->assertSame(CURLAUTH_DIGEST, $options[CURLOPT_HTTPAUTH]); + + $options2 = $invoker([], ['auth' => ['user', 'pass', 'basic']]); + $this->assertSame(CURLAUTH_BASIC, $options2[CURLOPT_HTTPAUTH]); + + // Test SSL Options + $options3 = $invoker([], ['cert' => __FILE__]); + $this->assertSame(__FILE__, $options3[CURLOPT_SSLCERT]); + + $options4 = $invoker([], ['verify' => false]); + $this->assertFalse($options4[CURLOPT_SSL_VERIFYPEER]); + $this->assertSame(0, $options4[CURLOPT_SSL_VERIFYHOST]); + + // Test Proxy Options + $options5 = $invoker([], ['proxy' => 'http://proxy.example.com']); + $this->assertTrue($options5[CURLOPT_HTTPPROXYTUNNEL]); + $this->assertSame('http://proxy.example.com', $options5[CURLOPT_PROXY]); + + // Test Debug Options + $options6 = $invoker([], ['debug' => true]); + $this->assertSame(1, $options6[CURLOPT_VERBOSE]); + $this->assertIsResource($options6[CURLOPT_STDERR]); + + // Test Redirect Options + $options7 = $invoker([], ['allow_redirects' => false]); + $this->assertSame(0, $options7[CURLOPT_FOLLOWLOCATION]); + + $options8 = $invoker([], ['allow_redirects' => true]); + $this->assertSame(1, $options8[CURLOPT_FOLLOWLOCATION]); + $this->assertSame(5, $options8[CURLOPT_MAXREDIRS]); + + // Test Connection Options + $options9 = $invoker([], [ + 'dns_cache_timeout' => 120, + 'fresh_connect' => false, + 'timeout' => 10, + 'connect_timeout' => 5, + 'force_ip_resolve' => 'v4', + ]); + $this->assertSame(120, $options9[CURLOPT_DNS_CACHE_TIMEOUT]); + $this->assertFalse($options9[CURLOPT_FRESH_CONNECT]); + $this->assertEqualsWithDelta(10000.0, $options9[CURLOPT_TIMEOUT_MS], PHP_FLOAT_EPSILON); + $this->assertEqualsWithDelta(5000.0, $options9[CURLOPT_CONNECTTIMEOUT_MS], PHP_FLOAT_EPSILON); + $this->assertSame(CURL_IPRESOLVE_V4, $options9[CURLOPT_IPRESOLVE]); + + // Test Body Options (form_params / multipart / json) + $options10 = $invoker([], ['form_params' => ['foo' => 'bar']]); + $this->assertSame('foo=bar', $options10[CURLOPT_POSTFIELDS]); + + $options11 = $invoker([], ['multipart' => ['file' => 'data']]); + $this->assertSame(['file' => 'data'], $options11[CURLOPT_POSTFIELDS]); + + // Test Response Options (decode_content / http_errors) + $options12 = $invoker([], ['decode_content' => true]); + $this->assertSame('', $options12[CURLOPT_ENCODING]); + $this->assertSame('Accept-Encoding', $options12[CURLOPT_HTTPHEADER]); + + // Test Protocol/Misc/Client Options + $options13 = $invoker([], [ + 'http_errors' => false, + 'version' => '2.0', + 'cookie' => 'cookies.txt', + 'user_agent' => 'TestAgent', + ]); + $this->assertFalse($options13[CURLOPT_FAILONERROR]); + $this->assertSame(CURL_HTTP_VERSION_2_0, $options13[CURLOPT_HTTP_VERSION]); + $this->assertSame('cookies.txt', $options13[CURLOPT_COOKIEJAR]); + $this->assertSame('cookies.txt', $options13[CURLOPT_COOKIEFILE]); + $this->assertSame('TestAgent', $options13[CURLOPT_USERAGENT]); + } + + public function testCURLOptionsPreservesIntegerKeys(): void + { + // cURL options use integer constants as keys. This test ensures they are not re-indexed. + $request = $this->getRequest(); + $method = self::getPrivateMethodInvoker($request, 'setCURLOptions'); + + $initialOptions = [ + CURLOPT_RETURNTRANSFER => true, + ]; + + $config = [ + 'auth' => ['user', 'pass'], + ]; + + $options = $method($initialOptions, $config); + + // Verify keys are preserved and not re-indexed to 0, 1... + $this->assertArrayHasKey(CURLOPT_RETURNTRANSFER, $options); + $this->assertArrayHasKey(CURLOPT_USERPWD, $options); + } } diff --git a/utils/phpstan-baseline/empty.notAllowed.neon b/utils/phpstan-baseline/empty.notAllowed.neon index 3172047e0c1e..827126f98676 100644 --- a/utils/phpstan-baseline/empty.notAllowed.neon +++ b/utils/phpstan-baseline/empty.notAllowed.neon @@ -209,7 +209,7 @@ parameters: - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 10 + count: 3 path: ../../system/HTTP/CURLRequest.php - From 76b040adc9c937d92e8579725b8674f7575d4f25 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Thu, 9 Jul 2026 03:29:50 +0800 Subject: [PATCH 010/163] chore: automate "Config" and "All Changes" sections of the upgrade guide (#10392) --- admin/README.md | 6 +- admin/RELEASE.md | 13 +- admin/update-upgrade-guide.php | 197 ++++++++++++++++++ .../AutoReview/UpdateUpgradeGuideTest.php | 113 ++++++++++ 4 files changed, 320 insertions(+), 9 deletions(-) create mode 100644 admin/update-upgrade-guide.php create mode 100644 tests/system/AutoReview/UpdateUpgradeGuideTest.php diff --git a/admin/README.md b/admin/README.md index 46973c1f71ce..958004b41967 100644 --- a/admin/README.md +++ b/admin/README.md @@ -48,7 +48,7 @@ are used as part of that process: - **generate-changelog.php** prepends the CHANGELOG.md entry for a new release, built from GitHub's auto-generated release notes and the - SECURITY section of the version's detailed changelog. + SECURITY section of the version's detailed changelog. Usage: `php admin/generate-changelog.php 4.x.x [--dry-run]` - **prepare-release.php** creates the `release-4.x.x` branch and updates version references in the framework source, the user guide, and the @@ -61,6 +61,10 @@ are used as part of that process: latest) that appear to be missing the labels used to generate the changelog. Usage: `php admin/check-pr-labels.php []` +- **update-upgrade-guide.php** fills the "Config" and "All Changes" sections + of the version's upgrade guide with the project space files changed since + the last release. + Usage: `php admin/update-upgrade-guide.php [--dry-run]` ## Other Stuff diff --git a/admin/RELEASE.md b/admin/RELEASE.md index 405e9af10a6a..89f5a4f33e86 100644 --- a/admin/RELEASE.md +++ b/admin/RELEASE.md @@ -114,14 +114,11 @@ existing content. * [ ] Update **user_guide_src/source/changelogs/v4.x.x.rst** * Remove the section titles that have no items * [ ] Update **user_guide_src/source/installation/upgrade_4xx.rst** - * [ ] fill in the "All Changes" section using the following command, and add it to **upgrade_4xx.rst**: - ``` - git diff --name-status upstream/master -- . ':!.github/' ':!admin/' ':!changelogs/' ':!contributing/' \ - ':!system/' ':!tests/' ':!user_guide_src/' ':!utils/' \ - ':!*.json' ':!*.xml' ':!*.dist' ':!rector.php' ':!structarmed.php' \ - ':!phpstan*' ':!psalm*' ':!.php-cs-fixer.*' ':!LICENSE' ':!CHANGELOG.md' - ``` - * Note: `tests/` is not used for distribution repos. See `admin/starter/tests/`. + * [ ] Run `php admin/update-upgrade-guide.php 4.x.x` to fill in the "Config" and + "All Changes" sections with the project space files changed since the last release. + * The "Config" section is not modified if it already has content. The script + prints any missing entries to merge manually. + * Add notes to the "Config" entries as needed. * [ ] Remove the section titles that have no items * [ ] [Minor version only] Update the "from" version in the title, (e.g., `from 4.3.x` → `from 4.3.8`). * [ ] Run `php admin/prepare-release.php 4.x.x` and push to origin. diff --git a/admin/update-upgrade-guide.php b/admin/update-upgrade-guide.php new file mode 100644 index 000000000000..c80f0a75b635 --- /dev/null +++ b/admin/update-upgrade-guide.php @@ -0,0 +1,197 @@ + [--dry-run] + */ +function previous_release_tag(string $version): ?string +{ + // Not `git describe`: the base must be the highest stable tag below the + // given version regardless of the checked out branch, and prerelease + // tags (e.g. v4.0.0-rc.4) must be excluded. + exec("git tag -l 'v*' 2>&1", $tags, $exitCode); + + if ($exitCode !== 0) { + return null; + } + + $tags = array_filter( + $tags, + static fn (string $tag): bool => preg_match('/\Av\d+\.\d+\.\d+\z/', $tag) === 1 + && version_compare($tag, "v{$version}", '<'), + ); + + if ($tags === []) { + return null; + } + + usort($tags, version_compare(...)); + + return end($tags); +} + +/** + * @param list $items + */ +function bullets(array $items): string +{ + return implode("\n", array_map(static fn (string $item): string => "- {$item}", $items)); +} + +chdir(__DIR__ . '/..'); + +$args = array_slice($argv, 1); +$options = array_values(array_filter($args, static fn (string $arg): bool => str_starts_with($arg, '--'))); +$params = array_values(array_diff($args, $options)); + +if (count($params) !== 1 || preg_match('/\A\d+\.\d+\.\d+\z/', $params[0]) !== 1) { + echo sprintf("Usage: php %s [--dry-run]\n", $argv[0]); + echo sprintf("E.g.,: php %s 4.7.5\n", $argv[0]); + + exit(1); +} + +$version = $params[0]; +$dryRun = in_array('--dry-run', $options, true); + +$upgradePath = sprintf('./user_guide_src/source/installation/upgrade_%s.rst', str_replace('.', '', $version)); + +if (! is_file($upgradePath)) { + echo sprintf("%s is not found. Run \"php admin/create-new-changelog.php\" first.\n", $upgradePath); + + exit(1); +} + +$baseTag = previous_release_tag($version); + +if ($baseTag === null) { + echo "Could not determine the previous release tag. Are the tags fetched?\n"; + + exit(1); +} + +echo sprintf("Checking the project space files changed since %s.\n", $baseTag); + +// Paths that are not part of the project space. Keep in sync with the +// distribution repos: `tests/` is not used there, see "admin/starter/tests". +$excludes = [ + ':!.github/', ':!admin/', ':!changelogs/', ':!contributing/', + ':!system/', ':!tests/', ':!user_guide_src/', ':!utils/', + ':!*.json', ':!*.xml', ':!*.dist', ':!rector.php', ':!structarmed.php', + ':!phpstan*', ':!psalm*', ':!.php-cs-fixer.*', ':!LICENSE', ':!CHANGELOG.md', +]; + +$command = sprintf( + 'git diff --name-status %s -- . %s 2>&1', + escapeshellarg($baseTag), + implode(' ', array_map(escapeshellarg(...), $excludes)), +); +exec($command, $diffOutput, $exitCode); + +if ($exitCode !== 0) { + echo "Failed to diff against the latest release tag:\n"; + echo implode("\n", $diffOutput) . "\n"; + + exit(1); +} + +$allChanges = []; +$configChanged = []; +$configNew = []; + +foreach ($diffOutput as $line) { + $fields = preg_split('/\t/', $line); + + if ($fields === false || count($fields) < 2) { + continue; + } + + $status = $fields[0][0]; + $path = end($fields); + + $allChanges[] = $status === 'D' ? "{$path} (deleted)" : $path; + + if (! str_starts_with($path, 'app/Config/')) { + continue; + } + + if ($status === 'A') { + $configNew[] = $path; + } elseif ($status !== 'D') { + $configChanged[] = $path; + } +} + +sort($allChanges); +sort($configChanged); +sort($configNew); + +// Builds the "Config" section contents. +$configContent = bullets($configChanged); + +if ($configNew !== []) { + $configContent .= ($configContent === '' ? '' : "\n\n") . "These files are new in this release:\n\n" . bullets($configNew); +} + +if ($configContent === '') { + $configContent = '- No config files were changed in this release.'; +} + +// Builds the "All Changes" section contents. +$allChangesContent = $allChanges === [] + ? '- No project files were changed in this release.' + : bullets($allChanges); + +if ($dryRun) { + echo "\nConfig\n------\n\n{$configContent}\n"; + echo "\nAll Changes\n===========\n\n{$allChangesContent}\n"; + + exit(0); +} + +$rst = file_get_contents($upgradePath); + +// Fills the "Config" section, only if it still has the placeholder. Entries +// may have been added ahead of time for changes that need discussion, and +// those must not be overwritten. +if (preg_match('/^Config\n------\n\n(.*?)(?=^[^\s-][^\n]*\n(?:-+|=+)$|\z)/msu', $rst, $matches) !== 1) { + echo "Could not find the \"Config\" section in {$upgradePath}. Update it manually.\n"; +} elseif (trim($matches[1]) === '- @TODO') { + $rst = str_replace("Config\n------\n\n{$matches[1]}", "Config\n------\n\n{$configContent}\n\n", $rst); + + echo "The \"Config\" section was updated. Add notes to each entry as needed.\n"; +} else { + $mentioned = []; + + if (preg_match_all('~app/Config/[\w./-]+~u', $matches[1], $found) > 0) { + $mentioned = $found[0]; + } + + $missing = array_diff([...$configChanged, ...$configNew], $mentioned); + + if ($missing === []) { + echo "The \"Config\" section already has content and mentions all changed config files. It was not modified.\n"; + } else { + echo "The \"Config\" section already has content, so it was not modified.\n"; + echo "Merge these missing entries manually:\n" . bullets(array_values($missing)) . "\n"; + } +} + +// Fills the "All Changes" section, replacing any previous list. +$pattern = '/^(All Changes\n===========\n\n.*?:\n\n)(.*?)(?=^\*+$|\z)/msu'; +$updated = preg_replace($pattern, "\$1{$allChangesContent}\n", $rst, 1, $count); + +if ($count !== 1) { + echo "Could not find the \"All Changes\" section in {$upgradePath}. Update it manually.\n"; + + exit(1); +} + +file_put_contents($upgradePath, $updated); + +echo sprintf("The \"All Changes\" section of %s was updated.\n", $upgradePath); +echo "Remember to remove the section titles that have no items.\n"; diff --git a/tests/system/AutoReview/UpdateUpgradeGuideTest.php b/tests/system/AutoReview/UpdateUpgradeGuideTest.php new file mode 100644 index 000000000000..f982ac375dfa --- /dev/null +++ b/tests/system/AutoReview/UpdateUpgradeGuideTest.php @@ -0,0 +1,113 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\AutoReview; + +use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\TestCase; + +/** + * @internal + */ +#[CoversNothing] +#[Group('AutoReview')] +final class UpdateUpgradeGuideTest extends TestCase +{ + private string $nextVersion; + private string $upgradePath; + + protected function setUp(): void + { + parent::setUp(); + + exec('git describe --tags --abbrev=0 2>&1', $output, $exitCode); + + if ($exitCode !== 0) { + $this->markTestSkipped(sprintf( + "Unable to get the latest git tag.\nOutput: %s", + implode("\n", $output), + )); + } + + $parts = explode('.', trim($output[0], 'v')); + + $this->nextVersion = sprintf('%d.%d.%d', $parts[0], $parts[1], ++$parts[2]); + $this->upgradePath = sprintf( + './user_guide_src/source/installation/upgrade_%s.rst', + str_replace('.', '', $this->nextVersion), + ); + } + + public function testUsageErrorWithoutArguments(): void + { + exec('php ./admin/update-upgrade-guide.php 2>&1', $output, $exitCode); + + $this->assertSame(1, $exitCode); + $this->assertStringContainsString('Usage:', implode("\n", $output)); + } + + public function testUsageErrorWithInvalidVersion(): void + { + exec('php ./admin/update-upgrade-guide.php 4.7 2>&1', $output, $exitCode); + + $this->assertSame(1, $exitCode); + $this->assertStringContainsString('Usage:', implode("\n", $output)); + } + + public function testErrorWhenUpgradeGuideIsMissing(): void + { + exec('php ./admin/update-upgrade-guide.php 9.9.8 2>&1', $output, $exitCode); + + $this->assertSame(1, $exitCode); + $this->assertStringContainsString('is not found', implode("\n", $output)); + } + + public function testDryRunPrintsSectionsWithoutWriting(): void + { + $this->assertFileExists($this->upgradePath); + + exec(sprintf('php ./admin/update-upgrade-guide.php %s --dry-run 2>&1', $this->nextVersion), $output, $exitCode); + $outputString = implode("\n", $output); + + $this->assertSame(0, $exitCode, "Script exited with code {$exitCode}. Output: {$outputString}"); + $this->assertMatchesRegularExpression('/Checking the project space files changed since v\d+\.\d+\.\d+\./', $outputString); + $this->assertStringContainsString('All Changes', $outputString); + $this->assertSame('', trim((string) exec("git status --porcelain -- {$this->upgradePath}"))); + } + + public function testUpdatesUpgradeGuide(): void + { + $this->assertFileExists($this->upgradePath); + + if (trim((string) exec("git status --porcelain -- {$this->upgradePath}")) !== '') { + $this->markTestSkipped('You have uncommitted changes to the upgrade guide that will be erased by this test.'); + } + + try { + exec(sprintf('php ./admin/update-upgrade-guide.php %s 2>&1', $this->nextVersion), $output, $exitCode); + $outputString = implode("\n", $output); + + $this->assertSame(0, $exitCode, "Script exited with code {$exitCode}. Output: {$outputString}"); + $this->assertStringContainsString('"All Changes" section', $outputString); + + $contents = (string) file_get_contents($this->upgradePath); + $allChanges = substr($contents, (int) strpos($contents, "All Changes\n")); + + $this->assertStringNotContainsString('- @TODO', $contents); + $this->assertMatchesRegularExpression('/^- \S/m', $allChanges); + } finally { + exec("git restore -- {$this->upgradePath}"); + } + } +} From dca192ec94c588da403a12f73a43a33421933561 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Thu, 9 Jul 2026 03:31:48 +0800 Subject: [PATCH 011/163] chore: add "Verify Release" workflow to automate post-release checks (#10393) --- .github/workflows/verify-release.yml | 185 +++++++++++++++++++++++++++ admin/RELEASE.md | 26 ++-- 2 files changed, 195 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/verify-release.yml diff --git a/.github/workflows/verify-release.yml b/.github/workflows/verify-release.yml new file mode 100644 index 000000000000..0488b76a1a70 --- /dev/null +++ b/.github/workflows/verify-release.yml @@ -0,0 +1,185 @@ +# When a release is published, verify that the release pipeline delivered +# everything: the distributable repos, the new framework version on +# Packagist, and the user guide with its ePub. +# +# Use "Run workflow" with the release tag to verify an existing release. +name: Verify Release + +on: + release: + types: [published] + workflow_dispatch: + inputs: + version: + description: The release tag to verify (e.g., v4.7.5) + type: string + required: true + +permissions: + contents: read + +env: + TAG: ${{ github.event.release.tag_name || inputs.version }} + PUBLISHED_AT: ${{ github.event.release.published_at }} + +jobs: + wait-for-deploy: + name: Wait for distributables deploy + if: github.repository == 'codeigniter4/CodeIgniter4' + runs-on: ubuntu-24.04 + timeout-minutes: 30 + permissions: + actions: read + contents: read + + steps: + - name: Wait for "Deploy Distributable Repos" to succeed + env: + GH_TOKEN: ${{ github.token }} + run: | + # On workflow_dispatch the event carries no publish time. + if [ -z "${PUBLISHED_AT}" ]; then + PUBLISHED_AT=$(gh api "repos/${{ github.repository }}/releases/tags/${TAG}" --jq '.published_at') + echo "Resolved published_at=${PUBLISHED_AT} for ${TAG}." + fi + + query="repos/${{ github.repository }}/actions/workflows/deploy-distributables.yml/runs?event=release&per_page=1&created=>=${PUBLISHED_AT}" + + while true; do + run=$(gh api "${query}" --jq '.workflow_runs[0] | "\(.status) \(.conclusion)"' || true) + status=${run% *} + conclusion=${run#* } + echo "Deploy Distributable Repos: status=${status:-not found} conclusion=${conclusion:-n/a}" + + if [ "${status}" = "completed" ]; then + if [ "${conclusion}" = "success" ]; then + exit 0 + fi + + echo "Deploy Distributable Repos did not succeed." + exit 1 + fi + + sleep 30 + done + + appstarter: + name: Install and test appstarter + needs: wait-for-deploy + runs-on: ubuntu-24.04 + timeout-minutes: 45 + + steps: + - name: Setup PHP + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 + with: + php-version: '8.2' + tools: composer + extensions: intl, gd + coverage: none + + - name: Wait for Packagist to serve the new framework version + run: | + version="${TAG#v}" + + for i in $(seq 1 30); do + if curl -fsSL https://repo.packagist.org/p2/codeigniter4/framework.json \ + | jq -e --arg version "v${version}" \ + '.packages."codeigniter4/framework" | map(select(.version == $version)) | length > 0' > /dev/null; then + echo "Packagist serves codeigniter4/framework v${version}." + exit 0 + fi + + echo "Attempt ${i}: v${version} is not yet on Packagist." + sleep 60 + done + + echo "Timed out waiting for Packagist to serve v${version}." + exit 1 + + - name: Install appstarter + run: composer create-project codeigniter4/appstarter release-test --no-interaction + + - name: Verify the framework version + working-directory: release-test + run: | + version="${TAG#v}" + installed=$(composer show codeigniter4/framework --format=json | jq -r '.versions[0]') + echo "Installed codeigniter4/framework: ${installed}" + + if [ "${installed}" != "v${version}" ]; then + echo "Expected v${version}." + exit 1 + fi + + - name: Test appstarter + working-directory: release-test + run: composer test + + userguide: + name: Verify user guide deployment + needs: wait-for-deploy + runs-on: ubuntu-24.04 + timeout-minutes: 30 + + steps: + - name: Check that the ePub was added + env: + GH_TOKEN: ${{ github.token }} + run: | + version="${TAG#v}" + + for i in $(seq 1 30); do + if gh api "repos/codeigniter4/userguide/contents/CodeIgniter${version}.epub" --jq '.name' > /dev/null 2>&1; then + echo "CodeIgniter${version}.epub is in the userguide repo." + exit 0 + fi + + echo "Attempt ${i}: CodeIgniter${version}.epub is not yet in the userguide repo." + sleep 30 + done + + echo "Timed out waiting for CodeIgniter${version}.epub." + exit 1 + + - name: Check that the user guide workflows succeeded + env: + GH_TOKEN: ${{ github.token }} + run: | + # On workflow_dispatch the event carries no publish time. + if [ -z "${PUBLISHED_AT}" ]; then + PUBLISHED_AT=$(gh api "repos/${{ github.repository }}/releases/tags/${TAG}" --jq '.published_at') + echo "Resolved published_at=${PUBLISHED_AT} for ${TAG}." + fi + + for workflow in "Deploy Production" "pages build and deployment"; do + query="repos/codeigniter4/userguide/actions/runs?per_page=30&created=>=${PUBLISHED_AT}" + + success="" + + for _ in $(seq 1 30); do + run=$(gh api "${query}" \ + | jq -r --arg name "${workflow}" \ + '[.workflow_runs[] | select(.name == $name)][0] | "\(.status) \(.conclusion)"') + status=${run% *} + conclusion=${run#* } + echo "${workflow}: status=${status:-not found} conclusion=${conclusion:-n/a}" + + if [ "${status}" = "completed" ]; then + if [ "${conclusion}" = "success" ]; then + success=1 + break + fi + + echo "\"${workflow}\" did not succeed." + exit 1 + fi + + sleep 30 + done + + if [ -z "${success}" ]; then + echo "Timed out waiting for \"${workflow}\"." + exit 1 + fi + done diff --git a/admin/RELEASE.md b/admin/RELEASE.md index 89f5a4f33e86..8fa117e4f8a4 100644 --- a/admin/RELEASE.md +++ b/admin/RELEASE.md @@ -171,22 +171,16 @@ existing content. **Full Changelog**: https://github.com/codeigniter4/CodeIgniter4/compare/v4.x.w...v4.x.x ``` Click the "Generate release notes" button, and get the "New Contributors". -* [ ] Watch for the "Deploy Distributable Repos" action to make sure **framework**, - **appstarter**, and **userguide** get updated -* [ ] Run the following commands to install and test `appstarter` and verify the new - version: - ```console - rm -rf release-test - composer create-project codeigniter4/appstarter release-test - cd release-test - composer test && composer info codeigniter4/framework - ``` -* [ ] Verify that the user guide actions succeeded: - * [ ] "[Deploy Distributable Repos](https://github.com/codeigniter4/CodeIgniter4/actions/workflows/deploy-distributables.yml)", the main repo - * [ ] "[Deploy Production](https://github.com/codeigniter4/userguide/actions/workflows/deploy.yml)", UG repo - * [ ] "[pages-build-deployment](https://github.com/codeigniter4/userguide/actions/workflows/pages/pages-build-deployment)", UG repo - * [ ] Check if "CodeIgniter4.x.x.epub" is added to UG repo. "CodeIgniter.epub" was - created when v4.3.8 was released. +* [ ] Watch the "[Verify Release](https://github.com/codeigniter4/CodeIgniter4/actions/workflows/verify-release.yml)" + workflow and make sure it passes. It does the following: + * Wait for the "[Deploy Distributable Repos](https://github.com/codeigniter4/CodeIgniter4/actions/workflows/deploy-distributables.yml)" + action to update **framework**, **appstarter**, and **userguide** + * Install `appstarter` from Packagist, verify it pulls framework `v4.x.x`, + and run its tests + * Verify that the user guide deployments succeeded ("Deploy Production" and + "pages build and deployment" in the UG repo) and that **CodeIgniter4.x.x.epub** + was added + * If it fails, fix the cause and re-run it via "Run workflow" with the tag `v4.x.x`. * [ ] Fast-forward `develop` branch to catch the merge commit from `master` ```console git fetch upstream From f9c71a7f8c34859008c426a507f40dbc883529f5 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Thu, 9 Jul 2026 04:00:04 +0800 Subject: [PATCH 012/163] chore: add script to sync release branches (#10394) --- admin/README.md | 4 + admin/RELEASE.md | 27 ++---- admin/sync-release-branches.php | 96 +++++++++++++++++++ .../AutoReview/SyncReleaseBranchesTest.php | 57 +++++++++++ 4 files changed, 164 insertions(+), 20 deletions(-) create mode 100644 admin/sync-release-branches.php create mode 100644 tests/system/AutoReview/SyncReleaseBranchesTest.php diff --git a/admin/README.md b/admin/README.md index 958004b41967..a4048bf59152 100644 --- a/admin/README.md +++ b/admin/README.md @@ -65,6 +65,10 @@ are used as part of that process: of the version's upgrade guide with the project space files changed since the last release. Usage: `php admin/update-upgrade-guide.php [--dry-run]` +- **sync-release-branches.php** checks out the target branch and merges + upstream and the source branch into it, as done at several points of the + release process. Without `--push`, the result is left unpushed for review. + Usage: `php admin/sync-release-branches.php [--push]` ## Other Stuff diff --git a/admin/RELEASE.md b/admin/RELEASE.md index 8fa117e4f8a4..75f2a0fbf35a 100644 --- a/admin/RELEASE.md +++ b/admin/RELEASE.md @@ -1,11 +1,10 @@ # Release Process -> Documentation guide based on the releases of `4.0.5` and `4.1.0` on January 31, 2021. +> Documentation guide based on the releases of `4.0.5` and `4.1.0` on January 31, 2021 (MGatner) > -> Updated for `4.5.0` on April 7, 2024. -> Updated for `4.6.0` on January 19, 2025. -> -> -MGatner, kenjis +> Updated for `4.5.0` on April 7, 2024 (kenjis) +> Updated for `4.6.0` on January 19, 2025 (paulbalandan) +> Updated for `4.7.5` on July 8, 2026 (paulbalandan) ## Notation @@ -24,11 +23,7 @@ merging will take time. * [ ] Merge `develop` into `4.y`: ```console - git fetch upstream - git switch 4.y - git merge upstream/4.y - git merge upstream/develop - git push upstream HEAD + php admin/sync-release-branches.php 4.y develop --push ``` ## [Minor version only] Merge minor version branch into `develop` @@ -183,19 +178,11 @@ existing content. * If it fails, fix the cause and re-run it via "Run workflow" with the tag `v4.x.x`. * [ ] Fast-forward `develop` branch to catch the merge commit from `master` ```console - git fetch upstream - git checkout develop - git merge upstream/develop - git merge upstream/master - git push upstream HEAD + php admin/sync-release-branches.php develop master --push ``` * [ ] Update the next minor version branch `4.y`: ```console - git fetch upstream - git switch 4.y - git merge upstream/4.y - git merge upstream/develop - git push upstream HEAD + php admin/sync-release-branches.php 4.y develop --push ``` * [ ] [Minor version only] Create the new next minor version branch `4.z`: ```console diff --git a/admin/sync-release-branches.php b/admin/sync-release-branches.php new file mode 100644 index 000000000000..4f82a424bc65 --- /dev/null +++ b/admin/sync-release-branches.php @@ -0,0 +1,96 @@ + [--push] + */ +function run_command(string $command): void +{ + echo sprintf("$ %s\n", $command); + system($command, $exitCode); + + if ($exitCode !== 0) { + exit($exitCode); + } +} + +function merge_branch(string $ref): void +{ + echo sprintf("$ git merge %s\n", $ref); + system(sprintf('git merge %s', escapeshellarg($ref)), $exitCode); + + if ($exitCode !== 0) { + echo sprintf("\nMerging %s failed. Resolve the conflicts and run \"git merge --continue\",\n", $ref); + echo "then push with \"git push upstream HEAD\".\n"; + + exit(1); + } +} + +chdir(__DIR__ . '/..'); + +$args = array_slice($argv, 1); +$options = array_values(array_filter($args, static fn (string $arg): bool => str_starts_with($arg, '--'))); +$params = array_values(array_diff($args, $options)); + +$isBranch = static fn (string $name): bool => preg_match('/\A(?:develop|master|\d+\.\d+)\z/', $name) === 1; + +if ( + count($params) !== 2 + || ! $isBranch($params[0]) + || ! $isBranch($params[1]) + || $params[0] === $params[1] + || array_diff($options, ['--push']) !== [] +) { + echo sprintf("Usage: php %s [--push]\n", $argv[0]); + echo sprintf("E.g.,: php %s 4.8 develop --push # merges develop into 4.8\n", $argv[0]); + echo sprintf(" php %s develop master --push # merges master into develop\n", $argv[0]); + echo "Checks out , merges upstream/ and then upstream/ into it.\n"; + echo "Without --push, the result is left unpushed for review.\n"; + + exit(1); +} + +[$target, $source] = $params; +$push = in_array('--push', $options, true); + +exec('git remote get-url upstream 2>&1', $remoteOutput, $exitCode); + +if ($exitCode !== 0) { + echo "The \"upstream\" remote is not configured.\n"; + + exit(1); +} + +exec('git status --porcelain 2>&1', $statusOutput, $exitCode); + +if ($exitCode !== 0 || $statusOutput !== []) { + echo "The working tree is not clean. Commit or stash your changes first.\n"; + + exit(1); +} + +run_command('git fetch upstream'); + +exec(sprintf('git rev-parse --verify --quiet refs/heads/%s 2>&1', escapeshellarg($target)), $verifyOutput, $exitCode); + +if ($exitCode === 0) { + run_command(sprintf('git switch %s', escapeshellarg($target))); +} else { + run_command(sprintf('git switch -c %s upstream/%s', escapeshellarg($target), escapeshellarg($target))); +} + +merge_branch("upstream/{$target}"); +merge_branch("upstream/{$source}"); + +if ($push) { + run_command('git push upstream HEAD'); + + echo sprintf("Merged upstream/%s into %s and pushed to upstream.\n", $source, $target); +} else { + echo sprintf("Merged upstream/%s into %s. Review the result, then run \"git push upstream HEAD\".\n", $source, $target); +} diff --git a/tests/system/AutoReview/SyncReleaseBranchesTest.php b/tests/system/AutoReview/SyncReleaseBranchesTest.php new file mode 100644 index 000000000000..26d90c4b1592 --- /dev/null +++ b/tests/system/AutoReview/SyncReleaseBranchesTest.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\AutoReview; + +use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\TestCase; + +/** + * The successful path switches branches and merges, so only the argument + * validation is exercised here. + * + * @internal + */ +#[CoversNothing] +#[Group('AutoReview')] +final class SyncReleaseBranchesTest extends TestCase +{ + #[DataProvider('provideUsageErrors')] + public function testUsageErrors(string $arguments): void + { + exec(sprintf('php ./admin/sync-release-branches.php %s 2>&1', $arguments), $output, $exitCode); + + $this->assertSame(1, $exitCode); + $this->assertStringContainsString('Usage:', implode("\n", $output)); + } + + /** + * @return iterable + */ + public static function provideUsageErrors(): iterable + { + yield 'no arguments' => ['']; + + yield 'missing source' => ['4.8']; + + yield 'invalid branch' => ['release-4.8.0 develop']; + + yield 'invalid source' => ['4.8 upstream/develop']; + + yield 'same branch and source' => ['develop develop']; + + yield 'unknown option' => ['4.8 develop --force']; + } +} From 8b8c9e4ddef2b71afbf2a3731d24a202f9550b7e Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Thu, 9 Jul 2026 17:52:57 +0800 Subject: [PATCH 013/163] refactor: use `array-key` as the benevolent union of `int|string` in array shapes (#10396) --- system/API/TransformerInterface.php | 2 +- system/BaseModel.php | 4 ++-- system/CLI/BaseCommand.php | 4 ++-- system/CLI/CLI.php | 24 +++++++++---------- system/CLI/Commands.php | 2 +- system/CLI/Console.php | 4 ++-- system/CLI/GeneratorTrait.php | 10 ++++---- system/Commands/Database/MigrateRefresh.php | 2 +- system/Commands/Database/MigrateStatus.php | 2 -- system/Commands/Encryption/GenerateKey.php | 4 ++-- system/Commands/Utilities/FilterCheck.php | 2 +- system/Common.php | 14 +++++------ system/Config/BaseConfig.php | 2 +- system/Database/BaseBuilder.php | 2 +- system/Database/BaseConnection.php | 2 +- system/Database/ConnectionInterface.php | 2 +- system/Database/OCI8/Builder.php | 2 +- system/Database/Postgre/Builder.php | 2 +- system/Database/SQLSRV/Builder.php | 2 +- system/Encryption/Handlers/BaseHandler.php | 2 +- system/Encryption/KeyRotationDecorator.php | 2 +- system/Entity/Entity.php | 2 +- system/HTTP/Header.php | 10 ++++---- system/HTTP/IncomingRequest.php | 6 ++--- system/I18n/TimeTrait.php | 2 +- system/Model.php | 4 ++-- system/Test/CIUnitTestCase.php | 4 ++-- system/Test/DatabaseTestTrait.php | 10 ++++---- system/Test/FeatureTestTrait.php | 4 ++-- system/Test/Mock/MockConnection.php | 2 +- system/View/Parser.php | 2 +- system/View/Plugins.php | 2 +- tests/system/HTTP/HeaderTest.php | 6 ++--- .../system/Validation/CreditCardRulesTest.php | 2 +- tests/system/Validation/FileRulesTest.php | 2 +- tests/system/Validation/FormatRulesTest.php | 2 +- tests/system/Validation/RulesTest.php | 2 +- .../StrictRules/CreditCardRulesTest.php | 2 +- .../Validation/StrictRules/FileRulesTest.php | 2 +- .../StrictRules/FormatRulesTest.php | 2 +- .../Validation/StrictRules/RulesTest.php | 2 +- .../Validation/StrictRules/ValidationTest.php | 2 +- tests/system/Validation/ValidationTest.php | 2 +- utils/phpstan-baseline/argument.type.neon | 22 +---------------- utils/phpstan-baseline/loader.neon | 2 +- .../method.childParameterType.neon | 7 +----- 46 files changed, 85 insertions(+), 112 deletions(-) diff --git a/system/API/TransformerInterface.php b/system/API/TransformerInterface.php index 3d994251b302..7f8dd95759db 100644 --- a/system/API/TransformerInterface.php +++ b/system/API/TransformerInterface.php @@ -43,7 +43,7 @@ public function transform(array|object|null $resource): array; /** * Transforms a collection of resources using $this->transform() on each item. * - * @param array $resources + * @param array $resources * * @return array> */ diff --git a/system/BaseModel.php b/system/BaseModel.php index 6f52c02a48c6..5579bd8f810a 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -1855,7 +1855,7 @@ protected function objectToRawArray($object, bool $onlyChanged = true, bool $rec * * @param object|row_array|null $row * - * @return array + * @return array * * @throws DataException * @throws InvalidArgumentException @@ -1947,7 +1947,7 @@ public function __isset(string $name): bool /** * Provides direct access to method in the database connection. * - * @param array $params + * @param array $params * * @return mixed */ diff --git a/system/CLI/BaseCommand.php b/system/CLI/BaseCommand.php index 57df52d47f55..72769dfd85a9 100644 --- a/system/CLI/BaseCommand.php +++ b/system/CLI/BaseCommand.php @@ -99,7 +99,7 @@ public function __construct(LoggerInterface $logger, Commands $commands) /** * Actually execute a command. * - * @param array $params + * @param array $params * * @return int|void */ @@ -108,7 +108,7 @@ abstract public function run(array $params); /** * Can be used by a command to run other commands. * - * @param array $params + * @param array $params * * @return int|void * diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index 11f49bbc0c45..10e39cee0ad4 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -273,10 +273,10 @@ public static function prompt(string $field, $options = null, $validation = null /** * prompt(), but based on the option's key * - * @param list|string $text Output "field" text or an one or two value array where the first value is the text before listing the options - * and the second value the text before asking to select one option. Provide empty string to omit - * @param array $options A list of options (array(key => description)), the first option will be the default value - * @param list|string|null $validation Validation rules + * @param list|string $text Output "field" text or an one or two value array where the first value is the text before listing the options + * and the second value the text before asking to select one option. Provide empty string to omit + * @param array $options A list of options (array(key => description)), the first option will be the default value + * @param list|string|null $validation Validation rules * * @return string The selected key of $options */ @@ -302,11 +302,11 @@ public static function promptByKey($text, array $options, $validation = null): s /** * This method is the same as promptByKey(), but this method supports multiple keys, separated by commas. * - * @param string $text Output "field" text or an one or two value array where the first value is the text before listing the options - * and the second value the text before asking to select one option. Provide empty string to omit - * @param array $options A list of options (array(key => description)), the first option will be the default value + * @param string $text Output "field" text or an one or two value array where the first value is the text before listing the options + * and the second value the text before asking to select one option. Provide empty string to omit + * @param array $options A list of options (array(key => description)), the first option will be the default value * - * @return array The selected key(s) and value(s) of $options + * @return array The selected key(s) and value(s) of $options */ public static function promptByMultipleKeys(string $text, array $options): array { @@ -376,7 +376,7 @@ public static function promptByMultipleKeys(string $text, array $options): array /** * Validation for $options in promptByKey() and promptByMultipleKeys(). Return an error if $options is an empty array. * - * @param array $options + * @param array $options */ private static function isZeroOptions(array $options): void { @@ -388,7 +388,7 @@ private static function isZeroOptions(array $options): void /** * Print each key and value one by one * - * @param array $options + * @param array $options */ private static function printKeysAndValues(array $options): void { @@ -1029,8 +1029,8 @@ public static function getOptionString(bool $useLongOpts = false, bool $trim = f /** * Returns a well formatted table * - * @param list> $tbody List of rows - * @param list $thead List of columns + * @param list> $tbody List of rows + * @param list $thead List of columns * * @return void */ diff --git a/system/CLI/Commands.php b/system/CLI/Commands.php index 8f623d509c0a..bbccf6525f15 100644 --- a/system/CLI/Commands.php +++ b/system/CLI/Commands.php @@ -54,7 +54,7 @@ public function __construct($logger = null) /** * Runs a command given * - * @param array $params + * @param array $params * * @return int Exit code */ diff --git a/system/CLI/Console.php b/system/CLI/Console.php index b24562e52d46..82c31d60f7c5 100644 --- a/system/CLI/Console.php +++ b/system/CLI/Console.php @@ -74,9 +74,9 @@ public function showHeader(bool $suppress = false) * We'll remove that as an option from `$params` and * unshift it as argument instead. * - * @param array $params + * @param array $params * - * @return array + * @return array */ private function parseParamsForHelpOption(array $params): array { diff --git a/system/CLI/GeneratorTrait.php b/system/CLI/GeneratorTrait.php index 7d66b8b290af..45e72c7bda71 100644 --- a/system/CLI/GeneratorTrait.php +++ b/system/CLI/GeneratorTrait.php @@ -96,14 +96,14 @@ trait GeneratorTrait * * @internal * - * @var array + * @var array */ private $params = []; /** * Execute the command. * - * @param array $params + * @param array $params * * @deprecated use generateClass() instead */ @@ -115,7 +115,7 @@ protected function execute(array $params): void /** * Generates a class file from an existing template. * - * @param array $params + * @param array $params */ protected function generateClass(array $params): void { @@ -138,8 +138,8 @@ protected function generateClass(array $params): void /** * Generate a view file from an existing template. * - * @param string $view namespaced view name that is generated - * @param array $params + * @param string $view namespaced view name that is generated + * @param array $params */ protected function generateView(string $view, array $params): void { diff --git a/system/Commands/Database/MigrateRefresh.php b/system/Commands/Database/MigrateRefresh.php index b7863a001438..9ee39af58f1e 100644 --- a/system/Commands/Database/MigrateRefresh.php +++ b/system/Commands/Database/MigrateRefresh.php @@ -72,7 +72,7 @@ class MigrateRefresh extends BaseCommand */ public function run(array $params) { - $params['b'] = 0; + $params['b'] = '0'; if (ENVIRONMENT === 'production') { // @codeCoverageIgnoreStart diff --git a/system/Commands/Database/MigrateStatus.php b/system/Commands/Database/MigrateStatus.php index 9506c2eae082..68288353458d 100644 --- a/system/Commands/Database/MigrateStatus.php +++ b/system/Commands/Database/MigrateStatus.php @@ -77,8 +77,6 @@ class MigrateStatus extends BaseCommand /** * Displays a list of all migrations and whether they've been run or not. - * - * @param array $params */ public function run(array $params) { diff --git a/system/Commands/Encryption/GenerateKey.php b/system/Commands/Encryption/GenerateKey.php index 148fd264dc33..33858e5ab4c1 100644 --- a/system/Commands/Encryption/GenerateKey.php +++ b/system/Commands/Encryption/GenerateKey.php @@ -127,7 +127,7 @@ protected function generateRandomKey(string $prefix, int $length): string /** * Sets the new encryption key in your .env file. * - * @param array $params + * @param array $params */ protected function setNewEncryptionKey(string $key, array $params): bool { @@ -144,7 +144,7 @@ protected function setNewEncryptionKey(string $key, array $params): bool /** * Checks whether to overwrite existing encryption key. * - * @param array $params + * @param array $params */ protected function confirmOverwrite(array $params): bool { diff --git a/system/Commands/Utilities/FilterCheck.php b/system/Commands/Utilities/FilterCheck.php index e2043fe9a3a4..5013b7ddf054 100644 --- a/system/Commands/Utilities/FilterCheck.php +++ b/system/Commands/Utilities/FilterCheck.php @@ -108,7 +108,7 @@ public function run(array $params) } /** - * @param array $params + * @param array $params */ private function checkParams(array $params): bool { diff --git a/system/Common.php b/system/Common.php index fc8abb394f06..d99b4ef3ca3c 100644 --- a/system/Common.php +++ b/system/Common.php @@ -154,7 +154,7 @@ function command(string $command) $cursor += strlen($match[0]); } - /** @var array */ + /** @var array */ $params = []; $command = array_shift($args); $optionValue = false; @@ -450,13 +450,13 @@ function env(string $key, $default = null) * If $data is an array, then it loops over it, escaping each * 'value' of the key/value pairs. * - * @param array|string>|string $data - * @param 'attr'|'css'|'html'|'js'|'raw'|'url' $context - * @param string|null $encoding Current encoding for escaping. - * If not UTF-8, we convert strings from this encoding - * pre-escaping and back to this encoding post-escaping. + * @param array|string>|string $data + * @param 'attr'|'css'|'html'|'js'|'raw'|'url' $context + * @param string|null $encoding Current encoding for escaping. + * If not UTF-8, we convert strings from this encoding + * pre-escaping and back to this encoding post-escaping. * - * @return ($data is string ? string : array|string>) + * @return ($data is string ? string : array|string>) * * @throws InvalidArgumentException */ diff --git a/system/Config/BaseConfig.php b/system/Config/BaseConfig.php index 40782a243b96..baa030ed7b5c 100644 --- a/system/Config/BaseConfig.php +++ b/system/Config/BaseConfig.php @@ -166,7 +166,7 @@ protected function parseEncryptionKey(string $key): string /** * Initialization an environment-specific configuration setting * - * @param array|bool|float|int|string|null $property + * @param array|bool|float|int|string|null $property * * @return void */ diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 3989b9a2d011..c3ed39102ae6 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -2813,7 +2813,7 @@ public function getCompiledDelete(bool $reset = true): string /** * Compiles a delete string and runs the query * - * @param array|RawSql|string $where + * @param array|RawSql|string $where * * @return bool|string Returns a SQL string if in test mode. * diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index c0cb7174dd52..17a5775d425f 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -781,7 +781,7 @@ abstract protected function execute(string $sql); * Should automatically handle different connections for read/write * queries if needed. * - * @param array|string|null $binds + * @param array|string|null $binds * * @return BaseResult|bool|Query * diff --git a/system/Database/ConnectionInterface.php b/system/Database/ConnectionInterface.php index 15fd63e1c7aa..d1206155125c 100644 --- a/system/Database/ConnectionInterface.php +++ b/system/Database/ConnectionInterface.php @@ -100,7 +100,7 @@ public function getVersion(): string; * Should automatically handle different connections for read/write * queries if needed. * - * @param array|string|null $binds + * @param array|string|null $binds * * @return BaseResult|bool|Query */ diff --git a/system/Database/OCI8/Builder.php b/system/Database/OCI8/Builder.php index ff5b1289c34a..d1abead5060a 100644 --- a/system/Database/OCI8/Builder.php +++ b/system/Database/OCI8/Builder.php @@ -150,7 +150,7 @@ protected function _truncate(string $table): string /** * Compiles a delete string and runs the query * - * @param array|RawSql|string $where + * @param array|RawSql|string $where * * @return bool|string * diff --git a/system/Database/Postgre/Builder.php b/system/Database/Postgre/Builder.php index c3a673558ec8..68cd00fb6246 100644 --- a/system/Database/Postgre/Builder.php +++ b/system/Database/Postgre/Builder.php @@ -225,7 +225,7 @@ protected function _insertBatch(string $table, array $keys, array $values): stri /** * Compiles a delete string and runs the query * - * @param array|RawSql|string $where + * @param array|RawSql|string $where * * @return bool|string * diff --git a/system/Database/SQLSRV/Builder.php b/system/Database/SQLSRV/Builder.php index cd7e7968fe72..486b30c3a6b9 100644 --- a/system/Database/SQLSRV/Builder.php +++ b/system/Database/SQLSRV/Builder.php @@ -538,7 +538,7 @@ protected function _delete(string $table): string /** * Compiles a delete string and runs the query * - * @param array|RawSql|string $where + * @param array|RawSql|string $where * * @return bool|string * diff --git a/system/Encryption/Handlers/BaseHandler.php b/system/Encryption/Handlers/BaseHandler.php index c324a465dbb8..0f0627c654d6 100644 --- a/system/Encryption/Handlers/BaseHandler.php +++ b/system/Encryption/Handlers/BaseHandler.php @@ -55,7 +55,7 @@ protected static function substr($str, $start, $length = null) * * @param string $key Property name * - * @return array|bool|int|string|null + * @return array|bool|int|string|null */ public function __get($key) { diff --git a/system/Encryption/KeyRotationDecorator.php b/system/Encryption/KeyRotationDecorator.php index 9e4b6ef0002b..7202bcc06764 100644 --- a/system/Encryption/KeyRotationDecorator.php +++ b/system/Encryption/KeyRotationDecorator.php @@ -86,7 +86,7 @@ public function decrypt($data, #[SensitiveParameter] $params = null) /** * Delegate property access to the inner handler. * - * @return array|bool|int|string|null + * @return array|bool|int|string|null */ public function __get(string $key) { diff --git a/system/Entity/Entity.php b/system/Entity/Entity.php index 95261f607458..a67c44b6fd1c 100644 --- a/system/Entity/Entity.php +++ b/system/Entity/Entity.php @@ -421,7 +421,7 @@ public function hasChanged(?string $key = null): bool * Checks if an array contains only Entity instances. * This allows optimization for per-entity change tracking. * - * @param array $data + * @param array $data */ private function containsOnlyEntities(array $data): bool { diff --git a/system/HTTP/Header.php b/system/HTTP/Header.php index a21f9961536a..54f162eb7caa 100644 --- a/system/HTTP/Header.php +++ b/system/HTTP/Header.php @@ -44,14 +44,14 @@ class Header implements Stringable * 'baz' => 'buzz', * ] * - * @var array|string>|string + * @var array|string>|string */ protected $value; /** * Header constructor. name is mandatory, if a value is provided, it will be set. * - * @param array|string>|string|null $value + * @param array|string>|int|string|null $value */ public function __construct(string $name, $value = null) { @@ -71,7 +71,7 @@ public function getName(): string * Gets the raw value of the header. This may return either a string * or an array, depending on whether the header has multiple values or not. * - * @return array|string>|string + * @return array|string>|string */ public function getValue() { @@ -96,7 +96,7 @@ public function setName(string $name) /** * Sets the value of the header, overwriting any previous value(s). * - * @param array|string>|string|null $value + * @param array|int|string>|int|string|null $value * * @return $this * @@ -235,7 +235,7 @@ private function validateName(string $name): void * * @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2 * - * @param array|string>|int|string $value + * @param array|int|string>|int|string $value * * @throws InvalidArgumentException */ diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 3a43d6dec050..c980810b9b2b 100644 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -367,7 +367,7 @@ public function getDefaultLocale(): string * @param int|null $filter Filter constant * @param array|int|null $flags * - * @return array|bool|float|int|stdClass|string|null + * @return array|bool|float|int|stdClass|string|null */ public function getVar($index = null, $filter = null, $flags = null) { @@ -394,7 +394,7 @@ public function getVar($index = null, $filter = null, $flags = null) * * @see http://php.net/manual/en/function.json-decode.php * - * @return array|bool|float|int|stdClass|null + * @return array|bool|float|int|stdClass|null * * @throws HTTPException When the body is invalid as JSON. */ @@ -421,7 +421,7 @@ public function getJSON(bool $assoc = false, int $depth = 512, int $options = 0) * @param int|null $filter Filter Constant * @param array|int|null $flags Option * - * @return array|bool|float|int|stdClass|string|null + * @return array|bool|float|int|stdClass|string|null */ public function getJsonVar($index = null, bool $assoc = false, ?int $filter = null, $flags = null) { diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index 04499668f5ed..3c76f117f99e 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -1211,7 +1211,7 @@ public function __toString(): string * * @param string $name * - * @return array|bool|DateTimeInterface|DateTimeZone|int|IntlCalendar|self|string|null + * @return array|bool|DateTimeInterface|DateTimeZone|int|IntlCalendar|self|string|null */ public function __get($name) { diff --git a/system/Model.php b/system/Model.php index 0d3c55ee0568..ccade9cfb700 100644 --- a/system/Model.php +++ b/system/Model.php @@ -123,7 +123,7 @@ class Model extends BaseModel * so that we can capture it (not the builder) * and ensure it gets validated first. * - * @var array{escape: array, data: row_array}|array{} + * @var array{escape: array, data: row_array}|array{} */ protected $tempData = []; @@ -131,7 +131,7 @@ class Model extends BaseModel * Escape array that maps usage of escape * flag for every parameter. * - * @var array + * @var array */ protected $escape = []; diff --git a/system/Test/CIUnitTestCase.php b/system/Test/CIUnitTestCase.php index edfa98df5c06..92b5d8631bf2 100644 --- a/system/Test/CIUnitTestCase.php +++ b/system/Test/CIUnitTestCase.php @@ -169,7 +169,7 @@ abstract class CIUnitTestCase extends TestCase * Stores information needed to remove any * rows inserted via $this->hasInDatabase(). * - * @var list> + * @var list> */ protected $insertCache = []; @@ -189,7 +189,7 @@ abstract class CIUnitTestCase extends TestCase * Values to be set in the SESSION global * before running the test. * - * @var array + * @var array */ protected $session = []; diff --git a/system/Test/DatabaseTestTrait.php b/system/Test/DatabaseTestTrait.php index 14c47c95cee7..af2a1679328d 100644 --- a/system/Test/DatabaseTestTrait.php +++ b/system/Test/DatabaseTestTrait.php @@ -29,11 +29,11 @@ * Provides functionality for refreshing/seeding * the database during testing. * - * @property BaseConnection $db - * @property list> $insertCache - * @property Seeder|null $seeder - * @property MigrationRunner|null $migrations - * @property list|string|null $namespace + * @property BaseConnection $db + * @property list> $insertCache + * @property Seeder|null $seeder + * @property MigrationRunner|null $migrations + * @property list|string|null $namespace * * @mixin CIUnitTestCase */ diff --git a/system/Test/FeatureTestTrait.php b/system/Test/FeatureTestTrait.php index 087fc0a59d90..db298084e023 100644 --- a/system/Test/FeatureTestTrait.php +++ b/system/Test/FeatureTestTrait.php @@ -35,7 +35,7 @@ * Provides additional utilities for doing full HTTP testing * against your application in trait format. * - * @property array $session + * @property array $session * @property array|string> $headers * @property RouteCollection|null $routes * @@ -104,7 +104,7 @@ protected function withRoutes(?array $routes = null) /** * Sets any values that should exist during this session. * - * @param array|null $values Array of values, or null to use the current $_SESSION + * @param array|null $values Array of values, or null to use the current $_SESSION * * @return $this */ diff --git a/system/Test/Mock/MockConnection.php b/system/Test/Mock/MockConnection.php index 7f6aa5240e27..0db8aec916d5 100644 --- a/system/Test/Mock/MockConnection.php +++ b/system/Test/Mock/MockConnection.php @@ -70,7 +70,7 @@ public function shouldReturn(string $method, $return) * Should automatically handle different connections for read/write * queries if needed. * - * @param array|string|null $binds + * @param array|string|null $binds * * @return BaseResult|bool|Query * diff --git a/system/View/Parser.php b/system/View/Parser.php index 5de115f8236b..af4ebea346ac 100644 --- a/system/View/Parser.php +++ b/system/View/Parser.php @@ -721,7 +721,7 @@ protected function parsePlugins(string $template) /** * Makes a new plugin available during the parsing of the template. * - * @param (callable(array): string)|(callable(string, array): string) $callback + * @param (callable(array): string)|(callable(string, array): string) $callback * * @return $this */ diff --git a/system/View/Plugins.php b/system/View/Plugins.php index 7142e44c452e..b546b4a991d8 100644 --- a/system/View/Plugins.php +++ b/system/View/Plugins.php @@ -71,7 +71,7 @@ public static function safeMailto(array $params = []): string /** * Wrap helper function to use as view plugin. * - * @param array|list $params + * @param array|list $params */ public static function lang(array $params = []): string { diff --git a/tests/system/HTTP/HeaderTest.php b/tests/system/HTTP/HeaderTest.php index b7a1eff0e150..3d79da95f09b 100644 --- a/tests/system/HTTP/HeaderTest.php +++ b/tests/system/HTTP/HeaderTest.php @@ -67,7 +67,7 @@ public function testHeaderStoresBasicWithObject(): void $name = 'foo'; $value = new stdClass(); - new Header($name, $value); + new Header($name, $value); // @phpstan-ignore argument.type } public function testHeaderStoresArrayValues(): void @@ -275,7 +275,7 @@ public static function provideInvalidHeaderNames(): iterable } /** - * @param array|string>|string|null $value + * @param array|string>|string|null $value */ #[DataProvider('provideInvalidHeaderValues')] public function testInvalidHeaderValues($value): void @@ -286,7 +286,7 @@ public function testInvalidHeaderValues($value): void } /** - * @return list|string>> + * @return list|string>> */ public static function provideInvalidHeaderValues(): iterable { diff --git a/tests/system/Validation/CreditCardRulesTest.php b/tests/system/Validation/CreditCardRulesTest.php index e578c0b4d980..50c0bfecf50c 100644 --- a/tests/system/Validation/CreditCardRulesTest.php +++ b/tests/system/Validation/CreditCardRulesTest.php @@ -26,7 +26,7 @@ final class CreditCardRulesTest extends StrictCreditCardRulesTest protected Validation $validation; /** - * @var array|string>> + * @var array|string>> */ protected array $config = [ 'ruleSets' => [ diff --git a/tests/system/Validation/FileRulesTest.php b/tests/system/Validation/FileRulesTest.php index 2b43768e1ac8..a5153cd26868 100644 --- a/tests/system/Validation/FileRulesTest.php +++ b/tests/system/Validation/FileRulesTest.php @@ -26,7 +26,7 @@ final class FileRulesTest extends StrictFileRulesTest protected Validation $validation; /** - * @var array|string>> + * @var array|string>> */ protected array $config = [ 'ruleSets' => [ diff --git a/tests/system/Validation/FormatRulesTest.php b/tests/system/Validation/FormatRulesTest.php index b7d369e754a2..d35855865e05 100644 --- a/tests/system/Validation/FormatRulesTest.php +++ b/tests/system/Validation/FormatRulesTest.php @@ -33,7 +33,7 @@ class FormatRulesTest extends CIUnitTestCase protected Validation $validation; /** - * @var array|string>> + * @var array|string>> */ protected array $config = [ 'ruleSets' => [ diff --git a/tests/system/Validation/RulesTest.php b/tests/system/Validation/RulesTest.php index 288a6d247e50..deddab05dc4d 100644 --- a/tests/system/Validation/RulesTest.php +++ b/tests/system/Validation/RulesTest.php @@ -31,7 +31,7 @@ class RulesTest extends CIUnitTestCase protected Validation $validation; /** - * @var array|string>> + * @var array|string>> */ protected array $config = [ 'ruleSets' => [ diff --git a/tests/system/Validation/StrictRules/CreditCardRulesTest.php b/tests/system/Validation/StrictRules/CreditCardRulesTest.php index a1bc09aa6c0f..21691ebd6fae 100644 --- a/tests/system/Validation/StrictRules/CreditCardRulesTest.php +++ b/tests/system/Validation/StrictRules/CreditCardRulesTest.php @@ -30,7 +30,7 @@ class CreditCardRulesTest extends CIUnitTestCase protected Validation $validation; /** - * @var array|string>> + * @var array|string>> */ protected array $config = [ 'ruleSets' => [ diff --git a/tests/system/Validation/StrictRules/FileRulesTest.php b/tests/system/Validation/StrictRules/FileRulesTest.php index 5c58787b5f0b..da96b9f0c556 100644 --- a/tests/system/Validation/StrictRules/FileRulesTest.php +++ b/tests/system/Validation/StrictRules/FileRulesTest.php @@ -30,7 +30,7 @@ class FileRulesTest extends CIUnitTestCase protected Validation $validation; /** - * @var array|string>> + * @var array|string>> */ protected array $config = [ 'ruleSets' => [ diff --git a/tests/system/Validation/StrictRules/FormatRulesTest.php b/tests/system/Validation/StrictRules/FormatRulesTest.php index 92d12ad8ad1a..db7372d8179c 100644 --- a/tests/system/Validation/StrictRules/FormatRulesTest.php +++ b/tests/system/Validation/StrictRules/FormatRulesTest.php @@ -24,7 +24,7 @@ final class FormatRulesTest extends TraditionalFormatRulesTest { /** - * @var array|string>> + * @var array|string>> */ protected array $config = [ 'ruleSets' => [ diff --git a/tests/system/Validation/StrictRules/RulesTest.php b/tests/system/Validation/StrictRules/RulesTest.php index fc9a771715c8..0fc4f9e38c6e 100644 --- a/tests/system/Validation/StrictRules/RulesTest.php +++ b/tests/system/Validation/StrictRules/RulesTest.php @@ -28,7 +28,7 @@ final class RulesTest extends TraditionalRulesTest protected Validation $validation; /** - * @var array|string>> + * @var array|string>> */ protected array $config = [ 'ruleSets' => [ diff --git a/tests/system/Validation/StrictRules/ValidationTest.php b/tests/system/Validation/StrictRules/ValidationTest.php index 9473cd9f6b32..afff6c8f6a64 100644 --- a/tests/system/Validation/StrictRules/ValidationTest.php +++ b/tests/system/Validation/StrictRules/ValidationTest.php @@ -24,7 +24,7 @@ final class ValidationTest extends TraditionalValidationTest { /** - * @var array|string>|string>|string> + * @var array|string>|string>|string> */ protected static array $config = [ 'ruleSets' => [ diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php index 7cf70deaeb27..3a0f57a0e8e6 100644 --- a/tests/system/Validation/ValidationTest.php +++ b/tests/system/Validation/ValidationTest.php @@ -40,7 +40,7 @@ class ValidationTest extends CIUnitTestCase protected Validation $validation; /** - * @var array|string>|string>|string> + * @var array|string>|string>|string> */ protected static array $config = [ 'ruleSets' => [ diff --git a/utils/phpstan-baseline/argument.type.neon b/utils/phpstan-baseline/argument.type.neon index bbd0685fcf76..7fbc636537e7 100644 --- a/utils/phpstan-baseline/argument.type.neon +++ b/utils/phpstan-baseline/argument.type.neon @@ -1,12 +1,7 @@ -# total 73 errors +# total 68 errors parameters: ignoreErrors: - - - message: '#^Parameter \#2 \$params of method CodeIgniter\\CLI\\BaseCommand\:\:call\(\) expects array\, array\ given\.$#' - count: 2 - path: ../../system/Commands/Database/MigrateRefresh.php - - message: '#^Parameter \#3 \.\.\.\$arrays of function array_map expects array, int\|string given\.$#' count: 1 @@ -102,21 +97,6 @@ parameters: count: 1 path: ../../tests/system/Debug/TimerTest.php - - - message: '#^Parameter \#1 \$value of method CodeIgniter\\HTTP\\Header\:\:setValue\(\) expects array\\|string\>\|string\|null, array\ given\.$#' - count: 1 - path: ../../tests/system/HTTP/HeaderTest.php - - - - message: '#^Parameter \#2 \$value of class CodeIgniter\\HTTP\\Header constructor expects array\\|string\>\|string\|null, int given\.$#' - count: 1 - path: ../../tests/system/HTTP/HeaderTest.php - - - - message: '#^Parameter \#2 \$value of class CodeIgniter\\HTTP\\Header constructor expects array\\|string\>\|string\|null, stdClass given\.$#' - count: 1 - path: ../../tests/system/HTTP/HeaderTest.php - - message: '#^Parameter \#1 \$array of method CodeIgniter\\Superglobals\:\:setServerArray\(\) expects array\\|float\|int\|string\>, array\ given\.$#' count: 1 diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 2e67047327fc..5214bfa1141b 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 1825 errors +# total 1819 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/method.childParameterType.neon b/utils/phpstan-baseline/method.childParameterType.neon index 21752412ee34..f4316313dd85 100644 --- a/utils/phpstan-baseline/method.childParameterType.neon +++ b/utils/phpstan-baseline/method.childParameterType.neon @@ -1,12 +1,7 @@ -# total 11 errors +# total 10 errors parameters: ignoreErrors: - - - message: '#^Parameter \#1 \$params \(array\\) of method CodeIgniter\\Commands\\Database\\MigrateStatus\:\:run\(\) should be contravariant with parameter \$params \(array\\) of method CodeIgniter\\CLI\\BaseCommand\:\:run\(\)$#' - count: 1 - path: ../../system/Commands/Database/MigrateStatus.php - - message: '#^Parameter \#1 \$offset \(string\) of method CodeIgniter\\Cookie\\Cookie\:\:offsetSet\(\) should be contravariant with parameter \$offset \(string\|null\) of method ArrayAccess\\:\:offsetSet\(\)$#' count: 1 From 90b2e54993cd47cf564a7c4e4a229c0a45e0bdec Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Thu, 9 Jul 2026 11:53:47 +0200 Subject: [PATCH 014/163] fix: resolve global state pollution causing random-order test failures in HTTP suite (#10372) * fix: resolve random order execution failures in HTTP test suite * fix: resolve encoding negotiation pollution in NegotiateTest * style: run composer cs-fix on HTTP classes * fix: remove redundant null check for string typed parameter in MessageTrait * fix: address reviewer feedback regarding HTTP test isolation * test: isolate CorsTest to prevent global state pollution * test: enable random order execution verification for HTTP component * test: move resetServices before parent::setUp in HTTP test suite to preserve parent mocks --- .github/scripts/random-tests-config.txt | 2 +- tests/system/HTTP/CURLRequestTest.php | 2 +- tests/system/HTTP/CorsTest.php | 8 ++++++++ tests/system/HTTP/DownloadResponseTest.php | 2 ++ tests/system/HTTP/IncomingRequestTest.php | 7 ++++--- tests/system/HTTP/MessageTest.php | 6 +++++- tests/system/HTTP/NegotiateTest.php | 3 +++ tests/system/HTTP/RedirectResponseTest.php | 5 +++-- tests/system/HTTP/ResponseTest.php | 10 ++++++---- tests/system/HTTP/UserAgentTest.php | 3 +++ 10 files changed, 36 insertions(+), 12 deletions(-) diff --git a/.github/scripts/random-tests-config.txt b/.github/scripts/random-tests-config.txt index 2435f6ea7a17..413fe10c8d89 100644 --- a/.github/scripts/random-tests-config.txt +++ b/.github/scripts/random-tests-config.txt @@ -27,7 +27,7 @@ Events Files # Filters Format -# HTTP +HTTP # Helpers Honeypot HotReloader diff --git a/tests/system/HTTP/CURLRequestTest.php b/tests/system/HTTP/CURLRequestTest.php index 54cadb91bf9c..f0260ff839c9 100644 --- a/tests/system/HTTP/CURLRequestTest.php +++ b/tests/system/HTTP/CURLRequestTest.php @@ -39,9 +39,9 @@ class CURLRequestTest extends CIUnitTestCase protected function setUp(): void { + $this->resetServices(); parent::setUp(); - $this->resetServices(); Services::injectMock('superglobals', new Superglobals()); $this->request = $this->getRequest(); } diff --git a/tests/system/HTTP/CorsTest.php b/tests/system/HTTP/CorsTest.php index 5373959746ac..0da2bdcee55b 100644 --- a/tests/system/HTTP/CorsTest.php +++ b/tests/system/HTTP/CorsTest.php @@ -14,14 +14,22 @@ namespace CodeIgniter\HTTP; use CodeIgniter\Test\CIUnitTestCase; +use PHPUnit\Framework\Attributes\BackupGlobals; use PHPUnit\Framework\Attributes\Group; /** * @internal */ +#[BackupGlobals(true)] #[Group('Others')] final class CorsTest extends CIUnitTestCase { + protected function setUp(): void + { + $this->resetServices(); + parent::setUp(); + } + /** * @param array{ * allowedOrigins?: list, diff --git a/tests/system/HTTP/DownloadResponseTest.php b/tests/system/HTTP/DownloadResponseTest.php index d7cfcf937612..d5fea35e380c 100644 --- a/tests/system/HTTP/DownloadResponseTest.php +++ b/tests/system/HTTP/DownloadResponseTest.php @@ -20,6 +20,7 @@ use CodeIgniter\Test\CIUnitTestCase; use DateTime; use DateTimeZone; +use PHPUnit\Framework\Attributes\BackupGlobals; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\PreserveGlobalState; use PHPUnit\Framework\Attributes\RunInSeparateProcess; @@ -28,6 +29,7 @@ /** * @internal */ +#[BackupGlobals(true)] #[Group('SeparateProcess')] final class DownloadResponseTest extends CIUnitTestCase { diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index 2634e31a4b84..fe478105d43b 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -43,6 +43,7 @@ final class IncomingRequestTest extends CIUnitTestCase #[WithoutErrorHandler] protected function setUp(): void { + $this->resetServices(); parent::setUp(); $_ENV = $_SESSION = []; @@ -280,7 +281,7 @@ public function testSetValidLocales(): void */ public function testNegotiatesLocale(): void { - service('superglobals')->setServer('HTTP_ACCEPT_LANGUAGE', 'fr-FR); q=1.0, en; q=0.5'); + service('superglobals')->setServer('HTTP_ACCEPT_LANGUAGE', 'fr-FR; q=1.0, en; q=0.5'); $config = new App(); $config->negotiateLocale = true; @@ -295,7 +296,7 @@ public function testNegotiatesLocale(): void public function testNegotiatesLocaleOnlyBroad(): void { - service('superglobals')->setServer('HTTP_ACCEPT_LANGUAGE', 'fr); q=1.0, en; q=0.5'); + service('superglobals')->setServer('HTTP_ACCEPT_LANGUAGE', 'fr; q=1.0, en; q=0.5'); $config = new App(); $config->negotiateLocale = true; @@ -349,7 +350,7 @@ public function testNegotiatesEncoding(): void public function testNegotiatesLanguage(): void { $this->request->setHeader('Accept-Language', 'da, en-gb;q=0.8, en;q=0.7'); - $this->assertSame('en', $this->request->negotiate('language', ['en', 'da'])); + $this->assertSame('da', $this->request->negotiate('language', ['en', 'da'])); } public function testCanGrabGetRawJSON(): void diff --git a/tests/system/HTTP/MessageTest.php b/tests/system/HTTP/MessageTest.php index 7a691af39c08..9307c3eef999 100644 --- a/tests/system/HTTP/MessageTest.php +++ b/tests/system/HTTP/MessageTest.php @@ -259,7 +259,11 @@ public function testPopulateHeadersWithoutContentType(): void $this->assertNull($this->message->header('content-type')); - putenv("CONTENT_TYPE={$originalEnv}"); + if ($originalEnv !== false) { + putenv("CONTENT_TYPE={$originalEnv}"); + } else { + putenv('CONTENT_TYPE'); + } } public function testPopulateHeadersWithoutHTTP(): void diff --git a/tests/system/HTTP/NegotiateTest.php b/tests/system/HTTP/NegotiateTest.php index e52f6f7a3201..083846c8a1a8 100644 --- a/tests/system/HTTP/NegotiateTest.php +++ b/tests/system/HTTP/NegotiateTest.php @@ -17,11 +17,13 @@ use CodeIgniter\Test\CIUnitTestCase; use Config\App; use Config\Feature; +use PHPUnit\Framework\Attributes\BackupGlobals; use PHPUnit\Framework\Attributes\Group; /** * @internal */ +#[BackupGlobals(true)] #[Group('Others')] final class NegotiateTest extends CIUnitTestCase { @@ -30,6 +32,7 @@ final class NegotiateTest extends CIUnitTestCase protected function setUp(): void { + $this->resetServices(); parent::setUp(); $config = new App(); diff --git a/tests/system/HTTP/RedirectResponseTest.php b/tests/system/HTTP/RedirectResponseTest.php index 2ea700c03d88..b06e437fcffe 100644 --- a/tests/system/HTTP/RedirectResponseTest.php +++ b/tests/system/HTTP/RedirectResponseTest.php @@ -24,6 +24,7 @@ use Config\Modules; use Config\Routing; use Config\Services; +use PHPUnit\Framework\Attributes\BackupGlobals; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\PreserveGlobalState; use PHPUnit\Framework\Attributes\RunInSeparateProcess; @@ -32,6 +33,7 @@ /** * @internal */ +#[BackupGlobals(true)] #[Group('SeparateProcess')] final class RedirectResponseTest extends CIUnitTestCase { @@ -46,9 +48,8 @@ final class RedirectResponseTest extends CIUnitTestCase #[WithoutErrorHandler] protected function setUp(): void { - parent::setUp(); - $this->resetServices(); + parent::setUp(); Services::injectMock('superglobals', new Superglobals()); service('superglobals')->setServer('REQUEST_METHOD', 'GET'); diff --git a/tests/system/HTTP/ResponseTest.php b/tests/system/HTTP/ResponseTest.php index 17d1b4191134..97ab68be36c8 100644 --- a/tests/system/HTTP/ResponseTest.php +++ b/tests/system/HTTP/ResponseTest.php @@ -22,12 +22,14 @@ use Config\App; use DateTime; use DateTimeZone; +use PHPUnit\Framework\Attributes\BackupGlobals; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; /** * @internal */ +#[BackupGlobals(true)] #[Group('Others')] final class ResponseTest extends CIUnitTestCase { @@ -35,12 +37,11 @@ final class ResponseTest extends CIUnitTestCase protected function setUp(): void { - Services::injectMock('superglobals', new Superglobals()); - $this->server = service('superglobals')->getServerArray(); - + $this->resetServices(); parent::setUp(); - $this->resetServices(); + Services::injectMock('superglobals', new Superglobals()); + $this->server = service('superglobals')->getServerArray(); } protected function tearDown(): void @@ -169,6 +170,7 @@ public function testSetLink(): void Factories::injectMock('config', 'App', $config); $this->resetServices(); + Services::injectMock('superglobals', new Superglobals([], [])); $response = new Response($config); $pager = service('pager'); diff --git a/tests/system/HTTP/UserAgentTest.php b/tests/system/HTTP/UserAgentTest.php index b19e0c62dee5..cc6947d1dd50 100644 --- a/tests/system/HTTP/UserAgentTest.php +++ b/tests/system/HTTP/UserAgentTest.php @@ -14,11 +14,13 @@ namespace CodeIgniter\HTTP; use CodeIgniter\Test\CIUnitTestCase; +use PHPUnit\Framework\Attributes\BackupGlobals; use PHPUnit\Framework\Attributes\Group; /** * @internal */ +#[BackupGlobals(true)] #[Group('Others')] final class UserAgentTest extends CIUnitTestCase { @@ -28,6 +30,7 @@ final class UserAgentTest extends CIUnitTestCase protected function setUp(): void { + $this->resetServices(); parent::setUp(); // set a baseline user agent From 5b46a44c2da4af49d3eb6ad0a75e026a06a2099a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Jul 2026 22:33:58 +0700 Subject: [PATCH 015/163] chore(deps-dev): update rector/rector requirement (#10398) Updates the requirements on [rector/rector](https://github.com/rectorphp/rector) to permit the latest version. Updates `rector/rector` to 2.5.5 - [Release notes](https://github.com/rectorphp/rector/releases) - [Commits](https://github.com/rectorphp/rector/compare/2.5.4...2.5.5) --- updated-dependencies: - dependency-name: rector/rector dependency-version: 2.5.5 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index f0f8767776c6..4cc2bcbbb193 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "phpunit/phpcov": "^9.0.2 || ^10.0", "phpunit/phpunit": "^10.5.16 || ^11.2", "predis/predis": "^3.0", - "rector/rector": "2.5.4", + "rector/rector": "2.5.5", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { From 0c101bd61518a23c1c5744d75ba284451a05e2f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 07:47:24 +0200 Subject: [PATCH 016/163] chore(deps): bump actions/labeler in / (#10399) Bumps [actions/labeler](https://github.com/actions/labeler) in `/` from 6.1.0 to 6.2.0. Updates `actions/labeler` from 6.1.0 to 6.2.0 - [Release notes](https://github.com/actions/labeler/releases) - [Commits](https://github.com/actions/labeler/compare/f27b608878404679385c85cfa523b85ccb86e213...b8dd2d9be0f68b860e7dae5dae7d772984eacd6d) --- updated-dependencies: - dependency-name: actions/labeler dependency-version: 6.2.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/label-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml index 2e2659c6963a..1abf1dfb7b70 100644 --- a/.github/workflows/label-pr.yml +++ b/.github/workflows/label-pr.yml @@ -42,6 +42,6 @@ jobs: console.log('No changes to workflow file detected, proceeding with label addition.'); - name: Add labels - uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0 + uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0 with: sync-labels: true # Remove labels when matching files are reverted From 7d07ce209256beb411ae1ec99c6ebdec8ca831e0 Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Sat, 11 Jul 2026 12:06:16 +0200 Subject: [PATCH 017/163] test: fix Session tests execution in random order (#10370) --- .../Session/Handlers/Database/AbstractHandlerTestCase.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/system/Session/Handlers/Database/AbstractHandlerTestCase.php b/tests/system/Session/Handlers/Database/AbstractHandlerTestCase.php index b5760d7f4f4e..743508d7fe55 100644 --- a/tests/system/Session/Handlers/Database/AbstractHandlerTestCase.php +++ b/tests/system/Session/Handlers/Database/AbstractHandlerTestCase.php @@ -37,11 +37,11 @@ abstract class AbstractHandlerTestCase extends CIUnitTestCase protected function setUp(): void { - parent::setUp(); - if (! in_array(config(DatabaseConfig::class)->tests['DBDriver'], ['MySQLi', 'Postgre'], true)) { $this->markTestSkipped('Database Session Handler requires database driver to be MySQLi or Postgre'); } + + parent::setUp(); } /** From 8c164d05c5f94b4d34e6526c494fc4f6c58f750c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 22:58:03 +0700 Subject: [PATCH 018/163] chore(deps-dev): bump the composer-dependencies group across 1 directory with 2 updates (#10404) Updates the requirements on [boundwize/structarmed](https://github.com/boundwize/structarmed) and [rector/rector](https://github.com/rectorphp/rector) to permit the latest version. Updates `boundwize/structarmed` to 0.14.15 - [Release notes](https://github.com/boundwize/structarmed/releases) - [Commits](https://github.com/boundwize/structarmed/compare/0.14.11...0.14.15) Updates `rector/rector` to 2.5.7 - [Release notes](https://github.com/rectorphp/rector/releases) - [Commits](https://github.com/rectorphp/rector/compare/2.5.5...2.5.7) --- updated-dependencies: - dependency-name: boundwize/structarmed dependency-version: 0.14.15 dependency-type: direct:development dependency-group: composer-dependencies - dependency-name: rector/rector dependency-version: 2.5.6 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 4cc2bcbbb193..db006eb14f39 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "0.14.11", + "boundwize/structarmed": "0.14.15", "codeigniter/phpstan-codeigniter": "^2.1", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", @@ -29,7 +29,7 @@ "phpunit/phpcov": "^9.0.2 || ^10.0", "phpunit/phpunit": "^10.5.16 || ^11.2", "predis/predis": "^3.0", - "rector/rector": "2.5.5", + "rector/rector": "2.5.7", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { From b63543460a94794453db90e4299ae212226acff2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 17:54:23 +0200 Subject: [PATCH 019/163] chore(deps-dev): update boundwize/structarmed requirement (#10408) Updates the requirements on [boundwize/structarmed](https://github.com/boundwize/structarmed) to permit the latest version. Updates `boundwize/structarmed` to 0.14.16 - [Release notes](https://github.com/boundwize/structarmed/releases) - [Commits](https://github.com/boundwize/structarmed/compare/0.14.15...0.14.16) --- updated-dependencies: - dependency-name: boundwize/structarmed dependency-version: 0.14.16 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index db006eb14f39..ce68e084b381 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "0.14.15", + "boundwize/structarmed": "0.14.16", "codeigniter/phpstan-codeigniter": "^2.1", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", From e5719b51399892c5b624ad2d689c304d65a5319f Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Fri, 17 Jul 2026 17:32:33 +0200 Subject: [PATCH 020/163] fix: omit empty CSP headers (#10412) --- system/HTTP/ContentSecurityPolicy.php | 8 +- .../system/HTTP/ContentSecurityPolicyTest.php | 75 ++++++++++++++++++- user_guide_src/source/changelogs/v4.7.5.rst | 1 + 3 files changed, 75 insertions(+), 9 deletions(-) diff --git a/system/HTTP/ContentSecurityPolicy.php b/system/HTTP/ContentSecurityPolicy.php index c94fed4c8e73..acea6b2b5ad6 100644 --- a/system/HTTP/ContentSecurityPolicy.php +++ b/system/HTTP/ContentSecurityPolicy.php @@ -941,10 +941,6 @@ protected function buildHeaders(ResponseInterface $response) return; } - $response->setHeader('Content-Security-Policy', []); - $response->setHeader('Content-Security-Policy-Report-Only', []); - $response->setHeader('Reporting-Endpoints', []); - if (in_array($this->baseURI, ['', null, []], true)) { $this->baseURI = 'self'; } @@ -967,9 +963,7 @@ protected function buildHeaders(ResponseInterface $response) } } - // Compile our own header strings here since if we just - // append it to the response, it will be joined with - // commas, not semi-colons as we need. + // Compile each generated header value before appending it to the response. if ($this->reportingEndpoints !== []) { $endpoints = []; diff --git a/tests/system/HTTP/ContentSecurityPolicyTest.php b/tests/system/HTTP/ContentSecurityPolicyTest.php index 5bced590228a..780cb99f31ea 100644 --- a/tests/system/HTTP/ContentSecurityPolicyTest.php +++ b/tests/system/HTTP/ContentSecurityPolicyTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\HTTP; +use CodeIgniter\Config\Services; use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\TestResponse; @@ -90,15 +91,68 @@ public function testExistence(): void { $this->assertTrue($this->work()); $this->assertHeaderEmitted('Content-Security-Policy:'); + $this->assertHeaderNotEmitted('Content-Security-Policy-Report-Only:'); + $this->assertHeaderNotEmitted('Reporting-Endpoints:'); } #[PreserveGlobalState(false)] #[RunInSeparateProcess] public function testReportOnly(): void { - $this->csp->reportOnly(false); + $config = new CSPConfig(); + $config->reportOnly = true; + + Services::injectMock('csp', new ContentSecurityPolicy($config)); + + $this->response = new Response(config(App::class)); + $this->response->pretend(false); + $this->csp = $this->response->getCSP(); + $this->assertTrue($this->work()); - $this->assertHeaderEmitted('Content-Security-Policy:'); + $this->assertHeaderEmitted('Content-Security-Policy-Report-Only:'); + $this->assertHeaderNotEmitted('Content-Security-Policy:'); + $this->assertHeaderNotEmitted('Reporting-Endpoints:'); + } + + #[PreserveGlobalState(false)] + #[RunInSeparateProcess] + public function testExistingCSPHeaderStringIsPreserved(): void + { + $this->response->setHeader('Content-Security-Policy', "frame-src 'none'"); + + $this->assertTrue($this->work()); + + $header = $this->getHeaderEmitted('Content-Security-Policy:'); + $this->assertIsString($header); + $this->assertStringContainsString("frame-src 'none', base-uri 'self'", $header); + } + + #[PreserveGlobalState(false)] + #[RunInSeparateProcess] + public function testExistingCSPHeaderArrayIsPreserved(): void + { + $this->response->setHeader('Content-Security-Policy', [ + "frame-src 'none'", + "media-src 'none'", + ]); + + $this->assertTrue($this->work()); + + $header = $this->getHeaderEmitted('Content-Security-Policy:'); + $this->assertIsString($header); + $this->assertStringContainsString("frame-src 'none', media-src 'none', base-uri 'self'", $header); + } + + #[PreserveGlobalState(false)] + #[RunInSeparateProcess] + public function testExistingReportOnlyHeaderIsPreserved(): void + { + $this->response->setHeader('Content-Security-Policy-Report-Only', "frame-src 'none'"); + + $this->assertTrue($this->work()); + + $header = $this->getHeaderEmitted('Content-Security-Policy-Report-Only:'); + $this->assertSame("Content-Security-Policy-Report-Only: frame-src 'none'", $header); } #[PreserveGlobalState(false)] @@ -609,6 +663,23 @@ public function testReportTo(): void $this->assertSame('Reporting-Endpoints: default="http://example.com/csp-reports"', $header); } + #[PreserveGlobalState(false)] + #[RunInSeparateProcess] + public function testExistingReportingEndpointsHeaderIsPreserved(): void + { + $this->response->setHeader('Reporting-Endpoints', 'existing="http://example.com/existing-reports"'); + $this->csp->addReportingEndpoints(['default' => 'http://example.com/csp-reports']); + $this->csp->setReportToEndpoint('default'); + + $this->assertTrue($this->work()); + + $header = $this->getHeaderEmitted('Reporting-Endpoints:'); + $this->assertSame( + 'Reporting-Endpoints: existing="http://example.com/existing-reports", default="http://example.com/csp-reports"', + $header, + ); + } + #[PreserveGlobalState(false)] #[RunInSeparateProcess] public function testReportToMultipleEndpoints(): void diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index c4bb7900e07a..964133046c38 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -30,6 +30,7 @@ Deprecations Bugs Fixed ********** +- **Content Security Policy:** Fixed a bug where empty ``Content-Security-Policy``, ``Content-Security-Policy-Report-Only``, and ``Reporting-Endpoints`` response headers were generated when no corresponding values existed. - **Logger:** Fixed a bug where interpolating a log message with array or non-stringable context values could raise PHP warnings or errors. See the repo's From 95e25ec1d75613e605a0631b576b8b154b5306f5 Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Sat, 18 Jul 2026 13:43:14 +0200 Subject: [PATCH 021/163] fix: resolve service registry pollution in CSRFTest (#10373) --- .github/scripts/random-tests-config.txt | 2 +- tests/system/Filters/CSRFTest.php | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/scripts/random-tests-config.txt b/.github/scripts/random-tests-config.txt index 413fe10c8d89..79a74b5f9ef2 100644 --- a/.github/scripts/random-tests-config.txt +++ b/.github/scripts/random-tests-config.txt @@ -25,7 +25,7 @@ Cookie # Entity Events Files -# Filters +Filters Format HTTP # Helpers diff --git a/tests/system/Filters/CSRFTest.php b/tests/system/Filters/CSRFTest.php index d1977e4f5cea..71a6dfc28e7e 100644 --- a/tests/system/Filters/CSRFTest.php +++ b/tests/system/Filters/CSRFTest.php @@ -40,6 +40,9 @@ final class CSRFTest extends CIUnitTestCase protected function setUp(): void { parent::setUp(); + + $this->resetServices(); + $this->config = new \Config\Filters(); } From b030c7548d8193a04d17b11ac43ea271166b035c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 18 Jul 2026 20:03:41 +0800 Subject: [PATCH 022/163] chore(deps): bump actions/setup-node in / (#10409) Bumps [actions/setup-node](https://github.com/actions/setup-node) in `/` from 6.4.0 to 7.0.0. Updates `actions/setup-node` from 6.4.0 to 7.0.0 - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e...820762786026740c76f36085b0efc47a31fe5020) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: 7.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/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 3fcce79de33c..d501671bcd98 100644 --- a/.github/workflows/test-scss.yml +++ b/.github/workflows/test-scss.yml @@ -36,7 +36,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Setup Node - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '24' From 7505c73d9d6e3cc2fef93980c4b625fdf9b0b603 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Sat, 18 Jul 2026 14:04:27 +0200 Subject: [PATCH 023/163] test: fix cache test race conditions (#10413) --- .../system/Commands/Cache/ClearCacheTest.php | 30 +++++++++++++++-- tests/system/Commands/Cache/InfoCacheTest.php | 33 ++++++++++++++++--- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/tests/system/Commands/Cache/ClearCacheTest.php b/tests/system/Commands/Cache/ClearCacheTest.php index a5679b00b3d9..e64a24916e15 100644 --- a/tests/system/Commands/Cache/ClearCacheTest.php +++ b/tests/system/Commands/Cache/ClearCacheTest.php @@ -16,8 +16,10 @@ use CodeIgniter\Cache\CacheFactory; use CodeIgniter\Cache\Handlers\FileHandler; use CodeIgniter\CLI\CLI; +use CodeIgniter\Config\Factories; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; +use Config\Cache; use Config\Services; use PHPUnit\Framework\Attributes\Group; @@ -29,6 +31,8 @@ final class ClearCacheTest extends CIUnitTestCase { use StreamFilterTrait; + private Cache $config; + protected function setUp(): void { parent::setUp(); @@ -36,16 +40,36 @@ protected function setUp(): void CLI::reset(); $this->resetServices(); + $this->config = new Cache(); + $this->config->file['storePath'] = rtrim($this->config->file['storePath'], DIRECTORY_SEPARATOR) + . DIRECTORY_SEPARATOR . 'FileHandlerCommands'; + + if (! is_dir($this->config->file['storePath'])) { + mkdir($this->config->file['storePath'], 0777, true); + } + + Factories::injectMock('config', Cache::class, $this->config); + // Make sure we are testing with the correct handler (override injections) - Services::injectMock('cache', CacheFactory::getHandler(config('Cache'))); + $handler = CacheFactory::getHandler($this->config); + $handler->clean(); + Services::injectMock('cache', $handler); } protected function tearDown(): void { - parent::tearDown(); + $this->config->handler = 'file'; + + if (is_dir($this->config->file['storePath'])) { + CacheFactory::getHandler($this->config)->clean(); + rmdir($this->config->file['storePath']); + } CLI::reset(); + $this->resetFactories(); $this->resetServices(); + + parent::tearDown(); } public function testClearCacheInvalidHandler(): void @@ -72,7 +96,7 @@ public function testClearCacheWorks(): void public function testClearCacheFails(): void { $cache = $this->getMockBuilder(FileHandler::class) - ->setConstructorArgs([config('Cache')]) + ->setConstructorArgs([$this->config]) ->onlyMethods(['clean']) ->getMock(); $cache->expects($this->once())->method('clean')->willReturn(false); diff --git a/tests/system/Commands/Cache/InfoCacheTest.php b/tests/system/Commands/Cache/InfoCacheTest.php index 44aa389338cc..0898df6f5ffd 100644 --- a/tests/system/Commands/Cache/InfoCacheTest.php +++ b/tests/system/Commands/Cache/InfoCacheTest.php @@ -14,8 +14,10 @@ namespace CodeIgniter\Commands\Cache; use CodeIgniter\Cache\CacheFactory; +use CodeIgniter\Config\Factories; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; +use Config\Cache; use Config\Services; use PHPUnit\Framework\Attributes\Group; @@ -27,18 +29,41 @@ final class InfoCacheTest extends CIUnitTestCase { use StreamFilterTrait; + private Cache $config; + protected function setUp(): void { parent::setUp(); + $this->config = new Cache(); + $this->config->file['storePath'] = rtrim($this->config->file['storePath'], DIRECTORY_SEPARATOR) + . DIRECTORY_SEPARATOR . 'FileHandlerCommands'; + + if (! is_dir($this->config->file['storePath'])) { + mkdir($this->config->file['storePath'], 0777, true); + } + + Factories::injectMock('config', Cache::class, $this->config); + // Make sure we are testing with the correct handler (override injections) - Services::injectMock('cache', CacheFactory::getHandler(config('Cache'))); + $handler = CacheFactory::getHandler($this->config); + $handler->clean(); + Services::injectMock('cache', $handler); } protected function tearDown(): void { - // restore default cache handler - config('Cache')->handler = 'file'; + $this->config->handler = 'file'; + + if (is_dir($this->config->file['storePath'])) { + CacheFactory::getHandler($this->config)->clean(); + rmdir($this->config->file['storePath']); + } + + $this->resetFactories(); + $this->resetServices(); + + parent::tearDown(); } protected function getBuffer(): string @@ -48,7 +73,7 @@ protected function getBuffer(): string public function testInfoCacheErrorsOnInvalidHandler(): void { - config('Cache')->handler = 'redis'; + $this->config->handler = 'redis'; cache()->save('foo', 'bar'); command('cache:info'); From d1b316e10918e290ebb949d18eb1776c38a5ec03 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Sun, 19 Jul 2026 20:56:02 +0200 Subject: [PATCH 024/163] chore: clarify branch rebase workflow (#10416) --- contributing/README.md | 1 + contributing/pull_request.md | 26 ++++--- contributing/workflow.md | 133 +++++++++++++++++------------------ 3 files changed, 81 insertions(+), 79 deletions(-) diff --git a/contributing/README.md b/contributing/README.md index 40796df376b3..981b69169aca 100644 --- a/contributing/README.md +++ b/contributing/README.md @@ -14,3 +14,4 @@ if you want to take the lead on one of them. - [Contributor Covenant Code of Conduct](../CODE_OF_CONDUCT.md) - [Reporting a Bug](./bug_report.md) - [Sending a Pull Request](./pull_request.md) +- [Configuring Upstream and Rebasing](./workflow.md#configuring-upstream-and-rebasing) diff --git a/contributing/pull_request.md b/contributing/pull_request.md index dbea94ccbd6f..844d62046cfa 100644 --- a/contributing/pull_request.md +++ b/contributing/pull_request.md @@ -192,11 +192,16 @@ specific examples to pay attention to: Your PRs need to be mergeable and GPG-signed before they will be considered. -We suggest that you synchronize your repository's `develop` branch with -that in the main repository, and then your feature branch and your -develop branch, before submitting a PR. You will need to resolve any -merge conflicts introduced by changes incorporated since you started -working on your contribution. +Before submitting a PR, rebase your contribution branch onto the latest +appropriate branch from the main repository. Bug fixes should be rebased onto +`upstream/develop`, while features and enhancements should be rebased onto the +applicable `upstream/4.x` branch. See +[Configuring Upstream and Rebasing](./workflow.md#configuring-upstream-and-rebasing) +for the commands and conflict-resolution steps. + +Do not merge the target branch into your contribution branch. This may add merge +commits that make the PR and project history harder to follow. Rebasing keeps +the commit history linear and easier to review. ### Branching @@ -328,10 +333,11 @@ GitHub means "making a copy of that repo to your account" and "cloning" means tests yet, please create one. - `> vendor/bin/phpunit tests/system/path/to/file/you/modified` - Make sure the tests pass to have a higher chance of merging. -11. [Push](https://docs.github.com/en/github/using-git/pushing-commits-to-a-remote-repository) your contribution branch to your fork. +11. [Rebase your branch](./workflow.md#configuring-upstream-and-rebasing) onto the appropriate upstream branch. +12. [Push](https://docs.github.com/en/github/using-git/pushing-commits-to-a-remote-repository) your contribution branch to your fork. - `> git push origin ` -12. Send a [pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork). -13. Label your pull request with the appropriate label if you can. +13. Send a [pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork). +14. Label your pull request with the appropriate label if you can. See [Contribution workflow](./workflow.md) for Git workflow details. @@ -340,8 +346,8 @@ the team will respond. If your change fails to meet the guidelines, it will be rejected or feedback will be provided to help you improve it. Once the maintainer handling your pull request is satisfied with it, they will -approve the pull request and merge it into the "develop" branch. Your patch will -now be part of the next release! +approve and merge the pull request into the appropriate target branch. Your +patch will now be part of the next release! ## Translating System Messages diff --git a/contributing/workflow.md b/contributing/workflow.md index a294cc9e485c..2bb3e2d7e127 100644 --- a/contributing/workflow.md +++ b/contributing/workflow.md @@ -67,39 +67,72 @@ Clone your repository, leaving a local folder for you to work with: > git clone ORIGIN_URL ``` -## Syncing Your Repository +## Configuring Upstream and Rebasing -Within your local repository, Git will have created an alias, -**origin**, for the GitHub repository it is bound to. You want to create -an alias for the shared repository as well, so that you can "synch" the -two, making sure that your repository includes any other contributions -that have been merged by us into the shared repo: +Git names your fork `origin` when you clone it. Add the main CodeIgniter4 +repository as `upstream` so that you can fetch changes made by other +contributors. You only need to add this remote once: ```console -> git remote add upstream UPSTREAM_URL +> git remote add upstream https://github.com/codeigniter4/CodeIgniter4.git +> git remote -v ``` -Then synchronizing is done by pulling from us and pushing to you. This -is normally done locally, so that you can resolve any merge conflicts. -For instance, to synchronize **develop** branches: +If `upstream` already exists but uses the wrong URL, update it instead: + +```console +> git remote set-url upstream https://github.com/codeigniter4/CodeIgniter4.git +``` + +Fetch the latest upstream branches before rebasing: ```console > git fetch upstream -> git switch develop -> git merge upstream/develop -> git push origin develop ``` -You might get merge conflicts when you merge. It is your -responsibility to resolve those locally, so that you can continue -collaborating with the shared repository. Basically, the shared -repository is updated in the order that contributions are merged into -it, not in the order that they might have been submitted. If two PRs -update the same piece of code, then the first one to be merged will take -precedence, even if it causes problems for other contributions. +Rebase bug-fix branches onto `upstream/develop`: + +```console +> git switch your-branch +> git rebase upstream/develop +``` + +Features and enhancements target the next minor-version branch. Replace `4.x` +with the branch used by your pull request: + +```console +> git switch your-branch +> git rebase upstream/4.x +``` + +> [!IMPORTANT] +> Do not merge the upstream target branch into your contribution branch. A merge +> may add merge commits that make the pull request and project history harder to +> follow. Rebasing keeps the commit history linear and easier to review. + +Make sure local work is committed or stashed before rebasing. If Git reports a +conflict, edit the affected files, mark them as resolved, and continue: + +```console +> git add path/to/resolved-file +> git rebase --continue +``` + +Repeat these commands until the rebase is complete. To cancel the rebase and +return the branch to its previous state, run: + +```console +> git rebase --abort +``` + +If the branch was already pushed to your fork, update it safely after rebasing: + +```console +> git push --force-with-lease origin your-branch +``` -It is a good idea to synchronize repositories when the shared one -changes. +Use `--force-with-lease` rather than `--force` so that Git does not overwrite +remote changes that you have not fetched. ## Branching Revisited @@ -250,25 +283,9 @@ are working in. At some point, you will decide that your feature branch is complete, or that it could benefit from a review by fellow developers. -> [!NOTE] -> Remember to sync your local repo with the shared one before pushing! -It is a lot easier to resolve conflicts at this stage. - -Synchronize your repository: - -```console -> git fetch upstream -> git switch develop -> git merge upstream/develop -> git push origin develop -``` - -Bring your feature branch up to date: - -```console -> git switch fix/issue-123 -> git rebase upstream/develop -``` +Before pushing, follow +[Configuring Upstream and Rebasing](#configuring-upstream-and-rebasing) to bring +your contribution branch up to date and resolve any conflicts. And finally push your local branch to your GitHub repository: @@ -325,34 +342,15 @@ If you are asked for changes in the review, commit the fix in your branch and push it to GitHub again. If the __"develop"__ or next minor version branch, e.g. __"4.6"__, progresses -and conflicts arise that prevent merging, or if you are asked to *rebase*, -do the following: - -Synchronize your repository: - -```console -> git fetch upstream -> git switch develop -> git merge upstream/develop -> git push origin develop -``` - -(Optional) Create a new branch as a backup, just in case: +and conflicts arise that prevent merging, or if you are asked to *rebase*, you +may first create a backup branch: ```console > git branch fix/problem123.bk fix/problem123 ``` -Bring your feature branch up to date: - -```console -> git switch fix/problem123 -> git rebase upstream/develop -``` - -You might get conflicts when you rebase. It is your -responsibility to resolve those locally, so that you can continue -collaborating with the shared repository. +Then follow +[Configuring Upstream and Rebasing](#configuring-upstream-and-rebasing). Occasionally, the Composer packages for development may be updated. Run the following command to use the latest packages: @@ -380,13 +378,10 @@ Copy the IDs of any commits you made that you want to keep: > git log ``` -Update your __"4.6"__ branch: +Fetch the latest upstream branches: ```console > git fetch upstream -> git switch 4.6 -> git merge upstream/4.6 -> git push origin 4.6 ``` (Optional) Create a new branch as a backup, just in case: @@ -398,7 +393,7 @@ Update your __"4.6"__ branch: Rebase your PR branch from `develop` onto __"4.6"__: ```console -> git rebase --onto 4.6 develop feat-abc +> git rebase --onto upstream/4.6 develop feat-abc ``` Force push. From 9ac583de75eb73dd1f75bc0d4ada4e0c48a61bd0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:31:20 +0700 Subject: [PATCH 025/163] chore(deps-dev): update boundwize/structarmed requirement (#10419) Updates the requirements on [boundwize/structarmed](https://github.com/boundwize/structarmed) to permit the latest version. Updates `boundwize/structarmed` to 0.15.1 - [Release notes](https://github.com/boundwize/structarmed/releases) - [Commits](https://github.com/boundwize/structarmed/compare/0.14.16...0.15.1) --- updated-dependencies: - dependency-name: boundwize/structarmed dependency-version: 0.15.1 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ce68e084b381..4826d42cbe6d 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "0.14.16", + "boundwize/structarmed": "0.15.1", "codeigniter/phpstan-codeigniter": "^2.1", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", From d23e5d76f2d2312ca19196a461be36703c948d00 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 20:19:11 +0700 Subject: [PATCH 026/163] chore(deps): bump actions/checkout in / (#10420) Bumps [actions/checkout](https://github.com/actions/checkout) in `/` from 7.0.0 to 7.0.1. Updates `actions/checkout` from 7.0.0 to 7.0.1 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0...3d3c42e5aac5ba805825da76410c181273ba90b1) --- updated-dependencies: - dependency-name: actions/checkout 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-apidocs.yml | 4 ++-- .github/workflows/deploy-distributables.yml | 14 +++++++------- .github/workflows/deploy-userguide-latest.yml | 2 +- .github/workflows/label-add-conflict-all-pr.yml | 2 +- .github/workflows/label-pr.yml | 2 +- .github/workflows/label-signing.yml | 2 +- .github/workflows/reusable-coveralls.yml | 2 +- .github/workflows/reusable-phpunit-test.yml | 2 +- .../reusable-serviceless-phpunit-test.yml | 2 +- .github/workflows/test-autoreview.yml | 2 +- .github/workflows/test-coding-standards.yml | 2 +- .github/workflows/test-file-permissions.yml | 2 +- .github/workflows/test-phpstan.yml | 2 +- .github/workflows/test-psalm.yml | 2 +- .github/workflows/test-random-execution.yml | 2 +- .github/workflows/test-rector.yml | 2 +- .github/workflows/test-scss.yml | 2 +- .github/workflows/test-structarmed.yml | 2 +- .github/workflows/test-userguide.yml | 2 +- 19 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.github/workflows/deploy-apidocs.yml b/.github/workflows/deploy-apidocs.yml index 3d24470ed846..8eb59179a946 100644 --- a/.github/workflows/deploy-apidocs.yml +++ b/.github/workflows/deploy-apidocs.yml @@ -30,13 +30,13 @@ jobs: git config --global user.name "${GITHUB_ACTOR}" - name: Checkout source - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: path: source persist-credentials: false - name: Checkout target - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: codeigniter4/api token: ${{ secrets.ACCESS_TOKEN }} diff --git a/.github/workflows/deploy-distributables.yml b/.github/workflows/deploy-distributables.yml index 221383aafd57..61cd6c65fc33 100644 --- a/.github/workflows/deploy-distributables.yml +++ b/.github/workflows/deploy-distributables.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 # fetch all tags persist-credentials: false @@ -50,13 +50,13 @@ jobs: git config --global user.name "${GITHUB_ACTOR}" - name: Checkout source - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: path: source persist-credentials: false - name: Checkout target - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: codeigniter4/framework token: ${{ secrets.ACCESS_TOKEN }} @@ -104,13 +104,13 @@ jobs: git config --global user.name "${GITHUB_ACTOR}" - name: Checkout source - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: path: source persist-credentials: false - name: Checkout target - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: codeigniter4/appstarter token: ${{ secrets.ACCESS_TOKEN }} @@ -158,13 +158,13 @@ jobs: git config --global user.name "${GITHUB_ACTOR}" - name: Checkout source - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: path: source persist-credentials: false - name: Checkout target - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: codeigniter4/userguide token: ${{ secrets.ACCESS_TOKEN }} diff --git a/.github/workflows/deploy-userguide-latest.yml b/.github/workflows/deploy-userguide-latest.yml index 9bbd7a5bb7ba..80b8ebe369cd 100644 --- a/.github/workflows/deploy-userguide-latest.yml +++ b/.github/workflows/deploy-userguide-latest.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/label-add-conflict-all-pr.yml b/.github/workflows/label-add-conflict-all-pr.yml index 774012da1f32..f9adde8207de 100644 --- a/.github/workflows/label-add-conflict-all-pr.yml +++ b/.github/workflows/label-add-conflict-all-pr.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Get PR List id: PR-list diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml index 1abf1dfb7b70..1a253ee5856c 100644 --- a/.github/workflows/label-pr.yml +++ b/.github/workflows/label-pr.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/label-signing.yml b/.github/workflows/label-signing.yml index 80c321c5b117..609e84a0a23e 100644 --- a/.github/workflows/label-signing.yml +++ b/.github/workflows/label-signing.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Check signed commits in PR uses: 1Password/check-signed-commits-action@ed2885f3ed2577a4f5d3c3fe895432a557d23d52 # v1.2.0 diff --git a/.github/workflows/reusable-coveralls.yml b/.github/workflows/reusable-coveralls.yml index e47cceb1771d..cf9c02b22e11 100644 --- a/.github/workflows/reusable-coveralls.yml +++ b/.github/workflows/reusable-coveralls.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/reusable-phpunit-test.yml b/.github/workflows/reusable-phpunit-test.yml index 43d520458bf0..b216a71493bb 100644 --- a/.github/workflows/reusable-phpunit-test.yml +++ b/.github/workflows/reusable-phpunit-test.yml @@ -170,7 +170,7 @@ jobs: sudo apt-get install -y imagemagick libmagickwand-dev ghostscript poppler-data libjbig2dec0:amd64 libopenjp2-7:amd64 - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/reusable-serviceless-phpunit-test.yml b/.github/workflows/reusable-serviceless-phpunit-test.yml index 7858913cf531..8420d2b8c978 100644 --- a/.github/workflows/reusable-serviceless-phpunit-test.yml +++ b/.github/workflows/reusable-serviceless-phpunit-test.yml @@ -66,7 +66,7 @@ jobs: sudo apt-get install -y imagemagick libmagickwand-dev ghostscript poppler-data libjbig2dec0:amd64 libopenjp2-7:amd64 - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false fetch-depth: 0 diff --git a/.github/workflows/test-autoreview.yml b/.github/workflows/test-autoreview.yml index fd7c83248b2e..f06e02619966 100644 --- a/.github/workflows/test-autoreview.yml +++ b/.github/workflows/test-autoreview.yml @@ -36,7 +36,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup PHP uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 diff --git a/.github/workflows/test-coding-standards.yml b/.github/workflows/test-coding-standards.yml index 125dbd5b3311..b02059733b13 100644 --- a/.github/workflows/test-coding-standards.yml +++ b/.github/workflows/test-coding-standards.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup PHP uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 diff --git a/.github/workflows/test-file-permissions.yml b/.github/workflows/test-file-permissions.yml index 7116f4620869..441d555137e8 100644 --- a/.github/workflows/test-file-permissions.yml +++ b/.github/workflows/test-file-permissions.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Detect unnecessary execution permissions run: php utils/check_permission_x.php diff --git a/.github/workflows/test-phpstan.yml b/.github/workflows/test-phpstan.yml index 586314917200..cc246f871808 100644 --- a/.github/workflows/test-phpstan.yml +++ b/.github/workflows/test-phpstan.yml @@ -46,7 +46,7 @@ jobs: fail-fast: false steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup PHP uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 diff --git a/.github/workflows/test-psalm.yml b/.github/workflows/test-psalm.yml index 43931032b9e4..3f385ce858fb 100644 --- a/.github/workflows/test-psalm.yml +++ b/.github/workflows/test-psalm.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/test-random-execution.yml b/.github/workflows/test-random-execution.yml index 4f29559b2e93..e90de6bb031d 100644 --- a/.github/workflows/test-random-execution.yml +++ b/.github/workflows/test-random-execution.yml @@ -168,7 +168,7 @@ jobs: -Q "CREATE DATABASE test COLLATE Latin1_General_100_CS_AS_SC_UTF8" - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false fetch-depth: 0 diff --git a/.github/workflows/test-rector.yml b/.github/workflows/test-rector.yml index 4f52dfe53681..2fa9b74c0e02 100644 --- a/.github/workflows/test-rector.yml +++ b/.github/workflows/test-rector.yml @@ -53,7 +53,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup PHP uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 diff --git a/.github/workflows/test-scss.yml b/.github/workflows/test-scss.yml index d501671bcd98..0000e700a422 100644 --- a/.github/workflows/test-scss.yml +++ b/.github/workflows/test-scss.yml @@ -33,7 +33,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 diff --git a/.github/workflows/test-structarmed.yml b/.github/workflows/test-structarmed.yml index 296b4d22870a..1c6d731c9e4b 100644 --- a/.github/workflows/test-structarmed.yml +++ b/.github/workflows/test-structarmed.yml @@ -51,7 +51,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup PHP uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 diff --git a/.github/workflows/test-userguide.yml b/.github/workflows/test-userguide.yml index 1dd382cd069b..98b19fc1e1b4 100644 --- a/.github/workflows/test-userguide.yml +++ b/.github/workflows/test-userguide.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 From ea14a55f1cdd3f4019436efe2dbd0226929d6ea9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:26:54 +0200 Subject: [PATCH 027/163] chore(deps): bump actions/labeler in / (#10424) Bumps [actions/labeler](https://github.com/actions/labeler) in `/` from 6.2.0 to 7.0.0. Updates `actions/labeler` from 6.2.0 to 7.0.0 - [Release notes](https://github.com/actions/labeler/releases) - [Commits](https://github.com/actions/labeler/compare/b8dd2d9be0f68b860e7dae5dae7d772984eacd6d...bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13) --- updated-dependencies: - dependency-name: actions/labeler dependency-version: 7.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/label-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml index 1a253ee5856c..bb84e2ad3504 100644 --- a/.github/workflows/label-pr.yml +++ b/.github/workflows/label-pr.yml @@ -42,6 +42,6 @@ jobs: console.log('No changes to workflow file detected, proceeding with label addition.'); - name: Add labels - uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0 + uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0 with: sync-labels: true # Remove labels when matching files are reverted From 3086d65d5e2e424d8a86b90edcb9d700dfa44642 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 22:47:15 +0700 Subject: [PATCH 028/163] chore(deps-dev): update boundwize/structarmed requirement (#10426) Updates the requirements on [boundwize/structarmed](https://github.com/boundwize/structarmed) to permit the latest version. Updates `boundwize/structarmed` to 0.15.2 - [Release notes](https://github.com/boundwize/structarmed/releases) - [Commits](https://github.com/boundwize/structarmed/compare/0.15.1...0.15.2) --- updated-dependencies: - dependency-name: boundwize/structarmed dependency-version: 0.15.2 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 4826d42cbe6d..3c8c26443867 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "0.15.1", + "boundwize/structarmed": "0.15.2", "codeigniter/phpstan-codeigniter": "^2.1", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", From 70635354082e0e6737a3372dcc1d430bf202ac1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:02:01 +0200 Subject: [PATCH 029/163] chore(deps): bump actions/setup-python in / (#10421) Bumps [actions/setup-python](https://github.com/actions/setup-python) in `/` from 6.3.0 to 7.0.0. Updates `actions/setup-python` from 6.3.0 to 7.0.0 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/ece7cb06caefa5fff74198d8649806c4678c61a1...5fda3b95a4ea91299a34e894583c3862153e4b97) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: 7.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 | 2 +- .github/workflows/deploy-userguide-latest.yml | 2 +- .github/workflows/test-userguide.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-distributables.yml b/.github/workflows/deploy-distributables.yml index 61cd6c65fc33..1eeb86f772cb 100644 --- a/.github/workflows/deploy-distributables.yml +++ b/.github/workflows/deploy-distributables.yml @@ -172,7 +172,7 @@ jobs: persist-credentials: false - name: Setup Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.12' diff --git a/.github/workflows/deploy-userguide-latest.yml b/.github/workflows/deploy-userguide-latest.yml index 80b8ebe369cd..c943254325d6 100644 --- a/.github/workflows/deploy-userguide-latest.yml +++ b/.github/workflows/deploy-userguide-latest.yml @@ -36,7 +36,7 @@ jobs: coverage: none - name: Setup Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.12' diff --git a/.github/workflows/test-userguide.yml b/.github/workflows/test-userguide.yml index 98b19fc1e1b4..88f316888f02 100644 --- a/.github/workflows/test-userguide.yml +++ b/.github/workflows/test-userguide.yml @@ -27,7 +27,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.12' From aa7f6bb1be6d15ac935c0ba5c3bd8072a38a0578 Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Wed, 22 Jul 2026 18:30:53 +0200 Subject: [PATCH 030/163] fix: remove destructive setServerArray([]) in FiltersTest::setUp to prevent random-order test failures (#10425) --- tests/system/Filters/FiltersTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php index b632df217043..11567f429c1d 100644 --- a/tests/system/Filters/FiltersTest.php +++ b/tests/system/Filters/FiltersTest.php @@ -68,7 +68,6 @@ protected function setUp(): void ]; service('autoloader')->addNamespace($defaults); - service('superglobals')->setServerArray([]); Services::injectMock('superglobals', new Superglobals()); $this->response = service('response'); From 2a300437b076d35453005aecb09aecdff1679e2f Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Wed, 22 Jul 2026 19:20:26 +0200 Subject: [PATCH 031/163] fix: resolve TypeError in CLIRequest::parseCommand when argv is missing (#10415) --- system/HTTP/CLIRequest.php | 2 +- tests/system/HTTP/CLIRequestTest.php | 11 +++++++++++ user_guide_src/source/changelogs/v4.7.5.rst | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/system/HTTP/CLIRequest.php b/system/HTTP/CLIRequest.php index 453272a4f59a..67c6ad4c1c7c 100644 --- a/system/HTTP/CLIRequest.php +++ b/system/HTTP/CLIRequest.php @@ -185,7 +185,7 @@ public function getOptionString(bool $useLongOpts = false): string */ protected function parseCommand() { - $args = $this->getServer('argv'); + $args = $this->getServer('argv') ?? []; array_shift($args); // Scrap index.php $optionValue = false; diff --git a/tests/system/HTTP/CLIRequestTest.php b/tests/system/HTTP/CLIRequestTest.php index 0bf3da0a6848..d12889448a30 100644 --- a/tests/system/HTTP/CLIRequestTest.php +++ b/tests/system/HTTP/CLIRequestTest.php @@ -574,4 +574,15 @@ public function testIs(): void { $this->assertFalse($this->request->is('get')); } + + public function testParseCommandWithMissingArgv(): void + { + Services::injectMock('superglobals', new Superglobals([], [], [], [], [])); + + $request = new CLIRequest(new App()); + + $this->assertSame('', $request->getPath()); + $this->assertSame([], $request->getSegments()); + $this->assertSame('', $request->getOptionString()); + } } diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index 964133046c38..c83924b0d500 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -30,6 +30,7 @@ Deprecations Bugs Fixed ********** +- **CLIRequest:** Fixed a bug where ``parseCommand()`` could throw a TypeError when ``argv`` is missing. - **Content Security Policy:** Fixed a bug where empty ``Content-Security-Policy``, ``Content-Security-Policy-Report-Only``, and ``Reporting-Endpoints`` response headers were generated when no corresponding values existed. - **Logger:** Fixed a bug where interpolating a log message with array or non-stringable context values could raise PHP warnings or errors. From 99ef7c8a0ae259088b3153feeb53d56c74695f7d Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Wed, 22 Jul 2026 19:21:27 +0200 Subject: [PATCH 032/163] fix: resolve race conditions in Redis TTL and prevent cache test state leakage (#10374) --- .github/scripts/random-tests-config.txt | 2 +- .../system/Cache/Handlers/ApcuHandlerTest.php | 22 ++++++---------- .../system/Cache/Handlers/FileHandlerTest.php | 25 +++---------------- .../Cache/Handlers/MemcachedHandlerTest.php | 18 +++---------- .../Cache/Handlers/PredisHandlerTest.php | 18 +++---------- .../Cache/Handlers/RedisHandlerTest.php | 18 +++---------- 6 files changed, 24 insertions(+), 79 deletions(-) diff --git a/.github/scripts/random-tests-config.txt b/.github/scripts/random-tests-config.txt index 79a74b5f9ef2..d1a9060780a4 100644 --- a/.github/scripts/random-tests-config.txt +++ b/.github/scripts/random-tests-config.txt @@ -11,7 +11,7 @@ API AutoReview Autoloader -# Cache +Cache CLI Commands Config diff --git a/tests/system/Cache/Handlers/ApcuHandlerTest.php b/tests/system/Cache/Handlers/ApcuHandlerTest.php index 5d5d5f7b2f6f..91176524d038 100644 --- a/tests/system/Cache/Handlers/ApcuHandlerTest.php +++ b/tests/system/Cache/Handlers/ApcuHandlerTest.php @@ -27,30 +27,24 @@ #[RequiresPhpExtension('apcu')] final class ApcuHandlerTest extends AbstractHandlerTestCase { - /** - * @return list - */ - private static function getKeyArray(): array - { - return [ - self::$key1, - self::$key2, - self::$key3, - ]; - } - protected function setUp(): void { parent::setUp(); + if (! function_exists('apcu_enabled') || ! apcu_enabled()) { + $this->markTestSkipped('APCu extension is not loaded or not enabled in CLI.'); + } + $this->handler = CacheFactory::getHandler(new Cache(), 'apcu'); } protected function tearDown(): void { - foreach (self::getKeyArray() as $key) { - $this->handler->delete($key); + if (isset($this->handler)) { + $this->handler->clean(); } + + parent::tearDown(); } public function testNew(): void diff --git a/tests/system/Cache/Handlers/FileHandlerTest.php b/tests/system/Cache/Handlers/FileHandlerTest.php index c4fa8481bbd4..ee26783ad992 100644 --- a/tests/system/Cache/Handlers/FileHandlerTest.php +++ b/tests/system/Cache/Handlers/FileHandlerTest.php @@ -31,18 +31,6 @@ final class FileHandlerTest extends AbstractHandlerTestCase private static string $directory = 'FileHandler'; private Cache $config; - /** - * @return list - */ - private static function getKeyArray(): array - { - return [ - self::$key1, - self::$key2, - self::$key3, - ]; - } - protected function setUp(): void { parent::setUp(); @@ -69,15 +57,8 @@ protected function tearDown(): void if (is_dir($this->config->file['storePath'])) { chmod($this->config->file['storePath'], 0777); - foreach (self::getKeyArray() as $key) { - if (is_file($this->config->file['storePath'] . DIRECTORY_SEPARATOR . $key)) { - chmod($this->config->file['storePath'] . DIRECTORY_SEPARATOR . $key, 0777); - unlink($this->config->file['storePath'] . DIRECTORY_SEPARATOR . $key); - } - if (is_file($this->config->file['storePath'] . DIRECTORY_SEPARATOR . $this->config->prefix . $key)) { - chmod($this->config->file['storePath'] . DIRECTORY_SEPARATOR . $this->config->prefix . $key, 0777); - unlink($this->config->file['storePath'] . DIRECTORY_SEPARATOR . $this->config->prefix . $key); - } + if (isset($this->handler)) { + $this->handler->clean(); } rmdir($this->config->file['storePath']); @@ -118,7 +99,7 @@ public function testSetDefaultPath(): void */ public function testGet(): void { - $this->handler->save(self::$key1, 'value', 2); + $this->assertTrue($this->handler->save(self::$key1, 'value', 2)); $this->assertSame('value', $this->handler->get(self::$key1)); $this->assertNull($this->handler->get(self::$dummy)); diff --git a/tests/system/Cache/Handlers/MemcachedHandlerTest.php b/tests/system/Cache/Handlers/MemcachedHandlerTest.php index e6bd5dd147ec..78412616e5bf 100644 --- a/tests/system/Cache/Handlers/MemcachedHandlerTest.php +++ b/tests/system/Cache/Handlers/MemcachedHandlerTest.php @@ -26,18 +26,6 @@ #[Group('CacheLive')] final class MemcachedHandlerTest extends AbstractHandlerTestCase { - /** - * @return list - */ - private static function getKeyArray(): array - { - return [ - self::$key1, - self::$key2, - self::$key3, - ]; - } - protected function setUp(): void { parent::setUp(); @@ -51,9 +39,11 @@ protected function setUp(): void protected function tearDown(): void { - foreach (self::getKeyArray() as $key) { - $this->handler->delete($key); + if (isset($this->handler)) { + $this->handler->clean(); } + + parent::tearDown(); } public function testNew(): void diff --git a/tests/system/Cache/Handlers/PredisHandlerTest.php b/tests/system/Cache/Handlers/PredisHandlerTest.php index 135d3ff083de..91949f53238f 100644 --- a/tests/system/Cache/Handlers/PredisHandlerTest.php +++ b/tests/system/Cache/Handlers/PredisHandlerTest.php @@ -27,18 +27,6 @@ final class PredisHandlerTest extends AbstractHandlerTestCase { private Cache $config; - /** - * @return list - */ - private static function getKeyArray(): array - { - return [ - self::$key1, - self::$key2, - self::$key3, - ]; - } - protected function setUp(): void { parent::setUp(); @@ -49,9 +37,11 @@ protected function setUp(): void protected function tearDown(): void { - foreach (self::getKeyArray() as $key) { - $this->handler->delete($key); + if (isset($this->handler)) { + $this->handler->clean(); } + + parent::tearDown(); } public function testNew(): void diff --git a/tests/system/Cache/Handlers/RedisHandlerTest.php b/tests/system/Cache/Handlers/RedisHandlerTest.php index d42123c6dd82..1f85ecc28349 100644 --- a/tests/system/Cache/Handlers/RedisHandlerTest.php +++ b/tests/system/Cache/Handlers/RedisHandlerTest.php @@ -28,18 +28,6 @@ final class RedisHandlerTest extends AbstractHandlerTestCase { private Cache $config; - /** - * @return list - */ - private static function getKeyArray(): array - { - return [ - self::$key1, - self::$key2, - self::$key3, - ]; - } - protected function setUp(): void { parent::setUp(); @@ -54,9 +42,11 @@ protected function setUp(): void protected function tearDown(): void { - foreach (self::getKeyArray() as $key) { - $this->handler->delete($key); + if (isset($this->handler)) { + $this->handler->clean(); } + + parent::tearDown(); } public function testNew(): void From c542e4155696ef7e913520a8e4aacbcf5fd2dfb8 Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Wed, 22 Jul 2026 19:24:39 +0200 Subject: [PATCH 033/163] test: Increase Email test coverage and enable component in random tests (#10411) --- .github/scripts/random-tests-config.txt | 2 +- tests/system/Email/EmailTest.php | 475 ++++++++++++++++++++++++ 2 files changed, 476 insertions(+), 1 deletion(-) diff --git a/.github/scripts/random-tests-config.txt b/.github/scripts/random-tests-config.txt index d1a9060780a4..5bf0fce66733 100644 --- a/.github/scripts/random-tests-config.txt +++ b/.github/scripts/random-tests-config.txt @@ -20,7 +20,7 @@ Cookie # DataConverter # Database # Debug -# Email +Email # Encryption # Entity Events diff --git a/tests/system/Email/EmailTest.php b/tests/system/Email/EmailTest.php index 9ca5c5a51a4a..319504237f08 100644 --- a/tests/system/Email/EmailTest.php +++ b/tests/system/Email/EmailTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Email; +use Closure; use CodeIgniter\Events\Events; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockEmail; @@ -409,4 +410,478 @@ public function testPrepQuotedPrintableUnwrapRemovesTagsOnly(): void $this->assertSame('keep =7Bbraces=7D', $prepQP('keep {braces}')); $this->assertSame('keep (parentheses)', $prepQP('keep (parentheses)')); } + + public function testBuildMessagePlain(): void + { + $email = new Email(); + $email->setMessage('This is a simple plain text message.'); + $email->setMailType('text'); + + $buildMessage = self::getPrivateMethodInvoker($email, 'buildMessage'); + $buildMessage(); + + $headerStr = $this->getPrivateProperty($email, 'headerStr'); + $finalBody = $this->getPrivateProperty($email, 'finalBody'); + $this->assertStringContainsString('This is a simple plain text message.', (string) $finalBody); + $this->assertStringContainsString('Content-Type: text/plain', (string) $headerStr); + } + + public function testBuildMessageHtmlNoMultipart(): void + { + $email = new Email(); + $email->setMessage('

This is a HTML message.

'); + $email->setMailType('html'); + $this->setPrivateProperty($email, 'sendMultipart', false); + + $buildMessage = self::getPrivateMethodInvoker($email, 'buildMessage'); + $buildMessage(); + + $headerStr = $this->getPrivateProperty($email, 'headerStr'); + $finalBody = $this->getPrivateProperty($email, 'finalBody'); + $this->assertStringContainsString('

This is a HTML message.

', (string) $finalBody); + $this->assertStringContainsString('Content-Type: text/html', (string) $headerStr); + $this->assertStringNotContainsString('multipart/alternative', (string) $headerStr); + } + + public function testBuildMessageHtmlMultipart(): void + { + $email = new Email(); + $email->setMessage('

This is a HTML message.

'); + $email->setMailType('html'); + $this->setPrivateProperty($email, 'sendMultipart', true); + $email->setAltMessage('This is alternative text.'); + + $buildMessage = self::getPrivateMethodInvoker($email, 'buildMessage'); + $buildMessage(); + + $headerStr = $this->getPrivateProperty($email, 'headerStr'); + $finalBody = $this->getPrivateProperty($email, 'finalBody'); + $this->assertStringContainsString('

This is a HTML message.

', (string) $finalBody); + $this->assertStringContainsString('Content-Type: multipart/alternative', (string) $headerStr); + $this->assertStringContainsString('This is alternative text.', (string) $finalBody); + } + + public function testBuildMessagePlainWithAttachments(): void + { + $email = new Email(); + $email->setMessage('Plain text with attachments.'); + $email->attach(SUPPORTPATH . 'Images/ci-logo.png'); + + $buildMessage = self::getPrivateMethodInvoker($email, 'buildMessage'); + $buildMessage(); + + $headerStr = $this->getPrivateProperty($email, 'headerStr'); + $finalBody = $this->getPrivateProperty($email, 'finalBody'); + $this->assertStringContainsString('Content-Type: multipart/mixed', (string) $headerStr); + $this->assertStringContainsString('Content-Type: image/png', (string) $finalBody); + $this->assertStringContainsString('Content-Disposition: attachment', (string) $finalBody); + } + + public function testWordWrap(): void + { + $email = new Email(); + $email->setNewline(); + + $text = 'This is a very long line of text that should be wrapped to a smaller character limit.'; + $wrapped = $email->wordWrap($text, 20); + + // Verify that long text gets wrapped at roughly 20 chars + $lines = explode("\n", trim($wrapped)); + + foreach ($lines as $line) { + $this->assertLessThanOrEqual(20, strlen($line)); + } + + // Verify unwrap behavior + $unwrapText = 'This is {unwrap}a very long word or link that should not be wrapped under any circumstances{/unwrap} even if it exceeds the limit.'; + $wrappedUnwrap = $email->wordWrap($unwrapText, 20); + $this->assertStringContainsString('a very long word or link that should not be wrapped under any circumstances', $wrappedUnwrap); + } + + public function testCleanEmail(): void + { + $email = new Email(); + + // Test with string input + $this->assertSame('john@example.com', $email->cleanEmail('John Doe ')); + $this->assertSame('simple@example.com', $email->cleanEmail('simple@example.com')); + + // Test with array input + $emails = [ + 'John Doe ', + 'simple@example.com', + '', + ]; + $expected = [ + 'john@example.com', + 'simple@example.com', + 'jane@example.com', + ]; + $this->assertSame($expected, $email->cleanEmail($emails)); + } + + public function testValidateEmail(): void + { + $email = new Email(); + + // Must be array + $this->assertFalse($email->validateEmail('string@example.com')); + + // Valid array + $this->assertTrue($email->validateEmail(['test@example.com', 'another@example.com'])); + + // Invalid array + $this->assertFalse($email->validateEmail(['test@example.com', 'invalid-email'])); + } + + public function testIsValidEmail(): void + { + $email = new Email(); + + $this->assertTrue($email->isValidEmail('test@example.com')); + $this->assertFalse($email->isValidEmail('invalid-email')); + $this->assertFalse($email->isValidEmail('test@example')); + + // IDN domain (internationalized) + if (function_exists('idn_to_ascii') && defined('INTL_IDNA_VARIANT_UTS46')) { + $this->assertTrue($email->isValidEmail('user@müller.de')); + } + } + + public function testClear(): void + { + $email = new Email(); + $email->setTo('test@example.com'); + $email->setSubject('My Subject'); + $email->setMessage('Hello'); + $email->attach(SUPPORTPATH . 'Images/ci-logo.png'); + + // Verify state before clear + $headers = $this->getPrivateProperty($email, 'headers'); + $this->assertSame('=?UTF-8?Q?My=20Subject?=', $headers['Subject']); + $this->assertNotEmpty($this->getPrivateProperty($email, 'attachments')); + + // Clear without clearing attachments + $email->clear(false); + $headersAfter = $this->getPrivateProperty($email, 'headers'); + $this->assertCount(1, $headersAfter); + $this->assertArrayHasKey('Date', $headersAfter); + $this->assertNotEmpty($this->getPrivateProperty($email, 'attachments')); + + // Clear with clearing attachments + $email->clear(true); + $this->assertEmpty($this->getPrivateProperty($email, 'attachments')); + } + + public function testSetters(): void + { + $email = new Email(); + + $email->setMailType('html'); + $this->assertSame('html', $this->getPrivateProperty($email, 'mailType')); + + $email->setWordWrap(false); + $this->assertFalse($this->getPrivateProperty($email, 'wordWrap')); + + $email->setProtocol('smtp'); + $this->assertSame('smtp', $this->getPrivateProperty($email, 'protocol')); + + $email->setPriority(1); + $this->assertSame(1, $this->getPrivateProperty($email, 'priority')); + + $email->setNewline("\r\n"); + $this->assertSame("\r\n", $this->getPrivateProperty($email, 'newline')); + + $email->setCRLF("\r\n"); + $this->assertSame("\r\n", $this->getPrivateProperty($email, 'CRLF')); + } + + public function testSetFrom(): void + { + $email = new Email(); + $email->setFrom('john@example.com', 'John Doe'); + + $headers = $this->getPrivateProperty($email, 'headers'); + $this->assertSame('"John Doe" ', $headers['From']); + $this->assertSame('', $headers['Return-Path']); + + // Testing Q Encoding + $email->setFrom('john@example.com', 'Jöhn Døe'); + $headersQ = $this->getPrivateProperty($email, 'headers'); + $this->assertStringContainsString('=?UTF-8?Q?', (string) $headersQ['From']); + } + + public function testSetReplyTo(): void + { + $email = new Email(); + $email->setReplyTo('support@example.com', 'Support Team'); + + $headers = $this->getPrivateProperty($email, 'headers'); + $this->assertSame('"Support Team" ', $headers['Reply-To']); + $this->assertTrue($this->getPrivateProperty($email, 'replyToFlag')); + } + + public function testSetToCCBCC(): void + { + $email = new Email(); + $email->setProtocol('smtp'); + + $email->setTo('one@example.com, two@example.com'); + $this->assertSame(['one@example.com', 'two@example.com'], $this->getPrivateProperty($email, 'recipients')); + + $email->setCC('cc@example.com'); + $this->assertSame(['cc@example.com'], $this->getPrivateProperty($email, 'CCArray')); + + $email->setBCC('bcc@example.com'); + $this->assertSame(['bcc@example.com'], $this->getPrivateProperty($email, 'BCCArray')); + } + + public function testAttach(): void + { + $email = new Email(); + $email->attach(SUPPORTPATH . 'Images/ci-logo.png', 'attachment', 'new-name.png'); + + $attachments = $this->getPrivateProperty($email, 'attachments'); + $this->assertCount(1, $attachments); + $this->assertSame('new-name.png', $attachments[0]['name'][1]); + $this->assertSame('attachment', $attachments[0]['disposition']); + $this->assertSame('image/png', $attachments[0]['type']); + } + + public function testPrintDebugger(): void + { + $email = new Email(); + $this->setPrivateProperty($email, 'headerStr', 'To: recipient@example.com'); + $this->setPrivateProperty($email, 'subject', 'Debug Test'); + $this->setPrivateProperty($email, 'finalBody', 'Testing debugger'); + $this->setPrivateProperty($email, 'debugMessage', ['SMTP success message
']); + + $debug = $email->printDebugger(); + $this->assertStringContainsString('SMTP success message', $debug); + $this->assertStringContainsString('Debug Test', $debug); + $this->assertStringContainsString('recipient@example.com', $debug); + } + + public function testSettersFallback(): void + { + $email = new Email(); + + // Invalid newline falls back to "\n" + $email->setNewline('invalid'); + $this->assertSame("\n", $this->getPrivateProperty($email, 'newline')); + + // Invalid CRLF falls back to "\n" + $email->setCRLF('invalid'); + $this->assertSame("\n", $this->getPrivateProperty($email, 'CRLF')); + } + + public function testGetMessageID(): void + { + $email = new Email(); + $email->setFrom('john@example.com'); + + $getMessageID = self::getPrivateMethodInvoker($email, 'getMessageID'); + $messageID = $getMessageID(); + + $this->assertStringStartsWith('<', $messageID); + $this->assertStringEndsWith('>', $messageID); + $this->assertStringContainsString('@example.com', $messageID); + } + + public function testValidateEmailForShell(): void + { + $email = new Email(); + + $validateForShell = Closure::bind(fn (&$email) => $this->validateEmailForShell($email), $email, Email::class); + + $address = 'test@example.com'; + $this->assertTrue($validateForShell($address)); + + $invalidAddress = 'invalid-email-address'; + $this->assertFalse($validateForShell($invalidAddress)); + + $shellInjection = 'test@example.com; rm -rf /'; + $this->assertFalse($validateForShell($shellInjection)); + } + + public function testSetPriorityFallback(): void + { + $email = new Email(); + + // Priority 6 is invalid, falls back to 3 + $email->setPriority(6); + $this->assertSame(3, $this->getPrivateProperty($email, 'priority')); + + // Priority 0 is invalid, falls back to 3 + $email->setPriority(0); + $this->assertSame(3, $this->getPrivateProperty($email, 'priority')); + } + + public function testSMTPAuthenticateValidation(): void + { + $email = new Email(); + + $smtpAuthenticate = self::getPrivateMethodInvoker($email, 'SMTPAuthenticate'); + + // SMTPAuth false returns true immediately + $this->setPrivateProperty($email, 'SMTPAuth', false); + $this->assertTrue($smtpAuthenticate()); + + // SMTPAuth true but empty credentials returns false + $this->setPrivateProperty($email, 'SMTPAuth', true); + $this->setPrivateProperty($email, 'SMTPUser', ''); + $this->setPrivateProperty($email, 'SMTPPass', ''); + $this->assertFalse($smtpAuthenticate()); + + // Invalid SMTP auth method returns false + $this->setPrivateProperty($email, 'SMTPUser', 'user'); + $this->setPrivateProperty($email, 'SMTPPass', 'pass'); + $this->setPrivateProperty($email, 'SMTPAuthMethod', 'oauth'); + $this->assertFalse($smtpAuthenticate()); + } + + public function testAttachMissingFile(): void + { + $email = new Email(); + $this->assertFalse($email->attach('/path/to/nonexistent/file.txt')); + } + + public function testSetAttachmentCID(): void + { + $email = new Email(); + + // Attach from buffer + $email->attach('file_content', 'inline', 'photo.jpg', 'image/jpeg'); + + $cid = $email->setAttachmentCID('photo.jpg'); + $this->assertIsString($cid); + $this->assertStringContainsString('photo.jpg@', $cid); + + // Non-existent file/buffer name + $this->assertFalse($email->setAttachmentCID('nonexistent.jpg')); + } + + public function testSmtpConnectAlreadyConnected(): void + { + $email = new Email(); + $this->setPrivateProperty($email, 'SMTPConnect', fopen('php://memory', 'r+b')); + + $smtpConnect = self::getPrivateMethodInvoker($email, 'SMTPConnect'); + $this->assertTrue($smtpConnect()); + } + + public function testBuildMessageHtmlWithInlineAttachments(): void + { + $email = new Email(); + $email->setMailType('html'); + $email->setFrom('sender@example.com'); + $email->setTo('recipient@example.com'); + $email->setSubject('Inline Test'); + $email->setMessage('

Test HTML

'); + $email->attach('image_data', 'inline', 'image.jpg', 'image/jpeg'); + $email->setAttachmentCID('image.jpg'); + + $buildMessage = self::getPrivateMethodInvoker($email, 'buildMessage'); + $buildMessage(); + + $body = $this->getPrivateProperty($email, 'finalBody'); + $this->assertStringContainsString('Content-Disposition: inline', (string) $body); + $this->assertStringContainsString('Content-ID:', (string) $body); + } + + public function testMimeTypesUnknown(): void + { + $email = new Email(); + $mimeTypes = self::getPrivateMethodInvoker($email, 'mimeTypes'); + $this->assertSame('application/x-unknown-content-type', $mimeTypes('invalid_ext')); + } + + public function testAttachRealFileDetectMime(): void + { + $email = new Email(); + $this->assertNotFalse($email->attach(__FILE__)); + + $attachments = $this->getPrivateProperty($email, 'attachments'); + $this->assertCount(1, $attachments); + $this->assertSame('application/x-php', $attachments[0]['type']); + } + + public function testValidateEmailIdn(): void + { + $email = new Email(); + $validateEmail = self::getPrivateMethodInvoker($email, 'validateEmail'); + $address = ['test@przykłady.pl']; + $this->assertTrue($validateEmail($address)); + } + + public function testSetSubjectPreventsHeaderInjection(): void + { + $email = new Email(); + $email->setSubject("Test Subject\r\nBcc: spy@example.com"); + + $headers = $this->getPrivateProperty($email, 'headers'); + $this->assertStringNotContainsString('Bcc: spy@example.com', (string) $headers['Subject']); + } + + public function testSetSubjectExtremelyLongUtf8(): void + { + $email = new Email(); + $subject = str_repeat('Zażółć gęślą jaźń ', 10); + $email->setSubject($subject); + + $headers = $this->getPrivateProperty($email, 'headers'); + $this->assertStringStartsWith('=?UTF-8?Q?', $headers['Subject']); + } + + public function testAttachWithUtf8Filename(): void + { + $email = new Email(); + $email->attach(SUPPORTPATH . 'Images/ci-logo.png', 'attachment', 'żółć-logo.png'); + + $attachments = $this->getPrivateProperty($email, 'attachments'); + $this->assertSame('żółć-logo.png', $attachments[0]['name'][1]); + + $buildMessage = self::getPrivateMethodInvoker($email, 'buildMessage'); + $buildMessage(); + + $finalBody = $this->getPrivateProperty($email, 'finalBody'); + $this->assertStringContainsString('name="żółć-logo.png"', (string) $finalBody); + } + + public function testGetProtocolFallback(): void + { + $email = new Email(); + $email->setProtocol('invalid'); + $getProtocol = self::getPrivateMethodInvoker($email, 'getProtocol'); + $this->assertSame('mail', $getProtocol()); + } + + public function testSetBccBatchMode(): void + { + $email = new Email(); + $email->setBCC('bcc@example.com', '10'); + $this->assertTrue($this->getPrivateProperty($email, 'BCCBatchMode')); + $this->assertSame('10', $this->getPrivateProperty($email, 'BCCBatchSize')); + } + + public function testSettersWithArray(): void + { + $email = new Email(); + $email->setTo(['one@example.com', 'two@example.com']); + $this->assertSame(['one@example.com', 'two@example.com'], $this->getPrivateProperty($email, 'recipients')); + } + + public function testSetFromAndReplyToWithValidation(): void + { + $config = config('Email'); + $config->validate = true; + $email = new Email($config); + + $email->setFrom('john@example.com', 'Jöhn Døe'); + $headers = $this->getPrivateProperty($email, 'headers'); + $this->assertStringContainsString('=?UTF-8?Q?', (string) $headers['From']); + + $email->setReplyTo('support@example.com', 'Jöhn Døe'); + $this->assertTrue($this->getPrivateProperty($email, 'replyToFlag')); + } } From bfa7a4942eae115322f867608563d2d3d73c59ad Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Sat, 25 Jul 2026 18:54:56 +0700 Subject: [PATCH 034/163] refactor: Enable Rector Code Quality set (#10429) --- rector.php | 11 +++++------ system/CLI/CLI.php | 2 +- system/Cookie/Cookie.php | 2 +- system/Pager/Pager.php | 2 +- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/rector.php b/rector.php index ba14babedae0..c3547b82f596 100644 --- a/rector.php +++ b/rector.php @@ -13,11 +13,10 @@ use Rector\Caching\ValueObject\Storage\FileCacheStorage; use Rector\CodeQuality\Rector\BooleanNot\NegatedAndsToPositiveOrsRector; -use Rector\CodeQuality\Rector\Empty_\SimplifyEmptyCheckOnEmptyArrayRector; +use Rector\CodeQuality\Rector\ClassMethod\LocallyCalledStaticMethodToNonStaticRector; use Rector\CodeQuality\Rector\FuncCall\CompactToVariablesRector; use Rector\CodeQuality\Rector\FunctionLike\SimplifyUselessVariableRector; use Rector\CodeQuality\Rector\Isset_\IssetOnPropertyObjectToPropertyExistsRector; -use Rector\CodeQuality\Rector\Ternary\TernaryEmptyArrayArrayDimFetchToCoalesceRector; use Rector\CodingStyle\Rector\ClassMethod\FuncGetArgsToVariadicParamRector; use Rector\CodingStyle\Rector\ClassMethod\MakeInheritedMethodVisibilitySameAsParentRector; use Rector\CodingStyle\Rector\FuncCall\CountArrayToEmptyArrayComparisonRector; @@ -49,13 +48,14 @@ use Rector\TypeDeclaration\Rector\Function_\AddFunctionVoidReturnTypeWhereNoReturnRector; use Rector\TypeDeclaration\Rector\Property\TypedPropertyFromAssignsRector; use Rector\TypeDeclaration\Rector\StmtsAwareInterface\DeclareStrictTypesRector; +use Rector\TypeDeclaration\Rector\StmtsAwareInterface\SafeDeclareStrictTypesRector; use Utils\Rector\PassStrictParameterToFunctionParameterRector; use Utils\Rector\RemoveErrorSuppressInTryCatchStmtsRector; use Utils\Rector\UnderscoreToCamelCaseVariableNameRector; return RectorConfig::configure() ->withPhpSets(php82: true) - ->withPreparedSets(deadCode: true, instanceOf: true, phpunitCodeQuality: true) + ->withPreparedSets(deadCode: true, codeQuality: true, instanceOf: true, phpunitCodeQuality: true) ->withComposerBased(phpunit: true) ->withParallel(120, 8, 10) ->withCache( @@ -177,6 +177,8 @@ // to be applied in separate PRs to ease review NegatedAndsToPositiveOrsRector::class, + SafeDeclareStrictTypesRector::class, + LocallyCalledStaticMethodToNonStaticRector::class, ]) // auto import fully qualified class names ->withImportNames() @@ -193,8 +195,6 @@ RemoveErrorSuppressInTryCatchStmtsRector::class, FuncGetArgsToVariadicParamRector::class, MakeInheritedMethodVisibilitySameAsParentRector::class, - SimplifyEmptyCheckOnEmptyArrayRector::class, - TernaryEmptyArrayArrayDimFetchToCoalesceRector::class, DisallowedEmptyRuleFixerRector::class, PrivatizeFinalClassPropertyRector::class, VersionCompareFuncCallToConstantRector::class, @@ -208,5 +208,4 @@ ->withConfiguredRule(RenameConstantRector::class, [ 'FILTER_DEFAULT' => 'FILTER_UNSAFE_RAW', ]) - ->withCodeQualityLevel(61) ->reportUnusedSkips(); diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index 10e39cee0ad4..aff27144aa39 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -819,7 +819,7 @@ public static function showProgress($thisStep = 1, int $totalSteps = 10) if ($thisStep !== false) { // Don't allow div by zero or negative numbers.... $thisStep = abs($thisStep); - $totalSteps = $totalSteps < 1 ? 1 : $totalSteps; + $totalSteps = max(1, $totalSteps); $percent = (int) (($thisStep / $totalSteps) * 100); $step = (int) round($percent / 10); diff --git a/system/Cookie/Cookie.php b/system/Cookie/Cookie.php index e144d84ba031..068f8772c71c 100644 --- a/system/Cookie/Cookie.php +++ b/system/Cookie/Cookie.php @@ -366,7 +366,7 @@ public function getMaxAge(): int { $maxAge = $this->expires - Time::now()->getTimestamp(); - return $maxAge >= 0 ? $maxAge : 0; + return max($maxAge, 0); } /** diff --git a/system/Pager/Pager.php b/system/Pager/Pager.php index a4883f3809ae..66b1dd13d78f 100644 --- a/system/Pager/Pager.php +++ b/system/Pager/Pager.php @@ -434,7 +434,7 @@ protected function calculateCurrentPage(string $group) $page = (int) (service('superglobals')->get($pageSelector, '1')); - $this->groups[$group]['currentPage'] = $page < 1 ? 1 : $page; + $this->groups[$group]['currentPage'] = max(1, $page); } } } From fc4312344696fbb58d611933830dbe303e4608da Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Tue, 28 Jul 2026 17:29:57 +0200 Subject: [PATCH 035/163] chore: add instructions for coding agents and Copilot review (#10428) --- .gitattributes | 1 + .github/copilot-instructions.md | 30 ++++ .github/instructions/database.instructions.md | 19 +++ .../framework-core.instructions.md | 23 +++ .github/instructions/tests.instructions.md | 21 +++ .../instructions/user-guide.instructions.md | 27 ++++ .../instructions/worker-mode.instructions.md | 38 +++++ AGENTS.md | 148 ++++++++++++++++++ 8 files changed, 307 insertions(+) create mode 100644 .github/copilot-instructions.md create mode 100644 .github/instructions/database.instructions.md create mode 100644 .github/instructions/framework-core.instructions.md create mode 100644 .github/instructions/tests.instructions.md create mode 100644 .github/instructions/user-guide.instructions.md create mode 100644 .github/instructions/worker-mode.instructions.md create mode 100644 AGENTS.md diff --git a/.gitattributes b/.gitattributes index 05fc1f38cde9..d87cc8aa8cf2 100644 --- a/.gitattributes +++ b/.gitattributes @@ -32,6 +32,7 @@ psalm-baseline.xml export-ignore psalm.xml export-ignore psalm_autoload.php export-ignore rector.php export-ignore +AGENTS.md export-ignore # source user guide user_guide_src/ export-ignore diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000000..4ad38ee3eadf --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,30 @@ +# Copilot Code Review Instructions + +Repository-wide conventions, the target-branch compatibility policy, coding +standards, and FrankenPHP Worker Mode requirements are defined in the root +`AGENTS.md`. Apply that policy in every review; this file covers review +behavior only. + +## Review priorities + +Prioritize actionable findings involving: + +- incorrect behavior or an unhandled edge case; +- backward compatibility; +- security, input validation, and context-appropriate output escaping; +- request-specific state leaking through globals, static properties, or + shared services, including under Worker Mode where one process serves + many requests; +- differences between supported database drivers (MySQLi, PostgreSQL, + SQLite3, SQLSRV, OCI8); +- missing regression tests, user-guide changes, changelog entries, or + upgrade instructions. + +## Review behavior + +- Determine the PR base branch and apply the compatibility policy from + `AGENTS.md`. If the base branch cannot be determined, assume `develop` + and state that assumption. +- Do not report formatting-only issues already enforced by PHP-CS-Fixer. +- Every finding should identify a concrete failure scenario and the + affected code. diff --git a/.github/instructions/database.instructions.md b/.github/instructions/database.instructions.md new file mode 100644 index 000000000000..b550a4a0573e --- /dev/null +++ b/.github/instructions/database.instructions.md @@ -0,0 +1,19 @@ +--- +applyTo: "system/Database/**/*.php,tests/system/Database/**/*.php" +--- + +# Database layer + +- Consider all supported drivers: MySQLi, PostgreSQL, SQLite3, SQLSRV, and + OCI8. +- Check identifier quoting, value binding, `NULL` behavior, transactions, + affected rows, return types, and platform-specific SQL. +- Do not implement driver-independent behavior in only one driver. +- When changing a base database class, inspect driver overrides and their + method signatures. +- Keep driver-independent tests separate from tests that require a live + database. +- Use the `DatabaseLive` PHPUnit group only when a real database connection is + required. +- Prefer focused tests for the affected base class and drivers. Leave the full + multi-driver matrix to GitHub Actions. diff --git a/.github/instructions/framework-core.instructions.md b/.github/instructions/framework-core.instructions.md new file mode 100644 index 000000000000..0ae53495c513 --- /dev/null +++ b/.github/instructions/framework-core.instructions.md @@ -0,0 +1,23 @@ +--- +applyTo: "system/**/*.php" +--- + +# Framework core + +- Assume public and protected symbols are extension points used by third-party + applications unless they are clearly marked internal. +- Apply the target-branch compatibility policy from the root `AGENTS.md` + before changing any extension point. +- Before changing a signature or behavior, inspect parent classes, implemented + interfaces, traits, service factories, configuration, and corresponding + tests. +- Keep file paths, class names, and namespaces aligned under `CodeIgniter\`. +- Preserve existing extension and dependency-injection points. +- Consider long-running worker environments. Do not retain request-specific + state in static properties, globals, or shared services after a request + finishes. +- Avoid introducing a framework-wide abstraction to solve a single local + problem. +- On `develop`, keep exceptions and observable side effects compatible. On a + `4.*` target, change them only as an explicit, narrow, documented breaking + enhancement with a migration path. diff --git a/.github/instructions/tests.instructions.md b/.github/instructions/tests.instructions.md new file mode 100644 index 000000000000..af2d9a96ba96 --- /dev/null +++ b/.github/instructions/tests.instructions.md @@ -0,0 +1,21 @@ +--- +applyTo: "tests/**/*.php" +--- + +# PHPUnit tests + +- Mirror the corresponding `system/` component and local test conventions + where practical. +- Add the appropriate PHPUnit `Group` attribute described in + `tests/README.md`. +- Prefer `assertSame()` and dedicated assertions over loose or generic + assertions. +- Cover the regression or behavior through a public API where possible. + Avoid testing private implementation details. +- Restore services, factories, environment variables, superglobals, handlers, + locale, time-related state, and other global state modified by a test. +- Tests must not depend on execution order or state left by another test. +- Do not replace a meaningful assertion with a weaker assertion to make a test + pass. +- After changing behavior, run the smallest relevant test file or component. + Do not run the complete test suite by default. diff --git a/.github/instructions/user-guide.instructions.md b/.github/instructions/user-guide.instructions.md new file mode 100644 index 000000000000..9b55a50ba3f1 --- /dev/null +++ b/.github/instructions/user-guide.instructions.md @@ -0,0 +1,27 @@ +--- +applyTo: "user_guide_src/**/*.rst" +--- + +# User guide + +- Preserve the existing reStructuredText structure, terminology, directives, + anchors, and surrounding writing style. +- Keep PHP examples compatible with PHP 8.2 and verify signatures and behavior + against the current framework code. +- Distinguish new behavior from existing behavior and document defaults + precisely. +- Determine the PR target branch before describing compatibility: + `develop` follows the patch-release policy, while `4.*` follows the + minor-release policy in the root `AGENTS.md`. +- Behavior changes, enhancements, deprecations, and important bug fixes may + require a changelog entry. +- Changes requiring users to modify code or configuration may require an + upgrading-guide entry. +- For an intentional compatibility break targeting `4.*`, document the old + behavior, new behavior, affected users, reason for the change, and the + smallest migration in both the minor changelog and upgrading guide. +- Keep code examples minimal but complete enough to run in a normal + CodeIgniter application. +- Leave the complete Sphinx build and documentation validation to GitHub + Actions unless the task specifically requires local documentation + validation. diff --git a/.github/instructions/worker-mode.instructions.md b/.github/instructions/worker-mode.instructions.md new file mode 100644 index 000000000000..ec6e1742ec5a --- /dev/null +++ b/.github/instructions/worker-mode.instructions.md @@ -0,0 +1,38 @@ +--- +applyTo: "system/Boot.php,system/CodeIgniter.php,system/Commands/Worker/**,system/Config/BaseService.php,system/Config/Factories.php,system/Config/Factory.php,system/Config/Services.php,system/Database/Config.php,system/Database/BaseConnection.php,system/Database/ConnectionInterface.php,system/Database/*/Connection.php,system/Events/Events.php,system/Session/**,system/Cache/**,system/HTTP/**,system/Filters/**,system/Router/**,system/Security/**,system/Debug/Toolbar.php,system/Debug/Toolbar/**,app/Config/WorkerMode.php,tests/system/Commands/Worker/**,user_guide_src/source/installation/worker_mode.rst" +--- + +# FrankenPHP Worker Mode + +Use `system/Commands/Worker/Views/frankenphp-worker.php.tpl` as the canonical +Worker Mode entry point. `worker:install` publishes this template to +`public/frankenphp-worker.php`. + +When changing bootstrap or request-lifecycle code, trace all three Worker Mode +phases: + +1. One-time process bootstrap through `Boot::bootWorker()`. +2. Per-request preparation: reconnect database and cache connections, reset + the `CodeIgniter` instance, and replace all request superglobals before + calling `$app->run()`. +3. Post-request cleanup: close the session, clean up unfinished database + transactions, reset factories and non-persistent services, clean up event + listeners and performance logs, and reset the debug toolbar. + +- Preserve the phase ordering unless the change explicitly proves a different + order is safe. +- Check every new mutable static property, singleton, shared service, event + listener, connection, handler, or global for cross-request state leakage. +- Do not preserve a service across requests merely for performance. Persistent + services must be safe to reuse and must not retain request, response, user, + route, security token, locale, or error state. +- Ensure exceptions and early exits cannot skip cleanup needed before the next + request. +- For state-isolation fixes, prefer a regression test that exercises two + sequential requests or reset cycles in the same process and proves the + second request cannot observe the first request's state. +- If the template contract changes, update focused assertions in + `tests/system/Commands/Worker/WorkerCommandsTest.php`. +- If existing installations need the new generated entry point, update the + changelog and upgrading guide and instruct users to run + `php spark worker:install --force`. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000000..208568025619 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,148 @@ +# CodeIgniter 4 — Repository Guidelines + +CodeIgniter 4 is a mature open-source PHP framework. Framework production +code lives in `system/`; its tests mirror that structure under +`tests/system/`. + +## Before making changes + +Consult the relevant sections of: + +- `contributing/pull_request.md` +- `contributing/internals.md` +- `contributing/styleguide.md` +- `tests/README.md` + +Follow existing code and tests in the affected component. Search for an +existing implementation before introducing a new abstraction or convention. + +## Target branch and compatibility policy + +Compatibility is judged against the pull request's base branch, not the +contributor's source or feature branch. When the base branch is unknown, +apply the stricter `develop` policy. + +### Base branch `develop` + +`develop` is the next patch-release line. Intentional backward-incompatible +changes are not allowed. + +- Preserve public and protected APIs, documented behavior, configuration + defaults, generated project files, exceptions, and observable side effects. +- Do not remove deprecated items or change public/protected signatures, + visibility, or interface/abstract requirements. +- A bug or security fix may correct faulty behavior but must preserve the + documented contract and ordinary valid usage. +- If a fix necessarily breaks compatibility, target the appropriate minor + branch instead. + +### Base branch `4.*` + +Minor-release lines (for example `4.9`). Small, deliberate compatibility +breaks may be accepted when they materially improve the framework or +complete the documented deprecation lifecycle. + +- Keep each break narrow, explicit, and justified; never an incidental + side effect of refactoring. +- Deprecated APIs may be removed no earlier than the second subsequent + minor release (deprecated in 4.6.x means removable in 4.8.0), with a + supported replacement. +- Every accepted break needs tests for the new behavior, a minor-version + changelog entry, and migration instructions in the upgrading guide. + +### All base branches + +- Production code must run on PHP 8.2. Do not require a newer PHP version, + even when a newer version is installed locally. +- Signatures, properties, constants, default values, exceptions, side + effects, configuration, and generated project files are all + compatibility-sensitive. +- Major-scale redesigns and ecosystem-wide migration requirements belong in + a major release, not a patch or minor release. +- Prefer the minimum useful abstraction and keep framework components as + independent as practical. +- Dependencies should be injectable. When a framework service is used as a + default, preserve the ability for applications to replace it. +- Do not add or update a Composer dependency unless the task explicitly + requires it and the change is justified. +- Do not modify code in `system/ThirdParty/`. + +## Coding standards + +- Do not add `declare(strict_types=1)` mechanically. Follow nearby code and + check the `DeclareStrictTypesRector` exclusions in `rector.php`; PHP files + use strict types unless excluded there. Treat the existing exclusions as + intentional compatibility constraints. Do not remove an exclusion without + dedicated justification and tests. +- Every new class property must have a native type declaration. Use the + most precise type supported by PHP 8.2; use `mixed` only when the + property intentionally accepts unrelated types. +- Do not add or change a type on an existing public or protected property + mechanically. Property types affect inheritance and must follow the + target-branch compatibility policy. +- Add PHPDoc only when it contributes information that native types cannot + express. Do not duplicate a parent or interface docblock. +- Never suppress a static-analysis error or update a baseline to make a + check pass. + +## FrankenPHP Worker Mode + +In Worker Mode one process serves many requests. The entry point's source +template is `system/Commands/Worker/Views/frankenphp-worker.php.tpl`; the +`worker:install` command publishes it as `public/frankenphp-worker.php`. + +- For every change affecting application bootstrap, request handling, + response sending, shutdown, superglobals, sessions, database or cache + connections, services, factories, events, toolbar state, static state, or + other request-lifecycle behavior, inspect the worker entry-point template + even when it is not part of the diff. +- Compare traditional per-process execution with worker execution, where + the framework boots once and handles multiple requests in the same + process. +- Classify mutable state as process-lifetime, intentionally persistent, or + request-specific. Request-specific state must be refreshed or reset for + every request and must not leak into the next request. +- Preserve the ordering requirements between per-request reconnection, + framework reset, superglobal refresh, application execution, and + post-request cleanup. +- Treat the template as the source of truth. Do not edit a generated + `public/frankenphp-worker.php` in place. +- When a release changes the template in a way existing installations must + receive, add an upgrading instruction telling Worker Mode users to + republish it with `php spark worker:install --force`. + +## Tests + +- Every bug fix should include a regression test that fails without the + fix. +- Test failure paths, exceptions, boundary conditions, and state cleanup, + not only the happy path. +- Prefer strict and dedicated PHPUnit assertions as described in the + project style guide. +- Do not weaken or remove an existing test unless the documented behavior + or specification changed. + +## Documentation + +- Public API, behavior, message, or default-value changes may require an + update to the user guide and changelog. +- Changes requiring user action, including configuration changes, may also + require an upgrading-guide entry. +- For a `4.*` target, every intentional compatibility break must be + documented in both the minor-version changelog and its upgrading guide. + +## Focused validation + +GitHub Actions is the authoritative source for full validation. Do not +reproduce the complete CI matrix locally. + +- Run only the narrowest PHPUnit test file or component that covers a code + change, for example + `vendor/bin/phpunit tests/system//Test.php`. +- Run a file-scoped analysis or formatting check only when it is quick and + directly relevant. +- Do not run the complete PHPUnit suite, `composer phpstan:check`, + `composer cs`, Psalm, Structarmed, or a repository-wide Rector analysis + by default. +- Report which focused checks were executed and which validation was left + to CI. From 977c8d7596fd8577f8db603f62dc48cd4893d4a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Jul 2026 17:35:26 +0200 Subject: [PATCH 036/163] chore(deps-dev): bump the composer-dependencies group with 2 updates (#10430) Updates the requirements on [boundwize/structarmed](https://github.com/boundwize/structarmed) and [rector/rector](https://github.com/rectorphp/rector) to permit the latest version. Updates `boundwize/structarmed` to 0.15.3 - [Release notes](https://github.com/boundwize/structarmed/releases) - [Commits](https://github.com/boundwize/structarmed/compare/0.15.2...0.15.3) Updates `rector/rector` to 2.5.8 - [Release notes](https://github.com/rectorphp/rector/releases) - [Commits](https://github.com/rectorphp/rector/compare/2.5.7...2.5.8) --- updated-dependencies: - dependency-name: boundwize/structarmed dependency-version: 0.15.3 dependency-type: direct:development dependency-group: composer-dependencies - dependency-name: rector/rector dependency-version: 2.5.8 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 3c8c26443867..fc745e939531 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "0.15.2", + "boundwize/structarmed": "0.15.3", "codeigniter/phpstan-codeigniter": "^2.1", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", @@ -29,7 +29,7 @@ "phpunit/phpcov": "^9.0.2 || ^10.0", "phpunit/phpunit": "^10.5.16 || ^11.2", "predis/predis": "^3.0", - "rector/rector": "2.5.7", + "rector/rector": "2.5.8", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { From 55f9876e5f352f39b17eb2ec3b48aac60a692ebb Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Wed, 29 Jul 2026 03:04:52 +0200 Subject: [PATCH 037/163] chore: update PHPStan baseline and Rector config (#10431) * fix phpstan * fix rector --- rector.php | 12 ++++++++++ system/Config/Services.php | 1 - system/Router/RouteCollection.php | 4 ++-- tests/system/AutoReview/FrameworkCodeTest.php | 1 - tests/system/Database/Live/GetNumRowsTest.php | 22 ------------------- .../Database/Migrations/MigrationTest.php | 5 ----- tests/system/Models/GeneralModelTest.php | 5 ----- tests/system/Models/LiveModelTestCase.php | 5 ----- tests/system/Test/TestResponseTest.php | 5 ----- utils/phpstan-baseline/empty.notAllowed.neon | 2 +- utils/phpstan-baseline/loader.neon | 3 +-- .../offsetAccess.notFound.neon | 8 ------- 12 files changed, 16 insertions(+), 57 deletions(-) delete mode 100644 utils/phpstan-baseline/offsetAccess.notFound.neon diff --git a/rector.php b/rector.php index c3547b82f596..cd5f3c21dd0f 100644 --- a/rector.php +++ b/rector.php @@ -25,6 +25,7 @@ use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedConstructorParamRector; use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodRector; use Rector\DeadCode\Rector\MethodCall\RemoveNullArgOnNullDefaultParamRector; +use Rector\DeadCode\Rector\Property\RemoveDefaultValueFromAssignedPropertyRector; use Rector\EarlyReturn\Rector\Foreach_\ChangeNestedForeachIfsToEarlyContinueRector; use Rector\EarlyReturn\Rector\If_\ChangeIfElseValueAssignToEarlyReturnRector; use Rector\EarlyReturn\Rector\If_\RemoveAlwaysElseRector; @@ -33,6 +34,7 @@ use Rector\Php71\Rector\FuncCall\RemoveExtraParametersRector; use Rector\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector; use Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector; +use Rector\Php81\Rector\Property\ReadOnlyPropertyRector; use Rector\PHPUnit\CodeQuality\Rector\Class_\YieldDataProviderRector; use Rector\PHPUnit\CodeQuality\Rector\FuncCall\AssertFuncCallToPHPUnitAssertRector; use Rector\PHPUnit\CodeQuality\Rector\StmtsAwareInterface\DeclareStrictTypesTestsRector; @@ -102,6 +104,16 @@ __DIR__ . '/system/HTTP/Response.php', ], + // Keep property defaults for backward compatibility. + RemoveDefaultValueFromAssignedPropertyRector::class, + + ReadOnlyPropertyRector::class => [ + __DIR__ . '/system/Cache/ResponseCache.php', + __DIR__ . '/system/HotReloader/IteratorFilter.php', + __DIR__ . '/system/Router/RouteCollection.php', + __DIR__ . '/system/Security/Security.php', + ], + // Exclude test file because `is_cli()` is mocked and Rector might remove needed parameters. RemoveExtraParametersRector::class => [ __DIR__ . '/tests/system/Debug/ToolbarTest.php', diff --git a/system/Config/Services.php b/system/Config/Services.php index cef65a8895a9..35db6c57241a 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -343,7 +343,6 @@ public static function image(?string $handler = null, ?Images $config = null, bo } $config ??= config(Images::class); - assert($config instanceof Images); $handler = in_array($handler, [null, '', '0'], true) ? $config->defaultHandler : $handler; $class = $config->handlers[$handler]; diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 3c08958604d2..8253973beb27 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -1335,7 +1335,7 @@ protected function fillRouteParams(string $from, ?array $params = null): string // Ensure that the param we're inserting matches // the expected param type. - $pos = strpos($from, $pattern); + $pos = strpos($from, (string) $pattern); $from = substr_replace($from, $params[$index], $pos, strlen($pattern)); } @@ -1397,7 +1397,7 @@ protected function buildReverseRoute(string $from, array $params): string // Ensure that the param we're inserting matches // the expected param type. - $pos = strpos($from, $placeholder); + $pos = strpos($from, (string) $placeholder); $from = substr_replace($from, (string) $params[$index], $pos, strlen($placeholder)); } diff --git a/tests/system/AutoReview/FrameworkCodeTest.php b/tests/system/AutoReview/FrameworkCodeTest.php index de515fffebf6..7345b5dfa537 100644 --- a/tests/system/AutoReview/FrameworkCodeTest.php +++ b/tests/system/AutoReview/FrameworkCodeTest.php @@ -62,7 +62,6 @@ public function testEachTestClassHasCorrectGroupAttributeName(string $class): vo $unrecognizedGroups = array_diff( array_map(static function (ReflectionAttribute $attribute): string { $groupAttribute = $attribute->newInstance(); - assert($groupAttribute instanceof Group); return $groupAttribute->name(); }, $attributes), diff --git a/tests/system/Database/Live/GetNumRowsTest.php b/tests/system/Database/Live/GetNumRowsTest.php index c7575462d598..302b1e313695 100644 --- a/tests/system/Database/Live/GetNumRowsTest.php +++ b/tests/system/Database/Live/GetNumRowsTest.php @@ -29,28 +29,6 @@ final class GetNumRowsTest extends CIUnitTestCase protected $refresh = true; protected $seed = CITestSeeder::class; - /** - * Added as instructed at https://codeigniter4.github.io/userguide/testing/database.html#the-test-class - * {@inheritDoc} - * - * @see \CodeIgniter\Test\CIDatabaseTestCase::setUp() - */ - protected function setUp(): void - { - parent::setUp(); - } - - /** - * Added as instructed at https://codeigniter4.github.io/userguide/testing/database.html#the-test-class - * {@inheritDoc} - * - * @see \CodeIgniter\Test\CIDatabaseTestCase::tearDown() - */ - protected function tearDown(): void - { - parent::tearDown(); - } - /** * tests newly added ResultInterface::getNumRows with a live db */ diff --git a/tests/system/Database/Migrations/MigrationTest.php b/tests/system/Database/Migrations/MigrationTest.php index 7e4049a113ce..90d71db35c15 100644 --- a/tests/system/Database/Migrations/MigrationTest.php +++ b/tests/system/Database/Migrations/MigrationTest.php @@ -26,11 +26,6 @@ final class MigrationTest extends CIUnitTestCase { use DatabaseTestTrait; - protected function setUp(): void - { - parent::setUp(); - } - public function testDBGroup(): void { $migration = new class () extends Migration { diff --git a/tests/system/Models/GeneralModelTest.php b/tests/system/Models/GeneralModelTest.php index c0d3d806dbdd..9bc62c2d3501 100644 --- a/tests/system/Models/GeneralModelTest.php +++ b/tests/system/Models/GeneralModelTest.php @@ -35,11 +35,6 @@ final class GeneralModelTest extends CIUnitTestCase */ private ?object $model = null; - protected function setUp(): void - { - parent::setUp(); - } - protected function tearDown(): void { parent::tearDown(); diff --git a/tests/system/Models/LiveModelTestCase.php b/tests/system/Models/LiveModelTestCase.php index 342daecdf302..31b2af8edbca 100644 --- a/tests/system/Models/LiveModelTestCase.php +++ b/tests/system/Models/LiveModelTestCase.php @@ -38,11 +38,6 @@ abstract class LiveModelTestCase extends CIUnitTestCase protected $seed = CITestSeeder::class; - protected function setUp(): void - { - parent::setUp(); - } - protected function tearDown(): void { parent::tearDown(); diff --git a/tests/system/Test/TestResponseTest.php b/tests/system/Test/TestResponseTest.php index dc175897b870..315e1f8db6bd 100644 --- a/tests/system/Test/TestResponseTest.php +++ b/tests/system/Test/TestResponseTest.php @@ -29,11 +29,6 @@ final class TestResponseTest extends CIUnitTestCase private ?TestResponse $testResponse = null; private Response $response; - protected function setUp(): void - { - parent::setUp(); - } - #[DataProvider('provideHttpStatusCodes')] public function testIsOK(int $code, bool $isOk): void { diff --git a/utils/phpstan-baseline/empty.notAllowed.neon b/utils/phpstan-baseline/empty.notAllowed.neon index 827126f98676..796292761d9c 100644 --- a/utils/phpstan-baseline/empty.notAllowed.neon +++ b/utils/phpstan-baseline/empty.notAllowed.neon @@ -1,4 +1,4 @@ -# total 212 errors +# total 205 errors parameters: ignoreErrors: diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 5214bfa1141b..03e4965a51a4 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 1819 errors +# total 1810 errors includes: - argument.type.neon @@ -16,7 +16,6 @@ includes: - missingType.parameter.neon - missingType.property.neon - nullCoalesce.property.neon - - offsetAccess.notFound.neon - property.defaultValue.neon - property.nonObject.neon - property.notFound.neon diff --git a/utils/phpstan-baseline/offsetAccess.notFound.neon b/utils/phpstan-baseline/offsetAccess.notFound.neon deleted file mode 100644 index ba50b4d14f96..000000000000 --- a/utils/phpstan-baseline/offsetAccess.notFound.neon +++ /dev/null @@ -1,8 +0,0 @@ -# total 2 errors - -parameters: - ignoreErrors: - - - message: '#^Offset ''_ci_old_input'' does not exist on array\{\}\.$#' - count: 2 - path: ../../tests/system/HTTP/RedirectResponseTest.php From f3829fe489b220b009454ddc22a2be3bff141601 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Thu, 30 Jul 2026 00:16:13 +0700 Subject: [PATCH 038/163] chore: Make use of ^0.15 for structarmed dependency (#10433) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index fc745e939531..9cabb129ff8c 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "0.15.3", + "boundwize/structarmed": "^0.15", "codeigniter/phpstan-codeigniter": "^2.1", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", From 299033f29f67e3d72362f3181f9a44dcdb0aedb5 Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Fri, 31 Jul 2026 19:40:02 +0200 Subject: [PATCH 039/163] fix: handle null results in get_dir_file_info() (#10407) --- system/Helpers/filesystem_helper.php | 7 +++++-- tests/system/Helpers/FilesystemHelperTest.php | 16 ++++++++++++++++ user_guide_src/source/changelogs/v4.7.5.rst | 1 + 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/system/Helpers/filesystem_helper.php b/system/Helpers/filesystem_helper.php index da6acabb039e..b33ad49a2823 100644 --- a/system/Helpers/filesystem_helper.php +++ b/system/Helpers/filesystem_helper.php @@ -281,8 +281,11 @@ function get_dir_file_info(string $sourceDir, bool $topLevelOnly = true, bool $r if (is_dir($sourceDir . $file) && $file[0] !== '.' && $topLevelOnly === false) { get_dir_file_info($sourceDir . $file . DIRECTORY_SEPARATOR, $topLevelOnly, true); } elseif ($file[0] !== '.') { - $fileData[$file] = get_file_info($sourceDir . $file); - $fileData[$file]['relative_path'] = $relativePath; + $info = get_file_info($sourceDir . $file); + if ($info !== null) { + $fileData[$file] = $info; + $fileData[$file]['relative_path'] = $relativePath; + } } } diff --git a/tests/system/Helpers/FilesystemHelperTest.php b/tests/system/Helpers/FilesystemHelperTest.php index a8ee76d6ab09..9d4bf74eb4e5 100644 --- a/tests/system/Helpers/FilesystemHelperTest.php +++ b/tests/system/Helpers/FilesystemHelperTest.php @@ -477,6 +477,22 @@ public function testGetDirFileInfoFailure(): void $this->assertSame($expected, get_dir_file_info(SUPPORTPATH . 'Files#baker')); } + public function testGetDirFileInfoIgnoresDirectoriesWhenTopLevelOnlyIsTrue(): void + { + $dir = WRITEPATH . 'test_get_dir_file_info'; + mkdir($dir . '/subdir', 0777, true); + file_put_contents($dir . '/file.txt', 'test'); + + $result = get_dir_file_info($dir, true); + + unlink($dir . '/file.txt'); + rmdir($dir . '/subdir'); + rmdir($dir); + + $this->assertArrayHasKey('file.txt', $result); + $this->assertArrayNotHasKey('subdir', $result); + } + public function testGetFileInfo(): void { $file = SUPPORTPATH . 'Files/baker/banana.php'; diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index c83924b0d500..9495108b28a6 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -32,6 +32,7 @@ Bugs Fixed - **CLIRequest:** Fixed a bug where ``parseCommand()`` could throw a TypeError when ``argv`` is missing. - **Content Security Policy:** Fixed a bug where empty ``Content-Security-Policy``, ``Content-Security-Policy-Report-Only``, and ``Reporting-Endpoints`` response headers were generated when no corresponding values existed. +- **Helpers:** Fixed a bug where ``get_dir_file_info()`` returned incomplete entries for subdirectories and missing files instead of omitting them. - **Logger:** Fixed a bug where interpolating a log message with array or non-stringable context values could raise PHP warnings or errors. See the repo's From 0d1d224f33c157d984e6b33fe35fed56d717b2eb Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Fri, 31 Jul 2026 19:42:21 +0200 Subject: [PATCH 040/163] test: cover GDHandler save failures and unsupported image types (#10414) --- tests/system/Images/GDHandlerTest.php | 54 +++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/system/Images/GDHandlerTest.php b/tests/system/Images/GDHandlerTest.php index d85d1a142ffd..8d634d0912cd 100644 --- a/tests/system/Images/GDHandlerTest.php +++ b/tests/system/Images/GDHandlerTest.php @@ -19,7 +19,9 @@ use CodeIgniter\Test\CIUnitTestCase; use org\bovigo\vfs\vfsStream; use org\bovigo\vfs\vfsStreamDirectory; +use org\bovigo\vfs\vfsStreamFile; use PHPUnit\Framework\Attributes\Group; +use Throwable; /** * Unit testing for the GD image handler. @@ -463,4 +465,56 @@ public function testClearMetadataReturnsSelf(): void $this->assertSame($this->handler, $result); } + + public function testSaveFailed(): void + { + foreach (['gif', 'jpeg', 'png', 'webp'] as $type) { + if ($type === 'webp' && ! function_exists('imagecreatefromwebp')) { + $this->markTestSkipped('webp is not supported.'); + } + + $this->handler->withFile($this->origin . 'ci-logo.' . $type); + $this->handler->getResource(); // make sure resource is loaded + + try { + $this->handler->save($this->start . 'wontwork/ci-logo.' . $type); + $this->fail('Exception should have been thrown for ' . $type); + } catch (Throwable $e) { + $this->assertInstanceOf(ImageException::class, $e); + $this->assertSame(lang('Images.saveFailed'), $e->getMessage()); + } + } + } + + public function testImageCopyTargetWithMaxQuality(): void + { + foreach (['gif', 'jpeg', 'png', 'webp'] as $type) { + $this->handler->withFile($this->origin . 'ci-logo.' . $type); + $this->assertTrue($this->handler->save($this->start . 'work/ci-logo.' . $type, 100)); + $this->assertTrue($this->root->hasChild('work/ci-logo.' . $type)); + + /** @var vfsStreamFile $child */ + $child = $this->root->getChild('work/ci-logo.' . $type); + + $this->assertSame( + file_get_contents($this->origin . 'ci-logo.' . $type), + $child->getContent(), + ); + } + } + + public function testProcessUnsupportedImageType(): void + { + $this->handler->withFile($this->path); + + // Force an invalid image type to trigger getImageResource() default case + $image = $this->handler->getFile(); + $image->imageType = 9999; + + $this->expectException(ImageException::class); + $this->expectExceptionMessage(lang('Images.unsupportedImageCreate')); + + // resize() calls ensureResource() -> getImageResource(), which should throw + $this->handler->resize(10, 10); + } } From ba6c2244641419aebd69b7e931273e4a4e764b24 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Aug 2026 12:30:55 +0700 Subject: [PATCH 041/163] chore(deps-dev): update rector/rector requirement (#10437) Updates the requirements on [rector/rector](https://github.com/rectorphp/rector) to permit the latest version. Updates `rector/rector` to 2.5.9 - [Release notes](https://github.com/rectorphp/rector/releases) - [Commits](https://github.com/rectorphp/rector/compare/2.5.8...2.5.9) --- updated-dependencies: - dependency-name: rector/rector dependency-version: 2.5.9 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- rector.php | 18 ------------------ system/Router/Router.php | 6 +++--- utils/phpstan-baseline/empty.notAllowed.neon | 4 ++-- utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 7 +------ 6 files changed, 8 insertions(+), 31 deletions(-) diff --git a/composer.json b/composer.json index 9cabb129ff8c..27b87b9e709c 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "phpunit/phpcov": "^9.0.2 || ^10.0", "phpunit/phpunit": "^10.5.16 || ^11.2", "predis/predis": "^3.0", - "rector/rector": "2.5.8", + "rector/rector": "2.5.9", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { diff --git a/rector.php b/rector.php index cd5f3c21dd0f..856952b19206 100644 --- a/rector.php +++ b/rector.php @@ -33,10 +33,7 @@ use Rector\Php70\Rector\FuncCall\RandomFunctionRector; use Rector\Php71\Rector\FuncCall\RemoveExtraParametersRector; use Rector\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector; -use Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector; -use Rector\Php81\Rector\Property\ReadOnlyPropertyRector; use Rector\PHPUnit\CodeQuality\Rector\Class_\YieldDataProviderRector; -use Rector\PHPUnit\CodeQuality\Rector\FuncCall\AssertFuncCallToPHPUnitAssertRector; use Rector\PHPUnit\CodeQuality\Rector\StmtsAwareInterface\DeclareStrictTypesTestsRector; use Rector\Privatization\Rector\Class_\FinalizeTestCaseClassRector; use Rector\Privatization\Rector\Property\PrivatizeFinalClassPropertyRector; @@ -107,13 +104,6 @@ // Keep property defaults for backward compatibility. RemoveDefaultValueFromAssignedPropertyRector::class, - ReadOnlyPropertyRector::class => [ - __DIR__ . '/system/Cache/ResponseCache.php', - __DIR__ . '/system/HotReloader/IteratorFilter.php', - __DIR__ . '/system/Router/RouteCollection.php', - __DIR__ . '/system/Security/Security.php', - ], - // Exclude test file because `is_cli()` is mocked and Rector might remove needed parameters. RemoveExtraParametersRector::class => [ __DIR__ . '/tests/system/Debug/ToolbarTest.php', @@ -160,19 +150,11 @@ __DIR__ . '/system/HTTP/SiteURI.php', ], - // Unnecessary (string) is inserted - NullToStrictStringFuncCallArgRector::class, - CompactToVariablesRector::class, // possibly isset() on purpose, on updated Config classes property across versions IssetOnPropertyObjectToPropertyExistsRector::class, - AssertFuncCallToPHPUnitAssertRector::class => [ - // use $this inside static closure - __DIR__ . '/tests/system/AutoReview/FrameworkCodeTest.php', - ], - // some tests extended by other tests FinalizeTestCaseClassRector::class, diff --git a/system/Router/Router.php b/system/Router/Router.php index 063bb5074cbb..1eda4c616d62 100644 --- a/system/Router/Router.php +++ b/system/Router/Router.php @@ -694,7 +694,7 @@ private function isValidSegment(string $segment): bool * Takes an array of URI segments as input and sets the class/method * to be called. * - * @param array $segments URI segments + * @param list $segments URI segments * * @return void */ @@ -705,13 +705,13 @@ protected function setRequest(array $segments = []) return; } - [$controller, $method] = array_pad(explode('::', $segments[0]), 2, null); + [$controller, $method] = explode('::', $segments[0], 2) + [null, null]; $this->controller = $controller; // $this->method already contains the default method name, // so don't overwrite it with emptiness. - if (! empty($method)) { + if ($method !== null && $method !== '') { $this->method = $method; } diff --git a/utils/phpstan-baseline/empty.notAllowed.neon b/utils/phpstan-baseline/empty.notAllowed.neon index 796292761d9c..9060c95a91ec 100644 --- a/utils/phpstan-baseline/empty.notAllowed.neon +++ b/utils/phpstan-baseline/empty.notAllowed.neon @@ -1,4 +1,4 @@ -# total 205 errors +# total 204 errors parameters: ignoreErrors: @@ -279,7 +279,7 @@ parameters: - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 2 + count: 1 path: ../../system/Router/Router.php - diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 03e4965a51a4..32ad4dacc2f5 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 1810 errors +# total 1808 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 677e7101b2c3..28aeaabd8f58 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 1140 errors +# total 1139 errors parameters: ignoreErrors: @@ -3637,11 +3637,6 @@ parameters: count: 1 path: ../../system/Router/Router.php - - - message: '#^Method CodeIgniter\\Router\\Router\:\:setRequest\(\) has parameter \$segments with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/Router.php - - message: '#^Method CodeIgniter\\Router\\Router\:\:validateRequest\(\) has parameter \$segments with no value type specified in iterable type array\.$#' count: 1 From 520fec9926608c6813e6ad88c180c0f631f76500 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 03:47:31 +0800 Subject: [PATCH 042/163] chore(deps-dev): update rector/rector requirement from 2.5.9 to 2.6.1 in the composer-dependencies group (#10438) * 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.6.1 - [Release notes](https://github.com/rectorphp/rector/releases) - [Commits](https://github.com/rectorphp/rector/compare/2.5.9...2.6.1) --- updated-dependencies: - dependency-name: rector/rector dependency-version: 2.6.1 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] * remove deprecated rule --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: michalsn --- composer.json | 2 +- rector.php | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 27b87b9e709c..b80db0577efe 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "phpunit/phpcov": "^9.0.2 || ^10.0", "phpunit/phpunit": "^10.5.16 || ^11.2", "predis/predis": "^3.0", - "rector/rector": "2.5.9", + "rector/rector": "2.6.1", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { diff --git a/rector.php b/rector.php index 856952b19206..f1bc1ae27374 100644 --- a/rector.php +++ b/rector.php @@ -19,7 +19,6 @@ use Rector\CodeQuality\Rector\Isset_\IssetOnPropertyObjectToPropertyExistsRector; use Rector\CodingStyle\Rector\ClassMethod\FuncGetArgsToVariadicParamRector; use Rector\CodingStyle\Rector\ClassMethod\MakeInheritedMethodVisibilitySameAsParentRector; -use Rector\CodingStyle\Rector\FuncCall\CountArrayToEmptyArrayComparisonRector; use Rector\CodingStyle\Rector\FuncCall\VersionCompareFuncCallToConstantRector; use Rector\Config\RectorConfig; use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedConstructorParamRector; @@ -182,7 +181,6 @@ SimplifyUselessVariableRector::class, RemoveAlwaysElseRector::class, PassStrictParameterToFunctionParameterRector::class, - CountArrayToEmptyArrayComparisonRector::class, ChangeNestedForeachIfsToEarlyContinueRector::class, ChangeIfElseValueAssignToEarlyReturnRector::class, PreparedValueToEarlyReturnRector::class, From 39e6884d6ab5c958d19aad127f05d895767f377e Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Wed, 12 Aug 2026 03:47:53 +0800 Subject: [PATCH 043/163] refactor: fix uses of `empty()` calls (#10439) --- system/Commands/Database/CreateDatabase.php | 2 +- system/Commands/Database/MigrateStatus.php | 2 +- system/Commands/Database/Seed.php | 2 +- system/Config/BaseService.php | 4 +- system/Config/DotEnv.php | 4 +- system/Config/Services.php | 2 +- system/Database/BaseBuilder.php | 54 +-- system/Database/BaseConnection.php | 27 +- system/Database/BasePreparedQuery.php | 6 +- system/Database/BaseResult.php | 4 +- system/Database/BaseUtils.php | 6 +- system/Database/Database.php | 4 +- system/Database/Forge.php | 24 +- system/Database/MigrationRunner.php | 10 +- system/Database/MySQLi/Connection.php | 20 +- system/Database/MySQLi/Forge.php | 8 +- system/Database/MySQLi/Result.php | 4 +- system/Database/OCI8/Builder.php | 6 +- system/Database/OCI8/Connection.php | 4 +- system/Database/OCI8/Forge.php | 6 +- system/Database/Postgre/Builder.php | 14 +- system/Database/Postgre/Connection.php | 4 +- system/Database/Postgre/Forge.php | 8 +- system/Database/Postgre/Result.php | 4 +- system/Database/Query.php | 8 +- system/Database/SQLSRV/Builder.php | 18 +- system/Database/SQLSRV/Connection.php | 10 +- system/Database/SQLSRV/Forge.php | 10 +- system/Database/SQLSRV/Result.php | 4 +- system/Database/SQLite3/Builder.php | 4 +- system/Database/SQLite3/Connection.php | 2 +- system/Database/SQLite3/Forge.php | 5 +- system/Database/SQLite3/Table.php | 4 +- system/Debug/Timer.php | 10 +- system/Debug/Toolbar.php | 4 +- system/Email/Email.php | 10 +- system/Encryption/Encryption.php | 4 +- system/Encryption/Handlers/OpenSSLHandler.php | 4 +- system/Encryption/Handlers/SodiumHandler.php | 4 +- system/Files/File.php | 2 +- system/Filters/Filters.php | 2 +- system/HTTP/CURLRequest.php | 10 +- system/HTTP/IncomingRequest.php | 9 +- system/HTTP/MessageTrait.php | 4 +- system/HTTP/Request.php | 4 +- system/HTTP/RequestTrait.php | 2 +- system/HTTP/Response.php | 4 +- system/Helpers/form_helper.php | 2 +- system/Helpers/test_helper.php | 4 +- system/Helpers/url_helper.php | 2 +- system/Honeypot/Honeypot.php | 2 +- system/Images/Handlers/BaseHandler.php | 2 +- system/Images/Image.php | 2 +- system/Pager/Pager.php | 2 +- system/RESTful/BaseResource.php | 4 +- system/Router/RouteCollection.php | 12 +- system/Router/Router.php | 2 +- system/Session/Handlers/RedisHandler.php | 2 +- system/Test/FeatureTestTrait.php | 6 +- system/Test/FilterTestTrait.php | 2 +- system/Validation/Rules.php | 19 +- system/View/Filters.php | 2 +- tests/system/Session/SessionTest.php | 3 +- utils/phpstan-baseline/empty.notAllowed.neon | 323 ------------------ utils/phpstan-baseline/loader.neon | 3 +- 65 files changed, 230 insertions(+), 531 deletions(-) delete mode 100644 utils/phpstan-baseline/empty.notAllowed.neon diff --git a/system/Commands/Database/CreateDatabase.php b/system/Commands/Database/CreateDatabase.php index facd060abf6e..459b243d5632 100644 --- a/system/Commands/Database/CreateDatabase.php +++ b/system/Commands/Database/CreateDatabase.php @@ -79,7 +79,7 @@ public function run(array $params) { $name = array_shift($params); - if (empty($name)) { + if ($name === null || $name === '') { $name = CLI::prompt('Database name', null, 'required'); // @codeCoverageIgnore } diff --git a/system/Commands/Database/MigrateStatus.php b/system/Commands/Database/MigrateStatus.php index 68288353458d..7307305307c1 100644 --- a/system/Commands/Database/MigrateStatus.php +++ b/system/Commands/Database/MigrateStatus.php @@ -105,7 +105,7 @@ public function run(array $params) $migrations = $runner->findNamespaceMigrations($namespace); - if (empty($migrations)) { + if ($migrations === []) { continue; } diff --git a/system/Commands/Database/Seed.php b/system/Commands/Database/Seed.php index fc1c0f1ae62e..5a536d0f1b50 100644 --- a/system/Commands/Database/Seed.php +++ b/system/Commands/Database/Seed.php @@ -71,7 +71,7 @@ public function run(array $params) $seeder = new Seeder(new Database()); $seedName = array_shift($params); - if (empty($seedName)) { + if ($seedName === null || $seedName === '') { $seedName = CLI::prompt(lang('Migrations.migSeeder'), null, 'required'); // @codeCoverageIgnore } diff --git a/system/Config/BaseService.php b/system/Config/BaseService.php index 64b5266da041..d59332b392ef 100644 --- a/system/Config/BaseService.php +++ b/system/Config/BaseService.php @@ -276,7 +276,7 @@ protected static function getSharedInstance(string $key, ...$params) public static function autoloader(bool $getShared = true) { if ($getShared) { - if (empty(static::$instances['autoloader'])) { + if (! isset(static::$instances['autoloader'])) { static::$instances['autoloader'] = new Autoloader(); } @@ -296,7 +296,7 @@ public static function autoloader(bool $getShared = true) public static function locator(bool $getShared = true) { if ($getShared) { - if (empty(static::$instances['locator'])) { + if (! isset(static::$instances['locator'])) { $cacheEnabled = class_exists(Optimize::class) && (new Optimize())->locatorCacheEnabled; diff --git a/system/Config/DotEnv.php b/system/Config/DotEnv.php index 6778c44e0d8d..2495b6b15e42 100644 --- a/system/Config/DotEnv.php +++ b/system/Config/DotEnv.php @@ -98,11 +98,11 @@ protected function setVariable(string $name, string $value = '') putenv("{$name}={$value}"); } - if (empty($_ENV[$name])) { + if (! isset($_ENV[$name]) || in_array($_ENV[$name], ['', '0'], true)) { $_ENV[$name] = $value; } - if (empty($_SERVER[$name])) { + if (! isset($_SERVER[$name]) || in_array($_SERVER[$name], ['', '0'], true)) { $_SERVER[$name] = $value; } } diff --git a/system/Config/Services.php b/system/Config/Services.php index 35db6c57241a..b465857905a1 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -230,7 +230,7 @@ public static function email($config = null, bool $getShared = true) return static::getSharedInstance('email', $config); } - if (empty($config) || (! is_array($config) && ! $config instanceof EmailConfig)) { + if (! $config instanceof EmailConfig && (! is_array($config) || $config === [])) { $config = config(EmailConfig::class); } diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index c3ed39102ae6..17f0690d8fb9 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -306,7 +306,7 @@ class BaseBuilder */ public function __construct($tableName, ConnectionInterface $db, ?array $options = null) { - if (empty($tableName)) { + if (in_array($tableName, ['', '0', []], true)) { throw new DatabaseException('A table must be specified when creating a new Query Builder.'); } @@ -769,7 +769,7 @@ protected function whereHaving(string $qbKey, $key, $value = null, string $type $escape = $this->db->protectIdentifiers; } - $prefix = empty($this->{$qbKey}) ? $this->groupGetType('') : $this->groupGetType($type); + $prefix = $this->{$qbKey} === [] ? $this->groupGetType('') : $this->groupGetType($type); foreach ($keyValue as $k => $v) { if ($rawSqlOnly) { @@ -778,7 +778,7 @@ protected function whereHaving(string $qbKey, $key, $value = null, string $type } elseif ($v !== null) { $op = $this->getOperatorFromWhereKey($k); - if (! empty($op)) { + if ($op !== false && $op !== []) { $k = trim($k); end($op); @@ -985,7 +985,7 @@ protected function _whereIn(?string $key = null, $values = null, bool $not = fal $ok = $this->setBind($ok, $whereIn, $escape); - $prefix = empty($this->{$clause}) ? $this->groupGetType('') : $this->groupGetType($type); + $prefix = $this->{$clause} === [] ? $this->groupGetType('') : $this->groupGetType($type); $whereIn = [ 'condition' => "{$prefix}{$key}{$not} IN :{$ok}:", @@ -1125,7 +1125,7 @@ protected function _like($field, string $match = '', string $type = 'AND ', stri $v = $match; $insensitiveSearch = false; - $prefix = empty($this->{$clause}) ? $this->groupGetType('') : $this->groupGetType($type); + $prefix = $this->{$clause} === [] ? $this->groupGetType('') : $this->groupGetType($type); if ($side === 'none') { $bind = $this->setBind($field->getBindingKey(), $v, $escape); @@ -1357,7 +1357,7 @@ protected function groupStartPrepare(string $not = '', string $type = 'AND ', st $type = $this->groupGetType($type); $this->QBWhereGroupStarted = true; - $prefix = empty($this->{$clause}) ? '' : $type; + $prefix = $this->{$clause} === [] ? '' : $type; $where = [ 'condition' => $prefix . $not . str_repeat(' ', ++$this->QBWhereGroupCount) . ' (', 'escape' => false, @@ -1626,7 +1626,7 @@ protected function compileFinalQuery(string $sql): string $query = new Query($this->db); $query->setQuery($sql, $this->binds, false); - if (! empty($this->db->swapPre) && ! empty($this->db->DBPrefix)) { + if ($this->db->swapPre !== '' && $this->db->DBPrefix !== '') { $query->swapPrefix($this->db->DBPrefix, $this->db->swapPre); } @@ -1683,7 +1683,7 @@ public function countAll(bool $reset = true) $query = $this->db->query($sql, null, false); - if (empty($query->getResult())) { + if ($query->getResult() === []) { return 0; } @@ -1709,7 +1709,7 @@ public function countAllResults(bool $reset = true) // for selecting COUNT(*) ... $orderBy = []; - if (! empty($this->QBOrderBy)) { + if (is_array($this->QBOrderBy) && $this->QBOrderBy !== []) { $orderBy = $this->QBOrderBy; $this->QBOrderBy = null; @@ -1720,7 +1720,7 @@ public function countAllResults(bool $reset = true) $this->QBLimit = false; - if ($this->QBDistinct === true || ! empty($this->QBGroupBy)) { + if ($this->QBDistinct === true || $this->QBGroupBy !== []) { // We need to backup the original SELECT in case DBPrefix is used $select = $this->QBSelect; $sql = $this->countString . $this->db->protectIdentifiers('numrows') . "\nFROM (\n" . $this->compileSelect() . "\n) CI_count_all_results"; @@ -1749,7 +1749,7 @@ public function countAllResults(bool $reset = true) $row = $result instanceof ResultInterface ? $result->getRow() : null; - if (empty($row)) { + if ($row === null) { return 0; } @@ -1813,7 +1813,7 @@ public function getWhere($where = null, ?int $limit = null, ?int $offset = 0, bo */ protected function batchExecute(string $renderMethod, int $batchSize = 100) { - if (empty($this->QBSet)) { + if ($this->QBSet === []) { if ($this->db->DBDebug) { throw new DatabaseException(trim($renderMethod, '_') . '() has no data.'); } @@ -1866,7 +1866,7 @@ protected function batchExecute(string $renderMethod, int $batchSize = 100) */ public function setData($set, ?bool $escape = null, string $alias = '') { - if (empty($set)) { + if ($set === []) { if ($this->db->DBDebug) { throw new DatabaseException('setData() has no data.'); } @@ -2073,7 +2073,7 @@ private function setAlias(string $alias): BaseBuilder */ public function updateFields($set, bool $addToDefault = false, ?array $ignore = null) { - if (! empty($set)) { + if (! in_array($set, [null, [], ''], true)) { if (! is_array($set)) { $set = explode(',', $set); } @@ -2109,13 +2109,13 @@ public function updateFields($set, bool $addToDefault = false, ?array $ignore = /** * Sets constraints for batch upsert, update * - * @param array|RawSql|string $set a string of columns, key value pairs, or RawSql + * @param array|RawSql|string|null $set A string of columns, key value pairs, or RawSql * * @return $this */ public function onConstraint($set) { - if (! empty($set)) { + if (! in_array($set, [null, [], ''], true)) { if (is_string($set)) { $set = explode(',', $set); @@ -2388,7 +2388,7 @@ protected function removeAlias(string $from): string */ protected function validateInsert(): bool { - if (empty($this->QBSet)) { + if ($this->QBSet === []) { if ($this->db->DBDebug) { throw new DatabaseException('You must use the "set" method to insert an entry.'); } @@ -2424,7 +2424,7 @@ public function replace(?array $set = null) $this->set($set); } - if (empty($this->QBSet)) { + if ($this->QBSet === []) { if ($this->db->DBDebug) { throw new DatabaseException('You must use the "set" method to update an entry.'); } @@ -2576,7 +2576,7 @@ protected function _update(string $table, array $values): string */ protected function validateUpdate(): bool { - if (empty($this->QBSet)) { + if ($this->QBSet === []) { if ($this->db->DBDebug) { throw new DatabaseException('You must use the "set" method to update an entry.'); } @@ -2827,7 +2827,7 @@ public function delete($where = '', ?int $limit = null, bool $resetData = true) $this->where($where); } - if (empty($this->QBWhere)) { + if ($this->QBWhere === []) { if ($this->db->DBDebug) { throw new DatabaseException('Deletes are not allowed unless they contain a "where" or "like" clause.'); } @@ -2846,7 +2846,7 @@ public function delete($where = '', ?int $limit = null, bool $resetData = true) $this->QBLimit = $limit; } - if (! empty($this->QBLimit)) { + if ($this->QBLimit !== false && $this->QBLimit !== 0) { if (! $this->canLimitDeletes) { throw new DatabaseException('SQLite3 does not allow LIMITs on DELETE queries.'); } @@ -3105,7 +3105,7 @@ protected function compileSelect($selectOverride = false): string } else { $sql = $this->QBDistinct ? 'SELECT DISTINCT ' : 'SELECT '; - if (empty($this->QBSelect)) { + if ($this->QBSelect === []) { $sql .= '*'; } else { // Cycle through the "select" portion of the query and prep each column name. @@ -3124,11 +3124,11 @@ protected function compileSelect($selectOverride = false): string } } - if (! empty($this->QBFrom)) { + if ($this->QBFrom !== []) { $sql .= "\nFROM " . $this->_fromTables(); } - if (! empty($this->QBJoin)) { + if ($this->QBJoin !== []) { $sql .= "\n" . implode("\n", $this->QBJoin); } @@ -3177,7 +3177,7 @@ protected function compileIgnore(string $statement) */ protected function compileWhereHaving(string $qbKey): string { - if (! empty($this->{$qbKey})) { + if ($this->{$qbKey} !== []) { foreach ($this->{$qbKey} as &$qbkey) { // Is this condition already compiled? if (is_string($qbkey)) { @@ -3266,7 +3266,7 @@ protected function compileWhereHaving(string $qbKey): string */ protected function compileGroupBy(): string { - if (! empty($this->QBGroupBy)) { + if ($this->QBGroupBy !== []) { foreach ($this->QBGroupBy as &$groupBy) { // Is it already compiled? if (is_string($groupBy)) { @@ -3453,7 +3453,7 @@ protected function resetSelect() } // Reset QBFrom part - if (! empty($this->QBFrom)) { + if ($this->QBFrom !== []) { $this->from(array_shift($this->QBFrom), true); } } diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index 17a5775d425f..f872ab2bdc06 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -566,7 +566,7 @@ public function initialize() // No connection resource? Check if there is a failover else throw an error if (! $this->connID) { // Check if there is a failover set - if (! empty($this->failover) && is_array($this->failover)) { + if (is_array($this->failover) && $this->failover !== []) { // Go over all the failovers foreach ($this->failover as $index => $failover) { $typedPropertyTypes = $this->getBuiltinPropertyTypesMap(array_keys($failover)); @@ -703,7 +703,7 @@ public function getConnection(?string $alias = null) */ public function getDatabase(): string { - return empty($this->database) ? '' : $this->database; + return is_string($this->database) ? $this->database : ''; } /** @@ -791,7 +791,7 @@ public function query(string $sql, $binds = null, bool $setEscapeFlags = true, s { $queryClass = $queryClass !== '' && $queryClass !== '0' ? $queryClass : $this->queryClass; - if (empty($this->connID)) { + if ($this->connID === false) { $this->initialize(); } @@ -800,7 +800,7 @@ public function query(string $sql, $binds = null, bool $setEscapeFlags = true, s $query->setQuery($sql, $binds, $setEscapeFlags); - if (! empty($this->swapPre) && ! empty($this->DBPrefix)) { + if ($this->swapPre !== '' && $this->DBPrefix !== '') { $query->swapPrefix($this->DBPrefix, $this->swapPre); } @@ -901,7 +901,7 @@ public function query(string $sql, $binds = null, bool $setEscapeFlags = true, s */ public function simpleQuery(string $sql) { - if (empty($this->connID)) { + if ($this->connID === false) { $this->initialize(); } @@ -1015,7 +1015,7 @@ public function transBegin(bool $testMode = false): bool return true; } - if (empty($this->connID)) { + if ($this->connID === false) { $this->initialize(); } @@ -1119,7 +1119,7 @@ abstract protected function _transRollback(): bool; */ public function table($tableName) { - if (empty($tableName)) { + if (in_array($tableName, ['', '0', []], true)) { throw new DatabaseException('You must set the database table to be used with your query.'); } @@ -1161,7 +1161,7 @@ public function newQuery(): BaseBuilder */ public function prepare(Closure $func, array $options = []) { - if (empty($this->connID)) { + if ($this->connID === false) { $this->initialize(); } @@ -1346,9 +1346,8 @@ private function protectDotItem(string $item, string $alias, bool $protectIdenti // one of the aliases previously identified? If so, // we have nothing more to do other than escape the item // - // NOTE: The ! empty() condition prevents this method - // from breaking when QB isn't enabled. - if (! empty($this->aliasedTables) && in_array($parts[0], $this->aliasedTables, true)) { + // $aliasedTables is empty when QB isn't enabled. + if ($this->aliasedTables !== [] && in_array($parts[0], $this->aliasedTables, true)) { if ($protectIdentifiers) { foreach ($parts as $key => $val) { if (! in_array($val, $this->reservedIdentifiers, true)) { @@ -1455,7 +1454,7 @@ private function escapeTableName(TableName $tableName): string */ public function escapeIdentifiers($item) { - if ($this->escapeChar === '' || empty($item) || in_array($item, $this->reservedIdentifiers, true)) { + if ($this->escapeChar === '' || in_array($item, ['', '0', []], true) || in_array($item, $this->reservedIdentifiers, true)) { return $item; } @@ -1737,7 +1736,7 @@ public function tableExists(string $tableName, bool $cached = true): bool $tableExists = $this->query($sql)->getResultArray() !== []; // if cache has been built already - if (! empty($this->dataCache['table_names'])) { + if (($this->dataCache['table_names'] ?? []) !== []) { $key = array_search( strtolower($tableName), array_map(strtolower(...), $this->dataCache['table_names']), @@ -1772,7 +1771,7 @@ public function getFieldNames($tableName) return $this->dataCache['field_names'][$table]; } - if (empty($this->connID)) { + if ($this->connID === false) { $this->initialize(); } diff --git a/system/Database/BasePreparedQuery.php b/system/Database/BasePreparedQuery.php index 540e9c15161c..16b23535e031 100644 --- a/system/Database/BasePreparedQuery.php +++ b/system/Database/BasePreparedQuery.php @@ -47,7 +47,7 @@ abstract class BasePreparedQuery implements PreparedQueryInterface * * @var string */ - protected $errorString; + protected $errorString = ''; /** * Holds the prepared query object @@ -90,7 +90,7 @@ public function prepare(string $sql, array $options = [], string $queryClass = Q $query->setQuery($sql); - if (! empty($this->db->swapPre) && ! empty($this->db->DBPrefix)) { + if ($this->db->swapPre !== '' && $this->db->DBPrefix !== '') { $query->swapPrefix($this->db->DBPrefix, $this->db->swapPre); } @@ -236,7 +236,7 @@ public function getQueryString(): string */ public function hasError(): bool { - return ! empty($this->errorString); + return $this->errorString !== ''; } /** diff --git a/system/Database/BaseResult.php b/system/Database/BaseResult.php index 5bede241a59d..532549262283 100644 --- a/system/Database/BaseResult.php +++ b/system/Database/BaseResult.php @@ -268,7 +268,7 @@ public function getRow($n = 0, string $type = 'object') } // array_key_exists() instead of isset() to allow for NULL values - if (empty($this->rowData) || ! array_key_exists($n, $this->rowData)) { + if ($this->rowData === null || ! array_key_exists($n, $this->rowData)) { return null; } @@ -304,7 +304,7 @@ public function getCustomRowObject(int $n, string $className) $this->getCustomResultObject($className); } - if (empty($this->customResultObject[$className])) { + if (($this->customResultObject[$className] ?? []) === []) { return null; } diff --git a/system/Database/BaseUtils.php b/system/Database/BaseUtils.php index 938c28364444..ddea4301c556 100644 --- a/system/Database/BaseUtils.php +++ b/system/Database/BaseUtils.php @@ -152,7 +152,7 @@ public function optimizeDatabase() $res = $res->getResultArray(); // Postgre & SQLite3 returns empty array - if (empty($res)) { + if ($res === []) { $key = $tableName; } else { $res = current($res); @@ -287,7 +287,7 @@ public function backup($params = []) 'foreign_key_checks' => true, ]; - if (! empty($params)) { + if ($params !== []) { foreach (array_keys($prefs) as $key) { if (isset($params[$key])) { $prefs[$key] = $params[$key]; @@ -295,7 +295,7 @@ public function backup($params = []) } } - if (empty($prefs['tables'])) { + if ($prefs['tables'] === [] || $prefs['tables'] === '') { $prefs['tables'] = $this->db->listTables(); } diff --git a/system/Database/Database.php b/system/Database/Database.php index 9598242f01bb..2955ce0d37c5 100644 --- a/system/Database/Database.php +++ b/system/Database/Database.php @@ -48,11 +48,11 @@ public function load(array $params = [], string $alias = '') throw new InvalidArgumentException('You must supply the parameter: alias.'); } - if (! empty($params['DSN']) && str_contains($params['DSN'], '://')) { + if (($params['DSN'] ?? '') !== '' && str_contains($params['DSN'], '://')) { $params = $this->parseDSN($params); } - if (empty($params['DBDriver'])) { + if (($params['DBDriver'] ?? '') === '') { throw new InvalidArgumentException('You have not selected a database type to connect to.'); } diff --git a/system/Database/Forge.php b/system/Database/Forge.php index 9291683acb5f..52ae71464573 100644 --- a/system/Database/Forge.php +++ b/system/Database/Forge.php @@ -246,7 +246,7 @@ public function createDatabase(string $dbName, bool $ifNotExists = false): bool // @codeCoverageIgnoreEnd } - if (! empty($this->db->dataCache['db_names'])) { + if (($this->db->dataCache['db_names'] ?? []) !== []) { $this->db->dataCache['db_names'][] = $dbName; } @@ -303,7 +303,7 @@ public function dropDatabase(string $dbName): bool return false; } - if (! empty($this->db->dataCache['db_names'])) { + if (($this->db->dataCache['db_names'] ?? []) !== []) { $key = array_search( strtolower($dbName), array_map(strtolower(...), $this->db->dataCache['db_names']), @@ -664,7 +664,7 @@ public function dropTable(string $tableName, bool $ifExists = false, bool $casca $this->db->enableForeignKeyChecks(); - if ($query && ! empty($this->db->dataCache['table_names'])) { + if ($query && ($this->db->dataCache['table_names'] ?? []) !== []) { $key = array_search( strtolower($this->db->DBPrefix . $tableName), array_map(strtolower(...), $this->db->dataCache['table_names']), @@ -726,7 +726,7 @@ public function renameTable(string $tableName, string $newTableName) $this->db->escapeIdentifiers($this->db->DBPrefix . $newTableName), )); - if ($result && ! empty($this->db->dataCache['table_names'])) { + if ($result && ($this->db->dataCache['table_names'] ?? []) !== []) { $key = array_search( strtolower($this->db->DBPrefix . $tableName), array_map(strtolower(...), $this->db->dataCache['table_names']), @@ -894,7 +894,7 @@ protected function _processFields(bool $createTable = false): array $attributes = array_change_key_case($attributes, CASE_UPPER); - if ($createTable && empty($attributes['TYPE'])) { + if ($createTable && ($attributes['TYPE'] ?? '') === '') { continue; } @@ -933,7 +933,7 @@ protected function _processFields(bool $createTable = false): array $nullString = ' ' . $this->null; if ($attributes['NULL'] === true) { - $field['null'] = empty($this->null) ? '' : $nullString; + $field['null'] = $this->null === '' ? '' : $nullString; } elseif ($attributes['NULL'] === $nullString) { $field['null'] = $nullString; } elseif ($attributes['NULL'] === '') { @@ -952,7 +952,7 @@ protected function _processFields(bool $createTable = false): array $field['comment'] = $this->db->escape($attributes['COMMENT']); } - if (isset($attributes['TYPE']) && ! empty($attributes['CONSTRAINT'])) { + if (isset($attributes['TYPE'], $attributes['CONSTRAINT']) && ! in_array($attributes['CONSTRAINT'], ['', '0', 0, []], true)) { if (is_array($attributes['CONSTRAINT'])) { $attributes['CONSTRAINT'] = $this->db->escape($attributes['CONSTRAINT']); $attributes['CONSTRAINT'] = implode(',', $attributes['CONSTRAINT']); @@ -1005,7 +1005,7 @@ protected function _attributeType(array &$attributes) */ protected function _attributeUnsigned(array &$attributes, array &$field) { - if (empty($attributes['UNSIGNED']) || $attributes['UNSIGNED'] !== true) { + if (($attributes['UNSIGNED'] ?? false) !== true) { return; } @@ -1044,11 +1044,11 @@ protected function _attributeDefault(array &$attributes, array &$field) if (array_key_exists('DEFAULT', $attributes)) { if ($attributes['DEFAULT'] === null) { - $field['default'] = empty($this->null) ? '' : $this->default . $this->null; + $field['default'] = $this->null === '' ? '' : $this->default . $this->null; // Override the NULL attribute if that's our default $attributes['NULL'] = true; - $field['null'] = empty($this->null) ? '' : ' ' . $this->null; + $field['null'] = $this->null === '' ? '' : ' ' . $this->null; } elseif ($attributes['DEFAULT'] instanceof RawSql) { $field['default'] = $this->default . $attributes['DEFAULT']; } else { @@ -1062,7 +1062,7 @@ protected function _attributeDefault(array &$attributes, array &$field) */ protected function _attributeUnique(array &$attributes, array &$field) { - if (! empty($attributes['UNIQUE']) && $attributes['UNIQUE'] === true) { + if (($attributes['UNIQUE'] ?? false) === true) { $field['unique'] = ' UNIQUE'; } } @@ -1072,7 +1072,7 @@ protected function _attributeUnique(array &$attributes, array &$field) */ protected function _attributeAutoIncrement(array &$attributes, array &$field) { - if (! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === true + if (($attributes['AUTO_INCREMENT'] ?? false) === true && str_contains(strtolower($field['type']), 'int') ) { $field['auto_increment'] = ' AUTO_INCREMENT'; diff --git a/system/Database/MigrationRunner.php b/system/Database/MigrationRunner.php index df5ee049f0b7..809fd84a718d 100644 --- a/system/Database/MigrationRunner.php +++ b/system/Database/MigrationRunner.php @@ -116,7 +116,7 @@ class MigrationRunner /** * The full path to locate migration files. * - * @var string + * @var string|null */ protected $path; @@ -471,7 +471,7 @@ public function findNamespaceMigrations(string $namespace): array $migrations = []; $locator = service('locator', true); - if (! empty($this->path)) { + if ($this->path !== null && $this->path !== '') { helper('filesystem'); $dir = rtrim($this->path, DIRECTORY_SEPARATOR) . '/'; $files = get_filenames($dir, true, false, false); @@ -480,7 +480,7 @@ public function findNamespaceMigrations(string $namespace): array } foreach ($files as $file) { - $file = empty($this->path) ? $file : $this->path . str_replace($this->path, '', $file); + $file = $this->path === null || $this->path === '' ? $file : $this->path . str_replace($this->path, '', $file); if ($migration = $this->migrationFromFile($file, $namespace)) { $migrations[] = $migration; @@ -712,7 +712,7 @@ public function getHistory(string $group = 'default'): array $query = $builder->orderBy('id', 'ASC')->get(); - return empty($query) ? [] : $query->getResultObject(); + return $query === false ? [] : $query->getResultObject(); } /** @@ -729,7 +729,7 @@ public function getBatchHistory(int $batch, $order = 'asc'): array ->orderBy('id', $order) ->get(); - return empty($query) ? [] : $query->getResultObject(); + return $query === false ? [] : $query->getResultObject(); } /** diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index 116acb7eaf9e..5a6daa5a692e 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -108,7 +108,7 @@ public function connect(bool $persistent = false) $socket = $this->hostname; } else { $hostname = $persistent ? 'p:' . $this->hostname : $this->hostname; - $port = empty($this->port) ? null : $this->port; + $port = $this->port === '' ? null : $this->port; $socket = ''; } @@ -147,19 +147,19 @@ public function connect(bool $persistent = false) if (is_array($this->encrypt)) { $ssl = []; - if (! empty($this->encrypt['ssl_key'])) { + if (($this->encrypt['ssl_key'] ?? '') !== '') { $ssl['key'] = $this->encrypt['ssl_key']; } - if (! empty($this->encrypt['ssl_cert'])) { + if (($this->encrypt['ssl_cert'] ?? '') !== '') { $ssl['cert'] = $this->encrypt['ssl_cert']; } - if (! empty($this->encrypt['ssl_ca'])) { + if (($this->encrypt['ssl_ca'] ?? '') !== '') { $ssl['ca'] = $this->encrypt['ssl_ca']; } - if (! empty($this->encrypt['ssl_capath'])) { + if (($this->encrypt['ssl_capath'] ?? '') !== '') { $ssl['capath'] = $this->encrypt['ssl_capath']; } - if (! empty($this->encrypt['ssl_cipher'])) { + if (($this->encrypt['ssl_cipher'] ?? '') !== '') { $ssl['cipher'] = $this->encrypt['ssl_cipher']; } @@ -253,7 +253,7 @@ public function setDatabase(string $databaseName): bool $databaseName = $this->database; } - if (empty($this->connID)) { + if ($this->connID === false) { $this->initialize(); } @@ -275,7 +275,7 @@ public function getVersion(): string return $this->dataCache['version']; } - if (empty($this->mysqli)) { + if (! $this->mysqli instanceof mysqli) { $this->initialize(); } @@ -473,7 +473,7 @@ protected function _indexData(string $table): array $keys = []; foreach ($indexes as $index) { - if (empty($keys[$index['Key_name']])) { + if (! isset($keys[$index['Key_name']])) { $keys[$index['Key_name']] = new stdClass(); $keys[$index['Key_name']]->name = $index['Key_name']; @@ -577,7 +577,7 @@ protected function _enableForeignKeyChecks() */ public function error(): array { - if (! empty($this->mysqli->connect_errno)) { + if ($this->mysqli instanceof mysqli && $this->mysqli->connect_errno !== 0) { return [ 'code' => $this->mysqli->connect_errno, 'message' => $this->mysqli->connect_error, diff --git a/system/Database/MySQLi/Forge.php b/system/Database/MySQLi/Forge.php index 9482f7e2329a..418cc08580ae 100644 --- a/system/Database/MySQLi/Forge.php +++ b/system/Database/MySQLi/Forge.php @@ -152,7 +152,7 @@ protected function _alterTable(string $alterType, string $table, $processedField if ($alterType === 'ADD') { $processedFields[$i]['_literal'] = "\n\tADD "; } else { - $processedFields[$i]['_literal'] = empty($field['new_name']) ? "\n\tMODIFY " : "\n\tCHANGE "; + $processedFields[$i]['_literal'] = ($field['new_name'] ?? '') === '' ? "\n\tMODIFY " : "\n\tCHANGE "; } $processedFields[$i] = $processedFields[$i]['_literal'] . $this->_processColumn($processedFields[$i]); @@ -169,19 +169,19 @@ protected function _processColumn(array $processedField): string { $extraClause = isset($processedField['after']) ? ' AFTER ' . $this->db->escapeIdentifiers($processedField['after']) : ''; - if (empty($extraClause) && isset($processedField['first']) && $processedField['first'] === true) { + if ($extraClause === '' && isset($processedField['first']) && $processedField['first'] === true) { $extraClause = ' FIRST'; } return $this->db->escapeIdentifiers($processedField['name']) - . (empty($processedField['new_name']) ? '' : ' ' . $this->db->escapeIdentifiers($processedField['new_name'])) + . (($processedField['new_name'] ?? '') === '' ? '' : ' ' . $this->db->escapeIdentifiers($processedField['new_name'])) . ' ' . $processedField['type'] . $processedField['length'] . $processedField['unsigned'] . $processedField['null'] . $processedField['default'] . $processedField['auto_increment'] . $processedField['unique'] - . (empty($processedField['comment']) ? '' : ' COMMENT ' . $processedField['comment']) + . (($processedField['comment'] ?? '') === '' ? '' : ' COMMENT ' . $processedField['comment']) . $extraClause; } diff --git a/system/Database/MySQLi/Result.php b/system/Database/MySQLi/Result.php index 873bbd08b9d9..05d07bb69078 100644 --- a/system/Database/MySQLi/Result.php +++ b/system/Database/MySQLi/Result.php @@ -150,7 +150,9 @@ protected function fetchAssoc() protected function fetchObject(string $className = 'stdClass') { if (is_subclass_of($className, Entity::class)) { - return empty($data = $this->fetchAssoc()) ? false : (new $className())->injectRawData($data); + $data = $this->fetchAssoc(); + + return in_array($data, [null, false, []], true) ? false : (new $className())->injectRawData($data); } return $this->resultID->fetch_object($className); diff --git a/system/Database/OCI8/Builder.php b/system/Database/OCI8/Builder.php index d1abead5060a..eb6f1ca88855 100644 --- a/system/Database/OCI8/Builder.php +++ b/system/Database/OCI8/Builder.php @@ -206,7 +206,7 @@ protected function _limit(string $sql, bool $offsetIgnore = false): string $offset = (int) ($offsetIgnore === false ? $this->QBOffset : 0); // OFFSET-FETCH can be used only with the ORDER BY clause - if (empty($this->QBOrderBy)) { + if (! is_array($this->QBOrderBy) || $this->QBOrderBy === []) { $sql .= ' ORDER BY 1'; } @@ -316,7 +316,7 @@ protected function _upsertBatch(string $table, array $keys, array $values): stri if ($sql === '') { $constraints = $this->QBOptions['constraints'] ?? []; - if (empty($constraints)) { + if ($constraints === []) { $fieldNames = array_map(static fn ($columnName): string => trim($columnName, '"'), $keys); $uniqueIndexes = array_filter($this->db->getIndexData($table), static function ($index) use ($fieldNames): bool { @@ -334,7 +334,7 @@ protected function _upsertBatch(string $table, array $keys, array $values): stri $constraints = $this->onConstraint($constraints)->QBOptions['constraints'] ?? []; } - if (empty($constraints)) { + if ($constraints === []) { if ($this->db->DBDebug) { throw new DatabaseException('No constraint found for upsert.'); } diff --git a/system/Database/OCI8/Connection.php b/system/Database/OCI8/Connection.php index dc884588a251..12fa3b4d3df6 100644 --- a/system/Database/OCI8/Connection.php +++ b/system/Database/OCI8/Connection.php @@ -628,7 +628,7 @@ public function error(): array public function insertID(): int { - if (empty($this->lastInsertedTableName)) { + if ($this->lastInsertedTableName === null || $this->lastInsertedTableName === '') { return 0; } @@ -764,7 +764,7 @@ protected function _transRollback(): bool */ public function getDatabase(): string { - if (! empty($this->database)) { + if (is_string($this->database) && $this->database !== '') { return $this->database; } diff --git a/system/Database/OCI8/Forge.php b/system/Database/OCI8/Forge.php index 8f71169115ce..e9dfb06885df 100644 --- a/system/Database/OCI8/Forge.php +++ b/system/Database/OCI8/Forge.php @@ -149,13 +149,13 @@ protected function _alterTable(string $alterType, string $table, $processedField } else { $processedFields[$i]['_literal'] = "\n\t" . $this->_processColumn($processedFields[$i]); - if (! empty($processedFields[$i]['comment'])) { + if (($processedFields[$i]['comment'] ?? '') !== '') { $sqls[] = 'COMMENT ON COLUMN ' . $this->db->escapeIdentifiers($table) . '.' . $this->db->escapeIdentifiers($processedFields[$i]['name']) . ' IS ' . $processedFields[$i]['comment']; } - if ($alterType === 'MODIFY' && ! empty($processedFields[$i]['new_name'])) { + if ($alterType === 'MODIFY' && ($processedFields[$i]['new_name'] ?? '') !== '') { $sqls[] = $sql . ' RENAME COLUMN ' . $this->db->escapeIdentifiers($processedFields[$i]['name']) . ' TO ' . $this->db->escapeIdentifiers($processedFields[$i]['new_name']); } @@ -182,7 +182,7 @@ protected function _alterTable(string $alterType, string $table, $processedField */ protected function _attributeAutoIncrement(array &$attributes, array &$field) { - if (! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === true + if (($attributes['AUTO_INCREMENT'] ?? false) === true && str_contains(strtolower($field['type']), 'number') && version_compare($this->db->getVersion(), '12.1', '>=') ) { diff --git a/system/Database/Postgre/Builder.php b/system/Database/Postgre/Builder.php index 68cd00fb6246..10b42b9b3b7c 100644 --- a/system/Database/Postgre/Builder.php +++ b/system/Database/Postgre/Builder.php @@ -54,7 +54,7 @@ protected function compileIgnore(string $statement) { $sql = parent::compileIgnore($statement); - if (! empty($sql)) { + if ($sql !== '') { $sql = ' ' . trim($sql); } @@ -171,9 +171,9 @@ public function replace(?array $set = null) $builder = $this->db->table($table); $exists = $builder->where($key, $value, true)->get()->getFirstRow(); - if (empty($exists) && $this->testMode) { + if ($exists === null && $this->testMode) { $result = $this->getCompiledInsert(); - } elseif (empty($exists)) { + } elseif ($exists === null) { $result = $builder->insert($set); } elseif ($this->testMode) { $result = $this->where($key, $value, true)->getCompiledUpdate(); @@ -233,7 +233,7 @@ protected function _insertBatch(string $table, array $keys, array $values): stri */ public function delete($where = '', ?int $limit = null, bool $resetData = true) { - if ($limit !== null && $limit !== 0 || ! empty($this->QBLimit)) { + if ($limit !== null && $limit !== 0 || ($this->QBLimit !== false && $this->QBLimit !== 0)) { throw new DatabaseException('PostgreSQL does not allow LIMITs on DELETE queries.'); } @@ -255,7 +255,7 @@ protected function _limit(string $sql, bool $offsetIgnore = false): string */ protected function _update(string $table, array $values): string { - if (! empty($this->QBLimit)) { + if ($this->QBLimit !== false && $this->QBLimit !== 0) { throw new DatabaseException('Postgres does not support LIMITs with UPDATE queries.'); } @@ -464,7 +464,7 @@ protected function _upsertBatch(string $table, array $keys, array $values): stri $constraints = $this->QBOptions['constraints'] ?? []; - if (empty($constraints)) { + if ($constraints === []) { $allIndexes = array_filter($this->db->getIndexData($table), static function ($index) use ($fieldNames): bool { $hasAllFields = count(array_intersect($index->fields, $fieldNames)) === count($index->fields); @@ -479,7 +479,7 @@ protected function _upsertBatch(string $table, array $keys, array $values): stri $constraints = $this->onConstraint($constraints)->QBOptions['constraints'] ?? []; } - if (empty($constraints)) { + if ($constraints === []) { if ($this->db->DBDebug) { throw new DatabaseException('No constraint found for upsert.'); } diff --git a/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php index 4c3358a4b470..2326eb06ac3b 100644 --- a/system/Database/Postgre/Connection.php +++ b/system/Database/Postgre/Connection.php @@ -63,7 +63,7 @@ class Connection extends BaseConnection */ public function connect(bool $persistent = false) { - if (empty($this->DSN)) { + if ($this->DSN === null || $this->DSN === '') { $this->buildDSN(); } @@ -88,7 +88,7 @@ public function connect(bool $persistent = false) throw new DatabaseException($error); } - if (! empty($this->schema)) { + if ($this->schema !== '') { $this->simpleQuery("SET search_path TO {$this->schema},public"); } diff --git a/system/Database/Postgre/Forge.php b/system/Database/Postgre/Forge.php index d8e7d5886604..fdf25aa2f9ca 100644 --- a/system/Database/Postgre/Forge.php +++ b/system/Database/Postgre/Forge.php @@ -107,7 +107,7 @@ protected function _alterTable(string $alterType, string $table, $processedField . " TYPE {$field['type']}{$field['length']}"; } - if (! empty($field['default'])) { + if (($field['default'] ?? '') !== '') { $sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($field['name']) . " SET {$field['default']}"; } @@ -119,12 +119,12 @@ protected function _alterTable(string $alterType, string $table, $processedField $sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($field['name']) . ($nullable ? ' DROP' : ' SET') . ' NOT NULL'; - if (! empty($field['new_name'])) { + if (($field['new_name'] ?? '') !== '') { $sqls[] = $sql . ' RENAME COLUMN ' . $this->db->escapeIdentifiers($field['name']) . ' TO ' . $this->db->escapeIdentifiers($field['new_name']); } - if (! empty($field['comment'])) { + if (($field['comment'] ?? '') !== '') { $sqls[] = 'COMMENT ON COLUMN' . $this->db->escapeIdentifiers($table) . '.' . $this->db->escapeIdentifiers($field['name']) . " IS {$field['comment']}"; @@ -186,7 +186,7 @@ protected function _attributeType(array &$attributes) */ protected function _attributeAutoIncrement(array &$attributes, array &$field) { - if (! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === true) { + if (($attributes['AUTO_INCREMENT'] ?? false) === true) { $field['type'] = $field['type'] === 'NUMERIC' || $field['type'] === 'BIGINT' ? 'BIGSERIAL' : 'SERIAL'; } } diff --git a/system/Database/Postgre/Result.php b/system/Database/Postgre/Result.php index e5d78c519c17..fee628296173 100644 --- a/system/Database/Postgre/Result.php +++ b/system/Database/Postgre/Result.php @@ -116,7 +116,9 @@ protected function fetchAssoc() protected function fetchObject(string $className = 'stdClass') { if (is_subclass_of($className, Entity::class)) { - return empty($data = $this->fetchAssoc()) ? false : (new $className())->injectRawData($data); + $data = $this->fetchAssoc(); + + return $data === false || $data === [] ? false : (new $className())->injectRawData($data); } return pg_fetch_object($this->resultID, null, $className); diff --git a/system/Database/Query.php b/system/Database/Query.php index 8040f54b666c..dbefaff6a09d 100644 --- a/system/Database/Query.php +++ b/system/Database/Query.php @@ -85,7 +85,7 @@ class Query implements QueryInterface, Stringable * * @var string */ - protected $errorString; + protected $errorString = ''; /** * Pointer to database connection. @@ -148,7 +148,7 @@ public function setBinds(array $binds, bool $setEscape = true) public function getQuery(): string { - if (empty($this->finalQueryString)) { + if (! isset($this->finalQueryString) || $this->finalQueryString === '') { $this->compileBinds(); } @@ -197,7 +197,7 @@ public function setError(int $code, string $error): self public function hasError(): bool { - return ! empty($this->errorString); + return $this->errorString !== ''; } public function getErrorCode(): int @@ -246,7 +246,7 @@ protected function compileBinds() $sql = $this->swappedQueryString ?? $this->originalQueryString; $binds = $this->binds; - if (empty($binds)) { + if ($binds === []) { $this->finalQueryString = $sql; return; diff --git a/system/Database/SQLSRV/Builder.php b/system/Database/SQLSRV/Builder.php index 486b30c3a6b9..f279ef750324 100644 --- a/system/Database/SQLSRV/Builder.php +++ b/system/Database/SQLSRV/Builder.php @@ -224,7 +224,7 @@ protected function _update(string $table, array $values): string $fullTableName = $this->getFullName($table); - $statement = sprintf('UPDATE %s%s SET ', empty($this->QBLimit) ? '' : 'TOP(' . $this->QBLimit . ') ', $fullTableName); + $statement = sprintf('UPDATE %s%s SET ', $this->QBLimit === false || $this->QBLimit === 0 ? '' : 'TOP(' . $this->QBLimit . ') ', $fullTableName); $statement .= implode(', ', $valstr) . $this->compileWhereHaving('QBWhere') @@ -344,7 +344,7 @@ protected function _limit(string $sql, bool $offsetIgnore = false): string return "SELECT * \nFROM " . $this->_fromTables() . ' WHERE 1=0 '; } - if (empty($this->QBOrderBy)) { + if (! is_array($this->QBOrderBy) || $this->QBOrderBy === []) { $sql .= ' ORDER BY (SELECT NULL) '; } @@ -514,7 +514,7 @@ public function countAll(bool $reset = true) } $query = $this->db->query($sql, null, false); - if (empty($query->getResult())) { + if ($query->getResult() === []) { return 0; } @@ -532,7 +532,7 @@ public function countAll(bool $reset = true) */ protected function _delete(string $table): string { - return 'DELETE' . (empty($this->QBLimit) ? '' : ' TOP (' . $this->QBLimit . ') ') . ' FROM ' . $this->getFullName($table) . $this->compileWhereHaving('QBWhere'); + return 'DELETE' . ($this->QBLimit === false || $this->QBLimit === 0 ? '' : ' TOP (' . $this->QBLimit . ') ') . ' FROM ' . $this->getFullName($table) . $this->compileWhereHaving('QBWhere'); } /** @@ -589,13 +589,13 @@ protected function compileSelect($selectOverride = false): string $sql = $this->QBDistinct ? 'SELECT DISTINCT ' : 'SELECT '; // SQL Server can't work with select * if group by is specified - if (empty($this->QBSelect) && $this->QBGroupBy !== [] && is_array($this->QBGroupBy)) { + if ($this->QBSelect === [] && $this->QBGroupBy !== [] && is_array($this->QBGroupBy)) { foreach ($this->QBGroupBy as $field) { $this->QBSelect[] = is_array($field) ? $field['field'] : $field; } } - if (empty($this->QBSelect)) { + if ($this->QBSelect === []) { $sql .= '*'; } else { // Cycle through the "select" portion of the query and prep each column name. @@ -616,7 +616,7 @@ protected function compileSelect($selectOverride = false): string } // Write the "JOIN" portion of the query - if (! empty($this->QBJoin)) { + if ($this->QBJoin !== []) { $sql .= "\n" . implode("\n", $this->QBJoin); } @@ -698,7 +698,7 @@ protected function _upsertBatch(string $table, array $keys, array $values): stri $fieldNames = array_map(static fn ($columnName): string => trim($columnName, '"'), $keys); - if (empty($constraints)) { + if ($constraints === []) { $tableIndexes = $this->db->getIndexData($table); $uniqueIndexes = array_filter($tableIndexes, static function ($index) use ($fieldNames): bool { @@ -725,7 +725,7 @@ protected function _upsertBatch(string $table, array $keys, array $values): stri $constraints = $this->onConstraint($constraints)->QBOptions['constraints'] ?? []; } - if (empty($constraints)) { + if ($constraints === []) { if ($this->db->DBDebug) { throw new DatabaseException('No constraint found for upsert.'); } diff --git a/system/Database/SQLSRV/Connection.php b/system/Database/SQLSRV/Connection.php index 970c582f60ed..ce1a210341d3 100644 --- a/system/Database/SQLSRV/Connection.php +++ b/system/Database/SQLSRV/Connection.php @@ -109,8 +109,8 @@ public function connect(bool $persistent = false) $charset = in_array(strtolower($this->charset), ['utf-8', 'utf8'], true) ? 'UTF-8' : SQLSRV_ENC_CHAR; $connection = [ - 'UID' => empty($this->username) ? '' : $this->username, - 'PWD' => empty($this->password) ? '' : $this->password, + 'UID' => is_string($this->username) ? $this->username : '', + 'PWD' => is_string($this->password) ? $this->password : '', 'Database' => $this->database, 'ConnectionPooling' => $persistent ? 1 : 0, 'CharacterSet' => $charset, @@ -120,7 +120,7 @@ public function connect(bool $persistent = false) // If the username and password are both empty, assume this is a // 'Windows Authentication Mode' connection. - if (empty($connection['UID']) && empty($connection['PWD'])) { + if ($connection['UID'] === '' && $connection['PWD'] === '') { unset($connection['UID'], $connection['PWD']); } @@ -136,7 +136,7 @@ public function connect(bool $persistent = false) $query = $this->query('SELECT CASE WHEN (@@OPTIONS | 256) = @@OPTIONS THEN 1 ELSE 0 END AS qi'); $query = $query->getResultObject(); - $this->_quoted_identifier = empty($query) ? false : (bool) $query[0]->qi; + $this->_quoted_identifier = $query === [] ? false : (bool) $query[0]->qi; $this->escapeChar = ($this->_quoted_identifier) ? '"' : ['[', ']']; return $this->connID; @@ -493,7 +493,7 @@ public function setDatabase(?string $databaseName = null) $databaseName = $this->database; } - if (empty($this->connID)) { + if ($this->connID === false) { $this->initialize(); } diff --git a/system/Database/SQLSRV/Forge.php b/system/Database/SQLSRV/Forge.php index 64a4ffaf6d35..f9f5b973a455 100644 --- a/system/Database/SQLSRV/Forge.php +++ b/system/Database/SQLSRV/Forge.php @@ -271,7 +271,7 @@ protected function _alterTable(string $alterType, string $table, $processedField . " {$field['type']}{$field['length']}"; } - if (! empty($field['default'])) { + if (($field['default'] ?? '') !== '') { $fullTable = $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table); $colName = $field['name']; // bare, for sys.columns lookup @@ -302,7 +302,7 @@ protected function _alterTable(string $alterType, string $table, $processedField $sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($field['name']) . " {$field['type']}{$field['length']} " . ($nullable ? '' : 'NOT') . ' NULL'; - if (! empty($field['comment'])) { + if (($field['comment'] ?? '') !== '') { $sqls[] = 'EXEC sys.sp_addextendedproperty ' . "@name=N'Caption', @value=N'" . $field['comment'] . "' , " . "@level0type=N'SCHEMA',@level0name=N'" . $this->db->schema . "', " @@ -310,7 +310,7 @@ protected function _alterTable(string $alterType, string $table, $processedField . "@level2type=N'COLUMN',@level2name=N'" . $this->db->escapeIdentifiers($field['name']) . "'"; } - if (! empty($field['new_name'])) { + if (($field['new_name'] ?? '') !== '') { $sqls[] = "EXEC sp_rename '[" . $this->db->schema . '].[' . $table . '].[' . $field['name'] . "]' , '" . $field['new_name'] . "', 'COLUMN';"; } } @@ -382,7 +382,7 @@ protected function _processIndexes(string $table, bool $asQuery = false): array protected function _processColumn(array $processedField): string { return $this->db->escapeIdentifiers($processedField['name']) - . (empty($processedField['new_name']) ? '' : ' ' . $this->db->escapeIdentifiers($processedField['new_name'])) + . (($processedField['new_name'] ?? '') === '' ? '' : ' ' . $this->db->escapeIdentifiers($processedField['new_name'])) . ' ' . $processedField['type'] . ($processedField['type'] === 'text' ? '' : $processedField['length']) . $processedField['default'] . $processedField['null'] @@ -449,7 +449,7 @@ protected function _attributeType(array &$attributes) */ protected function _attributeAutoIncrement(array &$attributes, array &$field) { - if (! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === true && str_contains(strtolower($field['type']), strtolower('INT'))) { + if (($attributes['AUTO_INCREMENT'] ?? false) === true && str_contains(strtolower($field['type']), strtolower('INT'))) { $field['auto_increment'] = ' IDENTITY(1,1)'; } } diff --git a/system/Database/SQLSRV/Result.php b/system/Database/SQLSRV/Result.php index 4d9b836fdda0..27e3ee1e5256 100644 --- a/system/Database/SQLSRV/Result.php +++ b/system/Database/SQLSRV/Result.php @@ -156,7 +156,9 @@ protected function fetchAssoc() protected function fetchObject(string $className = 'stdClass') { if (is_subclass_of($className, Entity::class)) { - return empty($data = $this->fetchAssoc()) ? false : (new $className())->injectRawData($data); + $data = $this->fetchAssoc(); + + return in_array($data, [null, false, []], true) ? false : (new $className())->injectRawData($data); } return sqlsrv_fetch_object($this->resultID, $className); diff --git a/system/Database/SQLite3/Builder.php b/system/Database/SQLite3/Builder.php index 4f8dff97a0ea..68b62a219230 100644 --- a/system/Database/SQLite3/Builder.php +++ b/system/Database/SQLite3/Builder.php @@ -142,7 +142,7 @@ protected function _upsertBatch(string $table, array $keys, array $values): stri if ($sql === '') { $constraints = $this->QBOptions['constraints'] ?? []; - if (empty($constraints)) { + if ($constraints === []) { $fieldNames = array_map(static fn ($columnName): string => trim($columnName, '`'), $keys); $allIndexes = array_filter($this->db->getIndexData($table), static function ($index) use ($fieldNames): bool { @@ -159,7 +159,7 @@ protected function _upsertBatch(string $table, array $keys, array $values): stri $constraints = $this->onConstraint($constraints)->QBOptions['constraints'] ?? []; } - if (empty($constraints)) { + if ($constraints === []) { if ($this->db->DBDebug) { throw new DatabaseException('No constraint found for upsert.'); } diff --git a/system/Database/SQLite3/Connection.php b/system/Database/SQLite3/Connection.php index 268ceaa7bea2..a78fca8c8b65 100644 --- a/system/Database/SQLite3/Connection.php +++ b/system/Database/SQLite3/Connection.php @@ -295,7 +295,7 @@ protected function _fieldData(string $table): array $query = $query->getResultObject(); - if (empty($query)) { + if ($query === []) { return []; } diff --git a/system/Database/SQLite3/Forge.php b/system/Database/SQLite3/Forge.php index 2bb9386f6c9e..ed5aee740895 100644 --- a/system/Database/SQLite3/Forge.php +++ b/system/Database/SQLite3/Forge.php @@ -100,7 +100,7 @@ public function dropDatabase(string $dbName): bool return false; } - if (! empty($this->db->dataCache['db_names'])) { + if (($this->db->dataCache['db_names'] ?? []) !== []) { $key = array_search(strtolower($dbName), array_map(strtolower(...), $this->db->dataCache['db_names']), true); if ($key !== false) { unset($this->db->dataCache['db_names'][$key]); @@ -222,8 +222,7 @@ protected function _attributeType(array &$attributes) protected function _attributeAutoIncrement(array &$attributes, array &$field) { if ( - ! empty($attributes['AUTO_INCREMENT']) - && $attributes['AUTO_INCREMENT'] === true + ($attributes['AUTO_INCREMENT'] ?? false) === true && str_contains(strtolower($field['type']), 'int') ) { $field['type'] = 'INTEGER PRIMARY KEY'; diff --git a/system/Database/SQLite3/Table.php b/system/Database/SQLite3/Table.php index ea84e0011e3f..48e081ed906b 100644 --- a/system/Database/SQLite3/Table.php +++ b/system/Database/SQLite3/Table.php @@ -96,7 +96,7 @@ public function fromTable(string $table) $prefix = $this->db->DBPrefix; - if (! empty($prefix) && str_starts_with($table, $prefix)) { + if ($prefix !== '' && str_starts_with($table, $prefix)) { $table = substr($table, strlen($prefix)); } @@ -217,7 +217,7 @@ public function dropPrimaryKey(): Table */ public function dropForeignKey(string $foreignName) { - if (empty($this->foreignKeys)) { + if ($this->foreignKeys === []) { return $this; } diff --git a/system/Debug/Timer.php b/system/Debug/Timer.php index 98b1299a89f7..45be554ea7ce 100644 --- a/system/Debug/Timer.php +++ b/system/Debug/Timer.php @@ -46,7 +46,7 @@ class Timer public function start(string $name, ?float $time = null) { $this->timers[strtolower($name)] = [ - 'start' => empty($time) ? microtime(true) : $time, + 'start' => $time === null || $time === 0.0 ? microtime(true) : $time, 'end' => null, ]; @@ -67,7 +67,7 @@ public function stop(string $name) { $name = strtolower($name); - if (empty($this->timers[$name])) { + if (! isset($this->timers[$name])) { throw new RuntimeException('Cannot stop timer: invalid name given.'); } @@ -90,13 +90,13 @@ public function getElapsedTime(string $name, int $decimals = 4) { $name = strtolower($name); - if (empty($this->timers[$name])) { + if (! isset($this->timers[$name])) { return null; } $timer = $this->timers[$name]; - if (empty($timer['end'])) { + if ($timer['end'] === null) { $timer['end'] = microtime(true); } @@ -113,7 +113,7 @@ public function getTimers(int $decimals = 4): array $timers = $this->timers; foreach ($timers as &$timer) { - if (empty($timer['end'])) { + if ($timer['end'] === null) { $timer['end'] = microtime(true); } diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index f5885aa0555c..311a41a2bf9d 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -210,8 +210,8 @@ protected function renderTimelineRecursive(array $rows, float $startTime, int $s $output = ''; foreach ($rows as $row) { - $hasChildren = isset($row['children']) && ! empty($row['children']); - $isQuery = isset($row['query']) && ! empty($row['query']); + $hasChildren = ($row['children'] ?? []) !== []; + $isQuery = ($row['query'] ?? '') !== ''; // Open controller timeline by default $open = $row['name'] === 'Controller'; diff --git a/system/Email/Email.php b/system/Email/Email.php index 4b3e94dfb38b..71c68d9e2841 100644 --- a/system/Email/Email.php +++ b/system/Email/Email.php @@ -693,7 +693,7 @@ public function attach($file, $disposition = '', $newname = null, $mime = '') $this->attachments[] = [ 'name' => $namesAttached, - 'disposition' => empty($disposition) ? 'attachment' : $disposition, + 'disposition' => $disposition === '' ? 'attachment' : $disposition, // Can also be 'inline' Not sure if it matters 'type' => $mime, 'content' => chunk_split(base64_encode($fileContent)), @@ -1488,7 +1488,7 @@ protected function prepQEncoding($str) */ public function send($autoClear = true) { - if (! isset($this->headers['From']) && ! empty($this->fromEmail)) { + if (! isset($this->headers['From']) && $this->fromEmail !== null && $this->fromEmail !== '') { $this->setFrom($this->fromEmail, $this->fromName); } @@ -1503,8 +1503,8 @@ public function send($autoClear = true) } if ( - empty($this->recipients) && ! isset($this->headers['To']) - && empty($this->BCCArray) && ! isset($this->headers['Bcc']) + $this->recipients === [] && ! isset($this->headers['To']) + && $this->BCCArray === [] && ! isset($this->headers['Bcc']) && ! isset($this->headers['Cc']) ) { $this->setErrorMessage(lang('Email.noRecipients')); @@ -2180,7 +2180,7 @@ protected function mimeTypes($ext = '') { $mime = Mimes::guessTypeFromExtension(strtolower($ext)); - return empty($mime) ? 'application/x-unknown-content-type' : $mime; + return $mime ?? 'application/x-unknown-content-type'; } public function __destruct() diff --git a/system/Encryption/Encryption.php b/system/Encryption/Encryption.php index 73ec458b837a..47a6ec649e1d 100644 --- a/system/Encryption/Encryption.php +++ b/system/Encryption/Encryption.php @@ -121,7 +121,7 @@ public function initialize(?EncryptionConfig $config = null) $this->digest = $config->digest; } - if (empty($this->driver)) { + if ($this->driver === null || $this->driver === '') { throw EncryptionException::forNoDriverRequested(); } @@ -129,7 +129,7 @@ public function initialize(?EncryptionConfig $config = null) throw EncryptionException::forUnKnownHandler($this->driver); } - if (empty($this->key)) { + if (in_array($this->key, [null, '', '0'], true)) { throw EncryptionException::forNeedsStarterKey(); } diff --git a/system/Encryption/Handlers/OpenSSLHandler.php b/system/Encryption/Handlers/OpenSSLHandler.php index 8e1be06d60ed..26c10a258536 100644 --- a/system/Encryption/Handlers/OpenSSLHandler.php +++ b/system/Encryption/Handlers/OpenSSLHandler.php @@ -87,7 +87,7 @@ public function encrypt(#[SensitiveParameter] $data, #[SensitiveParameter] $para ? (is_array($params) && isset($params['key']) ? $params['key'] : $params) : $this->key; - if (empty($key)) { + if (! is_string($key) || in_array($key, ['', '0'], true)) { throw EncryptionException::forNeedsStarterKey(); } @@ -123,7 +123,7 @@ public function decrypt($data, #[SensitiveParameter] $params = null) ? (is_array($params) && isset($params['key']) ? $params['key'] : $params) : $this->key; - if (empty($key)) { + if (! is_string($key) || in_array($key, ['', '0'], true)) { throw EncryptionException::forNeedsStarterKey(); } diff --git a/system/Encryption/Handlers/SodiumHandler.php b/system/Encryption/Handlers/SodiumHandler.php index 55bf14893251..c759bdbf518f 100644 --- a/system/Encryption/Handlers/SodiumHandler.php +++ b/system/Encryption/Handlers/SodiumHandler.php @@ -56,7 +56,7 @@ public function encrypt(#[SensitiveParameter] $data, #[SensitiveParameter] $para } } - if (empty($key) || strlen((string) $key) !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) { + if (! is_string($key) || strlen($key) !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) { throw EncryptionException::forNeedsStarterKey(); } @@ -92,7 +92,7 @@ public function decrypt($data, #[SensitiveParameter] $params = null) } } - if (empty($key) || strlen((string) $key) !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) { + if (! is_string($key) || strlen($key) !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) { throw EncryptionException::forNeedsStarterKey(); } diff --git a/system/Files/File.php b/system/Files/File.php index 83a304f26e99..8a06b29edb81 100644 --- a/system/Files/File.php +++ b/system/Files/File.php @@ -143,7 +143,7 @@ public function getMimeType(): string public function getRandomName(): string { $extension = $this->getExtension(); - $extension = empty($extension) ? '' : '.' . $extension; + $extension = $extension === '' ? '' : '.' . $extension; return Time::now()->getTimestamp() . '_' . bin2hex(random_bytes(10)) . $extension; } diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index bf5419eab560..38bd553249bb 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -254,7 +254,7 @@ private function runBefore(array $filterClassList) } // Ignore an empty result - if (empty($result)) { + if (! is_string($result) || $result === '') { continue; } diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php index 76ded55c20a6..e1d9d3316b5e 100644 --- a/system/HTTP/CURLRequest.php +++ b/system/HTTP/CURLRequest.php @@ -371,11 +371,13 @@ public function send(string $method, string $url) // Reset our curl options so we're on a fresh slate. $curlOptions = []; - if (! empty($this->config['query']) && is_array($this->config['query'])) { + $query = $this->config['query'] ?? []; + + if (is_array($query) && $query !== []) { // This is likely too naive a solution. // Should look into handling when $url already // has query vars on it. - $url .= '?' . http_build_query($this->config['query']); + $url .= '?' . http_build_query($query); unset($this->config['query']); } @@ -431,7 +433,7 @@ public function send(string $method, string $url) */ protected function applyRequestHeaders(array $curlOptions = []): array { - if (empty($this->headers)) { + if ($this->headers === []) { return $curlOptions; } @@ -478,7 +480,7 @@ protected function applyMethod(string $method, array $curlOptions): array */ protected function applyBody(array $curlOptions = []): array { - if (! empty($this->body)) { + if (! in_array($this->body, [null, '', '0'], true)) { $curlOptions[CURLOPT_POSTFIELDS] = (string) $this->getBody(); } diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index c980810b9b2b..09f80d33253b 100644 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -280,7 +280,14 @@ public function isSecure(): bool return true; } - return $this->hasHeader('Front-End-Https') && ! empty($this->header('Front-End-Https')->getValue()) && strtolower($this->header('Front-End-Https')->getValue()) !== 'off'; + if (! $this->hasHeader('Front-End-Https')) { + return false; + } + + $frontEndHttps = $this->header('Front-End-Https')->getValue(); + + return is_string($frontEndHttps) && ! in_array($frontEndHttps, ['', '0'], true) + && strtolower($frontEndHttps) !== 'off'; } /** diff --git a/system/HTTP/MessageTrait.php b/system/HTTP/MessageTrait.php index 044594bcfcdd..ffef1f07bc3d 100644 --- a/system/HTTP/MessageTrait.php +++ b/system/HTTP/MessageTrait.php @@ -82,7 +82,7 @@ public function appendBody($data): self public function populateHeaders(): void { $contentType = service('superglobals')->server('CONTENT_TYPE', (string) getenv('CONTENT_TYPE')); - if (! empty($contentType)) { + if ($contentType !== '') { $this->setHeader('Content-Type', $contentType); } unset($contentType); @@ -113,7 +113,7 @@ public function headers(): array // If no headers are defined, but the user is // requesting it, then it's likely they want // it to be populated so do that... - if (empty($this->headers)) { + if ($this->headers === []) { $this->populateHeaders(); } diff --git a/system/HTTP/Request.php b/system/HTTP/Request.php index 125646b6c7c7..63b5f0a8dbfd 100644 --- a/system/HTTP/Request.php +++ b/system/HTTP/Request.php @@ -33,11 +33,11 @@ public function __construct($config = null) { $this->config = $config ?? config(App::class); - if (empty($this->method)) { + if ($this->method === null || $this->method === '') { $this->method = $this->getServer('REQUEST_METHOD') ?? Method::GET; } - if (empty($this->uri)) { + if (! $this->uri instanceof URI) { $this->uri = new URI(); } } diff --git a/system/HTTP/RequestTrait.php b/system/HTTP/RequestTrait.php index 9011f4148eba..beb5b30570a3 100644 --- a/system/HTTP/RequestTrait.php +++ b/system/HTTP/RequestTrait.php @@ -72,7 +72,7 @@ public function getIPAddress(): string $proxyIPs = $this->config->proxyIPs; - if (! empty($proxyIPs) && (! is_array($proxyIPs) || is_int(array_key_first($proxyIPs)))) { + if ($proxyIPs !== [] && (! is_array($proxyIPs) || is_int(array_key_first($proxyIPs)))) { throw new ConfigException( 'You must set an array with Proxy IP address key and HTTP header name value in Config\App::$proxyIPs.', ); diff --git a/system/HTTP/Response.php b/system/HTTP/Response.php index 3c59f2e9800d..e1193c063945 100644 --- a/system/HTTP/Response.php +++ b/system/HTTP/Response.php @@ -197,7 +197,7 @@ public function pretend(bool $pretend = true) */ public function getStatusCode(): int { - if (empty($this->statusCode)) { + if ($this->statusCode === 0) { throw HTTPException::forMissingResponseStatus(); } @@ -221,7 +221,7 @@ public function getStatusCode(): int public function getReasonPhrase() { if ($this->reason === '') { - return empty($this->statusCode) ? '' : static::$statusCodes[$this->statusCode]; + return $this->statusCode === 0 ? '' : static::$statusCodes[$this->statusCode]; } return $this->reason; diff --git a/system/Helpers/form_helper.php b/system/Helpers/form_helper.php index 6fa4777e2454..a1f7defc4d14 100644 --- a/system/Helpers/form_helper.php +++ b/system/Helpers/form_helper.php @@ -637,7 +637,7 @@ function set_checkbox(string $field, string $value = '', bool $default = false): $hasOldInput = $session->has('_ci_old_input'); // Unchecked checkbox and radio inputs are not even submitted by browsers ... - if ((string) $input === '0' || ! empty($request->getPost()) || $hasOldInput) { + if ((string) $input === '0' || $request->getPost() !== [] || $hasOldInput) { return ($input === $value) ? ' checked="checked"' : ''; } diff --git a/system/Helpers/test_helper.php b/system/Helpers/test_helper.php index f86ca468b0cc..645d407dfcbf 100644 --- a/system/Helpers/test_helper.php +++ b/system/Helpers/test_helper.php @@ -57,13 +57,13 @@ function mock(string $className) $mockClass = $className::$mockClass; $mockService = $className::$mockServiceName ?? ''; - if (empty($mockClass) || ! class_exists($mockClass)) { + if ($mockClass === '' || ! class_exists($mockClass)) { throw TestException::forInvalidMockClass($mockClass); } $mock = new $mockClass(); - if (! empty($mockService)) { + if ($mockService !== '') { Services::injectMock($mockService, $mock); } diff --git a/system/Helpers/url_helper.php b/system/Helpers/url_helper.php index f8cb6b2540f9..208ae722a2f2 100644 --- a/system/Helpers/url_helper.php +++ b/system/Helpers/url_helper.php @@ -205,7 +205,7 @@ function anchor_popup($uri = '', string $title = '', $attributes = false, ?App $ // Ref: http://www.w3schools.com/jsref/met_win_open.asp $windowName = '_blank'; - } elseif (! empty($attributes['window_name'])) { + } elseif (($attributes['window_name'] ?? '') !== '') { $windowName = $attributes['window_name']; unset($attributes['window_name']); } else { diff --git a/system/Honeypot/Honeypot.php b/system/Honeypot/Honeypot.php index a816bdd1ed60..badf1d719b53 100644 --- a/system/Honeypot/Honeypot.php +++ b/system/Honeypot/Honeypot.php @@ -64,7 +64,7 @@ public function hasContent(RequestInterface $request) { assert($request instanceof IncomingRequest); - return ! empty($request->getPost($this->config->name)); + return ! in_array($request->getPost($this->config->name), [null, false, 0, 0.0, '', '0', []], true); } /** diff --git a/system/Images/Handlers/BaseHandler.php b/system/Images/Handlers/BaseHandler.php index 9301b1573c23..1ceebf7fe7a4 100644 --- a/system/Images/Handlers/BaseHandler.php +++ b/system/Images/Handlers/BaseHandler.php @@ -564,7 +564,7 @@ public function fit(int $width, ?int $height = null, string $position = 'center' */ protected function calcAspectRatio($width, $height = null, $origWidth = 0, $origHeight = 0): array { - if (empty($origWidth) || empty($origHeight)) { + if (in_array($origWidth, [0, 0.0], true) || in_array($origHeight, [0, 0.0], true)) { throw new InvalidArgumentException('You must supply the parameters: origWidth, origHeight.'); } diff --git a/system/Images/Image.php b/system/Images/Image.php index 0634405c6ec0..3e0b0ba6876d 100644 --- a/system/Images/Image.php +++ b/system/Images/Image.php @@ -75,7 +75,7 @@ public function copy(string $targetPath, ?string $targetName = null, int $perms $targetName ??= $this->getFilename(); - if (empty($targetName)) { + if ($targetName === null || $targetName === '') { throw ImageException::forInvalidFile($targetName); } diff --git a/system/Pager/Pager.php b/system/Pager/Pager.php index 66b1dd13d78f..1a01de3f1aa9 100644 --- a/system/Pager/Pager.php +++ b/system/Pager/Pager.php @@ -310,7 +310,7 @@ public function getNextPageURI(string $group = 'default', bool $returnObject = f $curr = $this->getCurrentPage($group); $page = null; - if (! empty($last) && $curr !== 0 && $last === $curr) { + if (! in_array($last, [null, 0], true) && $curr !== 0 && $last === $curr) { return null; } diff --git a/system/RESTful/BaseResource.php b/system/RESTful/BaseResource.php index f2e171c8319e..638f46becbbe 100644 --- a/system/RESTful/BaseResource.php +++ b/system/RESTful/BaseResource.php @@ -66,11 +66,11 @@ public function setModel($which = null) $this->modelName = is_object($which) ? null : $which; } - if (empty($this->model) && ! empty($this->modelName) && class_exists($this->modelName)) { + if ($this->model === null && $this->modelName !== null && class_exists($this->modelName)) { $this->model = model($this->modelName); } - if (! empty($this->model) && empty($this->modelName)) { + if ($this->model !== null && ($this->modelName === null || $this->modelName === '')) { $this->modelName = $this->model::class; } } diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 8253973beb27..6dfb42286972 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -1017,7 +1017,7 @@ public function presenter(string $name, ?array $options = null): RouteCollection */ public function match(array $verbs = [], string $from = '', $to = '', ?array $options = null): RouteCollectionInterface { - if ($from === '' || empty($to)) { + if ($from === '' || in_array($to, ['', []], true)) { throw new InvalidArgumentException('You must supply the parameters: from, to.'); } @@ -1316,7 +1316,7 @@ protected function fillRouteParams(string $from, ?array $params = null): string // Find all of our back-references in the original route preg_match_all('/\(([^)]+)\)/', $from, $matches); - if (empty($matches[0])) { + if ($matches[0] === []) { return '/' . ltrim($from, '/'); } @@ -1355,7 +1355,7 @@ protected function buildReverseRoute(string $from, array $params): string // Find all of our back-references in the original route preg_match_all('/\(([^)]+)\)/', $from, $matches); - if (empty($matches[0])) { + if ($matches[0] === []) { if (str_contains($from, '{locale}')) { $locale = $params[0] ?? null; } @@ -1475,7 +1475,7 @@ protected function create(string $verb, string $from, $to, ?array $options = nul } // Hostname limiting? - if (! empty($options['hostname'])) { + if (! in_array($options['hostname'] ?? '', ['', '0', []], true)) { // @todo determine if there's a way to whitelist hosts? if (! $this->checkHostname($options['hostname'])) { return; @@ -1484,7 +1484,7 @@ protected function create(string $verb, string $from, $to, ?array $options = nul $overwrite = true; } // Limiting to subdomains? - elseif (! empty($options['subdomain'])) { + elseif (! in_array($options['subdomain'] ?? '', ['', '0', []], true)) { // If we don't match the current subdomain, then // we don't need to add the route. if (! $this->checkSubdomains($options['subdomain'])) { @@ -1646,7 +1646,7 @@ private function checkSubdomains($subdomains): bool // Routes can be limited to any sub-domain. In that case, though, // it does require a sub-domain to be present. - if (! empty($this->currentSubdomain) && in_array('*', $subdomains, true)) { + if (! in_array($this->currentSubdomain, [null, ''], true) && in_array('*', $subdomains, true)) { return true; } diff --git a/system/Router/Router.php b/system/Router/Router.php index 1eda4c616d62..cb53b439ea30 100644 --- a/system/Router/Router.php +++ b/system/Router/Router.php @@ -729,7 +729,7 @@ protected function setRequest(array $segments = []) */ protected function setDefaultController() { - if (empty($this->controller)) { + if (! is_string($this->controller) || $this->controller === '') { throw RouterException::forMissingDefaultRoute(); } diff --git a/system/Session/Handlers/RedisHandler.php b/system/Session/Handlers/RedisHandler.php index 54f628c4789b..d1c3c02e6fe4 100644 --- a/system/Session/Handlers/RedisHandler.php +++ b/system/Session/Handlers/RedisHandler.php @@ -174,7 +174,7 @@ protected function setSavePath(): void */ public function open($path, $name): bool { - if (empty($this->savePath)) { + if ($this->savePath === [] || $this->savePath === '') { return false; } diff --git a/system/Test/FeatureTestTrait.php b/system/Test/FeatureTestTrait.php index db298084e023..ce115761c117 100644 --- a/system/Test/FeatureTestTrait.php +++ b/system/Test/FeatureTestTrait.php @@ -366,10 +366,8 @@ protected function setupRequest(string $method, ?string $path = null): IncomingR */ protected function setupHeaders(IncomingRequest $request) { - if (! empty($this->headers)) { - foreach ($this->headers as $name => $value) { - $request->setHeader($name, $value); - } + foreach ($this->headers as $name => $value) { + $request->setHeader($name, $value); } return $request; diff --git a/system/Test/FilterTestTrait.php b/system/Test/FilterTestTrait.php index 1140adf7dc5e..59ffff646a79 100644 --- a/system/Test/FilterTestTrait.php +++ b/system/Test/FilterTestTrait.php @@ -178,7 +178,7 @@ protected function getFilterCaller($filter, string $position): Closure if ($result instanceof ResponseInterface) { return $result; } - if (empty($result)) { + if (! is_string($result) || $result === '') { continue; } } diff --git a/system/Validation/Rules.php b/system/Validation/Rules.php index 7617b283f099..31aa042c5d36 100644 --- a/system/Validation/Rules.php +++ b/system/Validation/Rules.php @@ -369,8 +369,8 @@ public function required_with($str = null, ?string $fields = null, array $data = foreach (explode(',', $fields) as $field) { if ( - (array_key_exists($field, $data) && ! empty($data[$field])) - || (str_contains($field, '.') && ! empty(dot_array_search($field, $data))) + (array_key_exists($field, $data) && ! $this->isBlank($data[$field])) + || (str_contains($field, '.') && ! $this->isBlank(dot_array_search($field, $data))) ) { $requiredFields[] = $field; } @@ -417,7 +417,7 @@ public function required_without( if ( (! str_contains($otherField, '.')) && (! array_key_exists($otherField, $data) - || empty($data[$otherField])) + || $this->isBlank($data[$otherField])) ) { return false; } @@ -432,7 +432,7 @@ public function required_without( $fieldKey = $fieldSplitArray[1] ?? null; if (is_array($fieldData)) { - if (empty($fieldData[$fieldKey])) { + if ($this->isBlank($fieldData[$fieldKey] ?? null)) { return false; } @@ -472,4 +472,15 @@ public function field_exists( return array_key_exists($field, $data); } + + /** + * Whether the value counts as blank for the `required_with` and + * `required_without` rules. + * + * @param mixed $value + */ + private function isBlank($value): bool + { + return in_array($value, [null, false, 0, 0.0, '', '0', []], true); + } } diff --git a/system/View/Filters.php b/system/View/Filters.php index c74e385cdb96..9e57976e575c 100644 --- a/system/View/Filters.php +++ b/system/View/Filters.php @@ -71,7 +71,7 @@ public static function date_modify($value, string $adjustment) */ public static function default($value, string $default): string { - return empty($value) ? $default : $value; + return in_array($value, [null, false, 0, 0.0, '', '0', []], true) ? $default : $value; } /** diff --git a/tests/system/Session/SessionTest.php b/tests/system/Session/SessionTest.php index 3b7cd3860ccd..e405325501d5 100644 --- a/tests/system/Session/SessionTest.php +++ b/tests/system/Session/SessionTest.php @@ -80,7 +80,8 @@ public function testSessionSetsRegenerateTime(): void $session = $this->getInstance(); $session->start(); - $this->assertTrue(isset($_SESSION['__ci_last_regenerate']) && ! empty($_SESSION['__ci_last_regenerate'])); + $this->assertArrayHasKey('__ci_last_regenerate', $_SESSION); + $this->assertGreaterThan(0, $_SESSION['__ci_last_regenerate']); } public function testWillRegenerateSessionAutomatically(): void diff --git a/utils/phpstan-baseline/empty.notAllowed.neon b/utils/phpstan-baseline/empty.notAllowed.neon deleted file mode 100644 index 9060c95a91ec..000000000000 --- a/utils/phpstan-baseline/empty.notAllowed.neon +++ /dev/null @@ -1,323 +0,0 @@ -# total 204 errors - -parameters: - ignoreErrors: - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 1 - path: ../../system/Commands/Database/CreateDatabase.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 1 - path: ../../system/Commands/Database/MigrateStatus.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 1 - path: ../../system/Commands/Database/Seed.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 2 - path: ../../system/Config/BaseService.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 2 - path: ../../system/Config/DotEnv.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 1 - path: ../../system/Config/Services.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 27 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 13 - path: ../../system/Database/BaseConnection.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 3 - path: ../../system/Database/BasePreparedQuery.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 2 - path: ../../system/Database/BaseResult.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 3 - path: ../../system/Database/BaseUtils.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 2 - path: ../../system/Database/Database.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 12 - path: ../../system/Database/Forge.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 4 - path: ../../system/Database/MigrationRunner.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 10 - path: ../../system/Database/MySQLi/Connection.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 4 - path: ../../system/Database/MySQLi/Forge.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 1 - path: ../../system/Database/MySQLi/Result.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 3 - path: ../../system/Database/OCI8/Builder.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 2 - path: ../../system/Database/OCI8/Connection.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 3 - path: ../../system/Database/OCI8/Forge.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 7 - path: ../../system/Database/Postgre/Builder.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 2 - path: ../../system/Database/Postgre/Connection.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 4 - path: ../../system/Database/Postgre/Forge.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 1 - path: ../../system/Database/Postgre/Result.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 3 - path: ../../system/Database/Query.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 9 - path: ../../system/Database/SQLSRV/Builder.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 6 - path: ../../system/Database/SQLSRV/Connection.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 5 - path: ../../system/Database/SQLSRV/Forge.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 1 - path: ../../system/Database/SQLSRV/Result.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 2 - path: ../../system/Database/SQLite3/Builder.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 1 - path: ../../system/Database/SQLite3/Connection.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 2 - path: ../../system/Database/SQLite3/Forge.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 2 - path: ../../system/Database/SQLite3/Table.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 5 - path: ../../system/Debug/Timer.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 2 - path: ../../system/Debug/Toolbar.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 5 - path: ../../system/Email/Email.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 2 - path: ../../system/Encryption/Encryption.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 2 - path: ../../system/Encryption/Handlers/OpenSSLHandler.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 2 - path: ../../system/Encryption/Handlers/SodiumHandler.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 1 - path: ../../system/Files/File.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 1 - path: ../../system/Filters/Filters.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 3 - path: ../../system/HTTP/CURLRequest.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 2 - path: ../../system/HTTP/Message.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 3 - path: ../../system/HTTP/Request.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 2 - path: ../../system/HTTP/Response.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 2 - path: ../../system/Helpers/test_helper.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 1 - path: ../../system/Helpers/url_helper.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 1 - path: ../../system/Honeypot/Honeypot.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 2 - path: ../../system/Images/Handlers/BaseHandler.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 1 - path: ../../system/Images/Image.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 1 - path: ../../system/Pager/Pager.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 4 - path: ../../system/RESTful/BaseResource.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 6 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 1 - path: ../../system/Router/Router.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 1 - path: ../../system/Session/Handlers/RedisHandler.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 4 - path: ../../system/Validation/Rules.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 1 - path: ../../system/View/Filters.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 1 - path: ../../tests/system/HomeTest.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 1 - path: ../../tests/system/Session/SessionTest.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestAutoRoutingImprovedTest.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestTraitTest.php - - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - count: 1 - path: ../../tests/system/Test/FilterTestTraitTest.php diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 32ad4dacc2f5..588d3cec14bd 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 1808 errors +# total 1604 errors includes: - argument.type.neon @@ -6,7 +6,6 @@ includes: - assign.propertyType.neon - codeigniter.modelArgumentType.neon - deadCode.unreachable.neon - - empty.notAllowed.neon - function.resultUnused.neon - method.alreadyNarrowedType.neon - method.childParameterType.neon From 29d26fe467c51b7773db0afa868fc24f4d23096c Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Wed, 12 Aug 2026 12:47:12 +0800 Subject: [PATCH 044/163] refactor: fix `method.alreadyNarrowedType` errors (#10440) --- admin/starter/tests/unit/HealthTest.php | 2 +- tests/system/CLI/CLITest.php | 9 +- tests/system/CodeIgniterTest.php | 2 +- tests/system/CommonFunctionsTest.php | 1 - tests/system/Config/FactoriesTest.php | 2 - tests/system/Database/Live/UpdateTest.php | 4 +- tests/system/HTTP/CLIRequestTest.php | 1 - tests/system/HTTP/RequestTest.php | 1 - tests/system/HTTP/URITest.php | 2 +- tests/system/Helpers/TextHelperTest.php | 3 +- .../HotReloader/DirectoryHasherTest.php | 1 - .../system/Publisher/PublisherSupportTest.php | 1 - tests/system/Security/SecurityTest.php | 10 +-- tests/system/Test/FabricatorTest.php | 4 +- tests/system/Test/FilterTestTraitTest.php | 4 +- utils/phpstan-baseline/loader.neon | 3 +- .../method.alreadyNarrowedType.neon | 88 ------------------- 17 files changed, 23 insertions(+), 115 deletions(-) delete mode 100644 utils/phpstan-baseline/method.alreadyNarrowedType.neon diff --git a/admin/starter/tests/unit/HealthTest.php b/admin/starter/tests/unit/HealthTest.php index 1df24df823a7..bae45560b788 100644 --- a/admin/starter/tests/unit/HealthTest.php +++ b/admin/starter/tests/unit/HealthTest.php @@ -11,7 +11,7 @@ final class HealthTest extends CIUnitTestCase { public function testIsDefinedAppPath(): void { - $this->assertTrue(defined('APPPATH')); + $this->assertDirectoryExists(APPPATH); } public function testBaseUrlHasBeenSet(): void diff --git a/tests/system/CLI/CLITest.php b/tests/system/CLI/CLITest.php index 9391eae05d27..083f8792e31a 100644 --- a/tests/system/CLI/CLITest.php +++ b/tests/system/CLI/CLITest.php @@ -292,7 +292,10 @@ public function testColorSupportOnHyperTerminals(): void public function testStreamSupports(): void { $this->assertTrue(CLI::streamSupports('stream_isatty', STDOUT)); - $this->assertIsBool(CLI::streamSupports('sapi_windows_vt100_support', STDOUT)); + $this->assertSame( + function_exists('sapi_windows_vt100_support'), + CLI::streamSupports('sapi_windows_vt100_support', STDOUT), + ); } public function testColor(): void @@ -587,12 +590,12 @@ public function testWindow(): void $height = new ReflectionProperty(CLI::class, 'height'); $height->setValue(null, null); - $this->assertIsInt(CLI::getHeight()); + $this->assertGreaterThan(0, CLI::getHeight()); $width = new ReflectionProperty(CLI::class, 'width'); $width->setValue(null, null); - $this->assertIsInt(CLI::getWidth()); + $this->assertGreaterThan(0, CLI::getWidth()); } #[RequiresOperatingSystem('Darwin|Linux')] diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index e5d371bfb941..2d621f3b8703 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -984,7 +984,7 @@ public function testStartControllerPermitsInvoke(): void $startController(); // No PageNotFoundException - $this->assertTrue(true); + $this->expectNotToPerformAssertions(); } public function testRouteAttributeCacheIntegration(): void diff --git a/tests/system/CommonFunctionsTest.php b/tests/system/CommonFunctionsTest.php index 83750c220bc4..3f970d72a154 100644 --- a/tests/system/CommonFunctionsTest.php +++ b/tests/system/CommonFunctionsTest.php @@ -773,7 +773,6 @@ public static function provideCleanPathActuallyCleaningThePaths(): iterable public function testIsCli(): void { - $this->assertIsBool(is_cli()); $this->assertTrue(is_cli()); } diff --git a/tests/system/Config/FactoriesTest.php b/tests/system/Config/FactoriesTest.php index 37077c41b1a5..eafde84857e6 100644 --- a/tests/system/Config/FactoriesTest.php +++ b/tests/system/Config/FactoriesTest.php @@ -439,7 +439,6 @@ public function testGetComponentInstances(): array $data = Factories::getComponentInstances('config'); - $this->assertIsArray($data); $this->assertArrayHasKey('aliases', $data); $this->assertArrayHasKey('instances', $data); @@ -469,7 +468,6 @@ public function testSetComponentInstances(array $data): array $data = Factories::getComponentInstances('config'); - $this->assertIsArray($data); $this->assertArrayHasKey('aliases', $data); $this->assertArrayHasKey('instances', $data); diff --git a/tests/system/Database/Live/UpdateTest.php b/tests/system/Database/Live/UpdateTest.php index e1fed75e660e..7f9ce90944ae 100644 --- a/tests/system/Database/Live/UpdateTest.php +++ b/tests/system/Database/Live/UpdateTest.php @@ -75,7 +75,7 @@ public function testUpdateSetsAllWithoutWhereAndLimit(): void } catch (DatabaseException) { // This DB doesn't support Where and Limit together // but we don't want it called a "Risky" test. - $this->assertTrue(true); + $this->expectNotToPerformAssertions(); } } @@ -110,7 +110,7 @@ public function testUpdateWithWhereAndLimit(): void } catch (DatabaseException) { // This DB doesn't support Where and Limit together // but we don't want it called a "Risky" test. - $this->assertTrue(true); + $this->expectNotToPerformAssertions(); } } diff --git a/tests/system/HTTP/CLIRequestTest.php b/tests/system/HTTP/CLIRequestTest.php index d12889448a30..91155e445f86 100644 --- a/tests/system/HTTP/CLIRequestTest.php +++ b/tests/system/HTTP/CLIRequestTest.php @@ -431,7 +431,6 @@ public function testFetchGlobalReturnsArrayValues(): void $result = $this->request->fetchGlobal('post'); $this->assertSame($post, $result); - $this->assertIsArray($result['ANNOUNCEMENTS']); $this->assertCount(2, $result['ANNOUNCEMENTS']); } diff --git a/tests/system/HTTP/RequestTest.php b/tests/system/HTTP/RequestTest.php index eb15506e6fdf..e1f70bdd272f 100644 --- a/tests/system/HTTP/RequestTest.php +++ b/tests/system/HTTP/RequestTest.php @@ -195,7 +195,6 @@ public function testFetchGlobalReturnsArrayValues(): void $result = $this->request->fetchGlobal('post'); $this->assertSame($post, $result); - $this->assertIsArray($result['ANNOUNCEMENTS']); $this->assertCount(2, $result['ANNOUNCEMENTS']); } diff --git a/tests/system/HTTP/URITest.php b/tests/system/HTTP/URITest.php index c449d13af65c..8f2941af82c5 100644 --- a/tests/system/HTTP/URITest.php +++ b/tests/system/HTTP/URITest.php @@ -1192,7 +1192,7 @@ public function testSetURISilent(): void $uri->setSilent()->setURI($url); - $this->assertTrue(true); + $this->expectNotToPerformAssertions(); } public function testCreateURIStringNoArguments(): void diff --git a/tests/system/Helpers/TextHelperTest.php b/tests/system/Helpers/TextHelperTest.php index a14c722816ae..212f2797bacf 100644 --- a/tests/system/Helpers/TextHelperTest.php +++ b/tests/system/Helpers/TextHelperTest.php @@ -130,8 +130,7 @@ public function testRandomString(): void $this->assertSame(16, strlen(random_string('numeric', 16))); $this->assertSame(8, strlen(random_string('numeric'))); - $this->assertSame(16, strlen($random = random_string('crypto', 16))); - $this->assertIsString($random); + $this->assertSame(16, strlen(random_string('crypto', 16))); } /** diff --git a/tests/system/HotReloader/DirectoryHasherTest.php b/tests/system/HotReloader/DirectoryHasherTest.php index a8a112ffe84b..8a0428465cea 100644 --- a/tests/system/HotReloader/DirectoryHasherTest.php +++ b/tests/system/HotReloader/DirectoryHasherTest.php @@ -62,7 +62,6 @@ public function testHashApp(): void $results = $this->hasher->hashApp(); - $this->assertIsArray($results); $this->assertArrayHasKey($this->fixtureDirectory, $results); } diff --git a/tests/system/Publisher/PublisherSupportTest.php b/tests/system/Publisher/PublisherSupportTest.php index 7e6e21e430d4..e432f9d21d6c 100644 --- a/tests/system/Publisher/PublisherSupportTest.php +++ b/tests/system/Publisher/PublisherSupportTest.php @@ -106,7 +106,6 @@ public function testGetScratch(): void $scratch = $publisher->getScratch(); - $this->assertIsString($scratch); $this->assertDirectoryExists($scratch); $this->assertDirectoryIsWritable($scratch); $this->assertNotNull($this->getPrivateProperty($publisher, 'scratch')); diff --git a/tests/system/Security/SecurityTest.php b/tests/system/Security/SecurityTest.php index 932dfc0df2c0..db80d91cbd92 100644 --- a/tests/system/Security/SecurityTest.php +++ b/tests/system/Security/SecurityTest.php @@ -350,11 +350,11 @@ public function testGetters(): void { $security = $this->createMockSecurity(); - $this->assertIsString($security->getHash()); - $this->assertIsString($security->getTokenName()); - $this->assertIsString($security->getHeaderName()); - $this->assertIsString($security->getCookieName()); - $this->assertIsBool($security->shouldRedirect()); + $this->assertSame(32, strlen($security->getHash())); + $this->assertSame('csrf_test_name', $security->getTokenName()); + $this->assertSame('X-CSRF-TOKEN', $security->getHeaderName()); + $this->assertSame('csrf_cookie_name', $security->getCookieName()); + $this->assertFalse($security->shouldRedirect()); } public function testGetPostedTokenReturnsTokenFromPost(): void diff --git a/tests/system/Test/FabricatorTest.php b/tests/system/Test/FabricatorTest.php index 46ab9441dd74..1c3609f29b14 100644 --- a/tests/system/Test/FabricatorTest.php +++ b/tests/system/Test/FabricatorTest.php @@ -128,8 +128,10 @@ public function testGetFakerReturnsUsableGenerator(): void $fabricator = new Fabricator(UserModel::class); $faker = $fabricator->getFaker(); + $digit = $faker->randomDigit(); - $this->assertIsNumeric($faker->randomDigit()); + $this->assertGreaterThanOrEqual(0, $digit); + $this->assertLessThanOrEqual(9, $digit); } public function testSetFormattersChangesFormatters(): void diff --git a/tests/system/Test/FilterTestTraitTest.php b/tests/system/Test/FilterTestTraitTest.php index ff74e029be31..3343be2e47a1 100644 --- a/tests/system/Test/FilterTestTraitTest.php +++ b/tests/system/Test/FilterTestTraitTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Test; +use Closure; use CodeIgniter\HTTP\RequestInterface; use PHPUnit\Framework\Attributes\Group; use Tests\Support\Filters\Customfilter; @@ -53,8 +54,7 @@ public function testGetCallerReturnsClosure(): void { $caller = $this->getFilterCaller('test-customfilter', 'before'); - $this->assertIsCallable($caller); - $this->assertInstanceOf('Closure', $caller); + $this->assertInstanceOf(Closure::class, $caller); } public function testGetCallerInvalidPosition(): void diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 588d3cec14bd..19e526d098ec 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 1604 errors +# total 1582 errors includes: - argument.type.neon @@ -7,7 +7,6 @@ includes: - codeigniter.modelArgumentType.neon - deadCode.unreachable.neon - function.resultUnused.neon - - method.alreadyNarrowedType.neon - method.childParameterType.neon - method.childReturnType.neon - method.notFound.neon diff --git a/utils/phpstan-baseline/method.alreadyNarrowedType.neon b/utils/phpstan-baseline/method.alreadyNarrowedType.neon deleted file mode 100644 index f3c373da1f4a..000000000000 --- a/utils/phpstan-baseline/method.alreadyNarrowedType.neon +++ /dev/null @@ -1,88 +0,0 @@ -# total 22 errors - -parameters: - ignoreErrors: - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertTrue\(\) with bool will always evaluate to true\.$#' - count: 1 - path: ../../admin/starter/tests/unit/HealthTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertIsBool\(\) with bool will always evaluate to true\.$#' - count: 1 - path: ../../tests/system/CLI/CLITest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertIsInt\(\) with int will always evaluate to true\.$#' - count: 2 - path: ../../tests/system/CLI/CLITest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertTrue\(\) with true will always evaluate to true\.$#' - count: 1 - path: ../../tests/system/CodeIgniterTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertIsBool\(\) with bool will always evaluate to true\.$#' - count: 1 - path: ../../tests/system/CommonFunctionsTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertIsArray\(\) with array will always evaluate to true\.$#' - count: 2 - path: ../../tests/system/Config/FactoriesTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertTrue\(\) with true will always evaluate to true\.$#' - count: 2 - path: ../../tests/system/Database/Live/UpdateTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertIsArray\(\) with array\{1\: array\{DETAIL\: ''asdf''\}, 2\: array\{DETAIL\: ''sdfg''\}\} will always evaluate to true\.$#' - count: 1 - path: ../../tests/system/HTTP/CLIRequestTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertIsArray\(\) with array\{1\: array\{DETAIL\: ''asdf''\}, 2\: array\{DETAIL\: ''sdfg''\}\} will always evaluate to true\.$#' - count: 1 - path: ../../tests/system/HTTP/RequestTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertTrue\(\) with true will always evaluate to true\.$#' - count: 1 - path: ../../tests/system/HTTP/URITest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertIsString\(\) with string will always evaluate to true\.$#' - count: 1 - path: ../../tests/system/Helpers/TextHelperTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertIsArray\(\) with array will always evaluate to true\.$#' - count: 1 - path: ../../tests/system/HotReloader/DirectoryHasherTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertIsString\(\) with string will always evaluate to true\.$#' - count: 1 - path: ../../tests/system/Publisher/PublisherSupportTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertIsBool\(\) with bool will always evaluate to true\.$#' - count: 1 - path: ../../tests/system/Security/SecurityTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertIsString\(\) with string will always evaluate to true\.$#' - count: 3 - path: ../../tests/system/Security/SecurityTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertIsNumeric\(\) with int will always evaluate to true\.$#' - count: 1 - path: ../../tests/system/Test/FabricatorTest.php - - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertIsCallable\(\) with Closure will always evaluate to true\.$#' - count: 1 - path: ../../tests/system/Test/FilterTestTraitTest.php From f9f18fb3880331c44aa21ed869359823e54f07d2 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Wed, 12 Aug 2026 13:29:24 +0800 Subject: [PATCH 045/163] refactor: cleanup the Images library (#10441) --- phpstan.dist.neon | 1 - system/Images/Handlers/BaseHandler.php | 28 ++----- system/Images/Handlers/GDHandler.php | 79 ++++++++----------- system/Images/Handlers/ImageMagickHandler.php | 30 +------ utils/phpstan-baseline/argument.type.neon | 12 +-- utils/phpstan-baseline/loader.neon | 3 +- .../missingType.iterableValue.neon | 12 +-- .../phpstan-baseline/property.phpDocType.neon | 7 +- utils/phpstan-baseline/varTag.type.neon | 8 -- 9 files changed, 49 insertions(+), 131 deletions(-) delete mode 100644 utils/phpstan-baseline/varTag.type.neon diff --git a/phpstan.dist.neon b/phpstan.dist.neon index 2e05af3201a5..71e4151f6f3d 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -18,7 +18,6 @@ parameters: - app/Views/errors/html/* - system/Commands/Generators/Views/* - system/Debug/Toolbar/Views/toolbar.tpl.php - - system/Images/Handlers/GDHandler.php - system/Test/Mock/MockCommon.php - system/ThirdParty/* - system/Validation/Views/single.php diff --git a/system/Images/Handlers/BaseHandler.php b/system/Images/Handlers/BaseHandler.php index 1ceebf7fe7a4..bc70517a36a6 100644 --- a/system/Images/Handlers/BaseHandler.php +++ b/system/Images/Handlers/BaseHandler.php @@ -20,7 +20,9 @@ use Config\Images; /** - * Base image handling implementation + * Base image handling implementation. + * + * @template T of object */ abstract class BaseHandler implements ImageHandlerInterface { @@ -90,7 +92,7 @@ abstract class BaseHandler implements ImageHandlerInterface /** * Default options for text watermarking. * - * @var array + * @var array */ protected $textDefaults = [ 'fontPath' => null, @@ -110,7 +112,7 @@ abstract class BaseHandler implements ImageHandlerInterface /** * Image types with support for transparency. * - * @var array + * @var list */ protected $supportTransparency = [ IMAGETYPE_PNG, @@ -120,7 +122,7 @@ abstract class BaseHandler implements ImageHandlerInterface /** * Temporary image used by the different engines. * - * @var resource|null + * @var T|null */ protected $resource; @@ -193,7 +195,6 @@ protected function image(): Image throw ImageException::forMissingImage(); } - // Verify the loaded image is an Image instance if (! $this->image instanceof Image) { throw ImageException::forInvalidPath(); } @@ -203,7 +204,6 @@ protected function image(): Image throw ImageException::forFileNotSupported(); } - // Note that the image has been verified $this->verified = true; return $this->image; @@ -214,7 +214,7 @@ protected function image(): Image * Good for extending the system or doing things this library * is not intended to do. * - * @return resource + * @return T */ public function getResource() { @@ -246,7 +246,6 @@ public function withResource() */ public function resize(int $width, int $height, bool $maintainRatio = false, string $masterDim = 'auto') { - // If the target width/height match the source, then we have nothing to do here. if ($this->image()->origWidth === $width && $this->image()->origHeight === $height) { return $this; } @@ -316,28 +315,20 @@ public function convert(int $imageType) */ public function rotate(float $angle) { - // Allowed rotation values - $degs = [ - 90.0, - 180.0, - 270.0, - ]; + $degs = [90.0, 180.0, 270.0]; if (! in_array($angle, $degs, true)) { throw ImageException::forMissingAngle(); } - // cast angle as an int, for our use $angle = (int) $angle; - // Reassign the width and height if ($angle === 90 || $angle === 270) { $temp = $this->height; $this->width = $this->height; $this->height = $temp; } - // Call the Handler-specific version. $this->_rotate($angle); return $this; @@ -568,8 +559,6 @@ protected function calcAspectRatio($width, $height = null, $origWidth = 0, $orig throw new InvalidArgumentException('You must supply the parameters: origWidth, origHeight.'); } - // If $height is null, then we have it easy. - // Calc based on full image size and be done. if ($height === null) { $height = ($width / $origWidth) * $origHeight; @@ -724,7 +713,6 @@ protected function reproportion() return; } - // Sanitize $this->width = (int) $this->width; $this->height = (int) $this->height; diff --git a/system/Images/Handlers/GDHandler.php b/system/Images/Handlers/GDHandler.php index 01c05384c744..2ba97437edda 100644 --- a/system/Images/Handlers/GDHandler.php +++ b/system/Images/Handlers/GDHandler.php @@ -15,15 +15,16 @@ use CodeIgniter\Images\Exceptions\ImageException; use Config\Images; +use GdImage; /** * Image handler for GD package + * + * @extends BaseHandler */ class GDHandler extends BaseHandler { /** - * Constructor. - * * @param Images|null $config * * @throws ImageException @@ -43,17 +44,13 @@ public function __construct($config = null) */ protected function _rotate(int $angle): bool { - // Create the image handle $srcImg = $this->createImage(); - // Set the background color // This won't work with transparent PNG files so we are // going to have to figure out how to determine the color // of the alpha channel in a future release. - $white = imagecolorallocate($srcImg, 255, 255, 255); - // Rotate it! $destImg = imagerotate($srcImg, $angle, $white); $this->resource = $destImg; @@ -110,23 +107,23 @@ protected function _flip(string $direction) /** * Get GD version * - * @return mixed + * @return string */ public function getVersion() { - if (function_exists('gd_info')) { - $gdVersion = @gd_info(); - - return preg_replace('/\D/', '', $gdVersion['GD Version']); + if (! function_exists('gd_info')) { + return ''; // @codeCoverageIgnore } - return false; + $gdVersion = @gd_info(); + + return preg_replace('/\D/', '', $gdVersion['GD Version']) ?? ''; } /** * Resizes the image. * - * @return GDHandler + * @return $this */ public function _resize(bool $maintainRatio = false) { @@ -136,7 +133,7 @@ public function _resize(bool $maintainRatio = false) /** * Crops the image. * - * @return GDHandler + * @return $this */ public function _crop() { @@ -154,7 +151,6 @@ protected function process(string $action) $origHeight = $this->image()->origHeight; if ($action === 'crop') { - // Reassign the source width/height if cropping $origWidth = $this->width; $origHeight = $this->height; @@ -165,7 +161,6 @@ protected function process(string $action) $this->image()->origWidth = $this->width; } - // Create the image handle $src = $this->createImage(); if (function_exists('imagecreatetruecolor')) { @@ -178,7 +173,6 @@ protected function process(string $action) $dest = $create($this->width, $this->height); - // for png and webp we can actually preserve transparency if (in_array($this->image()->imageType, $this->supportTransparency, true)) { imagealphablending($dest, false); imagesavealpha($dest, true); @@ -209,7 +203,7 @@ public function save(?string $target = null, int $quality = 90): bool // If no new resource has been created, then we're // simply copy the existing one. - if (empty($this->resource) && $quality === 100) { + if ($this->resource === null && $quality === 100) { if ($original === null) { return true; } @@ -222,7 +216,6 @@ public function save(?string $target = null, int $quality = 90): bool $this->ensureResource(); - // for png and webp we can actually preserve transparency if (in_array($this->image()->imageType, $this->supportTransparency, true)) { imagepalettetotruecolor($this->resource); imagealphablending($this->resource, false); @@ -284,10 +277,7 @@ public function save(?string $target = null, int $quality = 90): bool /** * Create Image Resource * - * This simply creates an image resource handle - * based on the type of image being processed - * - * @return bool|resource + * @return GdImage */ protected function createImage(string $path = '', string $imageType = '') { @@ -312,7 +302,6 @@ protected function createImage(string $path = '', string $imageType = '') protected function ensureResource() { if ($this->resource === null) { - // if valid image type, make corresponding image resource $this->resource = $this->getImageResource( $this->image()->getPathname(), $this->image()->imageType, @@ -326,7 +315,7 @@ protected function ensureResource() * @param string $path Image path * @param int $imageType Image type * - * @return bool|resource + * @return GdImage * * @throws ImageException */ @@ -338,32 +327,42 @@ protected function getImageResource(string $path, int $imageType) throw ImageException::forInvalidImageCreate(lang('Images.gifNotSupported')); } - return imagecreatefromgif($path); + $resource = imagecreatefromgif($path); + break; case IMAGETYPE_JPEG: if (! function_exists('imagecreatefromjpeg')) { throw ImageException::forInvalidImageCreate(lang('Images.jpgNotSupported')); } - return imagecreatefromjpeg($path); + $resource = imagecreatefromjpeg($path); + break; case IMAGETYPE_PNG: if (! function_exists('imagecreatefrompng')) { throw ImageException::forInvalidImageCreate(lang('Images.pngNotSupported')); } - return @imagecreatefrompng($path); + $resource = @imagecreatefrompng($path); + break; case IMAGETYPE_WEBP: if (! function_exists('imagecreatefromwebp')) { throw ImageException::forInvalidImageCreate(lang('Images.webpNotSupported')); } - return imagecreatefromwebp($path); + $resource = imagecreatefromwebp($path); + break; default: throw ImageException::forInvalidImageCreate('Ima'); } + + if ($resource === false) { + throw ImageException::forInvalidImageCreate($path); // @codeCoverageIgnore + } + + return $resource; } /** @@ -386,10 +385,7 @@ protected function _text(string $text, array $options = []) $options['hOffset'] *= -1; } - // Set font width and height - // These are calculated differently depending on - // whether we are using the true type font or not - if (! empty($options['fontPath'])) { + if (($options['fontPath'] ?? '') !== '') { if (function_exists('imagettfbbox')) { $temp = imagettfbbox($options['fontSize'], 0, $options['fontPath'], $text); $temp = $temp[2] - $temp[0]; @@ -408,11 +404,9 @@ protected function _text(string $text, array $options = []) $options['fontheight'] = $fontheight; $options['fontwidth'] = $fontwidth; - // Set base X and Y axis values $xAxis = $options['hOffset'] + $options['padding']; $yAxis = $options['vOffset'] + $options['padding']; - // Set vertical alignment if ($options['vAlign'] === 'middle') { // Don't apply padding when you're in the middle of the image. $yAxis += ($this->image()->origHeight / 2) + ($fontheight / 2) - $options['padding'] - $fontheight - $options['shadowOffset']; @@ -420,7 +414,6 @@ protected function _text(string $text, array $options = []) $yAxis = ($this->image()->origHeight - $fontheight - $options['shadowOffset'] - ($fontheight / 2)) - $yAxis; } - // Set horizontal alignment if ($options['hAlign'] === 'right') { $xAxis += ($this->image()->origWidth - ($fontwidth * strlen($text)) - $options['shadowOffset']) - (2 * $options['padding']); } elseif ($options['hAlign'] === 'center') { @@ -431,7 +424,6 @@ protected function _text(string $text, array $options = []) $options['yAxis'] = $yAxis; if ($options['withShadow']) { - // Offset from text $options['xShadow'] = $xAxis + $options['shadowOffset']; $options['yShadow'] = $yAxis + $options['shadowOffset']; @@ -444,17 +436,15 @@ protected function _text(string $text, array $options = []) /** * Handler-specific method for overlaying text on an image. * - * @param bool $isShadow Whether we are drawing the dropshadow or actual text + * @param array $options + * @param bool $isShadow Whether we are drawing the dropshadow or actual text + * + * @return void */ protected function textOverlay(string $text, array $options = [], bool $isShadow = false) { $src = $this->createImage(); - /* Set RGB values for shadow - * - * Get the rest of the string and split it into 2-length - * hex values: - */ $opacity = (int) ($options['opacity'] * 127); // Allow opacity to be applied to the text @@ -473,8 +463,7 @@ protected function textOverlay(string $text, array $options = [], bool $isShadow $xAxis = $isShadow ? $options['xShadow'] : $options['xAxis']; $yAxis = $isShadow ? $options['yShadow'] : $options['yAxis']; - // Add the shadow to the source image - if (! empty($options['fontPath'])) { + if (($options['fontPath'] ?? '') !== '') { // We have to add fontheight because imagettftext locates the bottom left corner, not top-left corner. imagettftext($src, $options['fontSize'], 0, (int) $xAxis, (int) ($yAxis + $options['fontheight']), $color, $options['fontPath'], $text); } else { diff --git a/system/Images/Handlers/ImageMagickHandler.php b/system/Images/Handlers/ImageMagickHandler.php index c2b24c81f272..3215c84f1ab3 100644 --- a/system/Images/Handlers/ImageMagickHandler.php +++ b/system/Images/Handlers/ImageMagickHandler.php @@ -24,16 +24,11 @@ /** * Image handler for Imagick extension. + * + * @extends BaseHandler */ class ImageMagickHandler extends BaseHandler { - /** - * Stores Imagick instance. - * - * @var Imagick|null - */ - protected $resource; - /** * Constructor. * @@ -60,14 +55,12 @@ public function __construct($config = null) protected function ensureResource() { if (! $this->resource instanceof Imagick) { - // Verify that we have a valid image $this->image(); try { $this->resource = new Imagick(); $this->resource->readImage($this->image()->getPathname()); - // Check for valid image if ($this->resource->getImageWidth() === 0 || $this->resource->getImageHeight() === 0) { throw ImageException::forInvalidImageCreate($this->image()->getPathname()); } @@ -119,12 +112,10 @@ protected function process(string $action, int $quality = 100) $yAxis, ); - // Reset canvas to cropped size $this->resource->setImagePage(0, 0, 0, 0); break; } - // Handle transparency for supported image types if (in_array($this->image()->imageType, $this->supportTransparency, true) && $this->resource->getImageAlphaChannel() === Imagick::ALPHACHANNEL_UNDEFINED) { $this->resource->setImageAlphaChannel(Imagick::ALPHACHANNEL_OPAQUE); @@ -146,7 +137,6 @@ protected function process(string $action, int $quality = 100) public function _resize(bool $maintainRatio = false) { if ($maintainRatio) { - // If maintaining a ratio, we need a custom approach $this->ensureResource(); // Use thumbnailImage which preserves an aspect ratio @@ -155,7 +145,6 @@ public function _resize(bool $maintainRatio = false) return $this; } - // Use the common process() method for normal resizing return $this->process('resize'); } @@ -168,7 +157,6 @@ public function _resize(bool $maintainRatio = false) */ public function _crop() { - // Use the common process() method for cropping $result = $this->process('crop'); // Handle a case where crop dimensions exceed the original image size @@ -177,15 +165,12 @@ public function _crop() $imgHeight = $this->resource->getImageHeight(); if ($this->xAxis >= $imgWidth || $this->yAxis >= $imgHeight) { - // Create transparent background $background = new Imagick(); $background->newImage($this->width, $this->height, new ImagickPixel('transparent')); $background->setImageFormat($this->resource->getImageFormat()); - // Composite our image on the background $background->compositeImage($this->resource, Imagick::COMPOSITE_OVER, 0, 0); - // Replace our resource $this->resource = $background; } } @@ -205,11 +190,9 @@ protected function _rotate(int $angle) { $this->ensureResource(); - // Create transparent background $this->resource->setImageBackgroundColor(new ImagickPixel('transparent')); $this->resource->rotateImage(new ImagickPixel('transparent'), $angle); - // Reset canvas dimensions $this->resource->setImagePage($this->resource->getImageWidth(), $this->resource->getImageHeight(), 0, 0); return $this; @@ -226,10 +209,8 @@ protected function _flatten(int $red = 255, int $green = 255, int $blue = 255) { $this->ensureResource(); - // Create background $bg = new ImagickPixel("rgb({$red},{$green},{$blue})"); - // Create a new canvas with the background color $canvas = new Imagick(); $canvas->newImage( $this->resource->getImageWidth(), @@ -238,7 +219,6 @@ protected function _flatten(int $red = 255, int $green = 255, int $blue = 255) $this->resource->getImageFormat(), ); - // Composite our image on the background $canvas->compositeImage( $this->resource, Imagick::COMPOSITE_OVER, @@ -246,7 +226,6 @@ protected function _flatten(int $red = 255, int $green = 255, int $blue = 255) 0, ); - // Replace our resource with the flattened version $this->resource->clear(); $this->resource = $canvas; @@ -393,13 +372,11 @@ protected function _text(string $text, array $options = []) $draw->setFillColor(new ImagickPixel("rgba({$r},{$g},{$b},{$opacity})")); } - // Calculate text positioning $imgWidth = $this->resource->getImageWidth(); $imgHeight = $this->resource->getImageHeight(); $xAxis = 0; $yAxis = 0; - // Default padding $padding = $options['padding'] ?? 0; if (isset($options['hAlign'])) { @@ -436,7 +413,7 @@ protected function _text(string $text, array $options = []) break; case 'bottom': - // Note: Vertical offset is inverted for bottom alignment as per original implementation + // A negative offset pushes the text further up from the bottom edge. $yAxis = $vOffset < 0 ? $imgHeight + $vOffset - $padding : $imgHeight - $vOffset - $padding; break; } @@ -470,7 +447,6 @@ protected function _text(string $text, array $options = []) ); } - // Draw the main text $this->resource->annotateImage( $draw, $xAxis, diff --git a/utils/phpstan-baseline/argument.type.neon b/utils/phpstan-baseline/argument.type.neon index 7fbc636537e7..6f504bc90f90 100644 --- a/utils/phpstan-baseline/argument.type.neon +++ b/utils/phpstan-baseline/argument.type.neon @@ -1,4 +1,4 @@ -# total 68 errors +# total 64 errors parameters: ignoreErrors: @@ -142,16 +142,6 @@ parameters: count: 1 path: ../../tests/system/Honeypot/HoneypotTest.php - - - message: '#^Parameter \#1 \$image of function imagecolorat expects GdImage, resource given\.$#' - count: 2 - path: ../../tests/system/Images/GDHandlerTest.php - - - - message: '#^Parameter \#1 \$image of function imagecolorsforindex expects GdImage, resource given\.$#' - count: 2 - path: ../../tests/system/Images/GDHandlerTest.php - - message: '#^Parameter \#2 \$message of method CodeIgniter\\Log\\Handlers\\ChromeLoggerHandler\:\:handle\(\) expects string, stdClass given\.$#' count: 1 diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 19e526d098ec..2eca0fea6e34 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 1582 errors +# total 1573 errors includes: - argument.type.neon @@ -21,4 +21,3 @@ includes: - return.type.neon - staticMethod.notFound.neon - ternary.shortNotAllowed.neon - - varTag.type.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 28aeaabd8f58..78b41e9422dd 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 1139 errors +# total 1137 errors parameters: ignoreErrors: @@ -3267,16 +3267,6 @@ parameters: count: 1 path: ../../system/Images/Handlers/BaseHandler.php - - - message: '#^Property CodeIgniter\\Images\\Handlers\\BaseHandler\:\:\$supportTransparency type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Images/Handlers/BaseHandler.php - - - - message: '#^Property CodeIgniter\\Images\\Handlers\\BaseHandler\:\:\$textDefaults type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Images/Handlers/BaseHandler.php - - message: '#^Method CodeIgniter\\Images\\Image\:\:getProperties\(\) return type has no value type specified in iterable type array\.$#' count: 1 diff --git a/utils/phpstan-baseline/property.phpDocType.neon b/utils/phpstan-baseline/property.phpDocType.neon index 0ba558aded06..148b167e3e40 100644 --- a/utils/phpstan-baseline/property.phpDocType.neon +++ b/utils/phpstan-baseline/property.phpDocType.neon @@ -1,4 +1,4 @@ -# total 44 errors +# total 43 errors parameters: ignoreErrors: @@ -162,11 +162,6 @@ parameters: count: 1 path: ../../system/HTTP/IncomingRequest.php - - - message: '#^PHPDoc type Imagick\|null of property CodeIgniter\\Images\\Handlers\\ImageMagickHandler\:\:\$resource is not the same as PHPDoc type resource\|null of overridden property CodeIgniter\\Images\\Handlers\\BaseHandler\:\:\$resource\.$#' - count: 1 - path: ../../system/Images/Handlers/ImageMagickHandler.php - - message: '#^PHPDoc type string of property CodeIgniter\\Session\\Handlers\\FileHandler\:\:\$savePath is not the same as PHPDoc type array\\|string of overridden property CodeIgniter\\Session\\Handlers\\BaseHandler\:\:\$savePath\.$#' count: 1 diff --git a/utils/phpstan-baseline/varTag.type.neon b/utils/phpstan-baseline/varTag.type.neon deleted file mode 100644 index d2244f5a31ab..000000000000 --- a/utils/phpstan-baseline/varTag.type.neon +++ /dev/null @@ -1,8 +0,0 @@ -# total 2 errors - -parameters: - ignoreErrors: - - - message: '#^PHPDoc tag @var with type Imagick is not subtype of type resource\.$#' - count: 2 - path: ../../tests/system/Images/ImageMagickHandlerTest.php From f4ad14b43524b939861b086402cf18d943d935e9 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Wed, 12 Aug 2026 14:31:24 +0800 Subject: [PATCH 046/163] refactor: add generics to `Entity` and `DataCaster` casts (#10442) --- psalm.xml | 13 +++++++++ system/DataCaster/Cast/ArrayCast.php | 6 ++-- system/DataCaster/Cast/BaseCast.php | 6 ++++ system/DataCaster/Cast/BooleanCast.php | 4 +-- system/DataCaster/Cast/CSVCast.php | 4 +-- system/DataCaster/Cast/CastInterface.php | 28 ++++++++----------- system/DataCaster/Cast/DatetimeCast.php | 4 +-- system/DataCaster/Cast/EnumCast.php | 22 ++++----------- system/DataCaster/Cast/FloatCast.php | 4 +-- system/DataCaster/Cast/IntBoolCast.php | 2 ++ system/DataCaster/Cast/IntegerCast.php | 4 +-- system/DataCaster/Cast/JsonCast.php | 4 +-- system/DataCaster/Cast/TimestampCast.php | 4 +-- system/DataCaster/Cast/URICast.php | 4 +-- system/Entity/Cast/ArrayCast.php | 3 ++ system/Entity/Cast/BaseCast.php | 6 ++++ system/Entity/Cast/BooleanCast.php | 3 ++ system/Entity/Cast/CSVCast.php | 3 ++ system/Entity/Cast/CastInterface.php | 11 +++++--- system/Entity/Cast/DatetimeCast.php | 11 ++------ system/Entity/Cast/EnumCast.php | 27 ++++++------------ system/Entity/Cast/FloatCast.php | 3 ++ system/Entity/Cast/IntBoolCast.php | 8 ++---- system/Entity/Cast/IntegerCast.php | 3 ++ system/Entity/Cast/JsonCast.php | 6 ++-- system/Entity/Cast/ObjectCast.php | 3 ++ system/Entity/Cast/StringCast.php | 3 ++ system/Entity/Cast/TimestampCast.php | 3 ++ system/Entity/Cast/URICast.php | 3 ++ tests/_support/Entity/Cast/CastBase64.php | 3 ++ tests/_support/Entity/Cast/CastBinaryUUID.php | 3 ++ .../Entity/Cast/CastPassParameters.php | 3 ++ utils/phpstan-baseline/loader.neon | 2 +- .../method.childParameterType.neon | 22 +-------------- .../missingType.iterableValue.neon | 27 +----------------- utils/phpstan-baseline/return.type.neon | 7 +---- .../staticMethod.notFound.neon | 12 +------- 37 files changed, 128 insertions(+), 156 deletions(-) diff --git a/psalm.xml b/psalm.xml index d6033313a621..6b9ccb30f513 100644 --- a/psalm.xml +++ b/psalm.xml @@ -30,6 +30,19 @@ + + + + + + + + + + + + + diff --git a/system/DataCaster/Cast/ArrayCast.php b/system/DataCaster/Cast/ArrayCast.php index 1deddea6fc8e..81f343b3e4a8 100644 --- a/system/DataCaster/Cast/ArrayCast.php +++ b/system/DataCaster/Cast/ArrayCast.php @@ -14,12 +14,12 @@ namespace CodeIgniter\DataCaster\Cast; /** - * Class ArrayCast - * * (PHP) [array --> string] --> (DB driver) --> (DB column) string * [ <-- string] <-- (DB driver) <-- (DB column) string + * + * @extends BaseCast, string> */ -class ArrayCast extends BaseCast implements CastInterface +class ArrayCast extends BaseCast { public static function get( mixed $value, diff --git a/system/DataCaster/Cast/BaseCast.php b/system/DataCaster/Cast/BaseCast.php index 06fb0eb4fa69..e6cc5ae98f1c 100644 --- a/system/DataCaster/Cast/BaseCast.php +++ b/system/DataCaster/Cast/BaseCast.php @@ -15,6 +15,12 @@ use CodeIgniter\Exceptions\InvalidArgumentException; +/** + * @template TPhpNativeValue + * @template TDataSourceValue + * + * @implements CastInterface + */ abstract class BaseCast implements CastInterface { public static function get( diff --git a/system/DataCaster/Cast/BooleanCast.php b/system/DataCaster/Cast/BooleanCast.php index 6554081e155d..79b9dfe6d8d9 100644 --- a/system/DataCaster/Cast/BooleanCast.php +++ b/system/DataCaster/Cast/BooleanCast.php @@ -14,10 +14,10 @@ namespace CodeIgniter\DataCaster\Cast; /** - * Class BooleanCast - * * (PHP) [bool --> bool ] --> (DB driver) --> (DB column) bool|int(0/1) * [ <-- string|int] <-- (DB driver) <-- (DB column) bool|int(0/1) + * + * @extends BaseCast */ class BooleanCast extends BaseCast { diff --git a/system/DataCaster/Cast/CSVCast.php b/system/DataCaster/Cast/CSVCast.php index d77c64c87b57..3c09e1cc6103 100644 --- a/system/DataCaster/Cast/CSVCast.php +++ b/system/DataCaster/Cast/CSVCast.php @@ -14,10 +14,10 @@ namespace CodeIgniter\DataCaster\Cast; /** - * Class CSVCast - * * (PHP) [array --> string] --> (DB driver) --> (DB column) string * [ <-- string] <-- (DB driver) <-- (DB column) string + * + * @extends BaseCast, string> */ class CSVCast extends BaseCast { diff --git a/system/DataCaster/Cast/CastInterface.php b/system/DataCaster/Cast/CastInterface.php index 7f5b05102a7d..6f2a38d75c9e 100644 --- a/system/DataCaster/Cast/CastInterface.php +++ b/system/DataCaster/Cast/CastInterface.php @@ -13,35 +13,31 @@ namespace CodeIgniter\DataCaster\Cast; +/** + * @template TPhpNativeValue + * @template TDataSourceValue + */ interface CastInterface { /** - * Takes a value from DataSource, returns its value for PHP. + * Takes a value from a data source, returns its value for PHP. * - * @param mixed $value Data from database driver + * @param TDataSourceValue $value Data from database driver * @param array $params Additional param * @param object|null $helper Helper object. E.g., database connection * - * @return mixed PHP native value + * @return TPhpNativeValue */ - public static function get( - mixed $value, - array $params = [], - ?object $helper = null, - ): mixed; + public static function get(mixed $value, array $params = [], ?object $helper = null): mixed; /** - * Takes a PHP value, returns its value for DataSource. + * Takes a PHP value, returns its value for a data source. * - * @param mixed $value PHP native value + * @param TPhpNativeValue $value PHP native value * @param array $params Additional param * @param object|null $helper Helper object. E.g., database connection * - * @return mixed Data to pass to database driver + * @return TDataSourceValue */ - public static function set( - mixed $value, - array $params = [], - ?object $helper = null, - ): mixed; + public static function set(mixed $value, array $params = [], ?object $helper = null): mixed; } diff --git a/system/DataCaster/Cast/DatetimeCast.php b/system/DataCaster/Cast/DatetimeCast.php index 398fdc7beed7..043b7ccd81ea 100644 --- a/system/DataCaster/Cast/DatetimeCast.php +++ b/system/DataCaster/Cast/DatetimeCast.php @@ -18,10 +18,10 @@ use CodeIgniter\I18n\Time; /** - * Class DatetimeCast - * * (PHP) [Time --> string] --> (DB driver) --> (DB column) datetime * [ <-- string] <-- (DB driver) <-- (DB column) datetime + * + * @extends BaseCast */ class DatetimeCast extends BaseCast { diff --git a/system/DataCaster/Cast/EnumCast.php b/system/DataCaster/Cast/EnumCast.php index 454251dcba6b..fcf611fe63d2 100644 --- a/system/DataCaster/Cast/EnumCast.php +++ b/system/DataCaster/Cast/EnumCast.php @@ -19,14 +19,14 @@ use UnitEnum; /** - * Class EnumCast - * * Handles casting for PHP enums (both backed and unit enums) * * (PHP) [enum --> value/name] --> (DB driver) --> (DB column) int|string * [ <-- value/name] <-- (DB driver) <-- (DB column) int|string + * + * @extends BaseCast */ -class EnumCast extends BaseCast implements CastInterface +class EnumCast extends BaseCast { public static function get( mixed $value, @@ -49,9 +49,7 @@ public static function get( $reflection = new ReflectionEnum($enumClass); - // Unit enum - if (! $reflection->isBacked()) { - // Unit enum - match by name + if (! is_a($enumClass, BackedEnum::class, true)) { foreach ($enumClass::cases() as $case) { if ($case->name === $value) { return $case; @@ -61,10 +59,8 @@ public static function get( throw CastException::forInvalidEnumCaseName($enumClass, $value); } - // Backed enum - validate and cast the value to proper type $backingType = $reflection->getBackingType(); - // Cast to proper type (int or string) if ($backingType->getName() === 'int') { $value = (int) $value; } elseif ($backingType->getName() === 'string') { @@ -89,7 +85,6 @@ public static function set( self::invalidTypeValueError($value); } - // Get the expected enum class $enumClass = $params[0] ?? null; if ($enumClass === null) { @@ -100,21 +95,14 @@ public static function set( throw CastException::forNotEnum($enumClass); } - // Validate that the enum is of the expected type if (! $value instanceof $enumClass) { throw CastException::forInvalidEnumType($enumClass, $value::class); } - $reflection = new ReflectionEnum($value::class); - - // Backed enum - return the properly typed backing value - if ($reflection->isBacked()) { - /** @var BackedEnum $value */ + if ($value instanceof BackedEnum) { return $value->value; } - // Unit enum - return the case name - /** @var UnitEnum $value */ return $value->name; } } diff --git a/system/DataCaster/Cast/FloatCast.php b/system/DataCaster/Cast/FloatCast.php index d2173826265a..72d7db968e5f 100644 --- a/system/DataCaster/Cast/FloatCast.php +++ b/system/DataCaster/Cast/FloatCast.php @@ -14,10 +14,10 @@ namespace CodeIgniter\DataCaster\Cast; /** - * Class FloatCast - * * (PHP) [float --> float ] --> (DB driver) --> (DB column) float * [ <-- float|string] <-- (DB driver) <-- (DB column) float + * + * @extends BaseCast */ class FloatCast extends BaseCast { diff --git a/system/DataCaster/Cast/IntBoolCast.php b/system/DataCaster/Cast/IntBoolCast.php index 5aa1f2b2eb93..49b8861a4c6e 100644 --- a/system/DataCaster/Cast/IntBoolCast.php +++ b/system/DataCaster/Cast/IntBoolCast.php @@ -18,6 +18,8 @@ * * (PHP) [bool --> int ] --> (DB driver) --> (DB column) int(0/1) * [ <-- int|string] <-- (DB driver) <-- (DB column) int(0/1) + * + * @extends BaseCast */ final class IntBoolCast extends BaseCast { diff --git a/system/DataCaster/Cast/IntegerCast.php b/system/DataCaster/Cast/IntegerCast.php index f65baf83150f..9b02cf6fbbb9 100644 --- a/system/DataCaster/Cast/IntegerCast.php +++ b/system/DataCaster/Cast/IntegerCast.php @@ -14,10 +14,10 @@ namespace CodeIgniter\DataCaster\Cast; /** - * Class IntegerCast - * * (PHP) [int --> int ] --> (DB driver) --> (DB column) int * [ <-- int|string] <-- (DB driver) <-- (DB column) int + * + * @extends BaseCast */ class IntegerCast extends BaseCast { diff --git a/system/DataCaster/Cast/JsonCast.php b/system/DataCaster/Cast/JsonCast.php index 5ca10b385ba6..2a3fd5a729dd 100644 --- a/system/DataCaster/Cast/JsonCast.php +++ b/system/DataCaster/Cast/JsonCast.php @@ -18,10 +18,10 @@ use stdClass; /** - * Class JsonCast - * * (PHP) [array|stdClass --> string] --> (DB driver) --> (DB column) string * [ <-- string] <-- (DB driver) <-- (DB column) string + * + * @extends BaseCast|stdClass, string> */ class JsonCast extends BaseCast { diff --git a/system/DataCaster/Cast/TimestampCast.php b/system/DataCaster/Cast/TimestampCast.php index aa94266001ef..7dceaa7b1846 100644 --- a/system/DataCaster/Cast/TimestampCast.php +++ b/system/DataCaster/Cast/TimestampCast.php @@ -16,10 +16,10 @@ use CodeIgniter\I18n\Time; /** - * Class TimestampCast - * * (PHP) [Time --> int ] --> (DB driver) --> (DB column) int * [ <-- int|string] <-- (DB driver) <-- (DB column) int + * + * @extends BaseCast */ class TimestampCast extends BaseCast { diff --git a/system/DataCaster/Cast/URICast.php b/system/DataCaster/Cast/URICast.php index f9e2f561a2b2..f9f9487e42fb 100644 --- a/system/DataCaster/Cast/URICast.php +++ b/system/DataCaster/Cast/URICast.php @@ -16,10 +16,10 @@ use CodeIgniter\HTTP\URI; /** - * Class URICast - * * (PHP) [URI --> string] --> (DB driver) --> (DB column) string * [ <-- string] <-- (DB driver) <-- (DB column) string + * + * @extends BaseCast */ class URICast extends BaseCast { diff --git a/system/Entity/Cast/ArrayCast.php b/system/Entity/Cast/ArrayCast.php index 6860b41416b6..dc1d5d7e20d2 100644 --- a/system/Entity/Cast/ArrayCast.php +++ b/system/Entity/Cast/ArrayCast.php @@ -13,6 +13,9 @@ namespace CodeIgniter\Entity\Cast; +/** + * @extends BaseCast, string> + */ class ArrayCast extends BaseCast { public static function get($value, array $params = []): array diff --git a/system/Entity/Cast/BaseCast.php b/system/Entity/Cast/BaseCast.php index c93e32c0537c..68bd544a071f 100644 --- a/system/Entity/Cast/BaseCast.php +++ b/system/Entity/Cast/BaseCast.php @@ -13,6 +13,12 @@ namespace CodeIgniter\Entity\Cast; +/** + * @template TPhpNativeValue + * @template TEntityStoredValue + * + * @implements CastInterface + */ abstract class BaseCast implements CastInterface { public static function get($value, array $params = []) diff --git a/system/Entity/Cast/BooleanCast.php b/system/Entity/Cast/BooleanCast.php index 7ac9f0ba37ee..a8bdb8ac1e7d 100644 --- a/system/Entity/Cast/BooleanCast.php +++ b/system/Entity/Cast/BooleanCast.php @@ -13,6 +13,9 @@ namespace CodeIgniter\Entity\Cast; +/** + * @extends BaseCast + */ class BooleanCast extends BaseCast { public static function get($value, array $params = []): bool diff --git a/system/Entity/Cast/CSVCast.php b/system/Entity/Cast/CSVCast.php index c78a1315111a..6c4a65f816ab 100644 --- a/system/Entity/Cast/CSVCast.php +++ b/system/Entity/Cast/CSVCast.php @@ -13,6 +13,9 @@ namespace CodeIgniter\Entity\Cast; +/** + * @extends BaseCast, string> + */ class CSVCast extends BaseCast { public static function get($value, array $params = []): array diff --git a/system/Entity/Cast/CastInterface.php b/system/Entity/Cast/CastInterface.php index 9f5552826859..31b85f4ef4e9 100644 --- a/system/Entity/Cast/CastInterface.php +++ b/system/Entity/Cast/CastInterface.php @@ -17,26 +17,29 @@ * The methods work at (1)(4) only. * [App Code] --- (1) --> [Entity] --- (2) --> [Database] * [App Code] <-- (4) --- [Entity] <-- (3) --- [Database] + * + * @template TPhpNativeValue + * @template TEntityStoredValue */ interface CastInterface { /** * Takes a raw value from Entity, returns its value for PHP. * - * @param mixed $value Data + * @param TEntityStoredValue $value * @param array $params Additional param * - * @return mixed + * @return TPhpNativeValue */ public static function get($value, array $params = []); /** * Takes a PHP value, returns its raw value for Entity. * - * @param mixed $value Data + * @param TPhpNativeValue $value * @param array $params Additional param * - * @return mixed + * @return TEntityStoredValue */ public static function set($value, array $params = []); } diff --git a/system/Entity/Cast/DatetimeCast.php b/system/Entity/Cast/DatetimeCast.php index e49df121987e..332de01a99cf 100644 --- a/system/Entity/Cast/DatetimeCast.php +++ b/system/Entity/Cast/DatetimeCast.php @@ -15,17 +15,12 @@ use CodeIgniter\I18n\Time; use DateTimeInterface; -use Exception; +/** + * @extends BaseCast + */ class DatetimeCast extends BaseCast { - /** - * {@inheritDoc} - * - * @return Time - * - * @throws Exception - */ public static function get($value, array $params = []) { if ($value instanceof Time) { diff --git a/system/Entity/Cast/EnumCast.php b/system/Entity/Cast/EnumCast.php index 7ea82d6f24a5..caf6b1289294 100644 --- a/system/Entity/Cast/EnumCast.php +++ b/system/Entity/Cast/EnumCast.php @@ -18,6 +18,9 @@ use ReflectionEnum; use UnitEnum; +/** + * @extends BaseCast + */ class EnumCast extends BaseCast { public static function get($value, array $params = []): BackedEnum|UnitEnum @@ -34,11 +37,9 @@ public static function get($value, array $params = []): BackedEnum|UnitEnum $reflection = new ReflectionEnum($enumClass); - // Backed enum - validate and cast the value to proper type - if ($reflection->isBacked()) { + if (is_a($enumClass, BackedEnum::class, true)) { $backingType = $reflection->getBackingType(); - // Cast to proper type (int or string) if ($backingType->getName() === 'int') { $value = (int) $value; } elseif ($backingType->getName() === 'string') { @@ -54,7 +55,6 @@ public static function get($value, array $params = []): BackedEnum|UnitEnum return $enum; } - // Unit enum - match by name foreach ($enumClass::cases() as $case) { if ($case->name === $value) { return $case; @@ -64,9 +64,11 @@ public static function get($value, array $params = []): BackedEnum|UnitEnum throw CastException::forInvalidEnumCaseName($enumClass, $value); } + /** + * @param mixed $value + */ public static function set($value, array $params = []): int|string { - // Get the expected enum class $enumClass = $params[0] ?? null; if ($enumClass === null) { @@ -77,33 +79,23 @@ public static function set($value, array $params = []): int|string throw CastException::forNotEnum($enumClass); } - // If it's already an enum object, validate and extract its value if (is_object($value) && enum_exists($value::class)) { - // Validate that the enum is of the expected type if (! $value instanceof $enumClass) { throw CastException::forInvalidEnumType($enumClass, $value::class); } - $reflection = new ReflectionEnum($value::class); - - // Backed enum - return the properly typed backing value - if ($reflection->isBacked()) { - /** @var BackedEnum $value */ + if ($value instanceof BackedEnum) { return $value->value; } - // Unit enum - return the case name - /** @var UnitEnum $value */ return $value->name; } $reflection = new ReflectionEnum($enumClass); - // Validate backed enum values - if ($reflection->isBacked()) { + if (is_a($enumClass, BackedEnum::class, true)) { $backingType = $reflection->getBackingType(); - // Cast to proper type (int or string) if ($backingType->getName() === 'int') { $value = (int) $value; } elseif ($backingType->getName() === 'string') { @@ -117,7 +109,6 @@ public static function set($value, array $params = []): int|string return $value; } - // Validate unit enum case names - must be a string $value = (string) $value; foreach ($enumClass::cases() as $case) { diff --git a/system/Entity/Cast/FloatCast.php b/system/Entity/Cast/FloatCast.php index 1a767c0953f0..f629fe54016e 100644 --- a/system/Entity/Cast/FloatCast.php +++ b/system/Entity/Cast/FloatCast.php @@ -13,6 +13,9 @@ namespace CodeIgniter\Entity\Cast; +/** + * @extends BaseCast + */ class FloatCast extends BaseCast { public static function get($value, array $params = []): float diff --git a/system/Entity/Cast/IntBoolCast.php b/system/Entity/Cast/IntBoolCast.php index 94e65df8dde0..21e03c04a98a 100644 --- a/system/Entity/Cast/IntBoolCast.php +++ b/system/Entity/Cast/IntBoolCast.php @@ -15,20 +15,16 @@ /** * DB column: int (0/1) <--> Class property: bool + * + * @extends BaseCast */ final class IntBoolCast extends BaseCast { - /** - * @param int $value - */ public static function get($value, array $params = []): bool { return (bool) $value; } - /** - * @param bool|int|string $value - */ public static function set($value, array $params = []): int { return (int) $value; diff --git a/system/Entity/Cast/IntegerCast.php b/system/Entity/Cast/IntegerCast.php index 84bf09211c0f..dad299ef74c1 100644 --- a/system/Entity/Cast/IntegerCast.php +++ b/system/Entity/Cast/IntegerCast.php @@ -13,6 +13,9 @@ namespace CodeIgniter\Entity\Cast; +/** + * @extends BaseCast + */ class IntegerCast extends BaseCast { public static function get($value, array $params = []): int diff --git a/system/Entity/Cast/JsonCast.php b/system/Entity/Cast/JsonCast.php index 245de29f3956..c94e4d6f34a8 100644 --- a/system/Entity/Cast/JsonCast.php +++ b/system/Entity/Cast/JsonCast.php @@ -17,6 +17,9 @@ use JsonException; use stdClass; +/** + * @extends BaseCast + */ class JsonCast extends BaseCast { public static function get($value, array $params = []) @@ -43,9 +46,6 @@ public static function get($value, array $params = []) return $tmp; } - /** - * {@inheritDoc} - */ public static function set($value, array $params = []): string { if (function_exists('json_encode')) { diff --git a/system/Entity/Cast/ObjectCast.php b/system/Entity/Cast/ObjectCast.php index 10affad4c80c..c2c1499a3af0 100644 --- a/system/Entity/Cast/ObjectCast.php +++ b/system/Entity/Cast/ObjectCast.php @@ -13,6 +13,9 @@ namespace CodeIgniter\Entity\Cast; +/** + * @extends BaseCast + */ class ObjectCast extends BaseCast { public static function get($value, array $params = []): object diff --git a/system/Entity/Cast/StringCast.php b/system/Entity/Cast/StringCast.php index d538f4332dca..7c01c2c4a617 100644 --- a/system/Entity/Cast/StringCast.php +++ b/system/Entity/Cast/StringCast.php @@ -13,6 +13,9 @@ namespace CodeIgniter\Entity\Cast; +/** + * @extends BaseCast + */ class StringCast extends BaseCast { public static function get($value, array $params = []): string diff --git a/system/Entity/Cast/TimestampCast.php b/system/Entity/Cast/TimestampCast.php index 44bdf78864d1..b48211f221df 100644 --- a/system/Entity/Cast/TimestampCast.php +++ b/system/Entity/Cast/TimestampCast.php @@ -15,6 +15,9 @@ use CodeIgniter\Entity\Exceptions\CastException; +/** + * @extends BaseCast + */ class TimestampCast extends BaseCast { public static function get($value, array $params = []) diff --git a/system/Entity/Cast/URICast.php b/system/Entity/Cast/URICast.php index a66bfba3ef45..ef4346d1a5eb 100644 --- a/system/Entity/Cast/URICast.php +++ b/system/Entity/Cast/URICast.php @@ -15,6 +15,9 @@ use CodeIgniter\HTTP\URI; +/** + * @extends BaseCast + */ class URICast extends BaseCast { public static function get($value, array $params = []): URI diff --git a/tests/_support/Entity/Cast/CastBase64.php b/tests/_support/Entity/Cast/CastBase64.php index 5bf086d77685..1735d73d6f9d 100644 --- a/tests/_support/Entity/Cast/CastBase64.php +++ b/tests/_support/Entity/Cast/CastBase64.php @@ -15,6 +15,9 @@ use CodeIgniter\Entity\Cast\BaseCast; +/** + * @extends BaseCast + */ class CastBase64 extends BaseCast { /** diff --git a/tests/_support/Entity/Cast/CastBinaryUUID.php b/tests/_support/Entity/Cast/CastBinaryUUID.php index 230f3a31eaf0..10b111d95c34 100644 --- a/tests/_support/Entity/Cast/CastBinaryUUID.php +++ b/tests/_support/Entity/Cast/CastBinaryUUID.php @@ -15,6 +15,9 @@ use CodeIgniter\Entity\Cast\BaseCast; +/** + * @extends BaseCast + */ class CastBinaryUUID extends BaseCast { /** diff --git a/tests/_support/Entity/Cast/CastPassParameters.php b/tests/_support/Entity/Cast/CastPassParameters.php index 3f304e3622a0..d09521a228a7 100644 --- a/tests/_support/Entity/Cast/CastPassParameters.php +++ b/tests/_support/Entity/Cast/CastPassParameters.php @@ -15,6 +15,9 @@ use CodeIgniter\Entity\Cast\BaseCast; +/** + * @extends BaseCast + */ class CastPassParameters extends BaseCast { /** diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 2eca0fea6e34..d69dc372a109 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 1573 errors +# total 1560 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/method.childParameterType.neon b/utils/phpstan-baseline/method.childParameterType.neon index f4316313dd85..aa135b562c3e 100644 --- a/utils/phpstan-baseline/method.childParameterType.neon +++ b/utils/phpstan-baseline/method.childParameterType.neon @@ -1,4 +1,4 @@ -# total 10 errors +# total 6 errors parameters: ignoreErrors: @@ -12,26 +12,6 @@ parameters: count: 1 path: ../../system/Database/BaseResult.php - - - message: '#^Parameter \#1 \$value \(bool\|int\|string\) of method CodeIgniter\\Entity\\Cast\\IntBoolCast\:\:set\(\) should be contravariant with parameter \$value \(mixed\) of method CodeIgniter\\Entity\\Cast\\BaseCast\:\:set\(\)$#' - count: 1 - path: ../../system/Entity/Cast/IntBoolCast.php - - - - message: '#^Parameter \#1 \$value \(bool\|int\|string\) of method CodeIgniter\\Entity\\Cast\\IntBoolCast\:\:set\(\) should be contravariant with parameter \$value \(mixed\) of method CodeIgniter\\Entity\\Cast\\CastInterface\:\:set\(\)$#' - count: 1 - path: ../../system/Entity/Cast/IntBoolCast.php - - - - message: '#^Parameter \#1 \$value \(int\) of method CodeIgniter\\Entity\\Cast\\IntBoolCast\:\:get\(\) should be contravariant with parameter \$value \(mixed\) of method CodeIgniter\\Entity\\Cast\\BaseCast\:\:get\(\)$#' - count: 1 - path: ../../system/Entity/Cast/IntBoolCast.php - - - - message: '#^Parameter \#1 \$value \(int\) of method CodeIgniter\\Entity\\Cast\\IntBoolCast\:\:get\(\) should be contravariant with parameter \$value \(mixed\) of method CodeIgniter\\Entity\\Cast\\CastInterface\:\:get\(\)$#' - count: 1 - path: ../../system/Entity/Cast/IntBoolCast.php - - message: '#^Parameter \#1 \$data \(array\{date\: string, timezone\: string, timezone_type\: int\}\) of method CodeIgniter\\I18n\\Time\:\:__unserialize\(\) should be contravariant with parameter \$data \(array\) of method DateTimeImmutable\:\:__unserialize\(\)$#' count: 1 diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 78b41e9422dd..d9527fa8fee7 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 1137 errors +# total 1132 errors parameters: ignoreErrors: @@ -312,21 +312,6 @@ parameters: count: 1 path: ../../system/Cookie/CookieStore.php - - - message: '#^Method CodeIgniter\\DataCaster\\Cast\\ArrayCast\:\:get\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/DataCaster/Cast/ArrayCast.php - - - - message: '#^Method CodeIgniter\\DataCaster\\Cast\\CSVCast\:\:get\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/DataCaster/Cast/CSVCast.php - - - - message: '#^Method CodeIgniter\\DataCaster\\Cast\\JsonCast\:\:get\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/DataCaster/Cast/JsonCast.php - - message: '#^Method CodeIgniter\\DataConverter\\DataConverter\:\:fromDataSource\(\) return type has no value type specified in iterable type array\.$#' count: 1 @@ -2052,16 +2037,6 @@ parameters: count: 1 path: ../../system/Encryption/KeyRotationDecorator.php - - - message: '#^Method CodeIgniter\\Entity\\Cast\\ArrayCast\:\:get\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/ArrayCast.php - - - - message: '#^Method CodeIgniter\\Entity\\Cast\\CSVCast\:\:get\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Entity/Cast/CSVCast.php - - message: '#^Method CodeIgniter\\Exceptions\\PageNotFoundException\:\:lang\(\) has parameter \$args with no value type specified in iterable type array\.$#' count: 1 diff --git a/utils/phpstan-baseline/return.type.neon b/utils/phpstan-baseline/return.type.neon index 258159a982c6..acfc78a3c171 100644 --- a/utils/phpstan-baseline/return.type.neon +++ b/utils/phpstan-baseline/return.type.neon @@ -1,4 +1,4 @@ -# total 3 errors +# total 2 errors parameters: ignoreErrors: @@ -7,11 +7,6 @@ parameters: count: 1 path: ../../system/Database/BaseBuilder.php - - - message: '#^Method CodeIgniter\\Entity\\Cast\\DatetimeCast\:\:get\(\) should return CodeIgniter\\I18n\\Time but returns mixed\.$#' - count: 1 - path: ../../system/Entity/Cast/DatetimeCast.php - - message: '#^Method CodeIgniter\\Router\\Router\:\:getRouteAttributes\(\) should return array\{class\: list\, method\: list\\} but returns array\{class\: list\, method\: list\\}\.$#' count: 1 diff --git a/utils/phpstan-baseline/staticMethod.notFound.neon b/utils/phpstan-baseline/staticMethod.notFound.neon index ad04f6d249a3..3ad7dfbbd356 100644 --- a/utils/phpstan-baseline/staticMethod.notFound.neon +++ b/utils/phpstan-baseline/staticMethod.notFound.neon @@ -1,17 +1,7 @@ -# total 21 errors +# total 18 errors parameters: ignoreErrors: - - - message: '#^Call to an undefined static method UnitEnum\:\:tryFrom\(\)\.$#' - count: 1 - path: ../../system/DataCaster/Cast/EnumCast.php - - - - message: '#^Call to an undefined static method UnitEnum\:\:tryFrom\(\)\.$#' - count: 2 - path: ../../system/Entity/Cast/EnumCast.php - - message: '#^Call to an undefined static method CodeIgniter\\Config\\Factories\:\:cells\(\)\.$#' count: 1 From 6430554cc667605889f708179f1ab6fa2803230f Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Wed, 12 Aug 2026 16:37:20 +0200 Subject: [PATCH 047/163] fix: skip unreadable `.env` in `Boot::loadDotEnv` for separate-process tests (#10417) --- system/Config/DotEnv.php | 19 ++++++--- tests/system/Config/DotEnvTest.php | 62 ++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/system/Config/DotEnv.php b/system/Config/DotEnv.php index 2495b6b15e42..a820f42824f3 100644 --- a/system/Config/DotEnv.php +++ b/system/Config/DotEnv.php @@ -59,15 +59,24 @@ public function parse(): ?array return null; } - // Ensure the file is readable - if (! is_readable($this->path)) { - throw new InvalidArgumentException("The .env file is not readable: {$this->path}"); + $lines = @file($this->path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + + // The .env file may have been removed or replaced by a concurrent + // process between the is_file() check above and this read attempt + // (e.g. another test process renaming `.env`). A vanished file is + // treated as absent, so re-check with a fresh stat cache. + if ($lines === false) { + clearstatcache(true, $this->path); + + if (is_file($this->path)) { + throw new InvalidArgumentException("The .env file is not readable: {$this->path}"); + } + + return null; } $vars = []; - $lines = file($this->path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - foreach ($lines as $line) { // Is it a comment? if (str_starts_with(trim($line), '#')) { diff --git a/tests/system/Config/DotEnvTest.php b/tests/system/Config/DotEnvTest.php index c789169b1b87..58e66d412e9e 100644 --- a/tests/system/Config/DotEnvTest.php +++ b/tests/system/Config/DotEnvTest.php @@ -143,6 +143,68 @@ public function testLoadsUnreadableFile(): void $dotenv->load(); } + /** + * Regression test: a concurrent process may remove or replace the .env + * file between the is_file() check and the read attempt. In that case the + * file must be treated as absent (parse() returns null) instead of + * throwing an exception. + */ + public function testParseReturnsNullIfFileRemovedBetweenCheckAndRead(): void + { + $scheme = 'vanishenv'; + + $wrapper = new class () { + public static bool $vanished = false; + + /** + * @var resource|null + */ + public $context; + + /** + * @return array|false + */ + public function url_stat(string $path, int $flags): array|false + { + if (self::$vanished) { + return false; + } + + return [ + 'dev' => 0, + 'ino' => 0, + 'mode' => 0100644, + 'nlink' => 1, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 0, + 'size' => 1, + 'atime' => 1, + 'mtime' => 1, + 'ctime' => 1, + 'blksize' => 4096, + 'blocks' => 8, + ]; + } + + public function stream_open(string $path, string $mode, int $options, ?string &$openedPath): bool + { + self::$vanished = true; + + return false; + } + }; + + stream_wrapper_register($scheme, $wrapper::class, STREAM_IS_URL); + + try { + $dotenv = new DotEnv("{$scheme}://dir", '.env'); + $this->assertNull($dotenv->parse()); + } finally { + stream_wrapper_unregister($scheme); + } + } + public function testQuotedDotenvLoadsEnvironmentVars(): void { $dotenv = new DotEnv($this->fixturesFolder, 'quoted.env'); From 4e13c9e9e737552dcf97e543bb47d88a58b3230e Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Wed, 12 Aug 2026 22:38:16 +0800 Subject: [PATCH 048/163] refactor: fix `ternary.shortNotAllowed` errors (#10445) --- system/CLI/CLI.php | 4 +- system/Commands/Utilities/Namespaces.php | 3 +- system/Common.php | 9 +- system/Cookie/Cookie.php | 8 +- system/Debug/Toolbar/Collectors/Database.php | 2 +- system/Files/File.php | 11 +- system/HTTP/CURLRequest.php | 3 +- system/HTTP/ResponseTrait.php | 6 +- system/Helpers/filesystem_helper.php | 8 +- system/I18n/TimeTrait.php | 2 +- system/Pager/Pager.php | 4 +- system/Router/AutoRouter.php | 3 +- system/Test/Mock/MockCache.php | 4 +- system/Validation/Validation.php | 3 +- system/View/View.php | 4 +- .../Commands/Generators/CellGeneratorTest.php | 2 +- .../Generators/CommandGeneratorTest.php | 2 +- .../Generators/ControllerGeneratorTest.php | 2 +- .../Generators/ModelGeneratorTest.php | 2 +- .../Generators/ScaffoldGeneratorTest.php | 2 +- .../system/Publisher/PublisherSupportTest.php | 3 +- utils/phpstan-baseline/loader.neon | 3 +- .../ternary.shortNotAllowed.neon | 113 ------------------ 23 files changed, 54 insertions(+), 149 deletions(-) delete mode 100644 utils/phpstan-baseline/ternary.shortNotAllowed.neon diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index aff27144aa39..037eaf878ebb 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -741,7 +741,7 @@ public static function getWidth(int $default = 80): int static::generateDimensions(); } - return static::$width ?: $default; + return (static::$width === null || static::$width === 0) ? $default : static::$width; } /** @@ -753,7 +753,7 @@ public static function getHeight(int $default = 32): int static::generateDimensions(); } - return static::$height ?: $default; + return (static::$height === null || static::$height === 0) ? $default : static::$height; } /** diff --git a/system/Commands/Utilities/Namespaces.php b/system/Commands/Utilities/Namespaces.php index 8dda8ce0732e..6469d1afc97f 100644 --- a/system/Commands/Utilities/Namespaces.php +++ b/system/Commands/Utilities/Namespaces.php @@ -145,7 +145,8 @@ private function outputCINamespaces(array $params): array $pathOutput = $this->truncate(clean_path($path), $maxLength); } - $path = realpath($path) ?: $path; + $realPath = realpath($path); + $path = $realPath === false ? $path : $realPath; $tbody[] = [ $ns, diff --git a/system/Common.php b/system/Common.php index d99b4ef3ca3c..32f66d1803ee 100644 --- a/system/Common.php +++ b/system/Common.php @@ -95,7 +95,8 @@ function clean_path(string $path): string { // Resolve relative paths try { - $path = realpath($path) ?: $path; + $realPath = realpath($path); + $path = $realPath === false ? $path : $realPath; } catch (ErrorException|ValueError) { $path = 'error file path: ' . urlencode($path); } @@ -1347,7 +1348,11 @@ function class_uses_recursive($class) */ function trait_uses_recursive($trait) { - $traits = class_uses($trait) ?: []; + $traits = class_uses($trait); + + if ($traits === false) { + return []; + } foreach ($traits as $trait) { $traits += trait_uses_recursive($trait); diff --git a/system/Cookie/Cookie.php b/system/Cookie/Cookie.php index 068f8772c71c..61de7d6ce672 100644 --- a/system/Cookie/Cookie.php +++ b/system/Cookie/Cookie.php @@ -254,11 +254,11 @@ final public function __construct(string $name, string $value = '', array $optio // to preserve backward compatibility with array-based cookies in previous CI versions $prefix = ($options['prefix'] === '') ? self::$defaults['prefix'] : $options['prefix']; - $path = $options['path'] ?: self::$defaults['path']; - $domain = $options['domain'] ?: self::$defaults['domain']; + $path = ($options['path'] === '') ? self::$defaults['path'] : $options['path']; + $domain = ($options['domain'] === '') ? self::$defaults['domain'] : $options['domain']; // empty string SameSite should use the default for browsers - $samesite = $options['samesite'] ?: self::$defaults['samesite']; + $samesite = ($options['samesite'] === '') ? self::$defaults['samesite'] : $options['samesite']; $raw = $options['raw']; $secure = $options['secure']; @@ -429,7 +429,7 @@ public function getOptions(): array 'domain' => $this->domain, 'secure' => $this->secure, 'httponly' => $this->httponly, - 'samesite' => $this->samesite ?: ucfirst(self::SAMESITE_LAX), + 'samesite' => ($this->samesite === '') ? ucfirst(self::SAMESITE_LAX) : $this->samesite, ]; } diff --git a/system/Debug/Toolbar/Collectors/Database.php b/system/Debug/Toolbar/Collectors/Database.php index fdf961d2f974..4d5a06f40486 100644 --- a/system/Debug/Toolbar/Collectors/Database.php +++ b/system/Debug/Toolbar/Collectors/Database.php @@ -88,7 +88,7 @@ public static function collect(Query $query) $config = config(Toolbar::class); // Provide default in case it's not set - $max = $config->maxQueries ?: 100; + $max = ($config->maxQueries === 0) ? 100 : $config->maxQueries; if (count(static::$queries) < $max) { $queryString = $query->getQuery(); diff --git a/system/Files/File.php b/system/Files/File.php index 8a06b29edb81..181a9976362e 100644 --- a/system/Files/File.php +++ b/system/Files/File.php @@ -111,7 +111,8 @@ public function getSizeByUnit(string $unit = 'b') public function guessExtension(): ?string { // naively get the path extension using pathinfo - $pathinfo = pathinfo($this->getRealPath() ?: $this->__toString()) + ['extension' => '']; + $realPath = $this->getRealPath(); + $pathinfo = pathinfo($realPath === false ? $this->__toString() : $realPath) + ['extension' => '']; $proposedExtension = $pathinfo['extension']; @@ -131,9 +132,10 @@ public function getMimeType(): string return $this->originalMimeType ?? 'application/octet-stream'; // @codeCoverageIgnore } - $finfo = finfo_open(FILEINFO_MIME_TYPE); + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $realPath = $this->getRealPath(); - return finfo_file($finfo, $this->getRealPath() ?: $this->__toString()); + return finfo_file($finfo, $realPath === false ? $this->__toString() : $realPath); } /** @@ -159,7 +161,8 @@ public function move(string $targetPath, ?string $name = null, bool $overwrite = $name ??= $this->getBasename(); $destination = $overwrite ? $targetPath . $name : $this->getDestination($targetPath . $name); - $oldName = $this->getRealPath() ?: $this->__toString(); + $realPath = $this->getRealPath(); + $oldName = $realPath === false ? $this->__toString() : $realPath; if (! @rename($oldName, $destination)) { $error = error_get_last(); diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php index e1d9d3316b5e..b75c477ab9e1 100644 --- a/system/HTTP/CURLRequest.php +++ b/system/HTTP/CURLRequest.php @@ -587,7 +587,8 @@ private function applySslOptions(array $curlOptions, array $config): array // SSL Verification if (isset($config['verify'])) { if (is_string($config['verify'])) { - $file = realpath($config['verify']) ?: $config['verify']; + $realPath = realpath($config['verify']); + $file = $realPath === false ? $config['verify'] : $realPath; if (! is_file($file)) { throw HTTPException::forInvalidSSLKey($config['verify']); diff --git a/system/HTTP/ResponseTrait.php b/system/HTTP/ResponseTrait.php index 265a62fb0a1f..00f2dcc7ec40 100644 --- a/system/HTTP/ResponseTrait.php +++ b/system/HTTP/ResponseTrait.php @@ -195,7 +195,7 @@ public function getJSON() $body = service('format')->getFormatter('application/json')->format($body); } - return $body ?: null; + return ($body === null || $body === '') ? null : $body; } /** @@ -541,10 +541,12 @@ public function setCookie( if (is_numeric($expire)) { $expire = $expire > 0 ? Time::now()->getTimestamp() + $expire : 0; + } else { + $expire = 0; } $cookie = new Cookie($name, $value, [ - 'expires' => $expire ?: 0, + 'expires' => $expire, 'domain' => $domain, 'path' => $path, 'prefix' => $prefix, diff --git a/system/Helpers/filesystem_helper.php b/system/Helpers/filesystem_helper.php index b33ad49a2823..3f5ed25f8531 100644 --- a/system/Helpers/filesystem_helper.php +++ b/system/Helpers/filesystem_helper.php @@ -155,8 +155,9 @@ function write_file(string $path, string $data, string $mode = 'wb'): bool */ function delete_files(string $path, bool $delDir = false, bool $htdocs = false, bool $hidden = false): bool { - $path = realpath($path) ?: $path; - $path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + $realPath = realpath($path); + $path = $realPath === false ? $path : $realPath; + $path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; try { foreach (new RecursiveIteratorIterator( @@ -208,7 +209,8 @@ function get_filenames( ): array { $files = []; - $sourceDir = realpath($sourceDir) ?: $sourceDir; + $realPath = realpath($sourceDir); + $sourceDir = $realPath === false ? $sourceDir : $realPath; $sourceDir = rtrim($sourceDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; try { diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index 3c76f117f99e..3189c2076f0a 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -87,7 +87,7 @@ public function __construct(?string $time = null, $timezone = null, ?string $loc } } - $timezone = $timezone ?: date_default_timezone_get(); + $timezone = ($timezone === null || $timezone === '') ? date_default_timezone_get() : $timezone; $this->timezone = $timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone); // If the time string was a relative string (i.e. 'next Tuesday') diff --git a/system/Pager/Pager.php b/system/Pager/Pager.php index 1a01de3f1aa9..cb151a603a40 100644 --- a/system/Pager/Pager.php +++ b/system/Pager/Pager.php @@ -216,7 +216,9 @@ public function getCurrentPage(string $group = 'default'): int { $this->ensureGroup($group); - return $this->groups[$group]['currentPage'] ?: 1; + $currentPage = $this->groups[$group]['currentPage']; + + return ($currentPage === 0) ? 1 : $currentPage; } /** diff --git a/system/Router/AutoRouter.php b/system/Router/AutoRouter.php index 3a81f3a7bc6b..1033478c9e30 100644 --- a/system/Router/AutoRouter.php +++ b/system/Router/AutoRouter.php @@ -86,7 +86,8 @@ public function getRoute(string $uri, string $httpVerb): array // If it doesn't, no biggie - the default method name // has already been set. if ($segments !== []) { - $this->method = array_shift($segments) ?: $this->method; + $method = array_shift($segments); + $this->method = $method === '' ? $this->method : $method; } // Prevent access to initController method diff --git a/system/Test/Mock/MockCache.php b/system/Test/Mock/MockCache.php index 27af8a1ad213..5b736954a536 100644 --- a/system/Test/Mock/MockCache.php +++ b/system/Test/Mock/MockCache.php @@ -144,7 +144,7 @@ public function deleteMatching(string $pattern): int public function increment(string $key, int $offset = 1): bool { $key = static::validateKey($key, $this->prefix); - $data = $this->cache[$key] ?: null; + $data = $this->cache[$key] ?? null; if ($data === null) { $data = 0; @@ -162,7 +162,7 @@ public function decrement(string $key, int $offset = 1): bool { $key = static::validateKey($key, $this->prefix); - $data = $this->cache[$key] ?: null; + $data = $this->cache[$key] ?? null; if ($data === null) { $data = 0; diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index 38210fbdb705..ceaa557ea404 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -985,7 +985,8 @@ protected function splitRules(string $rules): array ) { // the pipe is inside the brackets causing the closing bracket to // not be included. so, we adjust the rule to include that portion. - $pos = strpos($string, '|', $cursor + strlen($rule) + 1) ?: $length; + $next = strpos($string, '|', $cursor + strlen($rule) + 1); + $pos = $next === false ? $length : $next; $rule = substr($string, $cursor, $pos - $cursor); } diff --git a/system/View/View.php b/system/View/View.php index 83b19a9313f6..e63de2bd6bb0 100644 --- a/system/View/View.php +++ b/system/View/View.php @@ -239,7 +239,7 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n ob_start(); include $this->renderVars['file']; - return ob_get_clean() ?: ''; + return (string) ob_get_clean(); })(); // Get back current vars @@ -331,7 +331,7 @@ public function renderString(string $view, ?array $options = null, ?bool $saveDa ob_start(); eval('?>' . $view); - return ob_get_clean() ?: ''; + return (string) ob_get_clean(); })($view); $this->logPerformance($start, microtime(true), $this->excerpt($view)); diff --git a/tests/system/Commands/Generators/CellGeneratorTest.php b/tests/system/Commands/Generators/CellGeneratorTest.php index 3412246966cd..10247a34538d 100644 --- a/tests/system/Commands/Generators/CellGeneratorTest.php +++ b/tests/system/Commands/Generators/CellGeneratorTest.php @@ -45,7 +45,7 @@ protected function getFileContents(string $filepath): string return ''; } - return file_get_contents($filepath) ?: ''; + return (string) file_get_contents($filepath); } public function testGenerateCell(): void diff --git a/tests/system/Commands/Generators/CommandGeneratorTest.php b/tests/system/Commands/Generators/CommandGeneratorTest.php index ee94fa591474..83b5cf5407c1 100644 --- a/tests/system/Commands/Generators/CommandGeneratorTest.php +++ b/tests/system/Commands/Generators/CommandGeneratorTest.php @@ -51,7 +51,7 @@ protected function getFileContents(string $filepath): string return ''; } - return file_get_contents($filepath) ?: ''; + return (string) file_get_contents($filepath); } public function testGenerateCommand(): void diff --git a/tests/system/Commands/Generators/ControllerGeneratorTest.php b/tests/system/Commands/Generators/ControllerGeneratorTest.php index e1c77bcb6d5d..5382c05e70f4 100644 --- a/tests/system/Commands/Generators/ControllerGeneratorTest.php +++ b/tests/system/Commands/Generators/ControllerGeneratorTest.php @@ -40,7 +40,7 @@ protected function getFileContents(string $filepath): string return ''; } - return file_get_contents($filepath) ?: ''; + return (string) file_get_contents($filepath); } public function testGenerateController(): void diff --git a/tests/system/Commands/Generators/ModelGeneratorTest.php b/tests/system/Commands/Generators/ModelGeneratorTest.php index 8dab8b007f19..bdcfdeae7553 100644 --- a/tests/system/Commands/Generators/ModelGeneratorTest.php +++ b/tests/system/Commands/Generators/ModelGeneratorTest.php @@ -43,7 +43,7 @@ private function getFileContent(string $filepath): string return ''; } - return file_get_contents($filepath) ?: ''; + return (string) file_get_contents($filepath); } public function testGenerateModel(): void diff --git a/tests/system/Commands/Generators/ScaffoldGeneratorTest.php b/tests/system/Commands/Generators/ScaffoldGeneratorTest.php index 7f12e6b93450..6ea74bb3c18f 100644 --- a/tests/system/Commands/Generators/ScaffoldGeneratorTest.php +++ b/tests/system/Commands/Generators/ScaffoldGeneratorTest.php @@ -73,7 +73,7 @@ protected function getFileContents(string $filepath): string return ''; } - return file_get_contents($filepath) ?: ''; + return (string) file_get_contents($filepath); } public function testCreateComponentProducesManyFiles(): void diff --git a/tests/system/Publisher/PublisherSupportTest.php b/tests/system/Publisher/PublisherSupportTest.php index e432f9d21d6c..c63973e63d36 100644 --- a/tests/system/Publisher/PublisherSupportTest.php +++ b/tests/system/Publisher/PublisherSupportTest.php @@ -158,7 +158,8 @@ public function testWipe(): void { $directory = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . bin2hex(random_bytes(6)); mkdir($directory, 0700); - $directory = realpath($directory) ?: $directory; + $realPath = realpath($directory); + $directory = $realPath === false ? $directory : $realPath; $this->assertDirectoryExists($directory); config('Publisher')->restrictions[$directory] = ''; // Allow the directory diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index d69dc372a109..1c720f3cb0d4 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 1560 errors +# total 1527 errors includes: - argument.type.neon @@ -20,4 +20,3 @@ includes: - property.phpDocType.neon - return.type.neon - staticMethod.notFound.neon - - ternary.shortNotAllowed.neon diff --git a/utils/phpstan-baseline/ternary.shortNotAllowed.neon b/utils/phpstan-baseline/ternary.shortNotAllowed.neon deleted file mode 100644 index 25ab1dce60e0..000000000000 --- a/utils/phpstan-baseline/ternary.shortNotAllowed.neon +++ /dev/null @@ -1,113 +0,0 @@ -# total 33 errors - -parameters: - ignoreErrors: - - - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' - count: 2 - path: ../../system/CLI/CLI.php - - - - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' - count: 1 - path: ../../system/Commands/Utilities/Namespaces.php - - - - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' - count: 2 - path: ../../system/Common.php - - - - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' - count: 4 - path: ../../system/Cookie/Cookie.php - - - - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' - count: 1 - path: ../../system/Debug/Toolbar/Collectors/Database.php - - - - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' - count: 3 - path: ../../system/Files/File.php - - - - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' - count: 1 - path: ../../system/HTTP/CURLRequest.php - - - - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' - count: 2 - path: ../../system/HTTP/Response.php - - - - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' - count: 2 - path: ../../system/Helpers/filesystem_helper.php - - - - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' - count: 1 - path: ../../system/I18n/Time.php - - - - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' - count: 1 - path: ../../system/I18n/TimeLegacy.php - - - - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' - count: 1 - path: ../../system/Pager/Pager.php - - - - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' - count: 1 - path: ../../system/Router/AutoRouter.php - - - - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' - count: 2 - path: ../../system/Test/Mock/MockCache.php - - - - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' - count: 2 - path: ../../system/View/View.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/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/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/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/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/Generators/ScaffoldGeneratorTest.php - - - - message: '#^Short ternary operator is not allowed\. Use null coalesce operator if applicable or consider using long ternary\.$#' - count: 1 - path: ../../tests/system/Publisher/PublisherSupportTest.php From c42ffa894f02606c8fb1adfd08b2c3bb27da0de8 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Wed, 12 Aug 2026 16:39:01 +0200 Subject: [PATCH 049/163] docs: warn against untrusted paths in UploadedFile move()/store() (#10446) --- system/HTTP/Files/UploadedFile.php | 10 +++++++--- user_guide_src/source/libraries/uploaded_files.rst | 10 ++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/system/HTTP/Files/UploadedFile.php b/system/HTTP/Files/UploadedFile.php index 95a393616dff..be2b74c2025c 100644 --- a/system/HTTP/Files/UploadedFile.php +++ b/system/HTTP/Files/UploadedFile.php @@ -122,7 +122,9 @@ public function __construct(string $path, string $originalName, ?string $mimeTyp * @see http://php.net/is_uploaded_file * @see http://php.net/move_uploaded_file * - * @param string $targetPath Path to which to move the uploaded file. + * @param string $targetPath Path to which to move the uploaded file. This is NOT sanitized. Never build it from + * untrusted input: a value containing "../" can move the file outside the intended + * directory. Use a path controlled by your application. * @param string|null $name The name to rename the file to. When null, the client-provided name is used and sanitized. * A caller-supplied name is NOT sanitized. * @param bool $overwrite State for indicating whether to overwrite the previously generated file with the same @@ -347,8 +349,10 @@ public function isValid(): bool * By default, upload files are saved in writable/uploads directory. The YYYYMMDD folder * and random file name will be created. * - * @param string|null $folderName the folder name to writable/uploads directory. - * @param string|null $fileName the name to rename the file to. + * @param string|null $folderName the folder name to writable/uploads directory. This is NOT sanitized. Never build + * it from untrusted input: a value containing "../" can move the file outside the + * uploads directory. + * @param string|null $fileName the name to rename the file to. A caller-supplied name is NOT sanitized. * * @return string file full path */ diff --git a/user_guide_src/source/libraries/uploaded_files.rst b/user_guide_src/source/libraries/uploaded_files.rst index eadc1d8ef2e9..beaadd8687af 100644 --- a/user_guide_src/source/libraries/uploaded_files.rst +++ b/user_guide_src/source/libraries/uploaded_files.rst @@ -331,6 +331,16 @@ In PHP versions below 8.1, this returns ``null`` Moving Files ============ +.. warning:: + Do not let user input directly decide the destination directory. The ``$targetPath`` argument of ``move()`` and the + ``$folderName`` argument of ``store()`` are never sanitized, so an attacker-controlled value containing ``../`` can + move the uploaded file outside the intended directory. Use directories controlled by your application. + + Filenames are only sanitized by default: ``move()`` sanitizes the client-provided name when the ``$name`` argument is + omitted, and ``store()`` generates a random name when ``$fileName`` is omitted. A caller-supplied ``$name`` or + ``$fileName`` is NOT sanitized, so if you pass one, generate it yourself or sanitize it with + :php:func:`sanitize_filename`. + with Original Filename ---------------------- From f0c1f2c18c28a5f20a62826a61afc6a4df9a3459 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Thu, 13 Aug 2026 21:56:31 +0800 Subject: [PATCH 050/163] refactor: fix `missingType.property` errors (#10447) --- system/Database/OCI8/Connection.php | 5 + system/Database/Postgre/Connection.php | 15 ++ tests/system/Config/FactoriesTest.php | 5 +- .../Config/fixtures/RegistrarConfig.php | 8 +- tests/system/Config/fixtures/SimpleConfig.php | 74 +++--- tests/system/Database/Live/ConnectTest.php | 17 +- tests/system/Database/Live/GetTest.php | 14 +- .../Database/Live/MySQLi/NumberNativeTest.php | 6 +- utils/phpstan-baseline/loader.neon | 3 +- .../missingType.property.neon | 238 ------------------ 10 files changed, 103 insertions(+), 282 deletions(-) delete mode 100644 utils/phpstan-baseline/missingType.property.neon diff --git a/system/Database/OCI8/Connection.php b/system/Database/OCI8/Connection.php index 12fa3b4d3df6..6f3d0dd7794c 100644 --- a/system/Database/OCI8/Connection.php +++ b/system/Database/OCI8/Connection.php @@ -53,6 +53,9 @@ class Connection extends BaseConnection 'rownum', ]; + /** + * @var array + */ protected $validDSNs = [ // TNS 'tns' => '/^\(DESCRIPTION=(\(.+\)){2,}\)$/', @@ -79,6 +82,8 @@ class Connection extends BaseConnection * * Used by storedProcedure() to prevent execute() from * re-setting the statement ID. + * + * @var bool */ protected $resetStmtId = true; diff --git a/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php index 2326eb06ac3b..fc5421a894ea 100644 --- a/system/Database/Postgre/Connection.php +++ b/system/Database/Postgre/Connection.php @@ -51,9 +51,24 @@ class Connection extends BaseConnection */ public $escapeChar = '"'; + /** + * @var string|null + */ protected $connect_timeout; + + /** + * @var string|null + */ protected $options; + + /** + * @var string|null + */ protected $sslmode; + + /** + * @var string|null + */ protected $service; /** diff --git a/tests/system/Config/FactoriesTest.php b/tests/system/Config/FactoriesTest.php index eafde84857e6..c87e4a135e2a 100644 --- a/tests/system/Config/FactoriesTest.php +++ b/tests/system/Config/FactoriesTest.php @@ -87,7 +87,10 @@ public function testUsesConfigOptions(): void { // Simulate having a $widgets property in App\Config\Factory $config = new class () extends Factory { - public $widgets = ['bar' => 'bam']; + /** + * @var array + */ + public array $widgets = ['bar' => 'bam']; }; Factories::injectMock('config', Factory::class, $config); diff --git a/tests/system/Config/fixtures/RegistrarConfig.php b/tests/system/Config/fixtures/RegistrarConfig.php index c2ea35927228..f42b8d5fa29c 100644 --- a/tests/system/Config/fixtures/RegistrarConfig.php +++ b/tests/system/Config/fixtures/RegistrarConfig.php @@ -15,8 +15,12 @@ class RegistrarConfig extends BaseConfig { - public $foo = 'bar'; - public $bar = [ + public string $foo = 'bar'; + + /** + * @var list + */ + public array $bar = [ 'baz', ]; } diff --git a/tests/system/Config/fixtures/SimpleConfig.php b/tests/system/Config/fixtures/SimpleConfig.php index 9c34d8561a3c..7f9be5b52dd8 100644 --- a/tests/system/Config/fixtures/SimpleConfig.php +++ b/tests/system/Config/fixtures/SimpleConfig.php @@ -15,45 +15,63 @@ class SimpleConfig extends BaseConfig { - public $QZERO; - public $QZEROSTR; - public $QEMPTYSTR; - public $QFALSE; - public $first = 'foo'; - public $second = 'bar'; - public $FOO; - public $onedeep; - public $default = [ + public ?string $QZERO = null; + public ?string $QZEROSTR = null; + public ?string $QEMPTYSTR = null; + public bool|string|null $QFALSE = null; + public string $first = 'foo'; + public string $second = 'bar'; + public ?string $FOO = null; + public ?string $onedeep = null; + + /** + * @var array + */ + public array $default = [ 'name' => null, ]; - public $simple = [ + + /** + * @var array + */ + public array $simple = [ 'name' => null, ]; // properties for environment override testing - public $alpha = 'one'; - public $bravo = 'two'; - public $charlie = 'three'; - public $delta = 'four'; - public $echo = ''; - public $foxtrot = 'false'; - public $fruit = 'pineapple'; - public $dessert = ''; - public $golf = 18; - public $crew = [ + public string $alpha = 'one'; + public string $bravo = 'two'; + public string $charlie = 'three'; + public string $delta = 'four'; + public string $echo = ''; + public bool|string $foxtrot = 'false'; + public string $fruit = 'pineapple'; + public string $dessert = ''; + public int $golf = 18; + + /** + * @var array + */ + public array $crew = [ 'captain' => 'Kirk', 'science' => 'Spock', 'doctor' => 'Bones', 'comms' => 'Uhuru', ]; - public $shortie; - public $longie; - public $onedeep_value; - public $one_deep = [ + + public ?string $shortie = null; + public ?string $longie = null; + public ?string $onedeep_value = null; + + /** + * @var array + */ + public array $one_deep = [ 'under_deep' => null, ]; - public $float = 12.34; - public $int = 1234; - public $password = 'secret'; - public ?int $size = null; + + public float $float = 12.34; + public int $int = 1234; + public string $password = 'secret'; + public ?int $size = null; } diff --git a/tests/system/Database/Live/ConnectTest.php b/tests/system/Database/Live/ConnectTest.php index e41fdbdfc114..e981d3ab33cf 100644 --- a/tests/system/Database/Live/ConnectTest.php +++ b/tests/system/Database/Live/ConnectTest.php @@ -28,9 +28,20 @@ final class ConnectTest extends CIUnitTestCase { use DatabaseTestTrait; - private $group1; - private $group2; - private $tests; + /** + * @var array + */ + private array $group1 = []; + + /** + * @var array + */ + private array $group2 = []; + + /** + * @var array + */ + private array $tests = []; protected function setUp(): void { diff --git a/tests/system/Database/Live/GetTest.php b/tests/system/Database/Live/GetTest.php index ce98ebf413e0..57a124d4fd0b 100644 --- a/tests/system/Database/Live/GetTest.php +++ b/tests/system/Database/Live/GetTest.php @@ -254,13 +254,13 @@ public function testGetRowWithReturnType(): void public function testGetRowWithCustomReturnType(): void { $testClass = new class () { - public $id; - public $name; - public $email; - public $country; - public $created_at; - public $updated_at; - public $deleted_at; + public mixed $id = null; + public mixed $name = null; + public mixed $email = null; + public mixed $country = null; + public mixed $created_at = null; + public mixed $updated_at = null; + public mixed $deleted_at = null; }; $user = $this->db->table('user')->get()->getRow(0, $testClass::class); diff --git a/tests/system/Database/Live/MySQLi/NumberNativeTest.php b/tests/system/Database/Live/MySQLi/NumberNativeTest.php index 4469e4c3659a..3834c4c3f9b0 100644 --- a/tests/system/Database/Live/MySQLi/NumberNativeTest.php +++ b/tests/system/Database/Live/MySQLi/NumberNativeTest.php @@ -27,7 +27,11 @@ final class NumberNativeTest extends CIUnitTestCase { use DatabaseTestTrait; - private $tests; + /** + * @var array + */ + private array $tests = []; + protected $refresh = true; protected $seed = CITestSeeder::class; diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 1c720f3cb0d4..37ac9a181531 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 1527 errors +# total 1480 errors includes: - argument.type.neon @@ -12,7 +12,6 @@ includes: - method.notFound.neon - missingType.iterableValue.neon - missingType.parameter.neon - - missingType.property.neon - nullCoalesce.property.neon - property.defaultValue.neon - property.nonObject.neon diff --git a/utils/phpstan-baseline/missingType.property.neon b/utils/phpstan-baseline/missingType.property.neon deleted file mode 100644 index 451773e8bc5d..000000000000 --- a/utils/phpstan-baseline/missingType.property.neon +++ /dev/null @@ -1,238 +0,0 @@ -# total 47 errors - -parameters: - ignoreErrors: - - - message: '#^Property CodeIgniter\\Database\\OCI8\\Connection\:\:\$resetStmtId has no type specified\.$#' - count: 1 - path: ../../system/Database/OCI8/Connection.php - - - - message: '#^Property CodeIgniter\\Database\\OCI8\\Connection\:\:\$validDSNs has no type specified\.$#' - count: 1 - path: ../../system/Database/OCI8/Connection.php - - - - message: '#^Property CodeIgniter\\Database\\Postgre\\Connection\:\:\$connect_timeout has no type specified\.$#' - count: 1 - path: ../../system/Database/Postgre/Connection.php - - - - message: '#^Property CodeIgniter\\Database\\Postgre\\Connection\:\:\$options has no type specified\.$#' - count: 1 - path: ../../system/Database/Postgre/Connection.php - - - - message: '#^Property CodeIgniter\\Database\\Postgre\\Connection\:\:\$service has no type specified\.$#' - count: 1 - path: ../../system/Database/Postgre/Connection.php - - - - message: '#^Property CodeIgniter\\Database\\Postgre\\Connection\:\:\$sslmode has no type specified\.$#' - count: 1 - path: ../../system/Database/Postgre/Connection.php - - - - message: '#^Property CodeIgniter\\Config\\Factory@anonymous/tests/system/Config/FactoriesTest\.php\:89\:\:\$widgets has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/FactoriesTest.php - - - - message: '#^Property RegistrarConfig\:\:\$bar has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/RegistrarConfig.php - - - - message: '#^Property RegistrarConfig\:\:\$foo has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/RegistrarConfig.php - - - - message: '#^Property SimpleConfig\:\:\$FOO has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/SimpleConfig.php - - - - message: '#^Property SimpleConfig\:\:\$QEMPTYSTR has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/SimpleConfig.php - - - - message: '#^Property SimpleConfig\:\:\$QFALSE has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/SimpleConfig.php - - - - message: '#^Property SimpleConfig\:\:\$QZERO has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/SimpleConfig.php - - - - message: '#^Property SimpleConfig\:\:\$QZEROSTR has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/SimpleConfig.php - - - - message: '#^Property SimpleConfig\:\:\$alpha has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/SimpleConfig.php - - - - message: '#^Property SimpleConfig\:\:\$bravo has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/SimpleConfig.php - - - - message: '#^Property SimpleConfig\:\:\$charlie has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/SimpleConfig.php - - - - message: '#^Property SimpleConfig\:\:\$crew has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/SimpleConfig.php - - - - message: '#^Property SimpleConfig\:\:\$default has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/SimpleConfig.php - - - - message: '#^Property SimpleConfig\:\:\$delta has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/SimpleConfig.php - - - - message: '#^Property SimpleConfig\:\:\$dessert has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/SimpleConfig.php - - - - message: '#^Property SimpleConfig\:\:\$echo has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/SimpleConfig.php - - - - message: '#^Property SimpleConfig\:\:\$first has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/SimpleConfig.php - - - - message: '#^Property SimpleConfig\:\:\$float has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/SimpleConfig.php - - - - message: '#^Property SimpleConfig\:\:\$foxtrot has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/SimpleConfig.php - - - - message: '#^Property SimpleConfig\:\:\$fruit has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/SimpleConfig.php - - - - message: '#^Property SimpleConfig\:\:\$golf has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/SimpleConfig.php - - - - message: '#^Property SimpleConfig\:\:\$int has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/SimpleConfig.php - - - - message: '#^Property SimpleConfig\:\:\$longie has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/SimpleConfig.php - - - - message: '#^Property SimpleConfig\:\:\$one_deep has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/SimpleConfig.php - - - - message: '#^Property SimpleConfig\:\:\$onedeep has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/SimpleConfig.php - - - - message: '#^Property SimpleConfig\:\:\$onedeep_value has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/SimpleConfig.php - - - - message: '#^Property SimpleConfig\:\:\$password has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/SimpleConfig.php - - - - message: '#^Property SimpleConfig\:\:\$second has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/SimpleConfig.php - - - - message: '#^Property SimpleConfig\:\:\$shortie has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/SimpleConfig.php - - - - message: '#^Property SimpleConfig\:\:\$simple has no type specified\.$#' - count: 1 - path: ../../tests/system/Config/fixtures/SimpleConfig.php - - - - message: '#^Property CodeIgniter\\Database\\Live\\ConnectTest\:\:\$group1 has no type specified\.$#' - count: 1 - path: ../../tests/system/Database/Live/ConnectTest.php - - - - message: '#^Property CodeIgniter\\Database\\Live\\ConnectTest\:\:\$group2 has no type specified\.$#' - count: 1 - path: ../../tests/system/Database/Live/ConnectTest.php - - - - message: '#^Property CodeIgniter\\Database\\Live\\ConnectTest\:\:\$tests has no type specified\.$#' - count: 1 - path: ../../tests/system/Database/Live/ConnectTest.php - - - - message: '#^Property class@anonymous/tests/system/Database/Live/GetTest\.php\:256\:\:\$country has no type specified\.$#' - count: 1 - path: ../../tests/system/Database/Live/GetTest.php - - - - message: '#^Property class@anonymous/tests/system/Database/Live/GetTest\.php\:256\:\:\$created_at has no type specified\.$#' - count: 1 - path: ../../tests/system/Database/Live/GetTest.php - - - - message: '#^Property class@anonymous/tests/system/Database/Live/GetTest\.php\:256\:\:\$deleted_at has no type specified\.$#' - count: 1 - path: ../../tests/system/Database/Live/GetTest.php - - - - message: '#^Property class@anonymous/tests/system/Database/Live/GetTest\.php\:256\:\:\$email has no type specified\.$#' - count: 1 - path: ../../tests/system/Database/Live/GetTest.php - - - - message: '#^Property class@anonymous/tests/system/Database/Live/GetTest\.php\:256\:\:\$id has no type specified\.$#' - count: 1 - path: ../../tests/system/Database/Live/GetTest.php - - - - message: '#^Property class@anonymous/tests/system/Database/Live/GetTest\.php\:256\:\:\$name has no type specified\.$#' - count: 1 - path: ../../tests/system/Database/Live/GetTest.php - - - - message: '#^Property class@anonymous/tests/system/Database/Live/GetTest\.php\:256\:\:\$updated_at has no type specified\.$#' - count: 1 - path: ../../tests/system/Database/Live/GetTest.php - - - - message: '#^Property CodeIgniter\\Database\\Live\\MySQLi\\NumberNativeTest\:\:\$tests has no type specified\.$#' - count: 1 - path: ../../tests/system/Database/Live/MySQLi/NumberNativeTest.php From a42d0bc61d4ca860f2b097d17f74ff975f483e16 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Thu, 13 Aug 2026 22:39:32 +0800 Subject: [PATCH 051/163] refactor: fix `return.type` errors (#10448) --- system/Database/BaseBuilder.php | 7 +++++-- system/Router/Router.php | 2 +- utils/phpstan-baseline/loader.neon | 3 +-- utils/phpstan-baseline/return.type.neon | 13 ------------- 4 files changed, 7 insertions(+), 18 deletions(-) delete mode 100644 utils/phpstan-baseline/return.type.neon diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 17f0690d8fb9..1383f006023a 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -3591,13 +3591,16 @@ protected function setBind(string $key, $value = null, bool $escape = true): str /** * Returns a clone of a Base Builder with reset query builder values. * - * @return $this + * @return static * * @deprecated */ protected function cleanClone() { - return (clone $this)->from([], true)->resetQuery(); + $clone = clone $this; + $clone->from([], true); + + return $clone->resetQuery(); } /** diff --git a/system/Router/Router.php b/system/Router/Router.php index cb53b439ea30..c886bf45f396 100644 --- a/system/Router/Router.php +++ b/system/Router/Router.php @@ -914,7 +914,7 @@ public function executeAfterAttributes(RequestInterface $request, ResponseInterf * Returns the route attributes collected during routing * for the current route. * - * @return array{class: list, method: list} + * @return array{class: list, method: list} */ public function getRouteAttributes(): array { diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 37ac9a181531..5c569b16cf71 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 1480 errors +# total 1478 errors includes: - argument.type.neon @@ -17,5 +17,4 @@ includes: - property.nonObject.neon - property.notFound.neon - property.phpDocType.neon - - return.type.neon - staticMethod.notFound.neon diff --git a/utils/phpstan-baseline/return.type.neon b/utils/phpstan-baseline/return.type.neon deleted file mode 100644 index acfc78a3c171..000000000000 --- a/utils/phpstan-baseline/return.type.neon +++ /dev/null @@ -1,13 +0,0 @@ -# total 2 errors - -parameters: - ignoreErrors: - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:cleanClone\(\) should return \$this\(CodeIgniter\\Database\\BaseBuilder\) but returns CodeIgniter\\Database\\BaseBuilder\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Router\\Router\:\:getRouteAttributes\(\) should return array\{class\: list\, method\: list\\} but returns array\{class\: list\, method\: list\\}\.$#' - count: 1 - path: ../../system/Router/Router.php From 7fe8e64d1150fff8a269997d87ce33964debf522 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Fri, 14 Aug 2026 00:52:10 +0800 Subject: [PATCH 052/163] refactor: add generics to `Forge`, `BaseBuilder` and `BasePreparedQuery` (#10449) --- system/Database/BaseBuilder.php | 6 ++- system/Database/BasePreparedQuery.php | 3 +- system/Database/Forge.php | 6 ++- system/Database/MySQLi/Builder.php | 2 + system/Database/MySQLi/Forge.php | 2 + system/Database/MySQLi/PreparedQuery.php | 2 +- system/Database/OCI8/Builder.php | 9 +--- system/Database/OCI8/Forge.php | 2 + system/Database/OCI8/PreparedQuery.php | 9 +--- system/Database/Postgre/Builder.php | 2 + system/Database/Postgre/Forge.php | 7 +-- system/Database/Postgre/PreparedQuery.php | 2 +- system/Database/SQLSRV/Builder.php | 2 + system/Database/SQLSRV/Forge.php | 2 + system/Database/SQLSRV/PreparedQuery.php | 9 +--- system/Database/SQLite3/Builder.php | 2 + system/Database/SQLite3/Forge.php | 7 +-- system/Database/SQLite3/PreparedQuery.php | 2 +- system/Test/Mock/MockBuilder.php | 3 ++ tests/_support/Mock/MockPreparedQuery.php | 3 +- .../system/Database/Forge/CreateTableTest.php | 2 +- utils/phpstan-baseline/argument.type.neon | 10 ++-- utils/phpstan-baseline/loader.neon | 2 +- utils/phpstan-baseline/property.notFound.neon | 17 +------ .../phpstan-baseline/property.phpDocType.neon | 47 +++++-------------- 25 files changed, 60 insertions(+), 100 deletions(-) diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 1383f006023a..1aebd7ae0d9b 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -26,6 +26,8 @@ * Provides the core Query Builder methods. * Database-specific Builders might need to override * certain methods to make them work. + * + * @template TDb of BaseConnection */ class BaseBuilder { @@ -183,7 +185,7 @@ class BaseBuilder /** * A reference to the database connection. * - * @var BaseConnection + * @var TDb */ protected $db; @@ -310,7 +312,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 TDb $db */ $this->db = $db; if ($tableName instanceof TableName) { diff --git a/system/Database/BasePreparedQuery.php b/system/Database/BasePreparedQuery.php index 16b23535e031..e770a8d35f11 100644 --- a/system/Database/BasePreparedQuery.php +++ b/system/Database/BasePreparedQuery.php @@ -23,6 +23,7 @@ * @template TConnection * @template TStatement * @template TResult + * @template TDb of BaseConnection * * @implements PreparedQueryInterface */ @@ -60,7 +61,7 @@ abstract class BasePreparedQuery implements PreparedQueryInterface /** * A reference to the db connection to use. * - * @var BaseConnection + * @var TDb */ protected $db; diff --git a/system/Database/Forge.php b/system/Database/Forge.php index 52ae71464573..a5d950d9620c 100644 --- a/system/Database/Forge.php +++ b/system/Database/Forge.php @@ -21,13 +21,15 @@ /** * The Forge class transforms migrations to executable * SQL statements. + * + * @template TDb of BaseConnection */ class Forge { /** * The active database connection. * - * @var BaseConnection + * @var TDb */ protected $db; @@ -187,6 +189,8 @@ class Forge /** * Constructor. + * + * @param TDb $db */ public function __construct(BaseConnection $db) { diff --git a/system/Database/MySQLi/Builder.php b/system/Database/MySQLi/Builder.php index bba90421e9e4..1d923d1982e6 100644 --- a/system/Database/MySQLi/Builder.php +++ b/system/Database/MySQLi/Builder.php @@ -19,6 +19,8 @@ /** * Builder for MySQLi + * + * @extends BaseBuilder */ class Builder extends BaseBuilder { diff --git a/system/Database/MySQLi/Forge.php b/system/Database/MySQLi/Forge.php index 418cc08580ae..d433efd335db 100644 --- a/system/Database/MySQLi/Forge.php +++ b/system/Database/MySQLi/Forge.php @@ -17,6 +17,8 @@ /** * Forge for MySQLi + * + * @extends BaseForge */ class Forge extends BaseForge { diff --git a/system/Database/MySQLi/PreparedQuery.php b/system/Database/MySQLi/PreparedQuery.php index 05ef5f151b7b..d018250bfc48 100644 --- a/system/Database/MySQLi/PreparedQuery.php +++ b/system/Database/MySQLi/PreparedQuery.php @@ -24,7 +24,7 @@ /** * Prepared query for MySQLi * - * @extends BasePreparedQuery + * @extends BasePreparedQuery */ class PreparedQuery extends BasePreparedQuery { diff --git a/system/Database/OCI8/Builder.php b/system/Database/OCI8/Builder.php index eb6f1ca88855..9f3fa28a95a0 100644 --- a/system/Database/OCI8/Builder.php +++ b/system/Database/OCI8/Builder.php @@ -19,6 +19,8 @@ /** * Builder for OCI8 + * + * @extends BaseBuilder */ class Builder extends BaseBuilder { @@ -48,13 +50,6 @@ class Builder extends BaseBuilder */ protected $countString = 'SELECT COUNT(1) '; - /** - * A reference to the database connection. - * - * @var Connection - */ - protected $db; - /** * Generates a platform-specific insert string from the supplied data. */ diff --git a/system/Database/OCI8/Forge.php b/system/Database/OCI8/Forge.php index e9dfb06885df..0cf8274e78ef 100644 --- a/system/Database/OCI8/Forge.php +++ b/system/Database/OCI8/Forge.php @@ -17,6 +17,8 @@ /** * Forge for OCI8 + * + * @extends BaseForge */ class Forge extends BaseForge { diff --git a/system/Database/OCI8/PreparedQuery.php b/system/Database/OCI8/PreparedQuery.php index 390ed938fc1c..bd027bcb1bc3 100644 --- a/system/Database/OCI8/PreparedQuery.php +++ b/system/Database/OCI8/PreparedQuery.php @@ -21,17 +21,10 @@ /** * Prepared query for OCI8 * - * @extends BasePreparedQuery + * @extends BasePreparedQuery */ class PreparedQuery extends BasePreparedQuery { - /** - * A reference to the db connection to use. - * - * @var Connection - */ - protected $db; - /** * Latest inserted table name. */ diff --git a/system/Database/Postgre/Builder.php b/system/Database/Postgre/Builder.php index 10b42b9b3b7c..452390a8f4ab 100644 --- a/system/Database/Postgre/Builder.php +++ b/system/Database/Postgre/Builder.php @@ -22,6 +22,8 @@ /** * Builder for Postgre + * + * @extends BaseBuilder */ class Builder extends BaseBuilder { diff --git a/system/Database/Postgre/Forge.php b/system/Database/Postgre/Forge.php index fdf25aa2f9ca..0423d6d54cd1 100644 --- a/system/Database/Postgre/Forge.php +++ b/system/Database/Postgre/Forge.php @@ -17,6 +17,8 @@ /** * Forge for Postgre + * + * @extends BaseForge */ class Forge extends BaseForge { @@ -67,11 +69,6 @@ class Forge extends BaseForge */ protected $null = 'NULL'; - /** - * @var Connection - */ - protected $db; - /** * CREATE TABLE attributes * diff --git a/system/Database/Postgre/PreparedQuery.php b/system/Database/Postgre/PreparedQuery.php index 08900a6ddcfd..646c55d91406 100644 --- a/system/Database/Postgre/PreparedQuery.php +++ b/system/Database/Postgre/PreparedQuery.php @@ -23,7 +23,7 @@ /** * Prepared query for Postgre * - * @extends BasePreparedQuery + * @extends BasePreparedQuery */ class PreparedQuery extends BasePreparedQuery { diff --git a/system/Database/SQLSRV/Builder.php b/system/Database/SQLSRV/Builder.php index f279ef750324..ebc656ddad4d 100644 --- a/system/Database/SQLSRV/Builder.php +++ b/system/Database/SQLSRV/Builder.php @@ -28,6 +28,8 @@ * @todo auto check for TextCastToInt * @todo auto check for InsertIndexValue * @todo replace: delete index entries before insert + * + * @extends BaseBuilder */ class Builder extends BaseBuilder { diff --git a/system/Database/SQLSRV/Forge.php b/system/Database/SQLSRV/Forge.php index f9f5b973a455..c9ae7557cc65 100644 --- a/system/Database/SQLSRV/Forge.php +++ b/system/Database/SQLSRV/Forge.php @@ -20,6 +20,8 @@ /** * Forge for SQLSRV + * + * @extends BaseForge */ class Forge extends BaseForge { diff --git a/system/Database/SQLSRV/PreparedQuery.php b/system/Database/SQLSRV/PreparedQuery.php index 19d2d5adfe6b..0cdc842b1196 100644 --- a/system/Database/SQLSRV/PreparedQuery.php +++ b/system/Database/SQLSRV/PreparedQuery.php @@ -20,7 +20,7 @@ /** * Prepared query for Postgre * - * @extends BasePreparedQuery + * @extends BasePreparedQuery */ class PreparedQuery extends BasePreparedQuery { @@ -31,13 +31,6 @@ class PreparedQuery extends BasePreparedQuery */ protected $parameters = []; - /** - * A reference to the db connection to use. - * - * @var Connection - */ - protected $db; - public function __construct(Connection $db) { parent::__construct($db); diff --git a/system/Database/SQLite3/Builder.php b/system/Database/SQLite3/Builder.php index 68b62a219230..57dc1ae6c3ac 100644 --- a/system/Database/SQLite3/Builder.php +++ b/system/Database/SQLite3/Builder.php @@ -20,6 +20,8 @@ /** * Builder for SQLite3 + * + * @extends BaseBuilder */ class Builder extends BaseBuilder { diff --git a/system/Database/SQLite3/Forge.php b/system/Database/SQLite3/Forge.php index ed5aee740895..688e0af559fb 100644 --- a/system/Database/SQLite3/Forge.php +++ b/system/Database/SQLite3/Forge.php @@ -19,6 +19,8 @@ /** * Forge for SQLite3 + * + * @extends BaseForge */ class Forge extends BaseForge { @@ -29,11 +31,6 @@ class Forge extends BaseForge */ protected $dropIndexStr = 'DROP INDEX %s'; - /** - * @var Connection - */ - protected $db; - /** * UNSIGNED support * diff --git a/system/Database/SQLite3/PreparedQuery.php b/system/Database/SQLite3/PreparedQuery.php index afd2669c6436..5a5153a5f7a3 100644 --- a/system/Database/SQLite3/PreparedQuery.php +++ b/system/Database/SQLite3/PreparedQuery.php @@ -24,7 +24,7 @@ /** * Prepared query for SQLite3 * - * @extends BasePreparedQuery + * @extends BasePreparedQuery */ class PreparedQuery extends BasePreparedQuery { diff --git a/system/Test/Mock/MockBuilder.php b/system/Test/Mock/MockBuilder.php index f95f09495a98..971ee7829a55 100644 --- a/system/Test/Mock/MockBuilder.php +++ b/system/Test/Mock/MockBuilder.php @@ -15,6 +15,9 @@ use CodeIgniter\Database\BaseBuilder; +/** + * @extends BaseBuilder + */ class MockBuilder extends BaseBuilder { /** diff --git a/tests/_support/Mock/MockPreparedQuery.php b/tests/_support/Mock/MockPreparedQuery.php index c31dc266d84d..60c9cf926e2d 100644 --- a/tests/_support/Mock/MockPreparedQuery.php +++ b/tests/_support/Mock/MockPreparedQuery.php @@ -14,11 +14,12 @@ namespace Tests\Support\Mock; use CodeIgniter\Database\BasePreparedQuery; +use CodeIgniter\Test\Mock\MockConnection; /** * @internal * - * @extends BasePreparedQuery + * @extends BasePreparedQuery */ final class MockPreparedQuery extends BasePreparedQuery { diff --git a/tests/system/Database/Forge/CreateTableTest.php b/tests/system/Database/Forge/CreateTableTest.php index 115c5506d379..b8932f446777 100644 --- a/tests/system/Database/Forge/CreateTableTest.php +++ b/tests/system/Database/Forge/CreateTableTest.php @@ -42,7 +42,7 @@ public function testCreateTableWithDefaultRawSql(): void ->with($sql) ->willReturn(true); - $forge = new class ($dbMock) extends Forge {}; + $forge = new Forge($dbMock); $fields = [ 'id' => [ diff --git a/utils/phpstan-baseline/argument.type.neon b/utils/phpstan-baseline/argument.type.neon index 6f504bc90f90..80cb40694994 100644 --- a/utils/phpstan-baseline/argument.type.neon +++ b/utils/phpstan-baseline/argument.type.neon @@ -53,7 +53,7 @@ parameters: path: ../../tests/system/Config/FactoriesTest.php - - message: '#^Parameter \#1 \$from of method CodeIgniter\\Database\\BaseBuilder\:\:from\(\) expects array\|string, null given\.$#' + message: '#^Parameter \#1 \$from of method CodeIgniter\\Database\\BaseBuilder\\:\:from\(\) expects array\|string, null given\.$#' count: 1 path: ../../tests/system/Database/Builder/FromTest.php @@ -68,7 +68,7 @@ parameters: path: ../../tests/system/Database/Builder/GetTest.php - - message: '#^Parameter \#1 \$fields of method CodeIgniter\\Database\\Forge\:\:addField\(\) expects array\\|string, array\ given\.$#' + message: '#^Parameter \#1 \$fields of method CodeIgniter\\Database\\Forge\\:\:addField\(\) expects array\\|string, array\ given\.$#' count: 2 path: ../../tests/system/Database/Live/ForgeTest.php @@ -78,17 +78,17 @@ parameters: path: ../../tests/system/Database/Live/SQLite3/AlterTableTest.php - - message: '#^Parameter \#1 \$set of method CodeIgniter\\Database\\BaseBuilder\:\:updateFields\(\) expects list\\|string, array\{0\: ''country'', updated_at\: CodeIgniter\\Database\\RawSql\} given\.$#' + message: '#^Parameter \#1 \$set of method CodeIgniter\\Database\\BaseBuilder\\:\:updateFields\(\) expects list\\|string, array\{0\: ''country'', updated_at\: CodeIgniter\\Database\\RawSql\} given\.$#' count: 2 path: ../../tests/system/Database/Live/UpdateTest.php - - message: '#^Parameter \#1 \$set of method CodeIgniter\\Database\\BaseBuilder\:\:updateFields\(\) expects list\\|string, array\{0\: ''name'', updated_at\: CodeIgniter\\Database\\RawSql\} given\.$#' + message: '#^Parameter \#1 \$set of method CodeIgniter\\Database\\BaseBuilder\\:\:updateFields\(\) expects list\\|string, array\{0\: ''name'', updated_at\: CodeIgniter\\Database\\RawSql\} given\.$#' count: 1 path: ../../tests/system/Database/Live/UpdateTest.php - - message: '#^Parameter \#1 \$set of method CodeIgniter\\Database\\BaseBuilder\:\:updateFields\(\) expects list\\|string, array\{updated_at\: CodeIgniter\\Database\\RawSql\} given\.$#' + message: '#^Parameter \#1 \$set of method CodeIgniter\\Database\\BaseBuilder\\:\:updateFields\(\) expects list\\|string, array\{updated_at\: CodeIgniter\\Database\\RawSql\} given\.$#' count: 2 path: ../../tests/system/Database/Live/UpsertTest.php diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 5c569b16cf71..c74a825b1bcf 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 1478 errors +# total 1454 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/property.notFound.neon b/utils/phpstan-baseline/property.notFound.neon index b64acebff15b..ebd40c7f58a3 100644 --- a/utils/phpstan-baseline/property.notFound.neon +++ b/utils/phpstan-baseline/property.notFound.neon @@ -1,22 +1,7 @@ -# total 47 errors +# total 28 errors parameters: ignoreErrors: - - - message: '#^Access to an undefined property CodeIgniter\\Database\\BaseConnection\\:\:\$mysqli\.$#' - count: 3 - path: ../../system/Database/MySQLi/PreparedQuery.php - - - - message: '#^Access to an undefined property CodeIgniter\\Database\\BaseConnection\:\:\$schema\.$#' - count: 2 - path: ../../system/Database/SQLSRV/Builder.php - - - - message: '#^Access to an undefined property CodeIgniter\\Database\\BaseConnection\:\:\$schema\.$#' - count: 14 - path: ../../system/Database/SQLSRV/Forge.php - - message: '#^Access to an undefined property CodeIgniter\\Database\\BaseConnection\:\:\$mysqli\.$#' count: 1 diff --git a/utils/phpstan-baseline/property.phpDocType.neon b/utils/phpstan-baseline/property.phpDocType.neon index 148b167e3e40..a6812dfb728d 100644 --- a/utils/phpstan-baseline/property.phpDocType.neon +++ b/utils/phpstan-baseline/property.phpDocType.neon @@ -1,4 +1,4 @@ -# total 43 errors +# total 38 errors parameters: ignoreErrors: @@ -8,7 +8,7 @@ parameters: path: ../../system/Database/MySQLi/Connection.php - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\MySQLi\\Forge\:\:\$createDatabaseStr is not the same as PHPDoc type string\|false of overridden property CodeIgniter\\Database\\Forge\:\:\$createDatabaseStr\.$#' + message: '#^PHPDoc type string of property CodeIgniter\\Database\\MySQLi\\Forge\:\:\$createDatabaseStr is not the same as PHPDoc type string\|false of overridden property CodeIgniter\\Database\\Forge\\:\:\$createDatabaseStr\.$#' count: 1 path: ../../system/Database/MySQLi/Forge.php @@ -22,46 +22,36 @@ parameters: count: 1 path: ../../system/Database/MySQLi/Utils.php - - - message: '#^PHPDoc type CodeIgniter\\Database\\OCI8\\Connection of property CodeIgniter\\Database\\OCI8\\Builder\:\:\$db is not the same as PHPDoc type CodeIgniter\\Database\\BaseConnection of overridden property CodeIgniter\\Database\\BaseBuilder\:\:\$db\.$#' - count: 1 - path: ../../system/Database/OCI8/Builder.php - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\OCI8\\Connection\:\:\$escapeChar is not the same as PHPDoc type array\|string of overridden property CodeIgniter\\Database\\BaseConnection\\:\:\$escapeChar\.$#' count: 1 path: ../../system/Database/OCI8/Connection.php - - message: '#^PHPDoc type false of property CodeIgniter\\Database\\OCI8\\Forge\:\:\$createDatabaseStr is not the same as PHPDoc type string\|false of overridden property CodeIgniter\\Database\\Forge\:\:\$createDatabaseStr\.$#' + message: '#^PHPDoc type false of property CodeIgniter\\Database\\OCI8\\Forge\:\:\$createDatabaseStr is not the same as PHPDoc type string\|false of overridden property CodeIgniter\\Database\\Forge\\:\:\$createDatabaseStr\.$#' count: 1 path: ../../system/Database/OCI8/Forge.php - - message: '#^PHPDoc type false of property CodeIgniter\\Database\\OCI8\\Forge\:\:\$createTableIfStr is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\Forge\:\:\$createTableIfStr\.$#' + message: '#^PHPDoc type false of property CodeIgniter\\Database\\OCI8\\Forge\:\:\$createTableIfStr is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\Forge\\:\:\$createTableIfStr\.$#' count: 1 path: ../../system/Database/OCI8/Forge.php - - message: '#^PHPDoc type false of property CodeIgniter\\Database\\OCI8\\Forge\:\:\$dropDatabaseStr is not the same as PHPDoc type string\|false of overridden property CodeIgniter\\Database\\Forge\:\:\$dropDatabaseStr\.$#' + message: '#^PHPDoc type false of property CodeIgniter\\Database\\OCI8\\Forge\:\:\$dropDatabaseStr is not the same as PHPDoc type string\|false of overridden property CodeIgniter\\Database\\Forge\\:\:\$dropDatabaseStr\.$#' count: 1 path: ../../system/Database/OCI8/Forge.php - - message: '#^PHPDoc type false of property CodeIgniter\\Database\\OCI8\\Forge\:\:\$dropTableIfStr is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\Forge\:\:\$dropTableIfStr\.$#' + message: '#^PHPDoc type false of property CodeIgniter\\Database\\OCI8\\Forge\:\:\$dropTableIfStr is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\Forge\\:\:\$dropTableIfStr\.$#' count: 1 path: ../../system/Database/OCI8/Forge.php - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\OCI8\\Forge\:\:\$renameTableStr is not the same as PHPDoc type string\|false of overridden property CodeIgniter\\Database\\Forge\:\:\$renameTableStr\.$#' + message: '#^PHPDoc type string of property CodeIgniter\\Database\\OCI8\\Forge\:\:\$renameTableStr is not the same as PHPDoc type string\|false of overridden property CodeIgniter\\Database\\Forge\\:\:\$renameTableStr\.$#' count: 1 path: ../../system/Database/OCI8/Forge.php - - - message: '#^PHPDoc type CodeIgniter\\Database\\OCI8\\Connection of property CodeIgniter\\Database\\OCI8\\PreparedQuery\:\:\$db is not the same as PHPDoc type CodeIgniter\\Database\\BaseConnection\ of overridden property CodeIgniter\\Database\\BasePreparedQuery\\:\:\$db\.$#' - count: 1 - path: ../../system/Database/OCI8/PreparedQuery.php - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\OCI8\\Utils\:\:\$listDatabases is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\BaseUtils\:\:\$listDatabases\.$#' count: 1 @@ -72,11 +62,6 @@ parameters: count: 1 path: ../../system/Database/Postgre/Connection.php - - - message: '#^PHPDoc type CodeIgniter\\Database\\Postgre\\Connection of property CodeIgniter\\Database\\Postgre\\Forge\:\:\$db is not the same as PHPDoc type CodeIgniter\\Database\\BaseConnection of overridden property CodeIgniter\\Database\\Forge\:\:\$db\.$#' - count: 1 - path: ../../system/Database/Postgre/Forge.php - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\Postgre\\Utils\:\:\$listDatabases is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\BaseUtils\:\:\$listDatabases\.$#' count: 1 @@ -93,30 +78,25 @@ parameters: path: ../../system/Database/SQLSRV/Connection.php - - message: '#^PHPDoc type array of property CodeIgniter\\Database\\SQLSRV\\Forge\:\:\$unsigned is not the same as PHPDoc type array\|bool of overridden property CodeIgniter\\Database\\Forge\:\:\$unsigned\.$#' + message: '#^PHPDoc type array of property CodeIgniter\\Database\\SQLSRV\\Forge\:\:\$unsigned is not the same as PHPDoc type array\|bool of overridden property CodeIgniter\\Database\\Forge\\:\:\$unsigned\.$#' count: 1 path: ../../system/Database/SQLSRV/Forge.php - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\SQLSRV\\Forge\:\:\$createDatabaseStr is not the same as PHPDoc type string\|false of overridden property CodeIgniter\\Database\\Forge\:\:\$createDatabaseStr\.$#' + message: '#^PHPDoc type string of property CodeIgniter\\Database\\SQLSRV\\Forge\:\:\$createDatabaseStr is not the same as PHPDoc type string\|false of overridden property CodeIgniter\\Database\\Forge\\:\:\$createDatabaseStr\.$#' count: 1 path: ../../system/Database/SQLSRV/Forge.php - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\SQLSRV\\Forge\:\:\$createTableIfStr is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\Forge\:\:\$createTableIfStr\.$#' + message: '#^PHPDoc type string of property CodeIgniter\\Database\\SQLSRV\\Forge\:\:\$createTableIfStr is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\Forge\\:\:\$createTableIfStr\.$#' count: 1 path: ../../system/Database/SQLSRV/Forge.php - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\SQLSRV\\Forge\:\:\$renameTableStr is not the same as PHPDoc type string\|false of overridden property CodeIgniter\\Database\\Forge\:\:\$renameTableStr\.$#' + message: '#^PHPDoc type string of property CodeIgniter\\Database\\SQLSRV\\Forge\:\:\$renameTableStr is not the same as PHPDoc type string\|false of overridden property CodeIgniter\\Database\\Forge\\:\:\$renameTableStr\.$#' count: 1 path: ../../system/Database/SQLSRV/Forge.php - - - message: '#^PHPDoc type CodeIgniter\\Database\\SQLSRV\\Connection of property CodeIgniter\\Database\\SQLSRV\\PreparedQuery\:\:\$db is not the same as PHPDoc type CodeIgniter\\Database\\BaseConnection\ of overridden property CodeIgniter\\Database\\BasePreparedQuery\\:\:\$db\.$#' - count: 1 - path: ../../system/Database/SQLSRV/PreparedQuery.php - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\SQLSRV\\Utils\:\:\$listDatabases is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\BaseUtils\:\:\$listDatabases\.$#' count: 1 @@ -132,11 +112,6 @@ parameters: count: 1 path: ../../system/Database/SQLite3/Connection.php - - - message: '#^PHPDoc type CodeIgniter\\Database\\SQLite3\\Connection of property CodeIgniter\\Database\\SQLite3\\Forge\:\:\$db is not the same as PHPDoc type CodeIgniter\\Database\\BaseConnection of overridden property CodeIgniter\\Database\\Forge\:\:\$db\.$#' - count: 1 - path: ../../system/Database/SQLite3/Forge.php - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\SQLite3\\Utils\:\:\$optimizeTable is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\BaseUtils\:\:\$optimizeTable\.$#' count: 1 From 22a237cd990fa0b4b03676f835bf18708afb9ae9 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Fri, 14 Aug 2026 01:47:25 +0800 Subject: [PATCH 053/163] refactor: fix `nullCoalesce.property` errors (#10450) --- system/Files/File.php | 4 +- system/HTTP/Files/UploadedFile.php | 2 +- system/HTTP/Message.php | 2 +- system/HTTP/URI.php | 2 +- system/Test/FilterTestTrait.php | 8 ++-- system/Throttle/Throttler.php | 2 +- utils/phpstan-baseline/loader.neon | 3 +- .../nullCoalesce.property.neon | 38 ------------------- 8 files changed, 11 insertions(+), 50 deletions(-) delete mode 100644 utils/phpstan-baseline/nullCoalesce.property.neon diff --git a/system/Files/File.php b/system/Files/File.php index 181a9976362e..38eae2ebb6de 100644 --- a/system/Files/File.php +++ b/system/Files/File.php @@ -30,7 +30,7 @@ class File extends SplFileInfo /** * The files size in bytes * - * @var int + * @var false|int|null */ protected $size; @@ -65,7 +65,7 @@ public function __construct(string $path, bool $checkFile = false) */ public function getSize(): false|int { - return $this->size ?? ($this->size = parent::getSize()); + return $this->size ??= parent::getSize(); } /** diff --git a/system/HTTP/Files/UploadedFile.php b/system/HTTP/Files/UploadedFile.php index be2b74c2025c..cf7fa414701a 100644 --- a/system/HTTP/Files/UploadedFile.php +++ b/system/HTTP/Files/UploadedFile.php @@ -66,7 +66,7 @@ class UploadedFile extends File implements UploadedFileInterface * The error constant of the upload * (one of PHP's UPLOADERRXXX constants) * - * @var int + * @var int|null */ protected $error; diff --git a/system/HTTP/Message.php b/system/HTTP/Message.php index 5a490e09f780..4a3c70e123cc 100644 --- a/system/HTTP/Message.php +++ b/system/HTTP/Message.php @@ -27,7 +27,7 @@ class Message implements MessageInterface /** * Protocol version * - * @var string + * @var string|null */ protected $protocolVersion; diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index bc848ba5c73e..4f97e1fa75a4 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -541,7 +541,7 @@ public function getQuery(array $options = []): string */ public function getFragment(): string { - return $this->fragment ?? ''; + return $this->fragment; } /** diff --git a/system/Test/FilterTestTrait.php b/system/Test/FilterTestTrait.php index 59ffff646a79..ac97db432133 100644 --- a/system/Test/FilterTestTrait.php +++ b/system/Test/FilterTestTrait.php @@ -44,14 +44,14 @@ trait FilterTestTrait /** * The active IncomingRequest or CLIRequest * - * @var RequestInterface + * @var RequestInterface|null */ protected $request; /** * The active Response instance * - * @var ResponseInterface + * @var ResponseInterface|null */ protected $response; @@ -128,6 +128,8 @@ protected function getFilterCaller($filter, string $position): Closure throw new InvalidArgumentException('Invalid filter position passed: ' . $position); } + $filterInstances = []; + if ($filter instanceof FilterInterface) { $filterInstances = [$filter]; } @@ -145,8 +147,6 @@ protected function getFilterCaller($filter, string $position): Closure $filterClasses = [$filter]; } - $filterInstances = []; - foreach ($filterClasses as $class) { // Get an instance $filter = new $class(); diff --git a/system/Throttle/Throttler.php b/system/Throttle/Throttler.php index fba7665de4a8..ce9d673b6f6a 100644 --- a/system/Throttle/Throttler.php +++ b/system/Throttle/Throttler.php @@ -55,7 +55,7 @@ class Throttler implements ThrottlerInterface /** * Timestamp to use (during testing) * - * @var int + * @var int|null */ protected $testTime; diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index c74a825b1bcf..93241798b548 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 1454 errors +# total 1446 errors includes: - argument.type.neon @@ -12,7 +12,6 @@ includes: - method.notFound.neon - missingType.iterableValue.neon - missingType.parameter.neon - - nullCoalesce.property.neon - property.defaultValue.neon - property.nonObject.neon - property.notFound.neon diff --git a/utils/phpstan-baseline/nullCoalesce.property.neon b/utils/phpstan-baseline/nullCoalesce.property.neon deleted file mode 100644 index a5889293f5fa..000000000000 --- a/utils/phpstan-baseline/nullCoalesce.property.neon +++ /dev/null @@ -1,38 +0,0 @@ -# total 8 errors - -parameters: - ignoreErrors: - - - message: '#^Property CodeIgniter\\Files\\File\:\:\$size \(int\) on left side of \?\? is not nullable\.$#' - count: 1 - path: ../../system/Files/File.php - - - - message: '#^Property CodeIgniter\\HTTP\\Files\\UploadedFile\:\:\$error \(int\) on left side of \?\? is not nullable\.$#' - count: 2 - path: ../../system/HTTP/Files/UploadedFile.php - - - - message: '#^Property CodeIgniter\\HTTP\\Message\:\:\$protocolVersion \(string\) on left side of \?\? is not nullable\.$#' - count: 1 - path: ../../system/HTTP/Message.php - - - - message: '#^Property CodeIgniter\\HTTP\\URI\:\:\$fragment \(string\) on left side of \?\? is not nullable\.$#' - count: 1 - path: ../../system/HTTP/URI.php - - - - message: '#^Property CodeIgniter\\Throttle\\Throttler\:\:\$testTime \(int\) on left side of \?\? is not nullable\.$#' - count: 1 - path: ../../system/Throttle/Throttler.php - - - - message: '#^Property CodeIgniter\\Test\\FilterTestTraitTest\:\:\$request \(CodeIgniter\\HTTP\\RequestInterface\) on left side of \?\?\= is not nullable\.$#' - count: 1 - path: ../../tests/system/Test/FilterTestTraitTest.php - - - - message: '#^Property CodeIgniter\\Test\\FilterTestTraitTest\:\:\$response \(CodeIgniter\\HTTP\\ResponseInterface\) on left side of \?\?\= is not nullable\.$#' - count: 1 - path: ../../tests/system/Test/FilterTestTraitTest.php From 9e990b54b40cb8cf7f2dc800559b72166d532cfd Mon Sep 17 00:00:00 2001 From: Micah Tongen <90456737+soccerlover29@users.noreply.github.com> Date: Fri, 14 Aug 2026 02:45:31 -0400 Subject: [PATCH 054/163] refactor: add precise return type to `CodeIgniter::getPerformanceStats()` (#10458) --- system/CodeIgniter.php | 2 ++ utils/phpstan-baseline/missingType.iterableValue.neon | 7 +------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index a291809938e5..6fef8cbb6362 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -775,6 +775,8 @@ public function cachePage(Cache $config) /** * Returns an array with our basic performance stats collected. + * + * @return array{startTime: float|null, totalTime: float} */ public function getPerformanceStats(): array { diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index d9527fa8fee7..dd6c35e453d0 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,12 +1,7 @@ -# total 1132 errors +# total 1131 errors parameters: ignoreErrors: - - - message: '#^Method CodeIgniter\\CodeIgniter\:\:getPerformanceStats\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/CodeIgniter.php - - message: '#^Method CodeIgniter\\Commands\\ListCommands\:\:listFull\(\) has parameter \$commands with no value type specified in iterable type array\.$#' count: 1 From e57247fe1882e7671008f7cd642fcd5270cc9e1b Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Fri, 14 Aug 2026 14:45:59 +0800 Subject: [PATCH 055/163] refactor: fix `property.nonObject` errors (#10451) --- system/Database/BaseResult.php | 8 ---- system/Database/ResultInterface.php | 24 ++++++++-- system/Test/Mock/MockConnection.php | 2 +- .../Database/Live/FabricatorLiveTest.php | 2 + tests/system/Database/Live/GetTest.php | 5 +- tests/system/Test/FabricatorTest.php | 2 + utils/phpstan-baseline/loader.neon | 3 +- .../phpstan-baseline/property.nonObject.neon | 48 ------------------- 8 files changed, 29 insertions(+), 65 deletions(-) delete mode 100644 utils/phpstan-baseline/property.nonObject.neon diff --git a/system/Database/BaseResult.php b/system/Database/BaseResult.php index 532549262283..1b8fcdbd0cf8 100644 --- a/system/Database/BaseResult.php +++ b/system/Database/BaseResult.php @@ -387,8 +387,6 @@ public function setRow($key, $value = null) /** * Returns the "first" row of the current results. - * - * @return array|object|null */ public function getFirstRow(string $type = 'object') { @@ -399,8 +397,6 @@ public function getFirstRow(string $type = 'object') /** * Returns the "last" row of the current results. - * - * @return array|object|null */ public function getLastRow(string $type = 'object') { @@ -411,8 +407,6 @@ public function getLastRow(string $type = 'object') /** * Returns the "next" row of the current results. - * - * @return array|object|null */ public function getNextRow(string $type = 'object') { @@ -426,8 +420,6 @@ public function getNextRow(string $type = 'object') /** * Returns the "previous" row of the current results. - * - * @return array|object|null */ public function getPreviousRow(string $type = 'object') { diff --git a/system/Database/ResultInterface.php b/system/Database/ResultInterface.php index 89febfef835c..0157a1df8149 100644 --- a/system/Database/ResultInterface.php +++ b/system/Database/ResultInterface.php @@ -113,28 +113,44 @@ public function setRow($key, $value = null); /** * Returns the "first" row of the current results. * - * @return array|object|null + * @template T of object + * + * @param 'array'|'object'|class-string $type The type of result object. 'array', 'object' or class name. + * + * @return ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null)) */ public function getFirstRow(string $type = 'object'); /** * Returns the "last" row of the current results. * - * @return array|object|null + * @template T of object + * + * @param 'array'|'object'|class-string $type The type of result object. 'array', 'object' or class name. + * + * @return ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null)) */ public function getLastRow(string $type = 'object'); /** * Returns the "next" row of the current results. * - * @return array|object|null + * @template T of object + * + * @param 'array'|'object'|class-string $type The type of result object. 'array', 'object' or class name. + * + * @return ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null)) */ public function getNextRow(string $type = 'object'); /** * Returns the "previous" row of the current results. * - * @return array|object|null + * @template T of object + * + * @param 'array'|'object'|class-string $type The type of result object. 'array', 'object' or class name. + * + * @return ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null)) */ public function getPreviousRow(string $type = 'object'); diff --git a/system/Test/Mock/MockConnection.php b/system/Test/Mock/MockConnection.php index 0db8aec916d5..e19e635568ea 100644 --- a/system/Test/Mock/MockConnection.php +++ b/system/Test/Mock/MockConnection.php @@ -196,7 +196,7 @@ public function error(): array public function insertID(): int { - return $this->connID->insert_id; + return 0; } /** diff --git a/tests/system/Database/Live/FabricatorLiveTest.php b/tests/system/Database/Live/FabricatorLiveTest.php index a865d2fef4c8..2a45b3ce19cc 100644 --- a/tests/system/Database/Live/FabricatorLiveTest.php +++ b/tests/system/Database/Live/FabricatorLiveTest.php @@ -18,6 +18,7 @@ use CodeIgniter\Test\DatabaseTestTrait; use CodeIgniter\Test\Fabricator; use PHPUnit\Framework\Attributes\Group; +use stdClass; use Tests\Support\Models\UserModel; use Tests\Support\Models\ValidModel; @@ -39,6 +40,7 @@ public function testCreateAddsToDatabase(): void $fabricator->setOverrides(['country' => 'Spain']); $result = $fabricator->create(); + $this->assertInstanceOf(stdClass::class, $result); $this->seeInDatabase('user', ['name' => $result->name]); } diff --git a/tests/system/Database/Live/GetTest.php b/tests/system/Database/Live/GetTest.php index 57a124d4fd0b..91035fe6b1d5 100644 --- a/tests/system/Database/Live/GetTest.php +++ b/tests/system/Database/Live/GetTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Database\Live; +use CodeIgniter\Database\BaseResult; use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\DatabaseTestTrait; @@ -227,13 +228,12 @@ public function testGetAnotherDataSeek(): void public function testFreeResult(): void { $data = $this->db->table('job')->where('id', 4)->get(); + $this->assertInstanceOf(BaseResult::class, $data); $details = $data->getResult(); - $this->assertSame('Musician', $details[0]->name); $data->freeResult(); - $this->assertFalse($data->resultID); } @@ -292,6 +292,7 @@ public function testGetNextRow(): void public function testGetPreviousRow(): void { $user = $this->db->table('user')->get(); + $this->assertInstanceOf(BaseResult::class, $user); $user->currentRow = 3; diff --git a/tests/system/Test/FabricatorTest.php b/tests/system/Test/FabricatorTest.php index 1c3609f29b14..0af1329aad8a 100644 --- a/tests/system/Test/FabricatorTest.php +++ b/tests/system/Test/FabricatorTest.php @@ -16,6 +16,7 @@ use CodeIgniter\Config\Factories; use CodeIgniter\Model; use PHPUnit\Framework\Attributes\Group; +use stdClass; use Tests\Support\Models\EntityModel; use Tests\Support\Models\EventModel; use Tests\Support\Models\FabricatorModel; @@ -410,6 +411,7 @@ public function testCreateMockSetsDatabaseFields(): void $fabricator = new Fabricator(FabricatorModel::class); $result = $fabricator->create(null, true); + $this->assertInstanceOf(stdClass::class, $result); $this->assertIsInt($result->id); $this->assertIsInt($result->created_at); diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 93241798b548..58a8f091d28e 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 1446 errors +# total 1434 errors includes: - argument.type.neon @@ -13,7 +13,6 @@ includes: - missingType.iterableValue.neon - missingType.parameter.neon - property.defaultValue.neon - - property.nonObject.neon - property.notFound.neon - property.phpDocType.neon - staticMethod.notFound.neon diff --git a/utils/phpstan-baseline/property.nonObject.neon b/utils/phpstan-baseline/property.nonObject.neon deleted file mode 100644 index 7268e376d119..000000000000 --- a/utils/phpstan-baseline/property.nonObject.neon +++ /dev/null @@ -1,48 +0,0 @@ -# total 12 errors - -parameters: - ignoreErrors: - - - message: '#^Cannot access property \$insert_id on object\|resource\|false\.$#' - count: 1 - path: ../../system/Test/Mock/MockConnection.php - - - - message: '#^Cannot access property \$name on array\|object\.$#' - count: 1 - path: ../../tests/system/Database/Live/FabricatorLiveTest.php - - - - message: '#^Cannot access property \$currentRow on CodeIgniter\\Database\\ResultInterface\|false\.$#' - count: 1 - path: ../../tests/system/Database/Live/GetTest.php - - - - message: '#^Cannot access property \$name on array\|object\.$#' - count: 4 - path: ../../tests/system/Database/Live/GetTest.php - - - - message: '#^Cannot access property \$resultID on CodeIgniter\\Database\\ResultInterface\|false\.$#' - count: 1 - path: ../../tests/system/Database/Live/GetTest.php - - - - message: '#^Cannot access property \$created_at on array\|object\.$#' - count: 1 - path: ../../tests/system/Test/FabricatorTest.php - - - - message: '#^Cannot access property \$deleted_at on array\|object\.$#' - count: 1 - path: ../../tests/system/Test/FabricatorTest.php - - - - message: '#^Cannot access property \$id on array\|object\.$#' - count: 1 - path: ../../tests/system/Test/FabricatorTest.php - - - - message: '#^Cannot access property \$updated_at on array\|object\.$#' - count: 1 - path: ../../tests/system/Test/FabricatorTest.php From ef04fcf55c4d714b5fe10366965fd9a979147ed4 Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Fri, 14 Aug 2026 08:48:35 +0200 Subject: [PATCH 056/163] refactor: sidestep PhpStorm warning on `@method` docblock in `Model` (#10444) --- system/Model.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/system/Model.php b/system/Model.php index ccade9cfb700..c73cdba448c7 100644 --- a/system/Model.php +++ b/system/Model.php @@ -40,6 +40,11 @@ * * @property-read BaseConnection $db * + * @phpstan-type WhenCallback callable(BaseBuilder, mixed): mixed + * @phpstan-type WhenDefaultCallback callable(BaseBuilder): mixed + * + * @phpstan-import-type row_array from BaseModel + * * @method $this groupBy($by, ?bool $escape = null) * @method $this groupEnd() * @method $this groupStart() @@ -78,15 +83,11 @@ * @method $this selectMax(string $select = '', string $alias = '') * @method $this selectMin(string $select = '', string $alias = '') * @method $this selectSum(string $select = '', string $alias = '') - * @method $this when($condition, callable $callback, ?callable $defaultCallback = null) - * @method $this whenNot($condition, callable $callback, ?callable $defaultCallback = null) + * @method $this when($condition, WhenCallback $callback, ?WhenDefaultCallback $defaultCallback = null) + * @method $this whenNot($condition, WhenCallback $callback, ?WhenDefaultCallback $defaultCallback = null) * @method $this where($key, $value = null, ?bool $escape = null) * @method $this whereIn(?string $key = null, $values = null, ?bool $escape = null) * @method $this whereNotIn(?string $key = null, $values = null, ?bool $escape = null) - * - * @phpstan-method $this when($condition, callable(BaseBuilder, mixed): mixed $callback, (callable(BaseBuilder): mixed)|null $defaultCallback = null) - * @phpstan-method $this whenNot($condition, callable(BaseBuilder, mixed): mixed $callback, (callable(BaseBuilder): mixed)|null $defaultCallback = null) - * @phpstan-import-type row_array from BaseModel */ class Model extends BaseModel { From e3a8f37f17107b29178377baa45ce2243cb02d53 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Fri, 14 Aug 2026 15:15:02 +0800 Subject: [PATCH 057/163] refactor: fix phpstan errors in `IncomingRequestTest` (#10452) --- tests/system/HTTP/IncomingRequestTest.php | 109 +++++++++--------- .../phpstan-baseline/assign.propertyType.neon | 12 +- utils/phpstan-baseline/loader.neon | 2 +- utils/phpstan-baseline/method.notFound.neon | 87 +------------- .../missingType.iterableValue.neon | 12 +- .../missingType.parameter.neon | 7 +- 6 files changed, 57 insertions(+), 172 deletions(-) diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index fe478105d43b..90e6c7d57e90 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -29,7 +29,6 @@ use PHPUnit\Framework\Attributes\PreserveGlobalState; use PHPUnit\Framework\Attributes\RunInSeparateProcess; use PHPUnit\Framework\Attributes\WithoutErrorHandler; -use TypeError; /** * @internal @@ -38,7 +37,7 @@ #[Group('SeparateProcess')] final class IncomingRequestTest extends CIUnitTestCase { - private Request $request; + private IncomingRequest $request; #[WithoutErrorHandler] protected function setUp(): void @@ -59,7 +58,7 @@ protected function setUp(): void $this->request = $this->createRequest($config); } - private function createRequest(?App $config = null, $body = null, ?string $path = null): IncomingRequest + private function createRequest(?App $config = null, false|string|null $body = null, ?string $path = null): IncomingRequest { $config ??= new App(); $path ??= ''; @@ -635,6 +634,9 @@ public function testCanGrabGetRawInputVar($rawstring, $var, $expected, $filter, $this->assertSame($expected, $request->getRawInputVar($var, $filter, $flag)); } + /** + * @return iterable + */ public static function provideCanGrabGetRawInputVar(): iterable { return [ @@ -729,6 +731,9 @@ public function testIsHTTPMethodLowerCase(string $value): void $this->assertTrue($request->is(strtolower($value))); } + /** + * @return iterable + */ public static function provideIsHTTPMethods(): iterable { yield from [ @@ -756,7 +761,6 @@ public function testIsInvalidValue(): void $this->expectExceptionMessage('Unknown type: invalid'); $request = $this->request->withMethod('GET'); - $request->is('invalid'); } @@ -764,6 +768,7 @@ public function testIsJson(): void { $request = $this->request->setHeader('Content-Type', 'application/json'); + $this->assertInstanceOf(IncomingRequest::class, $request); $this->assertTrue($request->is('json')); } @@ -771,6 +776,7 @@ public function testIsWithAjax(): void { $request = $this->request->setHeader('X-Requested-With', 'XMLHttpRequest'); + $this->assertInstanceOf(IncomingRequest::class, $request); $this->assertTrue($request->is('ajax')); } @@ -950,6 +956,7 @@ public function testGetFile(): void ]); $gotit = $this->request->getFile('userfile'); + $this->assertInstanceOf(UploadedFile::class, $gotit); $this->assertSame(124, $gotit->getSize()); } @@ -1047,12 +1054,12 @@ public function testGetIPAddressNormal(): void $expected = '123.123.123.123'; service('superglobals')->setServer('REMOTE_ADDR', $expected); - $this->request = new Request(new App()); - $this->request->populateHeaders(); + $request = new Request(new App()); + $request->populateHeaders(); - $this->assertSame($expected, $this->request->getIPAddress()); + $this->assertSame($expected, $request->getIPAddress()); // call a second time to exercise the initial conditional block in getIPAddress() - $this->assertSame($expected, $this->request->getIPAddress()); + $this->assertSame($expected, $request->getIPAddress()); } public function testGetIPAddressThruProxy(): void @@ -1068,11 +1075,11 @@ public function testGetIPAddressThruProxy(): void '192.168.5.0/24' => 'X-Forwarded-For', ]; Factories::injectMock('config', App::class, $config); - $this->request = new Request(); - $this->request->populateHeaders(); + $request = new Request(); + $request->populateHeaders(); // we should see the original forwarded address - $this->assertSame($expected, $this->request->getIPAddress()); + $this->assertSame($expected, $request->getIPAddress()); } public function testGetIPAddressThruProxyIPv6(): void @@ -1087,11 +1094,11 @@ public function testGetIPAddressThruProxyIPv6(): void '2001:db8::2:1' => 'X-Forwarded-For', ]; Factories::injectMock('config', App::class, $config); - $this->request = new Request(); - $this->request->populateHeaders(); + $request = new Request(); + $request->populateHeaders(); // we should see the original forwarded address - $this->assertSame($expected, $this->request->getIPAddress()); + $this->assertSame($expected, $request->getIPAddress()); } public function testGetIPAddressThruProxyInvalidIPAddress(): void @@ -1105,11 +1112,11 @@ public function testGetIPAddressThruProxyInvalidIPAddress(): void '10.0.1.200' => 'X-Forwarded-For', '192.168.5.0/24' => 'X-Forwarded-For', ]; - $this->request = new Request($config); - $this->request->populateHeaders(); + $request = new Request($config); + $request->populateHeaders(); // spoofed address invalid - $this->assertSame($expected, $this->request->getIPAddress()); + $this->assertSame($expected, $request->getIPAddress()); } public function testGetIPAddressThruProxyInvalidIPAddressIPv6(): void @@ -1122,11 +1129,11 @@ public function testGetIPAddressThruProxyInvalidIPAddressIPv6(): void $config->proxyIPs = [ '2001:db8::2:1' => 'X-Forwarded-For', ]; - $this->request = new Request($config); - $this->request->populateHeaders(); + $request = new Request($config); + $request->populateHeaders(); // spoofed address invalid - $this->assertSame($expected, $this->request->getIPAddress()); + $this->assertSame($expected, $request->getIPAddress()); } public function testGetIPAddressThruProxyNotWhitelisted(): void @@ -1140,11 +1147,11 @@ public function testGetIPAddressThruProxyNotWhitelisted(): void '10.0.1.200' => 'X-Forwarded-For', '192.168.5.0/24' => 'X-Forwarded-For', ]; - $this->request = new Request($config); - $this->request->populateHeaders(); + $request = new Request($config); + $request->populateHeaders(); // spoofed address invalid - $this->assertSame($expected, $this->request->getIPAddress()); + $this->assertSame($expected, $request->getIPAddress()); } public function testGetIPAddressThruProxyNotWhitelistedIPv6(): void @@ -1157,11 +1164,11 @@ public function testGetIPAddressThruProxyNotWhitelistedIPv6(): void $config->proxyIPs = [ '2001:db8::2:1' => 'X-Forwarded-For', ]; - $this->request = new Request($config); - $this->request->populateHeaders(); + $request = new Request($config); + $request->populateHeaders(); // spoofed address invalid - $this->assertSame($expected, $this->request->getIPAddress()); + $this->assertSame($expected, $request->getIPAddress()); } public function testGetIPAddressThruProxySubnet(): void @@ -1173,11 +1180,11 @@ public function testGetIPAddressThruProxySubnet(): void $config = new App(); $config->proxyIPs = ['192.168.5.0/24' => 'X-Forwarded-For']; Factories::injectMock('config', App::class, $config); - $this->request = new Request(); - $this->request->populateHeaders(); + $request = new Request(); + $request->populateHeaders(); // we should see the original forwarded address - $this->assertSame($expected, $this->request->getIPAddress()); + $this->assertSame($expected, $request->getIPAddress()); } public function testGetIPAddressThruProxySubnetIPv6(): void @@ -1189,11 +1196,11 @@ public function testGetIPAddressThruProxySubnetIPv6(): void $config = new App(); $config->proxyIPs = ['2001:db8:1234::/48' => 'X-Forwarded-For']; Factories::injectMock('config', App::class, $config); - $this->request = new Request(); - $this->request->populateHeaders(); + $request = new Request(); + $request->populateHeaders(); // we should see the original forwarded address - $this->assertSame($expected, $this->request->getIPAddress()); + $this->assertSame($expected, $request->getIPAddress()); } public function testGetIPAddressThruProxyOutOfSubnet(): void @@ -1204,11 +1211,11 @@ public function testGetIPAddressThruProxyOutOfSubnet(): void $config = new App(); $config->proxyIPs = ['192.168.5.0/28' => 'X-Forwarded-For']; - $this->request = new Request($config); - $this->request->populateHeaders(); + $request = new Request($config); + $request->populateHeaders(); // we should see the original forwarded address - $this->assertSame($expected, $this->request->getIPAddress()); + $this->assertSame($expected, $request->getIPAddress()); } public function testGetIPAddressThruProxyOutOfSubnetIPv6(): void @@ -1219,11 +1226,11 @@ public function testGetIPAddressThruProxyOutOfSubnetIPv6(): void $config = new App(); $config->proxyIPs = ['2001:db8:1234::/48' => 'X-Forwarded-For']; - $this->request = new Request($config); - $this->request->populateHeaders(); + $request = new Request($config); + $request->populateHeaders(); // we should see the original forwarded address - $this->assertSame($expected, $this->request->getIPAddress()); + $this->assertSame($expected, $request->getIPAddress()); } public function testGetIPAddressThruProxyBothIPv4AndIPv6(): void @@ -1237,23 +1244,11 @@ public function testGetIPAddressThruProxyBothIPv4AndIPv6(): void '192.168.5.0/28' => 'X-Forwarded-For', '2001:db8:1234::/48' => 'X-Forwarded-For', ]; - $this->request = new Request($config); - $this->request->populateHeaders(); + $request = new Request($config); + $request->populateHeaders(); // we should see the original forwarded address - $this->assertSame($expected, $this->request->getIPAddress()); - } - - public function testGetIPAddressThruProxyInvalidConfigString(): void - { - $this->expectException(TypeError::class); - - $config = new App(); - $config->proxyIPs = '192.168.5.0/28'; - $this->request = new Request($config); - $this->request->populateHeaders(); - - $this->request->getIPAddress(); + $this->assertSame($expected, $request->getIPAddress()); } public function testGetIPAddressThruProxyInvalidConfigArray(): void @@ -1264,12 +1259,12 @@ public function testGetIPAddressThruProxyInvalidConfigArray(): void ); $config = new App(); - $config->proxyIPs = ['192.168.5.0/28']; + $config->proxyIPs = ['192.168.5.0/28']; // @phpstan-ignore assign.propertyType (deliberately keyless, to assert the ConfigException) Factories::injectMock('config', App::class, $config); - $this->request = new Request(); - $this->request->populateHeaders(); + $request = new Request(); + $request->populateHeaders(); - $this->request->getIPAddress(); + $request->getIPAddress(); } // @TODO getIPAddress should have more testing, to 100% code coverage diff --git a/utils/phpstan-baseline/assign.propertyType.neon b/utils/phpstan-baseline/assign.propertyType.neon index e0569c49cd73..e782ea998087 100644 --- a/utils/phpstan-baseline/assign.propertyType.neon +++ b/utils/phpstan-baseline/assign.propertyType.neon @@ -1,4 +1,4 @@ -# total 28 errors +# total 26 errors parameters: ignoreErrors: @@ -57,16 +57,6 @@ parameters: count: 4 path: ../../tests/system/Filters/HoneypotTest.php - - - message: '#^Property Config\\App\:\:\$proxyIPs \(array\\) does not accept array\\.$#' - count: 1 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - - - message: '#^Property Config\\App\:\:\$proxyIPs \(array\\) does not accept string\.$#' - count: 1 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - message: '#^Property CodeIgniter\\Helpers\\CookieHelperTest\:\:\$response \(CodeIgniter\\HTTP\\Response\) does not accept CodeIgniter\\HTTP\\ResponseInterface\.$#' count: 1 diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 58a8f091d28e..57a1c5ee9257 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 1434 errors +# total 1381 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/method.notFound.neon b/utils/phpstan-baseline/method.notFound.neon index 8ced93d9ad27..f0497ca809ae 100644 --- a/utils/phpstan-baseline/method.notFound.neon +++ b/utils/phpstan-baseline/method.notFound.neon @@ -1,4 +1,4 @@ -# total 80 errors +# total 33 errors parameters: ignoreErrors: @@ -42,91 +42,6 @@ parameters: count: 3 path: ../../tests/system/Debug/ExceptionHandlerTest.php - - - message: '#^Call to an undefined method CodeIgniter\\HTTP\\Request\:\:getCookie\(\)\.$#' - count: 2 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\HTTP\\Request\:\:getDefaultLocale\(\)\.$#' - count: 1 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\HTTP\\Request\:\:getFileMultiple\(\)\.$#' - count: 1 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\HTTP\\Request\:\:getFile\(\)\.$#' - count: 1 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\HTTP\\Request\:\:getFiles\(\)\.$#' - count: 1 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\HTTP\\Request\:\:getGetPost\(\)\.$#' - count: 6 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\HTTP\\Request\:\:getGet\(\)\.$#' - count: 2 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\HTTP\\Request\:\:getLocale\(\)\.$#' - count: 1 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\HTTP\\Request\:\:getOldInput\(\)\.$#' - count: 9 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\HTTP\\Request\:\:getPostGet\(\)\.$#' - count: 6 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\HTTP\\Request\:\:getPost\(\)\.$#' - count: 2 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\HTTP\\Request\:\:getVar\(\)\.$#' - count: 2 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\HTTP\\Request\:\:isAJAX\(\)\.$#' - count: 1 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\HTTP\\Request\:\:isCLI\(\)\.$#' - count: 1 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\HTTP\\Request\:\:isSecure\(\)\.$#' - count: 1 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\HTTP\\Request\:\:is\(\)\.$#' - count: 5 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\HTTP\\Request\:\:negotiate\(\)\.$#' - count: 5 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - message: '#^Call to an undefined method CodeIgniter\\HTTP\\URI\:\:getRoutePath\(\)\.$#' count: 1 diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index dd6c35e453d0..518e931c13d8 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 1131 errors +# total 1129 errors parameters: ignoreErrors: @@ -4467,16 +4467,6 @@ parameters: count: 1 path: ../../tests/system/Filters/InvalidCharsTest.php - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequestTest\:\:provideCanGrabGetRawInputVar\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequestTest\:\:provideIsHTTPMethods\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - message: '#^Method CodeIgniter\\HTTP\\MessageTest\:\:provideArrayHeaderValue\(\) return type has no value type specified in iterable type iterable\.$#' count: 1 diff --git a/utils/phpstan-baseline/missingType.parameter.neon b/utils/phpstan-baseline/missingType.parameter.neon index 3512e2769b9f..0e1decdda219 100644 --- a/utils/phpstan-baseline/missingType.parameter.neon +++ b/utils/phpstan-baseline/missingType.parameter.neon @@ -1,4 +1,4 @@ -# total 31 errors +# total 30 errors parameters: ignoreErrors: @@ -72,11 +72,6 @@ parameters: count: 1 path: ../../tests/system/HTTP/Files/FileMovingTest.php - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequestTest\:\:createRequest\(\) has parameter \$body with no type specified\.$#' - count: 1 - path: ../../tests/system/HTTP/IncomingRequestTest.php - - message: '#^Method CodeIgniter\\Helpers\\URLHelper\\CurrentUrlTest\:\:createRequest\(\) has parameter \$body with no type specified\.$#' count: 1 From d608eafda01ca3ded3ee6c6d788af1a239127b21 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Mon, 17 Aug 2026 02:54:34 +0800 Subject: [PATCH 058/163] refactor: fix phpstan errors in `Helpers` (#10459) --- system/Helpers/Array/ArrayHelper.php | 29 +- system/Helpers/array_helper.php | 25 +- system/Helpers/filesystem_helper.php | 7 +- system/Helpers/form_helper.php | 82 +-- system/Helpers/html_helper.php | 53 +- system/Helpers/kint_helper.php | 6 +- system/Helpers/number_helper.php | 2 + system/Helpers/test_helper.php | 8 +- system/Helpers/text_helper.php | 10 +- system/Helpers/url_helper.php | 46 +- utils/phpstan-baseline/argument.type.neon | 7 +- utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 472 +----------------- 13 files changed, 155 insertions(+), 594 deletions(-) diff --git a/system/Helpers/Array/ArrayHelper.php b/system/Helpers/Array/ArrayHelper.php index 1e7d6904796c..f70f6905ca5d 100644 --- a/system/Helpers/Array/ArrayHelper.php +++ b/system/Helpers/Array/ArrayHelper.php @@ -33,7 +33,8 @@ final class ArrayHelper * * @used-by dot_array_search() * - * @param string $index The index as dot array syntax. + * @param string $index The index as dot array syntax. + * @param array $array * * @return mixed */ @@ -68,6 +69,9 @@ private static function convertToArray(string $index): array * * @used-by dotSearch() * + * @param list $indexes + * @param array $array + * * @return mixed */ private static function arraySearchDot(array $indexes, array $array) @@ -125,6 +129,8 @@ private static function arraySearchDot(array $indexes, array $array) * array_key_exists() with dot array syntax. * * If wildcard `*` is used, all items for the key after it must have the key. + * + * @param array $array */ public static function dotKeyExists(string $index, array $array): bool { @@ -179,11 +185,11 @@ public static function dotKeyExists(string $index, array $array): bool * * @used-by array_group_by() * - * @param array $array Data array (i.e. from query result) - * @param array $indexes Indexes to group by. Dot syntax used. Returns $array if empty - * @param bool $includeEmpty If true, null and '' are also added as valid keys to group + * @param array $array Data array (i.e. from query result) + * @param list $indexes Indexes to group by. Dot syntax used. Returns $array if empty + * @param bool $includeEmpty If true, null and '' are also added as valid keys to group * - * @return array Result array where rows are grouped together by indexes values. + * @return array Result array where rows are grouped together by indexes values. */ public static function groupBy(array $array, array $indexes, bool $includeEmpty = false): array { @@ -205,6 +211,12 @@ public static function groupBy(array $array, array $indexes, bool $includeEmpty * `dot_array_search()`. * * @used-by groupBy() + * + * @param array $result + * @param array $row + * @param list $indexes + * + * @return array */ private static function arrayAttachIndexedValue( array $result, @@ -244,6 +256,11 @@ private static function arrayAttachIndexedValue( /** * Compare recursively two associative arrays and return difference as new array. * Returns keys that exist in `$original` but not in `$compareWith`. + * + * @param array $original + * @param array $compareWith + * + * @return array */ public static function recursiveDiff(array $original, array $compareWith): array { @@ -284,6 +301,8 @@ public static function recursiveDiff(array $original, array $compareWith): array /** * Recursively count all keys. + * + * @param array $array */ public static function recursiveCount(array $array, int $counter = 0): int { diff --git a/system/Helpers/array_helper.php b/system/Helpers/array_helper.php index 4d5d12488d78..f0ea0637c545 100644 --- a/system/Helpers/array_helper.php +++ b/system/Helpers/array_helper.php @@ -20,6 +20,8 @@ * Searches an array through dot syntax. Supports * wildcard searches, like foo.*.bar * + * @param array $array + * * @return mixed */ function dot_array_search(string $index, array $array) @@ -32,7 +34,8 @@ function dot_array_search(string $index, array $array) /** * Returns the value of an element at a key in an array of uncertain depth. * - * @param int|string $key + * @param int|string $key + * @param array $array * * @return mixed */ @@ -78,9 +81,9 @@ function array_deep_search($key, array $array) * For information on multi-level array sorting, refer to Example #3 here: * https://www.php.net/manual/de/function.array-multisort.php * - * @param array $array the reference of the array to be sorted - * @param array $sortColumns an associative array of columns to sort - * after and their sorting flags + * @param array $array The reference of the array to be sorted + * @param array $sortColumns An associative array of columns to sort + * after and their sorting flags */ function array_sort_by_multiple_keys(array &$array, array $sortColumns): bool { @@ -129,10 +132,10 @@ function array_sort_by_multiple_keys(array &$array, array $sortColumns): bool /** * Flatten a multidimensional array using dots as separators. * - * @param iterable $array The multi-dimensional array - * @param string $id Something to initially prepend to the flattened keys + * @param iterable $array The multi-dimensional array + * @param string $id Something to initially prepend to the flattened keys * - * @return array The flattened array + * @return array The flattened array */ function array_flatten_with_dots(iterable $array, string $id = ''): array { @@ -156,11 +159,11 @@ function array_flatten_with_dots(iterable $array, string $id = ''): array /** * Groups all rows by their index values. Result's depth equals number of indexes * - * @param array $array Data array (i.e. from query result) - * @param array $indexes Indexes to group by. Dot syntax used. Returns $array if empty - * @param bool $includeEmpty If true, null and '' are also added as valid keys to group + * @param array $array Data array (i.e. from query result) + * @param list $indexes Indexes to group by. Dot syntax used. Returns $array if empty + * @param bool $includeEmpty If true, null and '' are also added as valid keys to group * - * @return array Result array where rows are grouped together by indexes values. + * @return array Result array where rows are grouped together by indexes values. */ function array_group_by(array $array, array $indexes, bool $includeEmpty = false): array { diff --git a/system/Helpers/filesystem_helper.php b/system/Helpers/filesystem_helper.php index 3f5ed25f8531..f9a04dd142df 100644 --- a/system/Helpers/filesystem_helper.php +++ b/system/Helpers/filesystem_helper.php @@ -24,9 +24,10 @@ * directory will be mapped as well. * * @param string $sourceDir Path to source - * @param int $directoryDepth Depth of directories to traverse - * (0 = fully recursive, 1 = current dir, etc) + * @param int $directoryDepth Depth of directories to traverse (0 = fully recursive, 1 = current dir, etc) * @param bool $hidden Whether to show hidden files + * + * @return array|string> */ function directory_map(string $sourceDir, int $directoryDepth = 0, bool $hidden = false): array { @@ -200,6 +201,8 @@ function delete_files(string $path, bool $delDir = false, bool $htdocs = false, * @param bool|null $includePath Whether to include the path as part of the filename; false for no path, null for a relative path, true for full path * @param bool $hidden Whether to include hidden files (files beginning with a period) * @param bool $includeDir Whether to include directories + * + * @return list */ function get_filenames( string $sourceDir, diff --git a/system/Helpers/form_helper.php b/system/Helpers/form_helper.php index a1f7defc4d14..fc67aac601bf 100644 --- a/system/Helpers/form_helper.php +++ b/system/Helpers/form_helper.php @@ -23,9 +23,9 @@ * * Creates the opening portion of the form. * - * @param string $action the URI segments of the form destination - * @param array|string $attributes a key/value pair of attributes, or string representation - * @param array $hidden a key/value pair hidden data + * @param string $action The URI segments of the form destination + * @param array|string $attributes A key/value pair of attributes, or string representation + * @param array $hidden A key/value pair hidden data */ function form_open(string $action = '', $attributes = [], array $hidden = []): string { @@ -80,9 +80,9 @@ function form_open(string $action = '', $attributes = [], array $hidden = []): s * * Creates the opening portion of the form, but with "multipart/form-data". * - * @param string $action The URI segments of the form destination - * @param array|string $attributes A key/value pair of attributes, or the same as a string - * @param array $hidden A key/value pair hidden data + * @param string $action The URI segments of the form destination + * @param array|string $attributes A key/value pair of attributes, or the same as a string + * @param array $hidden A key/value pair hidden data */ function form_open_multipart(string $action = '', $attributes = [], array $hidden = []): string { @@ -103,8 +103,8 @@ function form_open_multipart(string $action = '', $attributes = [], array $hidde * Generates hidden fields. You can pass a simple key/value string or * an associative array with multiple values. * - * @param array|string $name Field name or associative array to create multiple fields - * @param array|string $value Field value + * @param array|string $name Field name or associative array to create multiple fields + * @param array|string|null $value Field value */ function form_hidden($name, $value = '', bool $recursing = false): string { @@ -140,8 +140,8 @@ function form_hidden($name, $value = '', bool $recursing = false): string * Text Input Field. If 'type' is passed in the $type field, it will be * used as the input type, for making 'email', 'phone', etc input fields. * - * @param array|string $data - * @param array|object|string $extra string, array, object that can be cast to array + * @param array|string $data + * @param array|object|string $extra String, array, object that can be cast to array */ function form_input($data = '', string $value = '', $extra = '', string $type = 'text'): string { @@ -161,8 +161,8 @@ function form_input($data = '', string $value = '', $extra = '', string $type = * * Identical to the input function but adds the "password" type * - * @param array|string $data - * @param array|object|string $extra string, array, object that can be cast to array + * @param array|string $data + * @param array|object|string $extra String, array, object that can be cast to array */ function form_password($data = '', string $value = '', $extra = ''): string { @@ -181,8 +181,8 @@ function form_password($data = '', string $value = '', $extra = ''): string * * Identical to the input function but adds the "file" type * - * @param array|string $data - * @param array|object|string $extra string, array, object that can be cast to array + * @param array|string $data + * @param array|object|string $extra String, array, object that can be cast to array */ function form_upload($data = '', string $value = '', $extra = ''): string { @@ -205,8 +205,8 @@ function form_upload($data = '', string $value = '', $extra = ''): string /** * Textarea field * - * @param array|string $data - * @param array|object|string $extra string, array, object that can be cast to array + * @param array|string $data + * @param array|object|string $extra String, array, object that can be cast to array */ function form_textarea($data = '', string $value = '', $extra = ''): string { @@ -241,8 +241,10 @@ function form_textarea($data = '', string $value = '', $extra = ''): string /** * Multi-select menu * - * @param array|string $name - * @param array|object|string $extra string, array, object that can be cast to array + * @param array|string $name + * @param array|string> $options + * @param array $selected + * @param array|object|string $extra String, array, object that can be cast to array */ function form_multiselect($name = '', array $options = [], array $selected = [], $extra = ''): string { @@ -260,10 +262,10 @@ function form_multiselect($name = '', array $options = [], array $selected = [], /** * Drop-down Menu * - * @param array|string $data - * @param array|string $options - * @param array|string $selected - * @param array|object|string $extra string, array, object that can be cast to array + * @param array|string $data + * @param array|string>|string $options + * @param array|string $selected + * @param array|object|string $extra String, array, object that can be cast to array */ function form_dropdown($data = '', $options = [], $selected = [], $extra = ''): string { @@ -348,8 +350,8 @@ function form_dropdown($data = '', $options = [], $selected = [], $extra = ''): /** * Checkbox Field * - * @param array|string $data - * @param array|object|string $extra string, array, object that can be cast to array + * @param array|string $data + * @param array|object|string $extra String, array, object that can be cast to array */ function form_checkbox($data = '', string $value = '', bool $checked = false, $extra = ''): string { @@ -381,8 +383,8 @@ function form_checkbox($data = '', string $value = '', bool $checked = false, $e /** * Radio Button * - * @param array|string $data - * @param array|object|string $extra string, array, object that can be cast to array + * @param array|string $data + * @param array|object|string $extra String, array, object that can be cast to array */ function form_radio($data = '', string $value = '', bool $checked = false, $extra = ''): string { @@ -399,8 +401,8 @@ function form_radio($data = '', string $value = '', bool $checked = false, $extr /** * Submit Button * - * @param array|string $data - * @param array|object|string $extra string, array, object that can be cast to array + * @param array|string $data + * @param array|object|string $extra String, array, object that can be cast to array */ function form_submit($data = '', string $value = '', $extra = ''): string { @@ -412,8 +414,8 @@ function form_submit($data = '', string $value = '', $extra = ''): string /** * Reset Button * - * @param array|string $data - * @param array|object|string $extra string, array, object that can be cast to array + * @param array|string $data + * @param array|object|string $extra String, array, object that can be cast to array */ function form_reset($data = '', string $value = '', $extra = ''): string { @@ -425,8 +427,8 @@ function form_reset($data = '', string $value = '', $extra = ''): string /** * Form Button * - * @param array|string $data - * @param array|object|string $extra string, array, object that can be cast to array + * @param array|string $data + * @param array|object|string $extra String, array, object that can be cast to array */ function form_button($data = '', string $content = '', $extra = ''): string { @@ -450,9 +452,9 @@ function form_button($data = '', string $content = '', $extra = ''): string /** * Form Label Tag * - * @param string $labelText The text to appear onscreen - * @param string $id The id the label applies to - * @param array $attributes Additional attributes + * @param string $labelText The text to appear onscreen + * @param string $id The id the label applies to + * @param array $attributes Additional attributes */ function form_label(string $labelText = '', string $id = '', array $attributes = []): string { @@ -477,6 +479,8 @@ function form_label(string $labelText = '', string $id = '', array $attributes = * The element specifies a list of pre-defined options for an element. * Users will see a drop-down list of pre-defined options as they input data. * The list attribute of the element, must refer to the id attribute of the element. + * + * @param list $options */ function form_datalist(string $name, string $value, array $options): string { @@ -506,8 +510,8 @@ function form_datalist(string $name, string $value, array $options): string * Used to produce
text. To close fieldset * use form_fieldset_close() * - * @param string $legendText The legend text - * @param array $attributes Additional attributes + * @param string $legendText The legend text + * @param array $attributes Additional attributes */ function form_fieldset(string $legendText = '', array $attributes = []): string { @@ -770,8 +774,8 @@ function validation_show_error(string $field, string $template = 'single'): stri * * @internal * - * @param array|string $attributes List of attributes - * @param array $default Default values + * @param array|string $attributes List of attributes + * @param array $default Default values */ function parse_form_attributes($attributes, array $default): string { diff --git a/system/Helpers/html_helper.php b/system/Helpers/html_helper.php index 9dcf82536eeb..e206cbd0f5e5 100644 --- a/system/Helpers/html_helper.php +++ b/system/Helpers/html_helper.php @@ -24,8 +24,8 @@ * Generates an HTML unordered list from a single or * multidimensional array. * - * @param array $list List entries - * @param array|object|string $attributes HTML attributes string, array, object + * @param array|string> $list List entries + * @param array|object|string $attributes HTML attributes string, array, object */ function ul(array $list, $attributes = ''): string { @@ -39,8 +39,8 @@ function ul(array $list, $attributes = ''): string * * Generates an HTML ordered list from a single or multidimensional array. * - * @param array $list List entries - * @param array|object|string $attributes HTML attributes string, array, object + * @param array|string> $list List entries + * @param array|object|string $attributes HTML attributes string, array, object */ function ol(array $list, $attributes = ''): string { @@ -54,8 +54,8 @@ function ol(array $list, $attributes = ''): string * * Generates an HTML ordered list from a single or multidimensional array. * - * @param array $list List entries - * @param array|object|string $attributes HTML attributes string, array, object + * @param array|string> $list List entries + * @param array|object|string $attributes HTML attributes string, array, object */ function _list(string $type = 'ul', $list = [], $attributes = '', int $depth = 0): string { @@ -93,9 +93,9 @@ function _list(string $type = 'ul', $list = [], $attributes = '', int $depth = 0 * * Generates an image element * - * @param array|string $src Image source URI, or array of attributes and values - * @param bool $indexPage Should `Config\App::$indexPage` be added to the source path - * @param array|object|string $attributes Additional HTML attributes + * @param array|string $src Image source URI, or array of attributes and values + * @param bool $indexPage Should `Config\App::$indexPage` be added to the source path + * @param array|object|string $attributes Additional HTML attributes */ function img($src = '', bool $indexPage = false, $attributes = ''): string { @@ -193,8 +193,8 @@ function doctype(string $type = 'html5'): string * * Generates link to a JS file * - * @param array|string $src Script source or an array of attributes - * @param bool $indexPage Should `Config\App::$indexPage` be added to the JS path + * @param array|string $src Script source or an array of attributes + * @param bool $indexPage Should `Config\App::$indexPage` be added to the JS path */ function script_tag($src = '', bool $indexPage = false): string { @@ -287,10 +287,11 @@ function link_tag( * Generates a video element to embed videos. The video element can * contain one or more video sources * - * @param array|string $src Either a source string or an array of sources - * @param string $unsupportedMessage The message to display if the media tag is not supported by the browser - * @param string $attributes HTML attributes - * @param bool $indexPage Should `Config\App::$indexPage` be added to the source path + * @param list|string $src Either a source string or an array of sources + * @param string $unsupportedMessage The message to display if the media tag is not supported by the browser + * @param string $attributes HTML attributes + * @param list $tracks Track elements + * @param bool $indexPage Should `Config\App::$indexPage` be added to the source path */ function video($src, string $unsupportedMessage = '', string $attributes = '', array $tracks = [], bool $indexPage = false): string { @@ -334,10 +335,11 @@ function video($src, string $unsupportedMessage = '', string $attributes = '', a * * Generates an audio element to embed sounds * - * @param array|string $src Either a source string or an array of sources - * @param string $unsupportedMessage The message to display if the media tag is not supported by the browser. - * @param string $attributes HTML attributes - * @param bool $indexPage Should `Config\App::$indexPage` be added to the source path + * @param list|string $src Either a source string or an array of sources + * @param string $unsupportedMessage The message to display if the media tag is not supported by the browser. + * @param string $attributes HTML attributes + * @param list $tracks Track elements + * @param bool $indexPage Should `Config\App::$indexPage` be added to the source path */ function audio($src, string $unsupportedMessage = '', string $attributes = '', array $tracks = [], bool $indexPage = false): string { @@ -377,7 +379,9 @@ function audio($src, string $unsupportedMessage = '', string $attributes = '', a /** * Generate media based tag * - * @param string $unsupportedMessage The message to display if the media tag is not supported by the browser. + * @param list $types Source elements + * @param string $unsupportedMessage The message to display if the media tag is not supported by the browser. + * @param list $tracks Track elements */ function _media(string $name, array $types = [], string $unsupportedMessage = '', string $attributes = '', array $tracks = []): string { @@ -466,10 +470,11 @@ function track(string $src, string $kind, string $srcLanguage, string $label): s * as either image or a resource plugin such as audio, video, * Java applets, ActiveX, PDF and Flash * - * @param string $data A resource URL - * @param string $type Content-type of the resource - * @param string $attributes HTML attributes - * @param bool $indexPage Should `Config\App::$indexPage` be added to the data path + * @param string $data A resource URL + * @param string $type Content-type of the resource + * @param string $attributes HTML attributes + * @param list $params Param elements + * @param bool $indexPage Should `Config\App::$indexPage` be added to the data path */ function object(string $data, string $type = 'unknown', string $attributes = '', array $params = [], bool $indexPage = false): string { diff --git a/system/Helpers/kint_helper.php b/system/Helpers/kint_helper.php index 8d8e73bfc92f..02d180328e92 100644 --- a/system/Helpers/kint_helper.php +++ b/system/Helpers/kint_helper.php @@ -18,7 +18,7 @@ /** * Prints a Kint debug report and exits. * - * @param array $vars + * @param mixed ...$vars * * @return never * @@ -38,7 +38,7 @@ function dd(...$vars): void /** * dd function * - * @param array $vars + * @param mixed ...$vars * * @return int */ @@ -54,7 +54,7 @@ function dd(...$vars) /** * d function * - * @param array $vars + * @param mixed ...$vars * * @return int */ diff --git a/system/Helpers/number_helper.php b/system/Helpers/number_helper.php index bf577ed293f6..120e33d795cd 100644 --- a/system/Helpers/number_helper.php +++ b/system/Helpers/number_helper.php @@ -134,6 +134,8 @@ function number_to_currency(float $num, string $currency, ?string $locale = null /** * A general purpose, locale-aware, number_format method. * Used by all of the functions of the number_helper. + * + * @param array $options */ function format_number(float $num, int $precision = 1, ?string $locale = null, array $options = []): string { diff --git a/system/Helpers/test_helper.php b/system/Helpers/test_helper.php index 645d407dfcbf..b455aa301e2f 100644 --- a/system/Helpers/test_helper.php +++ b/system/Helpers/test_helper.php @@ -22,11 +22,11 @@ /** * Creates a single item using Fabricator. * - * @param Model|object|string $model Instance or name of the model - * @param array|null $overrides Overriding data to pass to Fabricator::setOverrides() - * @param bool $persist + * @param Model|object|string $model Instance or name of the model + * @param array|null $overrides Overriding data to pass to Fabricator::setOverrides() + * @param bool $persist * - * @return array|object + * @return array|object */ function fake($model, ?array $overrides = null, $persist = true) { diff --git a/system/Helpers/text_helper.php b/system/Helpers/text_helper.php index b8298953bdd3..f8ea0c15447c 100644 --- a/system/Helpers/text_helper.php +++ b/system/Helpers/text_helper.php @@ -174,9 +174,9 @@ function entities_to_ascii(string $str, bool $all = true): string * matched words will be converted to #### or to the replacement * word you've submitted. * - * @param string $str the text string - * @param array $censored the array of censored words - * @param string $replacement the optional replacement value + * @param string $str The text string + * @param list $censored The array of censored words + * @param string $replacement The optional replacement value */ function word_censor(string $str, array $censored, string $replacement = ''): string { @@ -454,9 +454,9 @@ function ellipsize(string $str, int $maxLength, $position = 1, string $ellipsis * * Removes slashes contained in a string or in an array * - * @param array|string $str string or array + * @param array|string $str string or array * - * @return array|string string or array + * @return array|string string or array */ function strip_slashes($str) { diff --git a/system/Helpers/url_helper.php b/system/Helpers/url_helper.php index 208ae722a2f2..0b3fa5f72c9c 100644 --- a/system/Helpers/url_helper.php +++ b/system/Helpers/url_helper.php @@ -25,11 +25,11 @@ /** * Returns a site URL as defined by the App config. * - * @param array|string $relativePath URI string or array of URI segments. - * @param string|null $scheme URI scheme. E.g., http, ftp. If empty - * string '' is set, a protocol-relative - * link is returned. - * @param App|null $config Alternate configuration to use. + * @param list|string $relativePath URI string or array of URI segments. + * @param string|null $scheme URI scheme. E.g., http, ftp. If empty + * string '' is set, a protocol-relative + * link is returned. + * @param App|null $config Alternate configuration to use. */ function site_url($relativePath = '', ?string $scheme = null, ?App $config = null): string { @@ -46,10 +46,10 @@ function site_url($relativePath = '', ?string $scheme = null, ?App $config = nul * Returns the base URL as defined by the App config. * Base URLs are trimmed site URLs without the index page. * - * @param array|string $relativePath URI string or array of URI segments. - * @param string|null $scheme URI scheme. E.g., http, ftp. If empty - * string '' is set, a protocol-relative - * link is returned. + * @param list|string $relativePath URI string or array of URI segments. + * @param string|null $scheme URI scheme. E.g., http, ftp. If empty + * string '' is set, a protocol-relative + * link is returned. */ function base_url($relativePath = '', ?string $scheme = null): string { @@ -146,10 +146,10 @@ function index_page(?App $altConfig = null): string * * Creates an anchor based on the local URL. * - * @param array|string $uri URI string or array of URI segments - * @param string $title The link title - * @param array|object|string $attributes Any attributes - * @param App|null $altConfig Alternate configuration to use + * @param list|string $uri URI string or array of URI segments + * @param string $title The link title + * @param array|object|string $attributes Any attributes + * @param App|null $altConfig Alternate configuration to use */ function anchor($uri = '', string $title = '', $attributes = '', ?App $altConfig = null): string { @@ -179,10 +179,10 @@ function anchor($uri = '', string $title = '', $attributes = '', ?App $altConfig * Creates an anchor based on the local URL. The link * opens a new window based on the attributes specified. * - * @param string $uri the URL - * @param string $title the link title - * @param array|false|object|string $attributes any attributes - * @param App|null $altConfig Alternate configuration to use + * @param string $uri the URL + * @param string $title the link title + * @param array|false|object|string $attributes any attributes + * @param App|null $altConfig Alternate configuration to use */ function anchor_popup($uri = '', string $title = '', $attributes = false, ?App $altConfig = null): string { @@ -231,9 +231,9 @@ function anchor_popup($uri = '', string $title = '', $attributes = false, ?App $ /** * Mailto Link * - * @param string $email the email address - * @param string $title the link title - * @param array|object|string $attributes any attributes + * @param string $email the email address + * @param string $title the link title + * @param array|object|string $attributes any attributes */ function mailto(string $email, string $title = '', $attributes = ''): string { @@ -251,9 +251,9 @@ function mailto(string $email, string $title = '', $attributes = ''): string * * Create a spam-protected mailto link written in Javascript * - * @param string $email the email address - * @param string $title the link title - * @param array|object|string $attributes any attributes + * @param string $email the email address + * @param string $title the link title + * @param array|object|string $attributes any attributes */ function safe_mailto(string $email, string $title = '', $attributes = ''): string { diff --git a/utils/phpstan-baseline/argument.type.neon b/utils/phpstan-baseline/argument.type.neon index 80cb40694994..282cf8b2aa8b 100644 --- a/utils/phpstan-baseline/argument.type.neon +++ b/utils/phpstan-baseline/argument.type.neon @@ -1,4 +1,4 @@ -# total 64 errors +# total 63 errors parameters: ignoreErrors: @@ -122,11 +122,6 @@ parameters: count: 1 path: ../../tests/system/HTTP/SiteURITest.php - - - message: '#^Parameter \#2 \$value of function form_hidden expects array\|string, null given\.$#' - count: 1 - path: ../../tests/system/Helpers/FormHelperTest.php - - message: '#^Parameter \#1 \$num of function number_to_size expects int\|string, float given\.$#' count: 1 diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 57a1c5ee9257..0c6937677154 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 1381 errors +# total 1285 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 518e931c13d8..5c67c58552fd 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 1129 errors +# total 1034 errors parameters: ignoreErrors: @@ -2737,476 +2737,6 @@ parameters: count: 1 path: ../../system/HTTP/URI.php - - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:arrayAttachIndexedValue\(\) has parameter \$indexes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/Array/ArrayHelper.php - - - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:arrayAttachIndexedValue\(\) has parameter \$result with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/Array/ArrayHelper.php - - - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:arrayAttachIndexedValue\(\) has parameter \$row with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/Array/ArrayHelper.php - - - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:arrayAttachIndexedValue\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/Array/ArrayHelper.php - - - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:arraySearchDot\(\) has parameter \$array with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/Array/ArrayHelper.php - - - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:arraySearchDot\(\) has parameter \$indexes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/Array/ArrayHelper.php - - - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:dotKeyExists\(\) has parameter \$array with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/Array/ArrayHelper.php - - - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:dotSearch\(\) has parameter \$array with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/Array/ArrayHelper.php - - - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:groupBy\(\) has parameter \$array with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/Array/ArrayHelper.php - - - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:groupBy\(\) has parameter \$indexes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/Array/ArrayHelper.php - - - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:groupBy\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/Array/ArrayHelper.php - - - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:recursiveCount\(\) has parameter \$array with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/Array/ArrayHelper.php - - - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:recursiveDiff\(\) has parameter \$compareWith with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/Array/ArrayHelper.php - - - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:recursiveDiff\(\) has parameter \$original with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/Array/ArrayHelper.php - - - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:recursiveDiff\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/Array/ArrayHelper.php - - - - message: '#^Function array_deep_search\(\) has parameter \$array with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/array_helper.php - - - - message: '#^Function array_flatten_with_dots\(\) has parameter \$array with no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../system/Helpers/array_helper.php - - - - message: '#^Function array_flatten_with_dots\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/array_helper.php - - - - message: '#^Function array_group_by\(\) has parameter \$array with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/array_helper.php - - - - message: '#^Function array_group_by\(\) has parameter \$indexes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/array_helper.php - - - - message: '#^Function array_group_by\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/array_helper.php - - - - message: '#^Function array_sort_by_multiple_keys\(\) has parameter \$array with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/array_helper.php - - - - message: '#^Function array_sort_by_multiple_keys\(\) has parameter \$sortColumns with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/array_helper.php - - - - message: '#^Function dot_array_search\(\) has parameter \$array with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/array_helper.php - - - - message: '#^Function directory_map\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/filesystem_helper.php - - - - message: '#^Function get_filenames\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/filesystem_helper.php - - - - message: '#^Function form_button\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_button\(\) has parameter \$extra with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_checkbox\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_checkbox\(\) has parameter \$extra with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_datalist\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_dropdown\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_dropdown\(\) has parameter \$extra with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_dropdown\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_dropdown\(\) has parameter \$selected with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_fieldset\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_hidden\(\) has parameter \$name with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_hidden\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_input\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_input\(\) has parameter \$extra with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_label\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_multiselect\(\) has parameter \$extra with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_multiselect\(\) has parameter \$name with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_multiselect\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_multiselect\(\) has parameter \$selected with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_open\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_open\(\) has parameter \$hidden with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_open_multipart\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_open_multipart\(\) has parameter \$hidden with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_password\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_password\(\) has parameter \$extra with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_radio\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_radio\(\) has parameter \$extra with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_reset\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_reset\(\) has parameter \$extra with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_submit\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_submit\(\) has parameter \$extra with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_textarea\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_textarea\(\) has parameter \$extra with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_upload\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function form_upload\(\) has parameter \$extra with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function parse_form_attributes\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function parse_form_attributes\(\) has parameter \$default with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/form_helper.php - - - - message: '#^Function _list\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/html_helper.php - - - - message: '#^Function _list\(\) has parameter \$list with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/html_helper.php - - - - message: '#^Function _media\(\) has parameter \$tracks with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/html_helper.php - - - - message: '#^Function _media\(\) has parameter \$types with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/html_helper.php - - - - message: '#^Function audio\(\) has parameter \$src with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/html_helper.php - - - - message: '#^Function audio\(\) has parameter \$tracks with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/html_helper.php - - - - message: '#^Function img\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/html_helper.php - - - - message: '#^Function img\(\) has parameter \$src with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/html_helper.php - - - - message: '#^Function object\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/html_helper.php - - - - message: '#^Function ol\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/html_helper.php - - - - message: '#^Function ol\(\) has parameter \$list with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/html_helper.php - - - - message: '#^Function script_tag\(\) has parameter \$src with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/html_helper.php - - - - message: '#^Function ul\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/html_helper.php - - - - message: '#^Function ul\(\) has parameter \$list with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/html_helper.php - - - - message: '#^Function video\(\) has parameter \$src with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/html_helper.php - - - - message: '#^Function video\(\) has parameter \$tracks with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/html_helper.php - - - - message: '#^Function d\(\) has parameter \$vars with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/kint_helper.php - - - - message: '#^Function dd\(\) has parameter \$vars with no value type specified in iterable type array\.$#' - count: 2 - path: ../../system/Helpers/kint_helper.php - - - - message: '#^Function format_number\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/number_helper.php - - - - message: '#^Function fake\(\) has parameter \$overrides with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/test_helper.php - - - - message: '#^Function fake\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/test_helper.php - - - - message: '#^Function strip_slashes\(\) has parameter \$str with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/text_helper.php - - - - message: '#^Function strip_slashes\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/text_helper.php - - - - message: '#^Function word_censor\(\) has parameter \$censored with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/text_helper.php - - - - message: '#^Function anchor\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/url_helper.php - - - - message: '#^Function anchor\(\) has parameter \$uri with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/url_helper.php - - - - message: '#^Function anchor_popup\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/url_helper.php - - - - message: '#^Function base_url\(\) has parameter \$relativePath with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/url_helper.php - - - - message: '#^Function mailto\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/url_helper.php - - - - message: '#^Function safe_mailto\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/url_helper.php - - - - message: '#^Function site_url\(\) has parameter \$relativePath with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/url_helper.php - - message: '#^Method CodeIgniter\\HotReloader\\DirectoryHasher\:\:hashApp\(\) return type has no value type specified in iterable type array\.$#' count: 1 From 2ac2615749abeb9639ac1d3e662d13a99c5bb196 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Mon, 17 Aug 2026 03:55:02 +0800 Subject: [PATCH 059/163] refactor: fix phpstan errors in `Router` (#10460) --- system/Router/Attributes/Filter.php | 6 + system/Router/Attributes/Restrict.php | 5 + system/Router/AutoRouter.php | 17 +- system/Router/AutoRouterImproved.php | 19 +- system/Router/AutoRouterInterface.php | 6 +- system/Router/RouteCollection.php | 237 +++--------- system/Router/RouteCollectionInterface.php | 21 +- system/Router/Router.php | 20 +- system/Router/RouterInterface.php | 2 +- tests/system/Router/RouteCollectionTest.php | 4 +- utils/phpstan-baseline/argument.type.neon | 8 +- utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 342 +----------------- 13 files changed, 115 insertions(+), 574 deletions(-) diff --git a/system/Router/Attributes/Filter.php b/system/Router/Attributes/Filter.php index 130d2946f0dc..edba9452619e 100644 --- a/system/Router/Attributes/Filter.php +++ b/system/Router/Attributes/Filter.php @@ -38,6 +38,9 @@ #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] class Filter implements RouteAttributeInterface { + /** + * @param array $having + */ public function __construct( public string $by, public array $having = [], @@ -56,6 +59,9 @@ public function after(RequestInterface $request, ResponseInterface $response): ? return null; } + /** + * @return list + */ public function getFilters(): array { if ($this->having === []) { diff --git a/system/Router/Attributes/Restrict.php b/system/Router/Attributes/Restrict.php index 79344a7c6982..3bf250ab7966 100644 --- a/system/Router/Attributes/Restrict.php +++ b/system/Router/Attributes/Restrict.php @@ -42,6 +42,11 @@ #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] class Restrict implements RouteAttributeInterface { + /** + * @param list|string|null $environment + * @param list|string|null $hostname + * @param list|string|null $subdomain + */ public function __construct( public array|string|null $environment = null, public array|string|null $hostname = null, diff --git a/system/Router/AutoRouter.php b/system/Router/AutoRouter.php index 1033478c9e30..e887469b9037 100644 --- a/system/Router/AutoRouter.php +++ b/system/Router/AutoRouter.php @@ -55,14 +55,6 @@ public function __construct( ) { } - /** - * Attempts to match a URI path against Controllers and directories - * found in APPPATH/Controllers, to find a matching route. - * - * @param string $httpVerb HTTP verb like `GET`,`POST` - * - * @return array [directory_name, controller_name, controller_method, params] - */ public function getRoute(string $uri, string $httpVerb): array { $segments = explode('/', $uri); @@ -95,7 +87,7 @@ public function getRoute(string $uri, string $httpVerb): array throw PageNotFoundException::forPageNotFound(); } - /** @var array $params An array of params to the controller method. */ + /** @var list $params An array of params to the controller method. */ $params = []; if ($segments !== []) { @@ -177,11 +169,12 @@ public function setTranslateURIDashes(bool $val = false): self } /** - * Scans the controller directory, attempting to locate a controller matching the supplied uri $segments + * Scans the controller directory, attempting to locate a controller matching the supplied uri `$segments`. + * Returns an array of remaining uri segments that don't map onto a directory. * - * @param array $segments URI segments + * @param list $segments URI segments * - * @return array returns an array of remaining uri segments that don't map onto a directory + * @return list */ private function scanControllers(array $segments): array { diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index a00c04d265d7..c7dbf04efec5 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -63,12 +63,11 @@ final class AutoRouterImproved implements AutoRouterInterface * Map of URI segments and namespaces. * * The key is the first URI segment. The value is the controller namespace. - * E.g., - * [ - * 'blog' => 'Acme\Blog\Controllers', - * ] + * ``` + * ['blog' => 'Acme\Blog\Controllers'] + * ``` * - * @var array [ uri_segment => namespace ] + * @var array */ private array $moduleRoutes; @@ -133,6 +132,9 @@ public function __construct( $this->controller = $this->defaultController; } + /** + * @return list + */ private function createSegments(string $uri): array { $segments = explode('/', $uri); @@ -250,13 +252,6 @@ private function searchLastDefaultController(): bool return false; } - /** - * Finds controller, method and params from the URI. - * - * @param string $httpVerb HTTP verb like `GET`,`POST` - * - * @return array [directory_name, controller_name, controller_method, params] - */ public function getRoute(string $uri, string $httpVerb): array { $this->uri = $uri; diff --git a/system/Router/AutoRouterInterface.php b/system/Router/AutoRouterInterface.php index 7edec821dbfd..58f22393881e 100644 --- a/system/Router/AutoRouterInterface.php +++ b/system/Router/AutoRouterInterface.php @@ -19,9 +19,11 @@ interface AutoRouterInterface { /** - * Returns controller, method and params from the URI. + * Returns the directory name, controller name, controller method, and any parameters for the given URI and HTTP verb. * - * @return array [directory_name, controller_name, controller_method, params] + * @param string $httpVerb HTTP verb like `GET`,`POST` + * + * @return array{string|null, string, string, list} */ public function getRoute(string $uri, string $httpVerb): array; } diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 6dfb42286972..b5bd4ee76597 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -96,6 +96,8 @@ class RouteCollection implements RouteCollectionInterface /** * An array of files that would contain route definitions. + * + * @var list */ protected array $routeFiles = []; @@ -116,9 +118,7 @@ class RouteCollection implements RouteCollectionInterface /** * An array of all routes and their mappings. - * - * @var array - * + *``` * [ * verb => [ * routeKey(regex) => [ @@ -137,6 +137,9 @@ class RouteCollection implements RouteCollectionInterface * ], * ], * ] + * ``` + * + * @var array>> */ protected $routes = [ '*' => [], @@ -154,14 +157,11 @@ class RouteCollection implements RouteCollectionInterface /** * Array of routes names + * ``` + * [verb => [routeName => routeKey(regex)]] + * ``` * - * @var array - * - * [ - * verb => [ - * routeName => routeKey(regex) - * ], - * ] + * @var array> */ protected $routesNames = [ '*' => [], @@ -179,9 +179,7 @@ class RouteCollection implements RouteCollectionInterface /** * Array of routes options - * - * @var array - * + * ``` * [ * verb => [ * routeKey(regex) => [ @@ -189,6 +187,9 @@ class RouteCollection implements RouteCollectionInterface * ] * ], * ] + * ``` + * + * @var array>> */ protected $routesOptions = []; @@ -225,7 +226,7 @@ class RouteCollection implements RouteCollectionInterface * Stores copy of current options being * applied during creation. * - * @var array|null + * @var array|null */ protected $currentOptions; @@ -376,16 +377,6 @@ protected function discoverRoutes() $this->didDiscover = true; } - /** - * Registers a new constraint with the system. Constraints are used - * by the routes as placeholders for regular expressions to make defining - * the routes more human-friendly. - * - * You can pass an associative array as $placeholder, and have - * multiple placeholders added at once. - * - * @param array|string $placeholder - */ public function addPlaceholder($placeholder, ?string $pattern = null): RouteCollectionInterface { if (! is_array($placeholder)) { @@ -409,10 +400,6 @@ public function getPlaceholders(): array return $this->placeholders; } - /** - * Sets the default namespace to use for Controllers when no other - * namespace has been specified. - */ public function setDefaultNamespace(string $value): RouteCollectionInterface { $this->defaultNamespace = esc(strip_tags($value)); @@ -421,10 +408,6 @@ public function setDefaultNamespace(string $value): RouteCollectionInterface return $this; } - /** - * Sets the default controller to use when no other controller has been - * specified. - */ public function setDefaultController(string $value): RouteCollectionInterface { $this->defaultController = esc(strip_tags($value)); @@ -432,10 +415,6 @@ public function setDefaultController(string $value): RouteCollectionInterface return $this; } - /** - * Sets the default method to call on the controller when no other - * method has been set in the route. - */ public function setDefaultMethod(string $value): RouteCollectionInterface { $this->defaultMethod = esc(strip_tags($value)); @@ -443,13 +422,6 @@ public function setDefaultMethod(string $value): RouteCollectionInterface return $this; } - /** - * Tells the system whether to convert dashes in URI strings into - * underscores. In some search engines, including Google, dashes - * create more meaning and make it easier for the search engine to - * find words and meaning in the URI for better SEO. But it - * doesn't work well with PHP method names.... - */ public function setTranslateURIDashes(bool $value): RouteCollectionInterface { $this->translateURIDashes = $value; @@ -457,14 +429,6 @@ public function setTranslateURIDashes(bool $value): RouteCollectionInterface return $this; } - /** - * If TRUE, the system will attempt to match the URI against - * Controllers by matching each segment against folders/files - * in APPPATH/Controllers, when a match wasn't found against - * defined routes. - * - * If FALSE, will stop searching and do NO automatic routing. - */ public function setAutoRoute(bool $value): RouteCollectionInterface { $this->autoRoute = $value; @@ -472,15 +436,6 @@ public function setAutoRoute(bool $value): RouteCollectionInterface return $this; } - /** - * Sets the class/method that should be called if routing doesn't - * find a match. It can be either a closure or the controller/method - * name exactly like a route is defined: Users::index - * - * This setting is passed to the Router class and handled there. - * - * @param (callable(string): (ResponseInterface|string|void))|string|null $callable - */ public function set404Override($callable = null): RouteCollectionInterface { $this->override404 = $callable; @@ -488,12 +443,6 @@ public function set404Override($callable = null): RouteCollectionInterface return $this; } - /** - * Returns the 404 Override setting, which can be null, a closure - * or the controller/string. - * - * @return (Closure(string): (ResponseInterface|string|void))|string|null - */ public function get404Override() { return $this->override404; @@ -512,52 +461,31 @@ public function setDefaultConstraint(string $placeholder): RouteCollectionInterf return $this; } - /** - * Returns the name of the default controller. With Namespace. - */ public function getDefaultController(): string { return $this->defaultController; } - /** - * Returns the name of the default method to use within the controller. - */ public function getDefaultMethod(): string { return $this->defaultMethod; } - /** - * Returns the default namespace as set in the Routes config file. - */ public function getDefaultNamespace(): string { return $this->defaultNamespace; } - /** - * Returns the current value of the translateURIDashes setting. - */ public function shouldTranslateURIDashes(): bool { return $this->translateURIDashes; } - /** - * Returns the flag that tells whether to autoRoute URI against Controllers. - */ public function shouldAutoRoute(): bool { return $this->autoRoute; } - /** - * Returns the raw array of available routes. - * - * @param non-empty-string|null $verb HTTP verb like `GET`,`POST` or `*` or `CLI`. - * @param bool $includeWildcard Whether to include '*' routes. - */ public function getRoutes(?string $verb = null, bool $includeWildcard = true): array { if ((string) $verb === '') { @@ -598,13 +526,6 @@ public function getRoutes(?string $verb = null, bool $includeWildcard = true): a return $routes; } - /** - * Returns one or all routes options - * - * @param string|null $verb HTTP verb like `GET`,`POST` or `*` or `CLI`. - * - * @return array [key => value] - */ public function getRoutesOptions(?string $from = null, ?string $verb = null): array { $options = $this->loadRoutesOptions($verb); @@ -612,22 +533,11 @@ public function getRoutesOptions(?string $from = null, ?string $verb = null): ar return ((string) $from !== '') ? $options[$from] ?? [] : $options; } - /** - * Returns the current HTTP Verb being used. - */ public function getHTTPVerb(): string { return $this->HTTPVerb; } - /** - * Sets the current HTTP verb. - * Used primarily for testing. - * - * @param string $verb HTTP verb - * - * @return $this - */ public function setHTTPVerb(string $verb) { if ($verb !== '*' && $verb === strtolower($verb)) { @@ -651,6 +561,9 @@ public function setHTTPVerb(string $verb) * A shortcut method to add a number of routes at a single time. * It does not allow any options to be set on the route, or to * define the method used. + * + * @param array $routes + * @param array|null $options */ public function map(array $routes = [], ?array $options = null): RouteCollectionInterface { @@ -661,14 +574,6 @@ public function map(array $routes = [], ?array $options = null): RouteCollection return $this; } - /** - * Adds a single route to the collection. - * - * Example: - * $routes->add('news', 'Posts::index'); - * - * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to - */ public function add(string $from, $to, ?array $options = null): RouteCollectionInterface { $this->create('*', $from, $to, $options); @@ -708,11 +613,6 @@ public function addRedirect(string $from, string $to, int $status = 302) return $this; } - /** - * Determines if the route is a redirecting route. - * - * @param string $routeKey routeKey or route name - */ public function isRedirect(string $routeKey): bool { if (isset($this->routes['*'][$routeKey]['redirect'])) { @@ -730,11 +630,6 @@ public function isRedirect(string $routeKey): bool return false; } - /** - * Grabs the HTTP status code from a redirecting Route. - * - * @param string $routeKey routeKey or route name - */ public function getRedirectCode(string $routeKey): int { if (isset($this->routes['*'][$routeKey]['redirect'])) { @@ -762,8 +657,8 @@ public function getRedirectCode(string $routeKey): int * $route->resource('users'); * }); * - * @param string $name The name to group/prefix the routes with. - * @param array|(callable(self): void) ...$params + * @param string $name The name to group/prefix the routes with. + * @param array|(callable(self): void) ...$params * * @return void */ @@ -844,8 +739,8 @@ public function group(string $name, ...$params) * POST /photos/{id}/delete delete * POST /photos/{id} update * - * @param string $name The name of the resource/controller to route to. - * @param array|null $options A list of possible ways to customize the routing. + * @param string $name The name of the resource/controller to route to. + * @param array|null $options A list of possible ways to customize the routing. */ public function resource(string $name, ?array $options = null): RouteCollectionInterface { @@ -938,8 +833,8 @@ public function resource(string $name, ?array $options = null): RouteCollectionI * GET /photos/remove/{id} remove show a form to confirm deletion of a specific photo object * POST /photos/delete/{id} delete deleting the specified photo object * - * @param string $name The name of the controller to route to. - * @param array|null $options A list of possible ways to customize the routing. + * @param string $name The name of the controller to route to. + * @param array|null $options A list of possible ways to customize the routing. */ public function presenter(string $name, ?array $options = null): RouteCollectionInterface { @@ -1013,7 +908,9 @@ public function presenter(string $name, ?array $options = null): RouteCollection * Example: * $route->match( ['GET', 'POST'], 'users/(:num)', 'users/$1); * - * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to + * @param list $verbs + * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to + * @param array|null $options */ public function match(array $verbs = [], string $from = '', $to = '', ?array $options = null): RouteCollectionInterface { @@ -1045,7 +942,8 @@ public function match(array $verbs = [], string $from = '', $to = '', ?array $op /** * Specifies a route that is only available to GET requests. * - * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to + * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to + * @param array|null $options */ public function get(string $from, $to, ?array $options = null): RouteCollectionInterface { @@ -1057,7 +955,8 @@ public function get(string $from, $to, ?array $options = null): RouteCollectionI /** * Specifies a route that is only available to POST requests. * - * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to + * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to + * @param array|null $options */ public function post(string $from, $to, ?array $options = null): RouteCollectionInterface { @@ -1069,7 +968,8 @@ public function post(string $from, $to, ?array $options = null): RouteCollection /** * Specifies a route that is only available to PUT requests. * - * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to + * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to + * @param array|null $options */ public function put(string $from, $to, ?array $options = null): RouteCollectionInterface { @@ -1081,7 +981,8 @@ public function put(string $from, $to, ?array $options = null): RouteCollectionI /** * Specifies a route that is only available to DELETE requests. * - * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to + * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to + * @param array|null $options */ public function delete(string $from, $to, ?array $options = null): RouteCollectionInterface { @@ -1093,7 +994,8 @@ public function delete(string $from, $to, ?array $options = null): RouteCollecti /** * Specifies a route that is only available to HEAD requests. * - * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to + * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to + * @param array|null $options */ public function head(string $from, $to, ?array $options = null): RouteCollectionInterface { @@ -1105,7 +1007,8 @@ public function head(string $from, $to, ?array $options = null): RouteCollection /** * Specifies a route that is only available to PATCH requests. * - * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to + * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to + * @param array|null $options */ public function patch(string $from, $to, ?array $options = null): RouteCollectionInterface { @@ -1117,7 +1020,8 @@ public function patch(string $from, $to, ?array $options = null): RouteCollectio /** * Specifies a route that is only available to OPTIONS requests. * - * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to + * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to + * @param array|null $options */ public function options(string $from, $to, ?array $options = null): RouteCollectionInterface { @@ -1129,7 +1033,8 @@ public function options(string $from, $to, ?array $options = null): RouteCollect /** * Specifies a route that is only available to command-line requests. * - * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to + * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to + * @param array|null $options */ public function cli(string $from, $to, ?array $options = null): RouteCollectionInterface { @@ -1141,6 +1046,8 @@ public function cli(string $from, $to, ?array $options = null): RouteCollectionI /** * Specifies a route that will only display a view. * Only works for GET requests. + * + * @param array|null $options */ public function view(string $from, string $view, ?array $options = null): RouteCollectionInterface { @@ -1170,25 +1077,6 @@ public function environment(string $env, Closure $callback): RouteCollectionInte return $this; } - /** - * Attempts to look up a route based on its destination. - * - * If a route exists: - * - * 'path/(:any)/(:any)' => 'Controller::method/$1/$2' - * - * This method allows you to know the Controller and method - * and get the route that leads to it. - * - * // Equals 'path/$param1/$param2' - * reverseRoute('Controller::method', $param1, $param2); - * - * @param string $search Route name or Controller::method - * @param int|string ...$params One or more parameters to be passed to the route. - * The last parameter allows you to set the locale. - * - * @return false|string The route (URI path relative to baseURL) or false if not found. - */ public function reverseRoute(string $search, ...$params) { if ($search === '') { @@ -1262,11 +1150,6 @@ protected function localizeRoute(string $route): string return strtr($route, ['{locale}' => service('request')->getLocale()]); } - /** - * Checks a route (using the "from") to see if it's filtered or not. - * - * @param string|null $verb HTTP verb like `GET`,`POST` or `*` or `CLI`. - */ public function isFiltered(string $search, ?string $verb = null): bool { $options = $this->loadRoutesOptions($verb); @@ -1274,21 +1157,6 @@ public function isFiltered(string $search, ?string $verb = null): bool return isset($options[$search]['filter']); } - /** - * Returns the filters that should be applied for a single route, along - * with any parameters it might have. Parameters are found by splitting - * the parameter name on a colon to separate the filter name from the parameter list, - * and the splitting the result on commas. So: - * - * 'role:admin,manager' - * - * has a filter of "role", with parameters of ['admin', 'manager']. - * - * @param string $search routeKey - * @param string|null $verb HTTP verb like `GET`,`POST` or `*` or `CLI`. - * - * @return list filter_name or filter_name:arguments like 'role:admin,manager' - */ public function getFiltersForRoute(string $search, ?string $verb = null): array { $options = $this->loadRoutesOptions($verb); @@ -1307,6 +1175,8 @@ public function getFiltersForRoute(string $search, ?string $verb = null): array /** * Given a * + * @param list|null $params + * * @throws RouterException * * @deprecated Unused. Now uses buildReverseRoute(). @@ -1345,8 +1215,8 @@ protected function fillRouteParams(string $from, ?array $params = null): string /** * Builds reverse route * - * @param array $params One or more parameters to be passed to the route. - * The last parameter allows you to set the locale. + * @param list $params One or more parameters to be passed to the route. + * The last parameter allows you to set the locale. */ protected function buildReverseRoute(string $from, array $params): string { @@ -1435,7 +1305,8 @@ private function replaceLocale(string $route, ?string $locale = null): string * the request method(s) that this route will work for. They can be separated * by a pipe character "|" if there is more than one. * - * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to + * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to + * @param array|null $options * * @return void */ @@ -1582,6 +1453,9 @@ private function checkHostname($hostname): bool return strtolower($this->httpHost) === strtolower($hostname); } + /** + * @param array $to + */ private function processArrayCallableSyntax(string $from, array $to): string { // [classname, method] @@ -1797,9 +1671,6 @@ public function useSupportedLocalesOnly(bool $useOnly): self return $this; } - /** - * Get the flag that limit or not the routes with {locale} placeholder to App::$supportedLocales - */ public function shouldUseSupportedLocalesOnly(): bool { return $this->useSupportedLocalesOnly; diff --git a/system/Router/RouteCollectionInterface.php b/system/Router/RouteCollectionInterface.php index 10587d81ee4c..e3c83955e28f 100644 --- a/system/Router/RouteCollectionInterface.php +++ b/system/Router/RouteCollectionInterface.php @@ -28,9 +28,12 @@ interface RouteCollectionInterface /** * Adds a single route to the collection. * - * @param string $from The route path (with placeholders or regex) - * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to The route handler - * @param array|null $options The route options + * Example: + * $routes->add('news', 'Posts::index'); + * + * @param string $from The route path (with placeholders or regex) + * @param array|(Closure(mixed...): (ResponseInterface|string|void))|string $to The route handler + * @param array|null $options The route options * * @return RouteCollectionInterface */ @@ -44,8 +47,8 @@ public function add(string $from, $to, ?array $options = null); * You can pass an associative array as $placeholder, and have * multiple placeholders added at once. * - * @param array|string $placeholder - * @param string|null $pattern The regex pattern + * @param array|string $placeholder + * @param string|null $pattern The regex pattern * * @return RouteCollectionInterface */ @@ -177,6 +180,8 @@ public function shouldAutoRoute(); * * @param non-empty-string|null $verb HTTP verb like `GET`,`POST` or `*` or `CLI`. * @param bool $includeWildcard Whether to include '*' routes. + * + * @return array|(Closure(mixed...): (ResponseInterface|string|void))|string> */ public function getRoutes(?string $verb = null, bool $includeWildcard = true): array; @@ -192,6 +197,7 @@ public function getRoutesOptions(?string $from = null, ?string $verb = null): ar /** * Sets the current HTTP verb. + * Used primarily for testing. * * @param string $verb HTTP verb * @@ -219,8 +225,9 @@ public function getHTTPVerb(); * // Equals 'path/$param1/$param2' * reverseRoute('Controller::method', $param1, $param2); * - * @param string $search Named route or Controller::method - * @param int|string ...$params + * @param string $search Route name or Controller::method + * @param int|string ...$params One or more parameters to be passed to the route. + * The last parameter allows you to set the locale. * * @return false|string The route (URI path relative to baseURL) or false if not found. */ diff --git a/system/Router/Router.php b/system/Router/Router.php index c886bf45f396..6e5a4630832b 100644 --- a/system/Router/Router.php +++ b/system/Router/Router.php @@ -86,7 +86,7 @@ class Router implements RouterInterface * An array of binds that were collected * so they can be sent to closure routes. * - * @var array + * @var list */ protected $params = []; @@ -108,14 +108,14 @@ class Router implements RouterInterface /** * The route that was matched for this request. * - * @var array|null + * @var array{string, (callable(mixed...): (ResponseInterface|string|void))|string}|null */ protected $matchedRoute; /** * The options set for the matched route. * - * @var array|null + * @var array|null */ protected $matchedRouteOptions; @@ -346,7 +346,7 @@ public function directory(): string * Returns the routing information that was matched for this * request, if a route was defined. * - * @return array|null + * @return array{string, (callable(mixed...): (ResponseInterface|string|void))|string}|null */ public function getMatchedRoute() { @@ -356,7 +356,7 @@ public function getMatchedRoute() /** * Returns all options set for the matched route * - * @return array|null + * @return array|null */ public function getMatchedRouteOptions() { @@ -562,6 +562,8 @@ protected function checkRoutes(string $uri): bool /** * Replace string `$n` with `$matches[n]` value. + * + * @param list $matches */ private function replaceBackReferences(string $input, array $matches): string { @@ -595,9 +597,9 @@ public function autoRoute(string $uri) /** * Scans the controller directory, attempting to locate a controller matching the supplied uri $segments * - * @param array $segments URI segments + * @param list $segments URI segments * - * @return array returns an array of remaining uri segments that don't map onto a directory + * @return list Returns an array of remaining uri segments that don't map onto a directory * * @deprecated this function name does not properly describe its behavior so it has been deprecated * @@ -611,9 +613,9 @@ protected function validateRequest(array $segments): array /** * Scans the controller directory, attempting to locate a controller matching the supplied uri $segments * - * @param array $segments URI segments + * @param list $segments URI segments * - * @return array returns an array of remaining uri segments that don't map onto a directory + * @return list Returns an array of remaining uri segments that don't map onto a directory * * @deprecated Not used. Moved to AutoRouter class. */ diff --git a/system/Router/RouterInterface.php b/system/Router/RouterInterface.php index c50de86d43df..05dd2caf279e 100644 --- a/system/Router/RouterInterface.php +++ b/system/Router/RouterInterface.php @@ -55,7 +55,7 @@ public function methodName(); * during the parsing process as an array, ready to send to * instance->method(...$params). * - * @return array + * @return list */ public function params(); diff --git a/tests/system/Router/RouteCollectionTest.php b/tests/system/Router/RouteCollectionTest.php index 6346697d206a..bf35a3f6fe63 100644 --- a/tests/system/Router/RouteCollectionTest.php +++ b/tests/system/Router/RouteCollectionTest.php @@ -196,13 +196,13 @@ public function testMatchIgnoresInvalidHTTPMethods(): void $this->assertSame([], $routes); } - public function testAddWorksWithArrayOFHTTPMethods(): void + public function testAddWorksWithArrayOfHTTPMethods(): void { service('request')->setMethod(Method::POST); $routes = $this->getCollector(); - $routes->add('home', 'controller', ['get', 'post']); + $routes->add('home', 'controller'); $expects = [ 'home' => '\controller', diff --git a/utils/phpstan-baseline/argument.type.neon b/utils/phpstan-baseline/argument.type.neon index 282cf8b2aa8b..addcb1d223e7 100644 --- a/utils/phpstan-baseline/argument.type.neon +++ b/utils/phpstan-baseline/argument.type.neon @@ -23,22 +23,22 @@ parameters: path: ../../system/Database/SQLite3/Builder.php - - message: '#^Parameter \#2 \$to of method CodeIgniter\\Router\\RouteCollection\:\:add\(\) expects array\|\(Closure\(mixed \.\.\.\)\: \(CodeIgniter\\HTTP\\ResponseInterface\|string\|void\)\)\|string, Closure\(mixed\)\: CodeIgniter\\HTTP\\ResponseInterface given\.$#' + message: '#^Parameter \#2 \$to of method CodeIgniter\\Router\\RouteCollection\:\:add\(\) expects array\\|\(Closure\(mixed \.\.\.\)\: \(CodeIgniter\\HTTP\\ResponseInterface\|string\|void\)\)\|string, Closure\(mixed\)\: CodeIgniter\\HTTP\\ResponseInterface given\.$#' count: 1 path: ../../tests/system/CodeIgniterTest.php - - message: '#^Parameter \#2 \$to of method CodeIgniter\\Router\\RouteCollection\:\:add\(\) expects array\|\(Closure\(mixed \.\.\.\)\: \(CodeIgniter\\HTTP\\ResponseInterface\|string\|void\)\)\|string, Closure\(mixed\)\: \(CodeIgniter\\HTTP\\DownloadResponse\|null\) given\.$#' + message: '#^Parameter \#2 \$to of method CodeIgniter\\Router\\RouteCollection\:\:add\(\) expects array\\|\(Closure\(mixed \.\.\.\)\: \(CodeIgniter\\HTTP\\ResponseInterface\|string\|void\)\)\|string, Closure\(mixed\)\: \(CodeIgniter\\HTTP\\DownloadResponse\|null\) given\.$#' count: 1 path: ../../tests/system/CodeIgniterTest.php - - message: '#^Parameter \#2 \$to of method CodeIgniter\\Router\\RouteCollection\:\:add\(\) expects array\|\(Closure\(mixed \.\.\.\)\: \(CodeIgniter\\HTTP\\ResponseInterface\|string\|void\)\)\|string, Closure\(mixed\)\: non\-falsy\-string given\.$#' + message: '#^Parameter \#2 \$to of method CodeIgniter\\Router\\RouteCollection\:\:add\(\) expects array\\|\(Closure\(mixed \.\.\.\)\: \(CodeIgniter\\HTTP\\ResponseInterface\|string\|void\)\)\|string, Closure\(mixed\)\: non\-falsy\-string given\.$#' count: 1 path: ../../tests/system/CodeIgniterTest.php - - message: '#^Parameter \#2 \$to of method CodeIgniter\\Router\\RouteCollection\:\:add\(\) expects array\|\(Closure\(mixed \.\.\.\)\: \(CodeIgniter\\HTTP\\ResponseInterface\|string\|void\)\)\|string, Closure\(mixed\)\: void given\.$#' + message: '#^Parameter \#2 \$to of method CodeIgniter\\Router\\RouteCollection\:\:add\(\) expects array\\|\(Closure\(mixed \.\.\.\)\: \(CodeIgniter\\HTTP\\ResponseInterface\|string\|void\)\)\|string, Closure\(mixed\)\: void given\.$#' count: 1 path: ../../tests/system/CodeIgniterTest.php diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 0c6937677154..a382fda9a5d4 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 1285 errors +# total 1217 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 5c67c58552fd..f67d1b49920c 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 1034 errors +# total 966 errors parameters: ignoreErrors: @@ -2817,346 +2817,6 @@ parameters: count: 1 path: ../../system/Publisher/Publisher.php - - - message: '#^Method CodeIgniter\\Router\\Attributes\\Filter\:\:__construct\(\) has parameter \$having with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/Attributes/Filter.php - - - - message: '#^Method CodeIgniter\\Router\\Attributes\\Filter\:\:getFilters\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/Attributes/Filter.php - - - - message: '#^Method CodeIgniter\\Router\\Attributes\\Restrict\:\:__construct\(\) has parameter \$environment with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/Attributes/Restrict.php - - - - message: '#^Method CodeIgniter\\Router\\Attributes\\Restrict\:\:__construct\(\) has parameter \$hostname with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/Attributes/Restrict.php - - - - message: '#^Method CodeIgniter\\Router\\Attributes\\Restrict\:\:__construct\(\) has parameter \$subdomain with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/Attributes/Restrict.php - - - - message: '#^Method CodeIgniter\\Router\\AutoRouter\:\:getRoute\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/AutoRouter.php - - - - message: '#^Method CodeIgniter\\Router\\AutoRouter\:\:scanControllers\(\) has parameter \$segments with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/AutoRouter.php - - - - message: '#^Method CodeIgniter\\Router\\AutoRouter\:\:scanControllers\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/AutoRouter.php - - - - message: '#^PHPDoc tag @var for variable \$params has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/AutoRouter.php - - - - message: '#^Method CodeIgniter\\Router\\AutoRouterImproved\:\:createSegments\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/AutoRouterImproved.php - - - - message: '#^Method CodeIgniter\\Router\\AutoRouterImproved\:\:getRoute\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/AutoRouterImproved.php - - - - message: '#^Property CodeIgniter\\Router\\AutoRouterImproved\:\:\$moduleRoutes type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/AutoRouterImproved.php - - - - message: '#^Method CodeIgniter\\Router\\AutoRouterInterface\:\:getRoute\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/AutoRouterInterface.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:addPlaceholder\(\) has parameter \$placeholder with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:add\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:add\(\) has parameter \$to with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:buildReverseRoute\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:cli\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:cli\(\) has parameter \$to with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:create\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:create\(\) has parameter \$to with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:delete\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:delete\(\) has parameter \$to with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:fillRouteParams\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:getRoutes\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:get\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:get\(\) has parameter \$to with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:group\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:head\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:head\(\) has parameter \$to with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:map\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:map\(\) has parameter \$routes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:match\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:match\(\) has parameter \$to with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:match\(\) has parameter \$verbs with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:options\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:options\(\) has parameter \$to with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:patch\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:patch\(\) has parameter \$to with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:post\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:post\(\) has parameter \$to with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:presenter\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:processArrayCallableSyntax\(\) has parameter \$to with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:put\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:put\(\) has parameter \$to with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:resource\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollection\:\:view\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Property CodeIgniter\\Router\\RouteCollection\:\:\$currentOptions type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Property CodeIgniter\\Router\\RouteCollection\:\:\$routeFiles type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Property CodeIgniter\\Router\\RouteCollection\:\:\$routes type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Property CodeIgniter\\Router\\RouteCollection\:\:\$routesNames type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Property CodeIgniter\\Router\\RouteCollection\:\:\$routesOptions type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollection.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollectionInterface\:\:addPlaceholder\(\) has parameter \$placeholder with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollectionInterface.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollectionInterface\:\:add\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollectionInterface.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollectionInterface\:\:add\(\) has parameter \$to with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollectionInterface.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollectionInterface\:\:getRoutes\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouteCollectionInterface.php - - - - message: '#^Method CodeIgniter\\Router\\Router\:\:getMatchedRouteOptions\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/Router.php - - - - message: '#^Method CodeIgniter\\Router\\Router\:\:getMatchedRoute\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/Router.php - - - - message: '#^Method CodeIgniter\\Router\\Router\:\:params\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/Router.php - - - - message: '#^Method CodeIgniter\\Router\\Router\:\:replaceBackReferences\(\) has parameter \$matches with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/Router.php - - - - message: '#^Method CodeIgniter\\Router\\Router\:\:scanControllers\(\) has parameter \$segments with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/Router.php - - - - message: '#^Method CodeIgniter\\Router\\Router\:\:scanControllers\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/Router.php - - - - message: '#^Method CodeIgniter\\Router\\Router\:\:validateRequest\(\) has parameter \$segments with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/Router.php - - - - message: '#^Method CodeIgniter\\Router\\Router\:\:validateRequest\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/Router.php - - - - message: '#^Property CodeIgniter\\Router\\Router\:\:\$matchedRoute type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/Router.php - - - - message: '#^Property CodeIgniter\\Router\\Router\:\:\$matchedRouteOptions type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/Router.php - - - - message: '#^Property CodeIgniter\\Router\\Router\:\:\$params type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/Router.php - - - - message: '#^Method CodeIgniter\\Router\\RouterInterface\:\:params\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Router/RouterInterface.php - - message: '#^Method CodeIgniter\\Test\\Constraints\\SeeInDatabase\:\:__construct\(\) has parameter \$data with no value type specified in iterable type array\.$#' count: 1 From cf54b82d0b8cb01b55f6421c55f2058091e7a20f Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Mon, 17 Aug 2026 04:24:49 +0800 Subject: [PATCH 060/163] refactor: clear out phpstan errors in `Validation` source and tests (#10461) --- system/Validation/CreditCardRules.php | 2 +- system/Validation/DotArrayFilter.php | 12 +- system/Validation/Rules.php | 44 +- system/Validation/StrictRules/Rules.php | 35 +- system/Validation/Validation.php | 132 +-- system/Validation/ValidationInterface.php | 34 +- tests/system/Validation/FormatRulesTest.php | 63 ++ tests/system/Validation/RulesTest.php | 102 ++- .../StrictRules/CreditCardRulesTest.php | 2 + .../Validation/StrictRules/RulesTest.php | 31 + tests/system/Validation/ValidationTest.php | 53 +- utils/phpstan-baseline/argument.type.neon | 7 +- utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 772 +----------------- 14 files changed, 352 insertions(+), 939 deletions(-) diff --git a/system/Validation/CreditCardRules.php b/system/Validation/CreditCardRules.php index e484c35f66cb..a31b303fcb86 100644 --- a/system/Validation/CreditCardRules.php +++ b/system/Validation/CreditCardRules.php @@ -31,7 +31,7 @@ class CreditCardRules * prefixes - List of possible prefixes for the card * checkdigit - Boolean on whether we should do a modulus10 check on the numbers. * - * @var array + * @var array */ protected $cards = [ 'American Express' => [ diff --git a/system/Validation/DotArrayFilter.php b/system/Validation/DotArrayFilter.php index 673fe67ed138..8e059c3661ae 100644 --- a/system/Validation/DotArrayFilter.php +++ b/system/Validation/DotArrayFilter.php @@ -21,10 +21,10 @@ final class DotArrayFilter /** * Creates a new array with only the elements specified in dot array syntax. * - * @param array $indexes The dot array syntax pattern to use for filtering. - * @param array $array The array to filter. + * @param list $indexes The dot array syntax pattern to use for filtering. + * @param array $array The array to filter. * - * @return array The filtered array. + * @return array The filtered array. */ public static function run(array $indexes, array $array): array { @@ -47,10 +47,10 @@ public static function run(array $indexes, array $array): array /** * Used by `run()` to recursively filter the array with wildcards. * - * @param array $indexes The dot array syntax pattern to use for filtering. - * @param array $array The array to filter. + * @param list $indexes The dot array syntax pattern to use for filtering. + * @param array $array The array to filter. * - * @return array The filtered array. + * @return array The filtered array. */ private static function filter(array $indexes, array $array): array { diff --git a/system/Validation/Rules.php b/system/Validation/Rules.php index 31aa042c5d36..c48465b327e0 100644 --- a/system/Validation/Rules.php +++ b/system/Validation/Rules.php @@ -28,8 +28,8 @@ class Rules /** * The value does not match another field in $data. * - * @param string|null $str - * @param array $data Other field/value pairs + * @param string|null $str + * @param array $data Other field/value pairs */ public function differs($str, string $field, array $data): bool { @@ -110,12 +110,14 @@ public function greater_than_equal_to($str, string $min): bool * Can ignore records by field/value to filter (currently * accept only one filter). * - * Example: + * ``` * is_not_unique[dbGroup.table.field,where_field,where_value] * is_not_unique[table.field,where_field,where_value] * is_not_unique[menu.id,active,1] + * ``` * - * @param string|null $str + * @param string|null $str + * @param array $data */ public function is_not_unique($str, string $field, array $data): bool { @@ -158,7 +160,8 @@ public function in_list($value, string $list): bool * is_unique[table.field,ignore_field,ignore_value] * is_unique[users.email,id,5] * - * @param string|null $str + * @param string|null $str + * @param array $data */ public function is_unique($str, string $field, array $data): bool { @@ -178,9 +181,9 @@ public function is_unique($str, string $field, array $data): bool /** * Prepares the database query for uniqueness checks. * - * @param mixed $value The value to check. - * @param string $field The field parameters. - * @param array $data Additional data. + * @param mixed $value The value to check. + * @param string $field The field parameters. + * @param array $data Additional data. * * @return array{0: BaseBuilder, 1: string|null, 2: string|null} */ @@ -247,8 +250,8 @@ public function less_than_equal_to($str, string $max): bool /** * Matches the value of another field in $data. * - * @param string|null $str - * @param array $data Other field/value pairs + * @param string|null $str + * @param array $data Other field/value pairs */ public function matches($str, string $field, array $data): bool { @@ -343,9 +346,9 @@ public function required($str = null): bool * * required_with[password] * - * @param string|null $str - * @param string|null $fields List of fields that we should check if present - * @param array $data Complete list of fields from the form + * @param string|null $str + * @param string|null $fields List of fields that we should check if present + * @param array $data Complete list of fields from the form */ public function required_with($str = null, ?string $fields = null, array $data = []): bool { @@ -387,9 +390,10 @@ public function required_with($str = null, ?string $fields = null, array $data = * * required_without[id,email] * - * @param string|null $str - * @param string|null $otherFields The param fields of required_without[]. - * @param string|null $field This rule param fields aren't present, this field is required. + * @param string|null $str + * @param string|null $otherFields The param fields of required_without[]. + * @param string|null $field This rule param fields aren't present, this field is required. + * @param array $data */ public function required_without( $str = null, @@ -454,10 +458,10 @@ public function required_without( /** * The field exists in $data. * - * @param mixed $value The field value. - * @param string|null $param The rule's parameter. - * @param array $data The data to be validated. - * @param string|null $field The field name. + * @param mixed $value The field value. + * @param string|null $param The rule's parameter. + * @param array $data The data to be validated. + * @param string|null $field The field name. */ public function field_exists( $value = null, diff --git a/system/Validation/StrictRules/Rules.php b/system/Validation/StrictRules/Rules.php index 6d8dce14588b..cca4fdf494be 100644 --- a/system/Validation/StrictRules/Rules.php +++ b/system/Validation/StrictRules/Rules.php @@ -33,8 +33,8 @@ public function __construct() /** * The value does not match another field in $data. * - * @param mixed $str - * @param array $data Other field/value pairs + * @param mixed $str + * @param array $data Other field/value pairs */ public function differs( $str, @@ -137,7 +137,8 @@ public function greater_than_equal_to($str, string $min): bool * is_not_unique[table.field,where_field,where_value] * is_not_unique[menu.id,active,1] * - * @param mixed $str + * @param mixed $str + * @param array $data */ public function is_not_unique($str, string $field, array $data): bool { @@ -176,7 +177,8 @@ public function in_list($value, string $list): bool * is_unique[table.field,ignore_field,ignore_value] * is_unique[users.email,id,5] * - * @param mixed $str + * @param mixed $str + * @param array $data */ public function is_unique($str, string $field, array $data): bool { @@ -226,8 +228,8 @@ public function less_than_equal_to($str, string $max): bool /** * Matches the value of another field in $data. * - * @param mixed $str - * @param array $data Other field/value pairs + * @param mixed $str + * @param array $data Other field/value pairs */ public function matches( $str, @@ -339,9 +341,9 @@ public function required($str = null): bool * * required_with[password] * - * @param mixed $str - * @param string|null $fields List of fields that we should check if present - * @param array $data Complete list of fields from the form + * @param mixed $str + * @param string|null $fields List of fields that we should check if present + * @param array $data Complete list of fields from the form */ public function required_with($str = null, ?string $fields = null, array $data = []): bool { @@ -356,9 +358,10 @@ public function required_with($str = null, ?string $fields = null, array $data = * * required_without[id,email] * - * @param mixed $str - * @param string|null $otherFields The param fields of required_without[]. - * @param string|null $field This rule param fields aren't present, this field is required. + * @param mixed $str + * @param string|null $otherFields The param fields of required_without[]. + * @param string|null $field This rule param fields aren't present, this field is required. + * @param array $data */ public function required_without( $str = null, @@ -373,10 +376,10 @@ public function required_without( /** * The field exists in $data. * - * @param mixed $value The field value. - * @param string|null $param The rule's parameter. - * @param array $data The data to be validated. - * @param string|null $field The field name. + * @param mixed $value The field value. + * @param string|null $param The rule's parameter. + * @param array $data The data to be validated. + * @param string|null $field The field name. */ public function field_exists( $value = null, diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php index ceaa557ea404..b7c702aee6bc 100644 --- a/system/Validation/Validation.php +++ b/system/Validation/Validation.php @@ -14,7 +14,6 @@ namespace CodeIgniter\Validation; use Closure; -use CodeIgniter\Database\BaseConnection; use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Exceptions\LogicException; use CodeIgniter\HTTP\Exceptions\HTTPException; @@ -34,22 +33,20 @@ class Validation implements ValidationInterface /** * Files to load with validation functions. * - * @var array + * @var list */ protected $ruleSetFiles; /** * The loaded instances of our validation files. * - * @var array + * @var array */ protected $ruleSetInstances = []; /** * Stores the actual rules that should be run against $data. - * - * @var array}> - * + * ``` * [ * field1 => [ * 'label' => label, @@ -58,6 +55,9 @@ class Validation implements ValidationInterface * ], * ], * ] + * ``` + * + * @var array=, string|null=, string|null=): (bool|string))|string>}> */ protected $rules = []; @@ -65,14 +65,14 @@ class Validation implements ValidationInterface * The data that should be validated, * where 'key' is the alias, with value. * - * @var array + * @var array */ protected $data = []; /** * The data that was actually validated. * - * @var array + * @var array */ protected $validated = []; @@ -80,7 +80,7 @@ class Validation implements ValidationInterface * Any generated errors during validation. * 'key' is the alias, 'value' is the message. * - * @var array + * @var array */ protected $errors = []; @@ -88,7 +88,7 @@ class Validation implements ValidationInterface * Stores custom error message to use * during validation. Where 'key' is the alias. * - * @var array + * @var array> */ protected $customErrors = []; @@ -122,14 +122,6 @@ public function __construct($config, RendererInterface $view) $this->loadRuleSets(); } - /** - * Runs the validation process, returning true/false determining whether - * validation was successful or not. - * - * @param array|null $data The array of data to validate. - * @param string|null $group The predefined group of rules to apply. - * @param array|BaseConnection|non-empty-string|null $dbGroup The database group to use. - */ public function run(?array $data = null, ?string $group = null, $dbGroup = null): bool { if ($data === null) { @@ -165,10 +157,6 @@ public function run(?array $data = null, ?string $group = null, $dbGroup = null) $rules = $setup['rules']; - if (is_string($rules)) { - $rules = $this->splitRules($rules); - } - if (str_contains($field, '*')) { $flattenedArray = array_flatten_with_dots($data); @@ -238,15 +226,6 @@ private static function getRegex(string $field): string . '\z/'; } - /** - * Runs the validation process, returning true or false determining whether - * validation was successful or not. - * - * @param mixed $value The data to validate. - * @param array|string $rules The validation rules. - * @param list $errors The custom error message. - * @param string|null $dbGroup The database group to use. - */ public function check($value, $rules, array $errors = [], $dbGroup = null): bool { $this->reset(); @@ -263,9 +242,6 @@ public function check($value, $rules, array $errors = [], $dbGroup = null): bool ); } - /** - * Returns the actual validated data. - */ public function getValidated(): array { return $this->validated; @@ -277,10 +253,10 @@ public function getValidated(): array * the error to $this->errors and moves on to the next, * so that we can collect all of the first errors. * - * @param array|string $value - * @param array $rules - * @param array $data The array of data to validate, with `DBGroup`. - * @param string|null $originalField The original asterisk field name like "foo.*.bar". + * @param array|string $value + * @param list<(Closure(mixed, array=, string|null=, string|null=): (bool|string))|string> $rules + * @param array $data The array of data to validate, with `DBGroup`. + * @param string|null $originalField The original asterisk field name like "foo.*.bar". */ protected function processRules( string $field, @@ -386,9 +362,10 @@ protected function processRules( } /** - * @param array $data The array of data to validate, with `DBGroup`. + * @param list<(Closure(mixed, array=, string|null=, string|null=): (bool|string))|string> $rules + * @param array $data The array of data to validate, with `DBGroup`. * - * @return array|true The modified rules or true if we return early + * @return list<(Closure(mixed, array=, string|null=, string|null=): (bool|string))|string>|true The modified rules or true if we return early */ private function processIfExist(string $field, array $rules, array $data) { @@ -427,10 +404,11 @@ private function processIfExist(string $field, array $rules, array $data) } /** - * @param array|string $value - * @param array $data The array of data to validate, with `DBGroup`. + * @param array|string $value + * @param list<(Closure(mixed, array=, string|null=, string|null=): (bool|string))|string> $rules + * @param array $data The array of data to validate, with `DBGroup`. * - * @return array|true The modified rules or true if we return early + * @return list<(Closure(mixed, array=, string|null=, string|null=): (bool|string))|string>|true The modified rules or true if we return early */ private function processPermitEmpty($value, array $rules, array $data) { @@ -474,7 +452,7 @@ private function processPermitEmpty($value, array $rules, array $data) } /** - * @param Closure(bool|float|int|list|object|string|null, bool|float|int|list|object|string|null, string|null, string|null): (bool|string) $rule + * @param Closure(mixed, array=, string|null=, string|null=): (bool|string) $rule */ private function isClosure($rule): bool { @@ -483,6 +461,8 @@ private function isClosure($rule): bool /** * Is the array a string list `list`? + * + * @param array $array */ private function isStringList(array $array): bool { @@ -535,24 +515,6 @@ public function withRequest(RequestInterface $request): ValidationInterface return $this; } - /** - * Sets (or adds) an individual rule and custom error messages for a single - * field. - * - * The custom error message should be just the messages that apply to - * this field, like so: - * [ - * 'rule1' => 'message1', - * 'rule2' => 'message2', - * ] - * - * @param array|string $rules The validation rules. - * @param array $errors The custom error message. - * - * @return $this - * - * @throws InvalidArgumentException - */ public function setRule(string $field, ?string $label, $rules, array $errors = []) { if (! is_array($rules) && ! is_string($rules)) { @@ -575,24 +537,6 @@ public function setRule(string $field, ?string $label, $rules, array $errors = [ return $this; } - /** - * Stores the rules that should be used to validate the items. - * - * Rules should be an array formatted like: - * [ - * 'field' => 'rule1|rule2' - * ] - * - * The $errors array should be formatted like: - * [ - * 'field' => [ - * 'rule1' => 'message1', - * 'rule2' => 'message2', - * ], - * ] - * - * @param array $errors An array of custom error messages - */ public function setRules(array $rules, array $errors = []): ValidationInterface { $this->customErrors = $errors; @@ -625,9 +569,6 @@ public function setRules(array $rules, array $errors = []): ValidationInterface return $this; } - /** - * Returns all of the rules currently defined. - */ public function getRules(): array { return $this->rules; @@ -736,20 +677,6 @@ protected function loadRuleSets() } } - /** - * Loads custom rule groups (if set) into the current rules. - * - * Rules can be pre-defined in Config\Validation and can - * be any name, but must all still be an array of the - * same format used with setRules(). Additionally, check - * for {group}_errors for an array of custom error messages. - * - * @param non-empty-string|null $group - * - * @return array [rules, customErrors] - * - * @throws ValidationException - */ public function loadRuleGroup(?string $group = null) { if ($group === null || $group === '') { @@ -791,6 +718,11 @@ public function loadRuleGroup(?string $group = null) * The value of {id} would be replaced with the actual id in the form data: * * 'is_unique[users,email,id,13]' + * + * @param array=, string|null=, string|null=): (bool|string))|string>}> $rules + * @param array $data + * + * @return array=, string|null=, string|null=): (bool|string))|string>}> */ protected function fillPlaceholders(array $rules, array $data): array { @@ -850,6 +782,10 @@ protected function fillPlaceholders(array $rules, array $data): array /** * Retrieves valid placeholder fields. + * + * @param array $data + * + * @return list */ private function retrievePlaceholders(string $rule, array $data): array { @@ -957,6 +893,8 @@ protected function getErrorMessage( /** * Split rules string by pipe operator. + * + * @return list */ protected function splitRules(string $rules): array { diff --git a/system/Validation/ValidationInterface.php b/system/Validation/ValidationInterface.php index 836516d561b8..e90c41ffd341 100644 --- a/system/Validation/ValidationInterface.php +++ b/system/Validation/ValidationInterface.php @@ -25,9 +25,9 @@ interface ValidationInterface * Runs the validation process, returning true/false determining whether * validation was successful or not. * - * @param array|null $data The array of data to validate. - * @param string|null $group The predefined group of rules to apply. - * @param array|BaseConnection|non-empty-string|null $dbGroup The database group to use. + * @param array|null $data The array of data to validate. + * @param string|null $group The predefined group of rules to apply. + * @param array|BaseConnection|non-empty-string|null $dbGroup The database group to use. */ public function run(?array $data = null, ?string $group = null, $dbGroup = null): bool; @@ -35,10 +35,10 @@ public function run(?array $data = null, ?string $group = null, $dbGroup = null) * Check; runs the validation process, returning true or false * determining whether or not validation was successful. * - * @param mixed $value Value to validate. - * @param array|string $rules - * @param list $errors - * @param string|null $dbGroup The database group to use. + * @param mixed $value Value to validate. + * @param array|string $rules + * @param array $errors + * @param string|null $dbGroup The database group to use. * * @return bool True if valid, else false. */ @@ -55,13 +55,12 @@ public function withRequest(RequestInterface $request): ValidationInterface; * * The custom error message should be just the messages that apply to * this field, like so: + * ``` + * ['rule1' => 'message', 'rule2' => 'message'] + * ``` * - * [ - * 'rule' => 'message', - * 'rule' => 'message', - * ] - * - * @param array|string $rules + * @param array|string $rules + * @param array $errors * * @return $this */ @@ -69,11 +68,16 @@ public function setRule(string $field, ?string $label, $rules, array $errors = [ /** * Stores the rules that should be used to validate the items. + * + * @param array|string> $rules + * @param array> $messages */ public function setRules(array $rules, array $messages = []): ValidationInterface; /** * Returns all of the rules currently defined. + * + * @return array}> */ public function getRules(): array; @@ -139,7 +143,7 @@ public function reset(): ValidationInterface; * * @param non-empty-string|null $group * - * @return array + * @return list> */ public function loadRuleGroup(?string $group = null); @@ -160,6 +164,8 @@ public function showError(string $field, string $template = 'single'): string; /** * Returns the actual validated data. + * + * @return array */ public function getValidated(): array; } diff --git a/tests/system/Validation/FormatRulesTest.php b/tests/system/Validation/FormatRulesTest.php index d35855865e05..c2b6e0ca480c 100644 --- a/tests/system/Validation/FormatRulesTest.php +++ b/tests/system/Validation/FormatRulesTest.php @@ -160,6 +160,9 @@ public function testValidURLStrict(?string $url, bool $isLoose, bool $isStrict): $this->assertSame($isStrict, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideValidUrl(): iterable { yield from [ @@ -286,6 +289,9 @@ public function testValidEmail(?string $email, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideValidEmail(): iterable { yield from [ @@ -318,6 +324,9 @@ public function testValidEmails(?string $email, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideValidEmails(): iterable { yield from [ @@ -362,6 +371,9 @@ public function testValidIP(?string $ip, ?string $which, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideValidIP(): iterable { yield from [ @@ -430,6 +442,9 @@ public function testString($str, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideString(): iterable { yield from [ @@ -462,6 +477,9 @@ public function testAlpha(?string $str, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideAlpha(): iterable { yield from [ @@ -502,6 +520,9 @@ public function testAlphaSpace(?string $value, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideAlphaSpace(): iterable { yield from [ @@ -546,6 +567,9 @@ public function testAlphaNumeric(?string $str, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function alphaNumericProvider(): iterable { yield from [ @@ -582,6 +606,9 @@ public function testAlphaNumericPunct(?string $str, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideAlphaNumericPunct(): iterable { yield from [ @@ -710,6 +737,9 @@ public function testAlphaDash(?string $str, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideAlphaDash(): iterable { yield from [ @@ -746,6 +776,9 @@ public function testHex(?string $str, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideHex(): iterable { yield from [ @@ -782,6 +815,9 @@ public function testNumeric(?string $str, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideNumeric(): iterable { yield from [ @@ -856,6 +892,9 @@ public function testNumericWithInvalidTypeData($value, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideInvalidIntegerType(): iterable { // TypeError : CodeIgniter\Validation\FormatRules::integer(): Argument #1 ($str) must be of type ?string, array given @@ -900,6 +939,9 @@ public function testInteger(?string $str, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideInteger(): iterable { yield from [ @@ -952,6 +994,9 @@ public function testDecimal(?string $str, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideDecimal(): iterable { yield from [ @@ -1008,6 +1053,9 @@ public function testNatural(?string $str, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideNatural(): iterable { yield from [ @@ -1048,6 +1096,9 @@ public function testNaturalNoZero(?string $str, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideNaturalNoZero(): iterable { yield from [ @@ -1088,6 +1139,9 @@ public function testBase64(?string $str, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideBase64(): iterable { yield from [ @@ -1124,6 +1178,9 @@ public function testJson(?string $str, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideJson(): iterable { yield from [ @@ -1180,6 +1237,9 @@ public function testTimeZone(?string $str, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideTimeZone(): iterable { yield from [ @@ -1216,6 +1276,9 @@ public function testValidDate(?string $str, ?string $format, bool $expected): vo $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideValidDate(): iterable { yield from [ diff --git a/tests/system/Validation/RulesTest.php b/tests/system/Validation/RulesTest.php index deddab05dc4d..27806ac0fdbc 100644 --- a/tests/system/Validation/RulesTest.php +++ b/tests/system/Validation/RulesTest.php @@ -59,6 +59,9 @@ protected function setUp(): void $this->validation->reset(); } + /** + * @param array $data + */ #[DataProvider('provideRequired')] public function testRequired(array $data, bool $expected): void { @@ -66,6 +69,9 @@ public function testRequired(array $data, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable, bool}> + */ public static function provideRequired(): iterable { yield from [ @@ -79,6 +85,10 @@ public static function provideRequired(): iterable ]; } + /** + * @param array|string> $rules + * @param array $data + */ #[DataProvider('provideIfExist')] public function testIfExist(array $rules, array $data, bool $expected): void { @@ -86,6 +96,9 @@ public function testIfExist(array $rules, array $data, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable|string>, array, bool}> + */ public static function provideIfExist(): iterable { yield from [ @@ -143,6 +156,10 @@ public function testIfExistArray(): void $this->validation->run($data); } + /** + * @param array|string> $rules + * @param array $data + */ #[DataProvider('providePermitEmpty')] public function testPermitEmpty(array $rules, array $data, bool $expected): void { @@ -150,6 +167,9 @@ public function testPermitEmpty(array $rules, array $data, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable|string>, array, bool}> + */ public static function providePermitEmpty(): iterable { yield from [ @@ -310,6 +330,9 @@ public static function providePermitEmpty(): iterable ]; } + /** + * @param array $data + */ #[DataProvider('provideMatches')] public function testMatches(array $data, bool $expected): void { @@ -317,6 +340,9 @@ public function testMatches(array $data, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable, bool}> + */ public static function provideMatches(): iterable { yield from [ @@ -332,6 +358,9 @@ public static function provideMatches(): iterable ]; } + /** + * @param array $data + */ #[DataProvider('provideMatchesNestedCases')] public function testMatchesNested(array $data, bool $expected): void { @@ -383,6 +412,9 @@ public function testMatchesWithDotArrayFail(): void ); } + /** + * @param array $data + */ #[DataProvider('provideDiffers')] public function testDiffers(array $data, bool $expected): void { @@ -390,6 +422,9 @@ public function testDiffers(array $data, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable, bool}> + */ public static function provideDiffers(): iterable { yield from [ @@ -405,6 +440,9 @@ public static function provideDiffers(): iterable ]; } + /** + * @param array $data + */ #[DataProvider('provideMatchesNestedCases')] public function testDiffersNested(array $data, bool $expected): void { @@ -412,6 +450,9 @@ public function testDiffersNested(array $data, bool $expected): void $this->assertSame(! $expected, $this->validation->run($data)); } + /** + * @return iterable, bool}> + */ public static function provideMatchesNestedCases(): iterable { yield from [ @@ -464,6 +505,9 @@ public function testDiffersWithDotArrayFail(): void ); } + /** + * @param array $data + */ #[DataProvider('provideEquals')] public function testEquals(array $data, string $param, bool $expected): void { @@ -471,6 +515,9 @@ public function testEquals(array $data, string $param, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable, string, bool}> + */ public static function provideEquals(): iterable { yield from [ @@ -489,6 +536,9 @@ public function testMinLength(?string $data, string $length, bool $expected): vo $this->assertSame($expected, $this->validation->run(['foo' => $data])); } + /** + * @return iterable + */ public static function provideMinLengthCases(): iterable { yield from [ @@ -522,6 +572,9 @@ public function testExactLength($data, bool $expected): void $this->assertSame($expected, $this->validation->run(['foo' => $data])); } + /** + * @return iterable + */ public static function provideExactLength(): iterable { yield from [ @@ -550,6 +603,9 @@ public function testGreaterThan(?string $first, ?string $second, bool $expected) $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideGreaterThan(): iterable { yield from [ @@ -573,6 +629,9 @@ public function testGreaterThanEqual(?string $first, ?string $second, bool $expe $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideGreaterThanEqual(): iterable { yield from [ @@ -597,6 +656,9 @@ public function testLessThan(?string $first, ?string $second, bool $expected): v $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideLessThan(): iterable { yield from [ @@ -621,6 +683,9 @@ public function testLessThanEqual(?string $first, ?string $second, bool $expecte $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideLessThanEqual(): iterable { yield from [ @@ -653,6 +718,9 @@ public function testNotInList(?string $first, ?string $second, bool $expected): $this->assertSame(! $expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideInList(): iterable { yield from [ @@ -687,6 +755,9 @@ public function testRequiredWith(string $field, ?string $check, bool $expected): $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideRequiredWith(): iterable { yield from [ @@ -719,8 +790,9 @@ public static function provideRequiredWith(): iterable ]; } + /** @see https://github.com/codeigniter4/CodeIgniter4/issues/7557 */ /** - * @see https://github.com/codeigniter4/CodeIgniter4/issues/7557 + * @param array $data */ #[DataProvider('provideRequiredWithAndOtherRules')] public function testRequiredWithAndOtherRules(bool $expected, array $data): void @@ -734,6 +806,9 @@ public function testRequiredWithAndOtherRules(bool $expected, array $data): void $this->assertSame($expected, $result); } + /** + * @return iterable}> + */ public static function provideRequiredWithAndOtherRules(): iterable { yield from [ @@ -756,6 +831,9 @@ public static function provideRequiredWithAndOtherRules(): iterable ]; } + /** + * @param array $data + */ #[DataProvider('provideRequiredWithAndOtherRuleWithValueZero')] public function testRequiredWithAndOtherRuleWithValueZero(bool $expected, array $data): void { @@ -769,6 +847,9 @@ public function testRequiredWithAndOtherRuleWithValueZero(bool $expected, array $this->assertSame($expected, $result); } + /** + * @return iterable}> + */ public static function provideRequiredWithAndOtherRuleWithValueZero(): iterable { yield from [ @@ -797,6 +878,9 @@ public function testRequiredWithout(string $field, ?string $check, bool $expecte $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideRequiredWithout(): iterable { yield from [ @@ -841,6 +925,9 @@ public function testRequiredWithoutMultiple(string $foo, string $bar, string $ba $this->assertSame($result, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideRequiredWithoutMultiple(): iterable { yield from [ @@ -877,6 +964,9 @@ public static function provideRequiredWithoutMultiple(): iterable ]; } + /** + * @param array $data + */ #[DataProvider('provideRequiredWithoutMultipleWithoutFields')] public function testRequiredWithoutMultipleWithoutFields(array $data, bool $result): void { @@ -885,6 +975,9 @@ public function testRequiredWithoutMultipleWithoutFields(array $data, bool $resu $this->assertSame($result, $this->validation->run($data)); } + /** + * @return iterable, bool}> + */ public static function provideRequiredWithoutMultipleWithoutFields(): iterable { yield from [ @@ -917,6 +1010,10 @@ public static function provideRequiredWithoutMultipleWithoutFields(): iterable ]; } + /** + * @param array|string> $rules + * @param array $data + */ #[DataProvider('provideFieldExists')] public function testFieldExists(array $rules, array $data, bool $expected): void { @@ -924,6 +1021,9 @@ public function testFieldExists(array $rules, array $data, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable|string>, array, bool}> + */ public static function provideFieldExists(): iterable { // Do not use `foo`, because there is a lang file `Foo`, and diff --git a/tests/system/Validation/StrictRules/CreditCardRulesTest.php b/tests/system/Validation/StrictRules/CreditCardRulesTest.php index 21691ebd6fae..deda7d76f984 100644 --- a/tests/system/Validation/StrictRules/CreditCardRulesTest.php +++ b/tests/system/Validation/StrictRules/CreditCardRulesTest.php @@ -69,6 +69,8 @@ public function testValidCCNumber(string $type, ?string $number, bool $expected) * Cards shown are test cards found around the web. * * @see https://www.paypalobjects.com/en_US/vhelp/paypalmanager_help/credit_card_numbers.htm + * + * @return iterable */ public static function provideValidCCNumber(): iterable { diff --git a/tests/system/Validation/StrictRules/RulesTest.php b/tests/system/Validation/StrictRules/RulesTest.php index 0fc4f9e38c6e..9cb5744a285d 100644 --- a/tests/system/Validation/StrictRules/RulesTest.php +++ b/tests/system/Validation/StrictRules/RulesTest.php @@ -48,6 +48,10 @@ final class RulesTest extends TraditionalRulesTest ], ]; + /** + * @param array|string> $rules + * @param array $data + */ #[DataProvider('providePermitEmptyStrict')] public function testPermitEmptyStrict(array $rules, array $data, bool $expected): void { @@ -55,6 +59,9 @@ public function testPermitEmptyStrict(array $rules, array $data, bool $expected) $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable|string>, array, bool}> + */ public static function providePermitEmptyStrict(): iterable { yield from [ @@ -114,6 +121,9 @@ public function testGreaterThanEqualStrict($value, string $param, bool $expected $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideGreaterThanEqualStrict(): iterable { yield from [ @@ -139,6 +149,9 @@ public function testGreaterThanStrict($value, string $param, bool $expected): vo $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideGreaterThanStrict(): iterable { yield from [ @@ -165,6 +178,9 @@ public function testLessThanStrict($value, string $param, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideLessThanStrict(): iterable { yield from [ @@ -192,6 +208,9 @@ public function testLessEqualThanStrict($value, ?string $param, bool $expected): $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideLessEqualThanStrict(): iterable { yield from [ @@ -205,6 +224,9 @@ public static function provideLessEqualThanStrict(): iterable ]; } + /** + * @param array $data + */ #[DataProvider('provideMatches')] public function testMatches(array $data, bool $expected): void { @@ -212,6 +234,9 @@ public function testMatches(array $data, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable, bool}> + */ public static function provideMatches(): iterable { yield from [ @@ -227,6 +252,9 @@ public static function provideMatches(): iterable ]; } + /** + * @param array $data + */ #[DataProvider('provideDiffers')] public function testDiffers(array $data, bool $expected): void { @@ -234,6 +262,9 @@ public function testDiffers(array $data, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable, bool}> + */ public static function provideDiffers(): iterable { yield from [ diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php index 3a0f57a0e8e6..6c7cb20de0e9 100644 --- a/tests/system/Validation/ValidationTest.php +++ b/tests/system/Validation/ValidationTest.php @@ -213,6 +213,9 @@ public function testSetRuleRulesFormat(bool $expected, $rules): void $this->addToAssertionCount(1); } + /** + * @return iterable + */ public static function provideSetRuleRulesFormat(): iterable { yield 'fail-simple-object' => [ @@ -403,6 +406,8 @@ public function testCallableRule(): void /** * Validation rule2 + * + * @param array $data */ public function rule2(mixed $value, array $data, ?string &$error, string $field): bool { @@ -473,6 +478,9 @@ public function testCanValidatetArrayData($value, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideCanValidatetArrayData(): iterable { yield 'list array' => [ @@ -524,6 +532,9 @@ public function testIsIntWithInvalidTypeData($value, bool $expected): void $this->assertSame($expected, $this->validation->run($data)); } + /** + * @return iterable + */ public static function provideIsIntWithInvalidTypeData(): iterable { yield 'array with int' => [ @@ -752,6 +763,11 @@ public function testRunWithCustomErrorsAndAsteriskField(): void * @param list|string $rules * @param string $expected */ + /** + * @param array|string> $errors + * @param mixed $rules + * @param mixed $expected + */ #[DataProvider('provideRulesSetup')] public function testRulesSetup($rules, $expected, array $errors = []): void { @@ -762,6 +778,9 @@ public function testRulesSetup($rules, $expected, array $errors = []): void $this->assertSame($expected, $this->validation->getError('foo')); } + /** + * @return iterable|string, string, array|string>}> + */ public static function provideRulesSetup(): iterable { yield from [ @@ -1054,9 +1073,9 @@ public function testShowBadTemplate(): void public function testNoRuleSetsSetup(): void { - try { - $rulesets = static::$config['ruleSets']; + $rulesets = static::$config['ruleSets']; + try { static::$config['ruleSets'] = null; (new Validation((object) static::$config, service('renderer'))) ->reset() @@ -1188,6 +1207,11 @@ public function testRulesForObjectField(): void ], $this->validation->getErrors()); } + /** + * @param array $body + * @param array|string> $rules + * @param array $results + */ #[DataProvider('provideRulesForArrayField')] public function testRulesForArrayField(array $body, array $rules, array $results): void { @@ -1201,6 +1225,9 @@ public function testRulesForArrayField(array $body, array $rules, array $results $this->assertSame($results, $this->validation->getErrors()); } + /** + * @return iterable, rules: array|string>, results: array}> + */ public static function provideRulesForArrayField(): iterable { yield from [ @@ -1450,8 +1477,10 @@ public function testTranslatedLabelTagReplacement(): void $this->assertSame($expected, $errors['Username']); } + /** @see https://github.com/codeigniter4/CodeIgniter4/issues/4521 */ /** - * @see https://github.com/codeigniter4/CodeIgniter4/issues/4521 + * @param array|string> $rules + * @param array $data */ #[DataProvider('provideIfExistRuleWithAsterisk')] public function testIfExistRuleWithAsterisk(bool $expected, array $rules, array $data): void @@ -1460,6 +1489,9 @@ public function testIfExistRuleWithAsterisk(bool $expected, array $rules, array $this->assertSame($expected, $actual); } + /** + * @return iterable|string>, array}> + */ public static function provideIfExistRuleWithAsterisk(): iterable { yield 'dot-on-end-fail' => [ @@ -1519,8 +1551,10 @@ public static function provideIfExistRuleWithAsterisk(): iterable ]; } + /** @see https://github.com/codeigniter4/CodeIgniter4/issues/4510 */ /** - * @see https://github.com/codeigniter4/CodeIgniter4/issues/4510 + * @param array|string> $rules + * @param array $data */ #[DataProvider('provideValidationOfArrayData')] public function testValidationOfArrayData(bool $expected, array $rules, array $data): void @@ -1529,6 +1563,9 @@ public function testValidationOfArrayData(bool $expected, array $rules, array $d $this->assertSame($expected, $actual); } + /** + * @return iterable|string>, array}> + */ public static function provideValidationOfArrayData(): iterable { yield 'fail-empty-string' => [ @@ -1574,8 +1611,9 @@ public static function provideValidationOfArrayData(): iterable ]; } + /** @see https://github.com/codeigniter4/CodeIgniter4/issues/4929 */ /** - * @see https://github.com/codeigniter4/CodeIgniter4/issues/4929 + * @param array $expected */ #[DataProvider('provideSplittingOfComplexStringRules')] public function testSplittingOfComplexStringRules(string $input, array $expected): void @@ -1584,6 +1622,9 @@ public function testSplittingOfComplexStringRules(string $input, array $expected $this->assertSame($expected, $splitter($input)); } + /** + * @return iterable}> + */ public static function provideSplittingOfComplexStringRules(): iterable { yield [ @@ -1636,7 +1677,7 @@ public static function provideSplittingOfComplexStringRules(): iterable * internal method to simplify placeholder replacement test * REQUIRES THE RULES TO BE SET FOR THE FIELD "foo" * - * @param array|null $data optional POST data, needs to contain the key $placeholderField to pass + * @param array|null $data Optional POST data, needs to contain the key $placeholderField to pass * * @source https://github.com/codeigniter4/CodeIgniter4/pull/3910#issuecomment-784922913 */ diff --git a/utils/phpstan-baseline/argument.type.neon b/utils/phpstan-baseline/argument.type.neon index addcb1d223e7..98aa86990719 100644 --- a/utils/phpstan-baseline/argument.type.neon +++ b/utils/phpstan-baseline/argument.type.neon @@ -1,4 +1,4 @@ -# total 63 errors +# total 62 errors parameters: ignoreErrors: @@ -167,11 +167,6 @@ parameters: count: 1 path: ../../tests/system/Test/TestResponseTest.php - - - message: '#^Parameter \#3 \$errors of method CodeIgniter\\Validation\\Validation\:\:check\(\) expects list\, array\{is_numeric\: ''Nope\. Not a number\.''\} given\.$#' - count: 1 - path: ../../tests/system/Validation/ValidationTest.php - - message: '#^Parameter \#2 \$context of method CodeIgniter\\View\\Parser\:\:setData\(\) expects ''attr''\|''css''\|''html''\|''js''\|''raw''\|''url''\|null, ''unknown'' given\.$#' count: 3 diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index a382fda9a5d4..1aabccca5c16 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 1217 errors +# total 1062 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index f67d1b49920c..94b09bc195b7 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 966 errors +# total 812 errors parameters: ignoreErrors: @@ -2922,326 +2922,6 @@ parameters: count: 1 path: ../../system/Typography/Typography.php - - - message: '#^Property CodeIgniter\\Validation\\CreditCardRules\:\:\$cards type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/CreditCardRules.php - - - - message: '#^Method CodeIgniter\\Validation\\DotArrayFilter\:\:filter\(\) has parameter \$array with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/DotArrayFilter.php - - - - message: '#^Method CodeIgniter\\Validation\\DotArrayFilter\:\:filter\(\) has parameter \$indexes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/DotArrayFilter.php - - - - message: '#^Method CodeIgniter\\Validation\\DotArrayFilter\:\:filter\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/DotArrayFilter.php - - - - message: '#^Method CodeIgniter\\Validation\\DotArrayFilter\:\:run\(\) has parameter \$array with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/DotArrayFilter.php - - - - message: '#^Method CodeIgniter\\Validation\\DotArrayFilter\:\:run\(\) has parameter \$indexes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/DotArrayFilter.php - - - - message: '#^Method CodeIgniter\\Validation\\DotArrayFilter\:\:run\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/DotArrayFilter.php - - - - message: '#^Method CodeIgniter\\Validation\\Rules\:\:differs\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Rules.php - - - - message: '#^Method CodeIgniter\\Validation\\Rules\:\:field_exists\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Rules.php - - - - message: '#^Method CodeIgniter\\Validation\\Rules\:\:is_not_unique\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Rules.php - - - - message: '#^Method CodeIgniter\\Validation\\Rules\:\:is_unique\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Rules.php - - - - message: '#^Method CodeIgniter\\Validation\\Rules\:\:matches\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Rules.php - - - - message: '#^Method CodeIgniter\\Validation\\Rules\:\:required_with\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Rules.php - - - - message: '#^Method CodeIgniter\\Validation\\Rules\:\:required_without\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Rules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:differs\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/Rules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:field_exists\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/Rules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:is_not_unique\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/Rules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:is_unique\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/Rules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:matches\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/Rules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:required_with\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/Rules.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\Rules\:\:required_without\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/StrictRules/Rules.php - - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:check\(\) has parameter \$rules with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:fillPlaceholders\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:fillPlaceholders\(\) has parameter \$rules with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:fillPlaceholders\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:getRules\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:getValidated\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:isStringList\(\) has parameter \$array with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:loadRuleGroup\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:processIfExist\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:processIfExist\(\) has parameter \$rules with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:processIfExist\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:processPermitEmpty\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:processPermitEmpty\(\) has parameter \$rules with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:processPermitEmpty\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:processPermitEmpty\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:processRules\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:processRules\(\) has parameter \$rules with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:processRules\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:retrievePlaceholders\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:retrievePlaceholders\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:run\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:run\(\) has parameter \$dbGroup with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:setRule\(\) has parameter \$errors with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:setRule\(\) has parameter \$rules with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:setRules\(\) has parameter \$errors with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:setRules\(\) has parameter \$rules with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Method CodeIgniter\\Validation\\Validation\:\:splitRules\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Property CodeIgniter\\Validation\\Validation\:\:\$customErrors type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Property CodeIgniter\\Validation\\Validation\:\:\$data type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Property CodeIgniter\\Validation\\Validation\:\:\$errors type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Property CodeIgniter\\Validation\\Validation\:\:\$ruleSetFiles type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Property CodeIgniter\\Validation\\Validation\:\:\$ruleSetInstances type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Property CodeIgniter\\Validation\\Validation\:\:\$validated type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/Validation.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationInterface\:\:check\(\) has parameter \$rules with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/ValidationInterface.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationInterface\:\:getRules\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/ValidationInterface.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationInterface\:\:getValidated\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/ValidationInterface.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationInterface\:\:loadRuleGroup\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/ValidationInterface.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationInterface\:\:run\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/ValidationInterface.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationInterface\:\:run\(\) has parameter \$dbGroup with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/ValidationInterface.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationInterface\:\:setRule\(\) has parameter \$errors with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/ValidationInterface.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationInterface\:\:setRule\(\) has parameter \$rules with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/ValidationInterface.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationInterface\:\:setRules\(\) has parameter \$messages with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/ValidationInterface.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationInterface\:\:setRules\(\) has parameter \$rules with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Validation/ValidationInterface.php - - message: '#^Method CodeIgniter\\View\\Cell\:\:determineClass\(\) return type has no value type specified in iterable type array\.$#' count: 1 @@ -4317,456 +3997,6 @@ parameters: count: 1 path: ../../tests/system/Throttle/ThrottleTest.php - - - message: '#^Method CodeIgniter\\Validation\\FormatRulesTest\:\:alphaNumericProvider\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/FormatRulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\FormatRulesTest\:\:provideAlphaDash\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/FormatRulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\FormatRulesTest\:\:provideAlphaNumericPunct\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/FormatRulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\FormatRulesTest\:\:provideAlphaSpace\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/FormatRulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\FormatRulesTest\:\:provideAlpha\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/FormatRulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\FormatRulesTest\:\:provideBase64\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/FormatRulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\FormatRulesTest\:\:provideDecimal\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/FormatRulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\FormatRulesTest\:\:provideHex\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/FormatRulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\FormatRulesTest\:\:provideInteger\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/FormatRulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\FormatRulesTest\:\:provideInvalidIntegerType\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/FormatRulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\FormatRulesTest\:\:provideJson\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/FormatRulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\FormatRulesTest\:\:provideNaturalNoZero\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/FormatRulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\FormatRulesTest\:\:provideNatural\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/FormatRulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\FormatRulesTest\:\:provideNumeric\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/FormatRulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\FormatRulesTest\:\:provideString\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/FormatRulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\FormatRulesTest\:\:provideTimeZone\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/FormatRulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\FormatRulesTest\:\:provideValidDate\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/FormatRulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\FormatRulesTest\:\:provideValidEmail\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/FormatRulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\FormatRulesTest\:\:provideValidEmails\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/FormatRulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\FormatRulesTest\:\:provideValidIP\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/FormatRulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\FormatRulesTest\:\:provideValidUrl\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/FormatRulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:provideDiffers\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:provideEquals\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:provideExactLength\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:provideFieldExists\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:provideGreaterThanEqual\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:provideGreaterThan\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:provideIfExist\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:provideInList\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:provideLessThanEqual\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:provideLessThan\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:provideMatchesNestedCases\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:provideMatches\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:provideMinLengthCases\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:providePermitEmpty\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:provideRequiredWithAndOtherRuleWithValueZero\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:provideRequiredWithAndOtherRules\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:provideRequiredWith\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:provideRequiredWithoutMultipleWithoutFields\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:provideRequiredWithoutMultiple\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:provideRequiredWithout\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:provideRequired\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:testDiffersNested\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:testDiffers\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:testEquals\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:testFieldExists\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:testFieldExists\(\) has parameter \$rules with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:testIfExist\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:testIfExist\(\) has parameter \$rules with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:testMatchesNested\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:testMatches\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:testPermitEmpty\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:testPermitEmpty\(\) has parameter \$rules with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:testRequiredWithAndOtherRuleWithValueZero\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:testRequiredWithAndOtherRules\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:testRequiredWithoutMultipleWithoutFields\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\RulesTest\:\:testRequired\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\CreditCardRulesTest\:\:provideValidCCNumber\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/StrictRules/CreditCardRulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\FormatRulesTest\:\:provideAlphaSpace\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/StrictRules/FormatRulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\FormatRulesTest\:\:provideInvalidIntegerType\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/StrictRules/FormatRulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\RulesTest\:\:provideDiffers\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/StrictRules/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\RulesTest\:\:provideGreaterThanEqualStrict\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/StrictRules/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\RulesTest\:\:provideGreaterThanStrict\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/StrictRules/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\RulesTest\:\:provideLessEqualThanStrict\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/StrictRules/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\RulesTest\:\:provideLessThanStrict\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/StrictRules/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\RulesTest\:\:provideMatches\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/StrictRules/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\RulesTest\:\:providePermitEmptyStrict\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/StrictRules/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\RulesTest\:\:testDiffers\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/StrictRules/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\RulesTest\:\:testMatches\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/StrictRules/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\RulesTest\:\:testPermitEmptyStrict\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/StrictRules/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\StrictRules\\RulesTest\:\:testPermitEmptyStrict\(\) has parameter \$rules with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/StrictRules/RulesTest.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationTest\:\:placeholderReplacementResultDetermination\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/ValidationTest.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationTest\:\:provideCanValidatetArrayData\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/ValidationTest.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationTest\:\:provideIfExistRuleWithAsterisk\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/ValidationTest.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationTest\:\:provideIsIntWithInvalidTypeData\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/ValidationTest.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationTest\:\:provideRulesForArrayField\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/ValidationTest.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationTest\:\:provideRulesSetup\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/ValidationTest.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationTest\:\:provideSetRuleRulesFormat\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/ValidationTest.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationTest\:\:provideSplittingOfComplexStringRules\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/ValidationTest.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationTest\:\:provideValidationOfArrayData\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Validation/ValidationTest.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationTest\:\:rule2\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/ValidationTest.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationTest\:\:testIfExistRuleWithAsterisk\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/ValidationTest.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationTest\:\:testIfExistRuleWithAsterisk\(\) has parameter \$rules with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/ValidationTest.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationTest\:\:testRulesForArrayField\(\) has parameter \$body with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/ValidationTest.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationTest\:\:testRulesForArrayField\(\) has parameter \$results with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/ValidationTest.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationTest\:\:testRulesForArrayField\(\) has parameter \$rules with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/ValidationTest.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationTest\:\:testRulesSetup\(\) has parameter \$errors with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/ValidationTest.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationTest\:\:testSplittingOfComplexStringRules\(\) has parameter \$expected with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/ValidationTest.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationTest\:\:testValidationOfArrayData\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/ValidationTest.php - - - - message: '#^Method CodeIgniter\\Validation\\ValidationTest\:\:testValidationOfArrayData\(\) has parameter \$rules with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Validation/ValidationTest.php - - message: '#^Method CodeIgniter\\View\\DBResultDummy\:\:getFieldNames\(\) return type has no value type specified in iterable type array\.$#' count: 1 From 69796fcaccac2f34617c02cc4149c0e393ac3cb1 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Mon, 17 Aug 2026 12:40:15 +0800 Subject: [PATCH 061/163] refactor: clear out phpstan errors in `HTTP` source and tests (#10462) --- system/HTTP/CLIRequest.php | 56 +- system/HTTP/CURLRequest.php | 49 +- system/HTTP/Files/FileCollection.php | 16 +- system/HTTP/Files/UploadedFile.php | 7 - system/HTTP/IncomingRequest.php | 66 +- system/HTTP/Message.php | 6 +- system/HTTP/MessageInterface.php | 4 +- system/HTTP/MessageTrait.php | 6 +- system/HTTP/Negotiate.php | 37 +- system/HTTP/OutgoingRequest.php | 5 +- system/HTTP/RedirectResponse.php | 6 +- system/HTTP/RequestInterface.php | 4 +- system/HTTP/RequestTrait.php | 18 +- system/HTTP/Response.php | 2 +- system/HTTP/ResponseInterface.php | 24 +- system/HTTP/ResponseTrait.php | 31 +- system/HTTP/SiteURI.php | 35 +- system/HTTP/URI.php | 5 +- system/Superglobals.php | 2 +- tests/system/HTTP/Files/FileMovingTest.php | 6 +- tests/system/HTTP/MessageTest.php | 7 +- tests/system/HTTP/RedirectResponseTest.php | 5 - tests/system/HTTP/ResponseCookieTest.php | 3 + tests/system/HTTP/ResponseTest.php | 9 + .../SiteURIFactoryDetectRoutePathTest.php | 6 + tests/system/HTTP/SiteURIFactoryTest.php | 6 + tests/system/HTTP/SiteURITest.php | 15 + tests/system/HTTP/URITest.php | 26 +- utils/phpstan-baseline/argument.type.neon | 52 +- utils/phpstan-baseline/loader.neon | 2 +- .../method.childReturnType.neon | 8 +- utils/phpstan-baseline/method.notFound.neon | 17 +- .../missingType.iterableValue.neon | 812 +----------------- .../missingType.parameter.neon | 22 +- .../phpstan-baseline/property.phpDocType.neon | 32 +- 35 files changed, 374 insertions(+), 1033 deletions(-) diff --git a/system/HTTP/CLIRequest.php b/system/HTTP/CLIRequest.php index 67c6ad4c1c7c..f1e98c50bf79 100644 --- a/system/HTTP/CLIRequest.php +++ b/system/HTTP/CLIRequest.php @@ -35,21 +35,21 @@ class CLIRequest extends Request /** * Stores the segments of our cli "URI" command. * - * @var array + * @var list */ protected $segments = []; /** * Command line options and their values. * - * @var array + * @var array */ protected $options = []; /** * Command line arguments (segments and options). * - * @var array + * @var array */ protected $args = []; @@ -101,6 +101,8 @@ public function getPath(): string /** * Returns an associative array of all CLI options found, with * their values. + * + * @return array */ public function getOptions(): array { @@ -109,6 +111,8 @@ public function getOptions(): array /** * Returns an array of all CLI arguments (segments and options). + * + * @return array */ public function getArgs(): array { @@ -117,6 +121,8 @@ public function getArgs(): array /** * Returns the path segments. + * + * @return list */ public function getSegments(): array { @@ -226,11 +232,11 @@ public function isCLI(): bool /** * Fetch an item from GET data. * - * @param array|string|null $index Index for item to fetch from $_GET. - * @param int|null $filter A filter name to apply. - * @param array|int|null $flags + * @param list|string|null $index Index for item to fetch from $_GET. + * @param int|null $filter A filter name to apply. + * @param array|int|null $flags * - * @return array|null + * @return array{}|null */ public function getGet($index = null, $filter = null, $flags = null) { @@ -240,11 +246,11 @@ public function getGet($index = null, $filter = null, $flags = null) /** * Fetch an item from POST. * - * @param array|string|null $index Index for item to fetch from $_POST. - * @param int|null $filter A filter name to apply - * @param array|int|null $flags + * @param list|string|null $index Index for item to fetch from $_POST. + * @param int|null $filter A filter name to apply + * @param array|int|null $flags * - * @return array|null + * @return array{}|null */ public function getPost($index = null, $filter = null, $flags = null) { @@ -254,11 +260,11 @@ public function getPost($index = null, $filter = null, $flags = null) /** * Fetch an item from POST data with fallback to GET. * - * @param array|string|null $index Index for item to fetch from $_POST or $_GET - * @param int|null $filter A filter name to apply - * @param array|int|null $flags + * @param list|string|null $index Index for item to fetch from $_POST or $_GET + * @param int|null $filter A filter name to apply + * @param array|int|null $flags * - * @return array|null + * @return array{}|null */ public function getPostGet($index = null, $filter = null, $flags = null) { @@ -268,11 +274,11 @@ public function getPostGet($index = null, $filter = null, $flags = null) /** * Fetch an item from GET data with fallback to POST. * - * @param array|string|null $index Index for item to be fetched from $_GET or $_POST - * @param int|null $filter A filter name to apply - * @param array|int|null $flags + * @param list|string|null $index Index for item to be fetched from $_GET or $_POST + * @param int|null $filter A filter name to apply + * @param array|int|null $flags * - * @return array|null + * @return array{}|null */ public function getGetPost($index = null, $filter = null, $flags = null) { @@ -282,11 +288,11 @@ public function getGetPost($index = null, $filter = null, $flags = null) /** * This is a place holder for calls from cookie_helper get_cookie(). * - * @param array|string|null $index Index for item to be fetched from $_COOKIE - * @param int|null $filter A filter name to be applied - * @param mixed $flags + * @param list|string|null $index Index for item to be fetched from $_COOKIE + * @param int|null $filter A filter name to be applied + * @param mixed $flags * - * @return array|null + * @return array{}|null */ public function getCookie($index = null, $filter = null, $flags = null) { @@ -294,9 +300,9 @@ public function getCookie($index = null, $filter = null, $flags = null) } /** - * @param array|string|null $index + * @param list|string|null $index * - * @return array|null + * @return array{}|null */ private function returnNullOrEmptyArray($index) { diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php index b75c477ab9e1..dc83f63763ca 100644 --- a/system/HTTP/CURLRequest.php +++ b/system/HTTP/CURLRequest.php @@ -51,14 +51,14 @@ class CURLRequest extends OutgoingRequest /** * The setting values * - * @var array + * @var array */ protected $config; /** * The default setting values * - * @var array + * @var array */ protected $defaultConfig = [ 'timeout' => 0.0, @@ -71,7 +71,7 @@ class CURLRequest extends OutgoingRequest * Default values for when 'allow_redirects' * option is true. * - * @var array + * @var array */ protected $redirectDefaults = [ 'max' => 5, @@ -92,6 +92,8 @@ class CURLRequest extends OutgoingRequest /** * The default options from the constructor. Applied to all requests. + * + * @var array */ private readonly array $defaultOptions; @@ -156,7 +158,8 @@ public function __construct(App $config, URI $uri, ?ResponseInterface $response * Sends an HTTP request to the specified $url. If this is a relative * URL, it will be merged with $this->baseURI to form a complete URL. * - * @param string $method HTTP method + * @param string $method HTTP method + * @param array $options */ public function request($method, string $url, array $options = []): ResponseInterface { @@ -200,6 +203,8 @@ protected function resetOptions() /** * Convenience method for sending a GET request. + * + * @param array $options */ public function get(string $url, array $options = []): ResponseInterface { @@ -208,6 +213,8 @@ public function get(string $url, array $options = []): ResponseInterface /** * Convenience method for sending a DELETE request. + * + * @param array $options */ public function delete(string $url, array $options = []): ResponseInterface { @@ -216,6 +223,8 @@ public function delete(string $url, array $options = []): ResponseInterface /** * Convenience method for sending a HEAD request. + * + * @param array $options */ public function head(string $url, array $options = []): ResponseInterface { @@ -224,6 +233,8 @@ public function head(string $url, array $options = []): ResponseInterface /** * Convenience method for sending an OPTIONS request. + * + * @param array $options */ public function options(string $url, array $options = []): ResponseInterface { @@ -232,6 +243,8 @@ public function options(string $url, array $options = []): ResponseInterface /** * Convenience method for sending a PATCH request. + * + * @param array $options */ public function patch(string $url, array $options = []): ResponseInterface { @@ -240,6 +253,8 @@ public function patch(string $url, array $options = []): ResponseInterface /** * Convenience method for sending a POST request. + * + * @param array $options */ public function post(string $url, array $options = []): ResponseInterface { @@ -248,6 +263,8 @@ public function post(string $url, array $options = []): ResponseInterface /** * Convenience method for sending a PUT request. + * + * @param array $options */ public function put(string $url, array $options = []): ResponseInterface { @@ -271,7 +288,8 @@ public function setAuth(string $username, #[SensitiveParameter] string $password /** * Set form data to be sent. * - * @param bool $multipart Set TRUE if you are sending CURLFiles + * @param bool $multipart Set TRUE if you are sending CURLFiles + * @param array $params * * @return $this */ @@ -304,6 +322,8 @@ public function setJSON($data) * Sets the correct settings based on the options array * passed in. * + * @param array $options + * * @return void */ protected function parseOptions(array $options) @@ -430,6 +450,10 @@ public function send(string $method, string $url) /** * Adds $this->headers to the cURL request. + * + * @param array $curlOptions + * + * @return array */ protected function applyRequestHeaders(array $curlOptions = []): array { @@ -450,6 +474,10 @@ protected function applyRequestHeaders(array $curlOptions = []): array /** * Apply method + * + * @param array $curlOptions + * + * @return array */ protected function applyMethod(string $method, array $curlOptions): array { @@ -477,6 +505,10 @@ protected function applyMethod(string $method, array $curlOptions): array /** * Apply body + * + * @param array $curlOptions + * + * @return array */ protected function applyBody(array $curlOptions = []): array { @@ -491,6 +523,8 @@ protected function applyBody(array $curlOptions = []): array * Parses the header retrieved from the cURL response into * our Response object. * + * @param list $headers + * * @return void */ protected function setResponseHeaders(array $headers = []) @@ -522,7 +556,10 @@ protected function setResponseHeaders(array $headers = []) /** * Set CURL options * - * @return array + * @param array $curlOptions + * @param array $config + * + * @return array * * @throws InvalidArgumentException */ diff --git a/system/HTTP/Files/FileCollection.php b/system/HTTP/Files/FileCollection.php index 5e3599b89853..058e86c3a4aa 100644 --- a/system/HTTP/Files/FileCollection.php +++ b/system/HTTP/Files/FileCollection.php @@ -31,7 +31,7 @@ class FileCollection * Populated the first time either files(), file(), or hasFile() * is called. * - * @var array|null + * @var array|UploadedFile>|null */ protected $files; @@ -40,7 +40,7 @@ class FileCollection * Each element in the array will be an instance of UploadedFile. * The key of each element will be the client filename. * - * @return array|null + * @return array|UploadedFile>|null */ public function all() { @@ -167,7 +167,9 @@ protected function populateFiles() * Given a file array, will create UploadedFile instances. Will * loop over an array and create objects for each. * - * @return list|UploadedFile + * @param array $array + * + * @return array|UploadedFile>|UploadedFile */ protected function createFileObject(array $array) { @@ -203,6 +205,10 @@ protected function createFileObject(array $array) * of this method. * * @see http://php.net/manual/en/reserved.variables.files.php#118294 + * + * @param array $data + * + * @return array */ protected function fixFilesArray(array $data): array { @@ -246,8 +252,8 @@ protected function fixFilesArray(array $data): array /** * Navigate through an array looking for a particular index * - * @param array $index The index sequence we are navigating down - * @param array $value The portion of the array to process + * @param list $index The index sequence we are navigating down + * @param array|UploadedFile> $value The portion of the array to process * * @return list|UploadedFile|null */ diff --git a/system/HTTP/Files/UploadedFile.php b/system/HTTP/Files/UploadedFile.php index cf7fa414701a..97c228df8f7c 100644 --- a/system/HTTP/Files/UploadedFile.php +++ b/system/HTTP/Files/UploadedFile.php @@ -55,13 +55,6 @@ class UploadedFile extends File implements UploadedFileInterface */ protected $name; - /** - * The type of file as provided by PHP - * - * @var string - */ - protected $originalMimeType; - /** * The error constant of the upload * (one of PHP's UPLOADERRXXX constants) diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 09f80d33253b..f630767bcc35 100644 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -105,14 +105,14 @@ class IncomingRequest extends Request /** * Stores the valid locale codes. * - * @var array + * @var list */ protected $validLocales = []; /** * Holds the old data from a redirect. * - * @var array + * @var array */ protected $oldInput = []; @@ -203,6 +203,8 @@ public function detectLocale($config) /** * Provides a convenient way to work with the Negotiate class * for content negotiation. + * + * @param list $supported */ public function negotiate(string $type, array $supported, bool $strictMatch = false): string { @@ -339,6 +341,8 @@ public function setLocale(string $locale) /** * Set the valid locales. * + * @param list $locales + * * @return $this */ public function setValidLocales(array $locales) @@ -370,9 +374,9 @@ public function getDefaultLocale(): string * to grab data from the request object and can be used in lieu of the * other get* methods in most cases. * - * @param array|string|null $index - * @param int|null $filter Filter constant - * @param array|int|null $flags + * @param list|string|null $index + * @param int|null $filter Filter constant + * @param array|int|null $flags * * @return array|bool|float|int|stdClass|string|null */ @@ -423,10 +427,10 @@ public function getJSON(bool $assoc = false, int $depth = 512, int $options = 0) /** * Get a specific variable from a JSON input stream * - * @param array|string|null $index The variable that you want which can use dot syntax for getting specific values. - * @param bool $assoc If true, return the result as an associative array. - * @param int|null $filter Filter Constant - * @param array|int|null $flags Option + * @param list|string|null $index The variable that you want which can use dot syntax for getting specific values. + * @param bool $assoc If true, return the result as an associative array. + * @param int|null $filter Filter Constant + * @param array|int|null $flags Option * * @return array|bool|float|int|stdClass|string|null */ @@ -503,7 +507,7 @@ public function getJsonVar($index = null, bool $assoc = false, ?int $filter = nu * A convenience method that grabs the raw input stream(send method in PUT, PATCH, DELETE) and decodes * the String into an array. * - * @return array + * @return array */ public function getRawInput() { @@ -515,9 +519,9 @@ public function getRawInput() /** * Gets a specific variable from raw input stream (send method in PUT, PATCH, DELETE). * - * @param array|string|null $index The variable that you want which can use dot syntax for getting specific values. - * @param int|null $filter Filter Constant - * @param array|int|null $flags Option + * @param list|string|null $index The variable that you want which can use dot syntax for getting specific values. + * @param int|null $filter Filter Constant + * @param array|int|null $flags Option * * @return mixed */ @@ -569,9 +573,9 @@ public function getRawInputVar($index = null, ?int $filter = null, $flags = null /** * Fetch an item from GET data. * - * @param array|string|null $index Index for item to fetch from $_GET. - * @param int|null $filter A filter name to apply. - * @param array|int|null $flags + * @param list|string|null $index Index for item to fetch from $_GET. + * @param int|null $filter A filter name to apply. + * @param array|int|null $flags * * @return mixed */ @@ -583,9 +587,9 @@ public function getGet($index = null, $filter = null, $flags = null) /** * Fetch an item from POST. * - * @param array|string|null $index Index for item to fetch from $_POST. - * @param int|null $filter A filter name to apply - * @param array|int|null $flags + * @param list|string|null $index Index for item to fetch from $_POST. + * @param int|null $filter A filter name to apply + * @param array|int|null $flags * * @return mixed */ @@ -597,9 +601,9 @@ public function getPost($index = null, $filter = null, $flags = null) /** * Fetch an item from POST data with fallback to GET. * - * @param array|string|null $index Index for item to fetch from $_POST or $_GET - * @param int|null $filter A filter name to apply - * @param array|int|null $flags + * @param list|string|null $index Index for item to fetch from $_POST or $_GET + * @param int|null $filter A filter name to apply + * @param array|int|null $flags * * @return mixed */ @@ -630,9 +634,9 @@ public function getPostGet($index = null, $filter = null, $flags = null) /** * Fetch an item from GET data with fallback to POST. * - * @param array|string|null $index Index for item to be fetched from $_GET or $_POST - * @param int|null $filter A filter name to apply - * @param array|int|null $flags + * @param list|string|null $index Index for item to be fetched from $_GET or $_POST + * @param int|null $filter A filter name to apply + * @param array|int|null $flags * * @return mixed */ @@ -663,9 +667,9 @@ public function getGetPost($index = null, $filter = null, $flags = null) /** * Fetch an item from the COOKIE array. * - * @param array|string|null $index Index for item to be fetched from $_COOKIE - * @param int|null $filter A filter name to be applied - * @param array|int|null $flags + * @param list|string|null $index Index for item to be fetched from $_COOKIE + * @param int|null $filter A filter name to be applied + * @param array|int|null $flags * * @return mixed */ @@ -689,7 +693,7 @@ public function getUserAgent() * with redirect_with_input(). It first checks for the data in the old * POST data, then the old GET data and finally check for dot arrays * - * @return array|string|null + * @return array|string|null */ public function getOldInput(string $key) { @@ -741,6 +745,8 @@ public function getOldInput(string $key) /** * Returns an array of all files that have been uploaded with this * request. Each file is represented by an UploadedFile instance. + * + * @return array|UploadedFile> */ public function getFiles(): array { @@ -755,7 +761,7 @@ public function getFiles(): array * Verify if a file exist, by the name of the input field used to upload it, in the collection * of uploaded files and if is have been uploaded with multiple option. * - * @return array|null + * @return list|null */ public function getFileMultiple(string $fileID) { diff --git a/system/HTTP/Message.php b/system/HTTP/Message.php index 4a3c70e123cc..7c5114f71f63 100644 --- a/system/HTTP/Message.php +++ b/system/HTTP/Message.php @@ -34,7 +34,7 @@ class Message implements MessageInterface /** * List of valid protocol versions * - * @var array + * @var list */ protected $validProtocolVersions = [ '1.0', @@ -80,13 +80,13 @@ public function getHeaders(): array * Returns a single header object. If multiple headers with the same * name exist, then will return an array of header objects. * - * @return array|Header|null - * * @deprecated Use Message::header() to make room for PSR-7 * * @TODO Incompatible return value with PSR-7 * * @codeCoverageIgnore + * + * @return array|Header|null */ public function getHeader(string $name) { diff --git a/system/HTTP/MessageInterface.php b/system/HTTP/MessageInterface.php index b8850a0ffb7c..11fef4880899 100644 --- a/system/HTTP/MessageInterface.php +++ b/system/HTTP/MessageInterface.php @@ -103,9 +103,9 @@ public function header($name); public function getHeaderLine(string $name): string; /** - * Sets a header and it's value. + * Sets a header and its value. * - * @param array|string|null $value + * @param array|string|null $value * * @return $this */ diff --git a/system/HTTP/MessageTrait.php b/system/HTTP/MessageTrait.php index ffef1f07bc3d..a55f9dc78c10 100644 --- a/system/HTTP/MessageTrait.php +++ b/system/HTTP/MessageTrait.php @@ -41,7 +41,7 @@ trait MessageTrait * and their normal-case key as it is in $headers. * Used for case-insensitive header access. * - * @var array + * @var array */ protected $headerMap = []; @@ -51,7 +51,7 @@ trait MessageTrait /** * Sets the body of the current message. * - * @param string $data + * @param array|string|null $data */ public function setBody($data): self { @@ -138,7 +138,7 @@ public function header($name) /** * Sets a header and it's value. * - * @param array|string|null $value + * @param array|string|null $value */ public function setHeader(string $name, $value): self { diff --git a/system/HTTP/Negotiate.php b/system/HTTP/Negotiate.php index f35a761c2fc4..d482d086e4c9 100644 --- a/system/HTTP/Negotiate.php +++ b/system/HTTP/Negotiate.php @@ -69,8 +69,9 @@ public function setRequest(RequestInterface $request) * If no match is found, the first, highest-ranking client requested * type is returned. * - * @param bool $strictMatch If TRUE, will return an empty string when no match found. - * If FALSE, will return the first supported element. + * @param bool $strictMatch If TRUE, will return an empty string when no match found. + * If FALSE, will return the first supported element. + * @param list $supported */ public function media(array $supported, bool $strictMatch = false): string { @@ -84,6 +85,8 @@ public function media(array $supported, bool $strictMatch = false): string * * If no match is found, the first, highest-ranking client requested * type is returned. + * + * @param list $supported */ public function charset(array $supported): string { @@ -110,6 +113,8 @@ public function charset(array $supported): string * * If no match is found, the first, highest-ranking client requested * type is returned. + * + * @param list $supported */ public function encoding(array $supported = []): string { @@ -125,6 +130,8 @@ public function encoding(array $supported = []): string * * If strict locale negotiation is disabled and no match is found, the first, highest-ranking client requested * type is returned. + * + * @param list $supported */ public function language(array $supported): string { @@ -145,12 +152,12 @@ public function language(array $supported): string * * Portions of this code base on Aura.Accept library. * - * @param array $supported App-supported values - * @param string $header header string - * @param bool $enforceTypes If TRUE, will compare media types and sub-types. - * @param bool $strictMatch If TRUE, will return empty string on no match. - * If FALSE, will return the first supported element. - * @param bool $matchLocales If TRUE, will match locale sub-types to a broad type (fr-FR = fr) + * @param list $supported App-supported values + * @param string $header header string + * @param bool $enforceTypes If TRUE, will compare media types and sub-types. + * @param bool $strictMatch If TRUE, will return empty string on no match. + * If FALSE, will return the first supported element. + * @param bool $matchLocales If TRUE, will match locale sub-types to a broad type (fr-FR = fr) * * @return string Best match */ @@ -261,6 +268,8 @@ protected function getBestLocaleMatch(array $supportedLocales, ?string $header): * Parses an Accept* header into it's multiple values. * * This is based on code from Aura.Accept library. + * + * @return list}> */ public function parseHeader(string $header): array { @@ -339,7 +348,8 @@ public function parseHeader(string $header): array /** * Match-maker * - * @param bool $matchLocales + * @param bool $matchLocales + * @param array{value: string, q: float, params: array} $acceptable */ protected function match(array $acceptable, string $supported, bool $enforceTypes = false, $matchLocales = false): bool { @@ -370,6 +380,9 @@ protected function match(array $acceptable, string $supported, bool $enforceType /** * Checks two Accept values with matching 'values' to see if their * 'params' are the same. + * + * @param array{value: string, q: float, params: array} $acceptable + * @param array{value: string, q: float, params: array} $supported */ protected function matchParameters(array $acceptable, array $supported): bool { @@ -391,6 +404,9 @@ protected function matchParameters(array $acceptable, array $supported): bool /** * Compares the types/subtypes of an acceptable Media type and * the supported string. + * + * @param array{value: string, q: float, params: array} $acceptable + * @param array{value: string, q: float, params: array} $supported */ public function matchTypes(array $acceptable, array $supported): bool { @@ -416,6 +432,9 @@ public function matchTypes(array $acceptable, array $supported): bool /** * Will match locales against their broader pairs, so that fr-FR would * match a supported localed of fr + * + * @param array{value: string, q: float, params: array} $acceptable + * @param array{value: string, q: float, params: array} $supported */ public function matchLocales(array $acceptable, array $supported): bool { diff --git a/system/HTTP/OutgoingRequest.php b/system/HTTP/OutgoingRequest.php index eba5babdf6bb..1534bdcb8308 100644 --- a/system/HTTP/OutgoingRequest.php +++ b/system/HTTP/OutgoingRequest.php @@ -35,8 +35,9 @@ class OutgoingRequest extends Message implements OutgoingRequestInterface protected $uri; /** - * @param string $method HTTP method - * @param string|null $body + * @param string $method HTTP method + * @param array|string> $headers + * @param string|null $body */ public function __construct( string $method, diff --git a/system/HTTP/RedirectResponse.php b/system/HTTP/RedirectResponse.php index d8c8f00e868d..ed464244a63d 100644 --- a/system/HTTP/RedirectResponse.php +++ b/system/HTTP/RedirectResponse.php @@ -47,7 +47,8 @@ public function to(string $uri, ?int $code = null, string $method = 'auto') * Sets the URI to redirect to but as a reverse-routed or named route * instead of a raw URI. * - * @param string $route Route name or Controller::method + * @param string $route Route name or Controller::method + * @param list $params * * @return $this * @@ -123,7 +124,8 @@ private function withErrors(): self /** * Adds a key and message to the session as Flashdata. * - * @param array|string $message + * @param array|string $message + * @param array|string $message * * @return $this */ diff --git a/system/HTTP/RequestInterface.php b/system/HTTP/RequestInterface.php index 26af51df9dab..533d3b9876a9 100644 --- a/system/HTTP/RequestInterface.php +++ b/system/HTTP/RequestInterface.php @@ -32,8 +32,8 @@ public function getIPAddress(): string; * Fetch an item from the $_SERVER array. * Supplied by RequestTrait. * - * @param array|string|null $index Index for item to be fetched from $_SERVER - * @param int|null $filter A filter name to be applied + * @param list|string|null $index Index for item to be fetched from $_SERVER + * @param int|null $filter A filter name to be applied * * @return mixed */ diff --git a/system/HTTP/RequestTrait.php b/system/HTTP/RequestTrait.php index beb5b30570a3..c08d1d0bb808 100644 --- a/system/HTTP/RequestTrait.php +++ b/system/HTTP/RequestTrait.php @@ -46,7 +46,7 @@ trait RequestTrait /** * Stores values we've retrieved from PHP globals. * - * @var array{get?: array, post?: array, request?: array, cookie?: array, server?: array} + * @var array{get?: array, post?: array, request?: array, cookie?: array, server?: array} * * @deprecated 4.7.0 Use the Superglobals service instead */ @@ -221,9 +221,9 @@ private function checkIPAgainstProxy(string $ip, string $proxyIP): bool /** * Fetch an item from the $_SERVER array. * - * @param array|string|null $index Index for item to be fetched from $_SERVER - * @param int|null $filter A filter name to be applied - * @param array|int|null $flags + * @param list|string|null $index Index for item to be fetched from $_SERVER + * @param int|null $filter A filter name to be applied + * @param array|int|null $flags * * @return mixed */ @@ -235,9 +235,9 @@ public function getServer($index = null, $filter = null, $flags = null) /** * Fetch an item from the $_ENV array. * - * @param array|string|null $index Index for item to be fetched from $_ENV - * @param int|null $filter A filter name to be applied - * @param array|int|null $flags + * @param list|string|null $index Index for item to be fetched from $_ENV + * @param int|null $filter A filter name to be applied + * @param array|int|null $flags * * @return mixed * @@ -279,9 +279,9 @@ public function setGlobal(string $name, $value) * http://php.net/manual/en/filter.filters.sanitize.php * * @param 'cookie'|'get'|'post'|'request'|'server' $name Superglobal name (lowercase) - * @param array|int|string|null $index + * @param int|list|string|null $index * @param int|null $filter Filter constant - * @param array|int|null $flags Options + * @param array|int|null $flags Options * * @return mixed */ diff --git a/system/HTTP/Response.php b/system/HTTP/Response.php index e1193c063945..2ac2ecb7e9d2 100644 --- a/system/HTTP/Response.php +++ b/system/HTTP/Response.php @@ -39,7 +39,7 @@ class Response extends Message implements ResponseInterface /** * HTTP status codes * - * @var array + * @var array */ protected static $statusCodes = [ // 1xx: Informational diff --git a/system/HTTP/ResponseInterface.php b/system/HTTP/ResponseInterface.php index 66053c412939..258cbcf3c452 100644 --- a/system/HTTP/ResponseInterface.php +++ b/system/HTTP/ResponseInterface.php @@ -200,7 +200,7 @@ public function setContentType(string $mime, string $charset = 'UTF-8'); /** * Converts the $body into JSON and sets the Content Type header. * - * @param array|object|string $body + * @param array|object|string $body * * @return $this */ @@ -218,7 +218,7 @@ public function getJSON(); /** * Converts $body into XML, and sets the correct Content-Type. * - * @param array|string $body + * @param array|string $body * * @return $this */ @@ -271,6 +271,8 @@ public function noCache(); * - proxy-revalidate * - no-transform * + * @param array $options + * * @return $this */ public function setCache(array $options = []); @@ -310,15 +312,15 @@ public function sendBody(); * Accepts an arbitrary number of binds (up to 7) or an associative * array in the first parameter containing all the values. * - * @param array|Cookie|string $name Cookie name / array containing binds / Cookie object - * @param string $value Cookie value - * @param int $expire Cookie expiration time in seconds - * @param string $domain Cookie domain (e.g.: '.yourdomain.com') - * @param string $path Cookie path (default: '/') - * @param string $prefix Cookie name prefix - * @param bool $secure Whether to only transfer cookies via SSL - * @param bool $httponly Whether only make the cookie accessible via HTTP (no javascript) - * @param string|null $samesite + * @param array|Cookie|string $name Cookie name / array containing binds / Cookie object + * @param string $value Cookie value + * @param int|string $expire Cookie expiration time in seconds + * @param string $domain Cookie domain (e.g.: '.yourdomain.com') + * @param string $path Cookie path (default: '/') + * @param string $prefix Cookie name prefix + * @param bool $secure Whether to only transfer cookies via SSL + * @param bool $httponly Whether only make the cookie accessible via HTTP (no javascript) + * @param string|null $samesite * * @return $this */ diff --git a/system/HTTP/ResponseTrait.php b/system/HTTP/ResponseTrait.php index 00f2dcc7ec40..d67a05765e0e 100644 --- a/system/HTTP/ResponseTrait.php +++ b/system/HTTP/ResponseTrait.php @@ -169,7 +169,7 @@ public function setContentType(string $mime, string $charset = 'UTF-8') /** * Converts the $body into JSON and sets the Content Type header. * - * @param array|object|string $body + * @param array|object|string $body * * @return $this */ @@ -201,7 +201,7 @@ public function getJSON() /** * Converts $body into XML, and sets the correct Content-Type. * - * @param array|string $body + * @param array|string $body * * @return $this */ @@ -234,8 +234,8 @@ public function getXML() * Handles conversion of the data into the appropriate format, * and sets the correct Content-Type header for our response. * - * @param array|object|string $body - * @param string $format Valid: json, xml + * @param array|object|string $body + * @param string $format Valid: json, xml * * @return false|string * @@ -495,15 +495,16 @@ public function redirect(string $uri, string $method = 'auto', ?int $code = null * Accepts an arbitrary number of binds (up to 7) or an associative * array in the first parameter containing all the values. * - * @param array|Cookie|string $name Cookie name / array containing binds / Cookie object - * @param string $value Cookie value - * @param int $expire Cookie expiration time in seconds - * @param string $domain Cookie domain (e.g.: '.yourdomain.com') - * @param string $path Cookie path (default: '/') - * @param string $prefix Cookie name prefix ('': the default prefix) - * @param bool|null $secure Whether to only transfer cookies via SSL - * @param bool|null $httponly Whether only make the cookie accessible via HTTP (no javascript) - * @param string|null $samesite + * @param array|Cookie|string $name Cookie name / array containing binds / Cookie object + * @param string $value Cookie value + * @param int|string $expire Cookie expiration time in seconds + * @param string $domain Cookie domain (e.g.: '.yourdomain.com') + * @param string $path Cookie path (default: '/') + * @param string $prefix Cookie name prefix ('': the default prefix) + * @param bool|null $secure Whether to only transfer cookies via SSL + * @param bool|null $httponly Whether only make the cookie accessible via HTTP (no javascript) + * @param string|null $samesite + * @param array|Cookie|string $name * * @return $this */ @@ -700,6 +701,8 @@ private function dispatchCookies(): void * Extracted call to `setrawcookie()` in order to run unit tests on it. * * @codeCoverageIgnore + * + * @param array $options */ private function doSetRawCookie(string $name, string $value, array $options): void { @@ -710,6 +713,8 @@ private function doSetRawCookie(string $name, string $value, array $options): vo * Extracted call to `setcookie()` in order to run unit tests on it. * * @codeCoverageIgnore + * + * @param array $options */ private function doSetCookie(string $name, string $value, array $options): void { diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index d6653b10f537..7d07eff4aeeb 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -53,6 +53,8 @@ class SiteURI extends URI * 1 => 'public', * 2 => 'index.php', * ]; + * + * @var list */ private array $baseSegments; @@ -85,10 +87,10 @@ class SiteURI extends URI private string $routePath; /** - * @param string $relativePath URI path relative to baseURL. May include - * queries or fragments. - * @param string|null $host Optional current hostname. - * @param 'http'|'https'|null $scheme Optional scheme. 'http' or 'https'. + * @param string $relativePath URI path relative to baseURL. May include + * queries or fragments. + * @param string|null $host Optional current hostname. + * @param ''|'http'|'https'|null $scheme Optional scheme. 'http' or 'https'. */ public function __construct( App $configApp, @@ -123,6 +125,9 @@ public function __construct( $this->setRoutePath($routePath); } + /** + * @return array{string, string, string} + */ private function parseRelativePath(string $relativePath): array { $parts = parse_url('http://dummy/' . $relativePath); @@ -299,6 +304,8 @@ private function setRoutePath(string $routePath): void /** * Converts path to segments + * + * @return list */ private function convertToSegments(string $path): array { @@ -383,10 +390,10 @@ protected function applyParts(array $parts): void /** * For base_url() helper. * - * @param array|string $relativePath URI string or array of URI segments. - * @param string|null $scheme URI scheme. E.g., http, ftp. If empty - * string '' is set, a protocol-relative - * link is returned. + * @param list|string $relativePath URI string or array of URI segments. + * @param string|null $scheme URI scheme. E.g., http, ftp. If empty + * string '' is set, a protocol-relative + * link is returned. */ public function baseUrl($relativePath = '', ?string $scheme = null): string { @@ -408,7 +415,7 @@ public function baseUrl($relativePath = '', ?string $scheme = null): string } /** - * @param array|string $relativePath URI string or array of URI segments + * @param list|string $relativePath URI string or array of URI segments */ private function stringifyRelativePath($relativePath): string { @@ -422,11 +429,11 @@ private function stringifyRelativePath($relativePath): string /** * For site_url() helper. * - * @param array|string $relativePath URI string or array of URI segments. - * @param string|null $scheme URI scheme. E.g., http, ftp. If empty - * string '' is set, a protocol-relative - * link is returned. - * @param App|null $config Alternate configuration to use. + * @param list|string $relativePath URI string or array of URI segments. + * @param string|null $scheme URI scheme. E.g., http, ftp. If empty + * string '' is set, a protocol-relative + * link is returned. + * @param App|null $config Alternate configuration to use. */ public function siteUrl($relativePath = '', ?string $scheme = null, ?App $config = null): string { diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index 4f97e1fa75a4..3b91a8e18e8a 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -917,9 +917,10 @@ public function setQuery(string $query) * A convenience method to pass an array of items in as the Query * portion of the URI. * - * @return URI - * * @TODO: PSR-7: Should be `withQueryParams(array $query)` + * @param array $query + * + * @return URI */ public function setQueryArray(array $query) { diff --git a/system/Superglobals.php b/system/Superglobals.php index 2175728e862e..ac0ea289bd77 100644 --- a/system/Superglobals.php +++ b/system/Superglobals.php @@ -145,7 +145,7 @@ public function getServerArray(): array /** * Set the entire $_SERVER array. * - * @param array $array + * @param array $array */ public function setServerArray(array $array): self { diff --git a/tests/system/HTTP/Files/FileMovingTest.php b/tests/system/HTTP/Files/FileMovingTest.php index 3284c33d0f26..bb4b637fdc47 100644 --- a/tests/system/HTTP/Files/FileMovingTest.php +++ b/tests/system/HTTP/Files/FileMovingTest.php @@ -366,7 +366,7 @@ public function testFailedMoveBecauseOfFalseReturned(): void * * This overwrite is for testing the move operation. */ -function is_uploaded_file($filename): bool +function is_uploaded_file(string $filename): bool { if (! is_file($filename)) { file_put_contents($filename, 'data'); @@ -380,7 +380,7 @@ function is_uploaded_file($filename): bool * * This overwrite is for testing the move operation. */ -function move_uploaded_file($filename, $destination, ?bool $setReturnValue = null): bool +function move_uploaded_file(string $filename, string $destination, ?bool $setReturnValue = null): bool { static $return = true; @@ -396,7 +396,7 @@ function move_uploaded_file($filename, $destination, ?bool $setReturnValue = nul return $return; } -function rrmdir($src): void +function rrmdir(string $src): void { $dir = opendir($src); diff --git a/tests/system/HTTP/MessageTest.php b/tests/system/HTTP/MessageTest.php index 9307c3eef999..f921795c2a48 100644 --- a/tests/system/HTTP/MessageTest.php +++ b/tests/system/HTTP/MessageTest.php @@ -196,7 +196,7 @@ public function testSetHeaderArrayValues(): void } /** - * @param array $arrayHeaderValue + * @param list $arrayHeaderValue */ #[DataProvider('provideArrayHeaderValue')] public function testSetHeaderWithExistingArrayValuesAppendStringValue($arrayHeaderValue): void @@ -208,7 +208,7 @@ public function testSetHeaderWithExistingArrayValuesAppendStringValue($arrayHead } /** - * @param array $arrayHeaderValue + * @param list $arrayHeaderValue */ #[DataProvider('provideArrayHeaderValue')] public function testSetHeaderWithExistingArrayValuesAppendArrayValue($arrayHeaderValue): void @@ -219,6 +219,9 @@ public function testSetHeaderWithExistingArrayValuesAppendArrayValue($arrayHeade $this->assertSame('json, html, xml', $this->message->getHeaderLine('Accept')); } + /** + * @return iterable}> + */ public static function provideArrayHeaderValue(): iterable { return [ diff --git a/tests/system/HTTP/RedirectResponseTest.php b/tests/system/HTTP/RedirectResponseTest.php index b06e437fcffe..bd597078f720 100644 --- a/tests/system/HTTP/RedirectResponseTest.php +++ b/tests/system/HTTP/RedirectResponseTest.php @@ -37,11 +37,6 @@ #[Group('SeparateProcess')] final class RedirectResponseTest extends CIUnitTestCase { - /** - * @var RouteCollection - */ - protected $routes; - private MockIncomingRequest $request; private App $config; diff --git a/tests/system/HTTP/ResponseCookieTest.php b/tests/system/HTTP/ResponseCookieTest.php index 6fb03d516a9f..55a832a3581d 100644 --- a/tests/system/HTTP/ResponseCookieTest.php +++ b/tests/system/HTTP/ResponseCookieTest.php @@ -28,6 +28,9 @@ #[Group('Others')] final class ResponseCookieTest extends CIUnitTestCase { + /** + * @var array + */ private array $defaults; protected function setUp(): void diff --git a/tests/system/HTTP/ResponseTest.php b/tests/system/HTTP/ResponseTest.php index 97ab68be36c8..939b29b148f5 100644 --- a/tests/system/HTTP/ResponseTest.php +++ b/tests/system/HTTP/ResponseTest.php @@ -33,6 +33,9 @@ #[Group('Others')] final class ResponseTest extends CIUnitTestCase { + /** + * @var array + */ private array $server; protected function setUp(): void @@ -296,6 +299,9 @@ public function testRedirect( $this->assertSame($expectedCode, $response->getStatusCode()); } + /** + * @return iterable + */ public static function provideRedirect(): iterable { yield from [ @@ -341,6 +347,9 @@ public function testRedirectWithIIS( service('superglobals')->unsetServer('SERVER_SOFTWARE'); } + /** + * @return iterable + */ public static function provideRedirectWithIIS(): iterable { yield from [ diff --git a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php index b864e493bb5f..4bc29fbecc4d 100644 --- a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php +++ b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php @@ -37,6 +37,9 @@ protected function setUp(): void Services::injectMock('superglobals', new Superglobals()); } + /** + * @param array $server + */ private function createSiteURIFactory(array $server, ?App $appConfig = null): SiteURIFactory { $appConfig ??= new App(); @@ -316,6 +319,9 @@ public function testExtensionPHP($path, $detectPath): void $this->assertSame($detectPath, $factory->detectRoutePath()); } + /** + * @return iterable + */ public static function provideExtensionPHP(): iterable { return [ diff --git a/tests/system/HTTP/SiteURIFactoryTest.php b/tests/system/HTTP/SiteURIFactoryTest.php index 8057c7826d62..5c062371a3ad 100644 --- a/tests/system/HTTP/SiteURIFactoryTest.php +++ b/tests/system/HTTP/SiteURIFactoryTest.php @@ -106,6 +106,9 @@ public function testCreateFromStringWithIndexPage( $this->assertSame($expectedRoutePath, $uri->getRoutePath()); } + /** + * @return iterable + */ public static function provideCreateFromStringWithIndexPage(): iterable { return [ @@ -149,6 +152,9 @@ public function testCreateFromStringWithoutIndexPage( $this->assertSame($expectedRoutePath, $uri->getRoutePath()); } + /** + * @return iterable + */ public static function provideCreateFromStringWithoutIndexPage(): iterable { return [ diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php index 1208edaf05b9..d4f0f6f66c62 100644 --- a/tests/system/HTTP/SiteURITest.php +++ b/tests/system/HTTP/SiteURITest.php @@ -29,6 +29,9 @@ #[Group('Others')] final class SiteURITest extends CIUnitTestCase { + /** + * @param list $expectedSegments + */ #[DataProvider('provideConstructor')] public function testConstructor( string $baseURL, @@ -61,11 +64,17 @@ public function testConstructor( $this->assertSame($expectedTotalSegments, $uri->getTotalSegments()); } + /** + * @return iterable, int}> + */ public static function provideConstructor(): iterable { return array_merge(self::provideSetPath(), self::provideRelativePathWithQueryOrFragment()); } + /** + * @return iterable + */ public static function provideRelativePathWithQueryOrFragment(): iterable { return [ @@ -177,6 +186,9 @@ public function testConstructorInvalidBaseURL(): void new SiteURI($config); } + /** + * @param list $expectedSegments + */ #[DataProvider('provideSetPath')] public function testSetPath( string $baseURL, @@ -209,6 +221,9 @@ public function testSetPath( $this->assertSame($expectedTotalSegments, $uri->getTotalSegments()); } + /** + * @return iterable, int}> + */ public static function provideSetPath(): iterable { return [ diff --git a/tests/system/HTTP/URITest.php b/tests/system/HTTP/URITest.php index 8f2941af82c5..273f6cba1095 100644 --- a/tests/system/HTTP/URITest.php +++ b/tests/system/HTTP/URITest.php @@ -137,6 +137,9 @@ public function testSimpleUri(string $url, string $expectedURL, string $expected $this->assertSame($expectedPath, $uri->getPath()); } + /** + * @return iterable + */ public static function provideSimpleUri(): iterable { return [ @@ -412,6 +415,9 @@ public function testSetPath(string $path, string $expectedURL, string $expectedP $this->assertSame($expectedPath, $uri->getPath()); } + /** + * @return iterable + */ public static function provideSetPath(): iterable { return [ @@ -481,6 +487,9 @@ public function testPathGetsFiltered($path, $expected): void $this->assertSame($expected, $uri->getPath()); } + /** + * @return iterable + */ public static function providePathGetsFiltered(): iterable { return [ @@ -615,6 +624,9 @@ public function testAuthorityReturnsExceptedValues($url, $expected): void $this->assertSame($expected, $uri->getAuthority()); } + /** + * @return iterable + */ public static function provideAuthorityReturnsExceptedValues(): iterable { return [ @@ -667,6 +679,9 @@ public function testAuthorityRemovesDefaultPorts($scheme, $port): void $this->assertSame($expected, (string) $uri); } + /** + * @return iterable + */ public static function provideAuthorityRemovesDefaultPorts(): iterable { return [ @@ -701,6 +716,9 @@ public function testRemoveDotSegments($path, $expected): void $this->assertSame($expected, URI::removeDotSegments($path)); } + /** + * @return iterable + */ public static function provideRemoveDotSegments(): iterable { return [ @@ -831,6 +849,9 @@ public function testResolveRelativeURIHTTPS($rel, $expected): void $this->assertSame($expected, (string) $new); } + /** + * @return iterable + */ public static function defaultResolutions(): iterable { return [ @@ -1071,7 +1092,10 @@ public function testBasedNoIndex(): void (string) $request->getUri(), ); $this->assertSame('/ci/v4/controller/method', $request->getUri()->getPath()); - $this->assertSame('controller/method', $request->getUri()->getRoutePath()); + + $requestUri = $request->getUri(); + $this->assertInstanceOf(SiteURI::class, $requestUri); + $this->assertSame('controller/method', $requestUri->getRoutePath()); // standalone $uri = new URI('http://example.com/ci/v4/controller/method'); diff --git a/utils/phpstan-baseline/argument.type.neon b/utils/phpstan-baseline/argument.type.neon index 98aa86990719..b24fbeb6d8b7 100644 --- a/utils/phpstan-baseline/argument.type.neon +++ b/utils/phpstan-baseline/argument.type.neon @@ -1,4 +1,4 @@ -# total 62 errors +# total 54 errors parameters: ignoreErrors: @@ -97,31 +97,6 @@ parameters: count: 1 path: ../../tests/system/Debug/TimerTest.php - - - message: '#^Parameter \#1 \$array of method CodeIgniter\\Superglobals\:\:setServerArray\(\) expects array\\|float\|int\|string\>, array\ given\.$#' - count: 1 - path: ../../tests/system/HTTP/MessageTest.php - - - - message: '#^Parameter \#3 \$expire of method CodeIgniter\\HTTP\\Response\:\:setCookie\(\) expects int, string given\.$#' - count: 1 - path: ../../tests/system/HTTP/ResponseSendTest.php - - - - message: '#^Parameter \#1 \$data of method CodeIgniter\\HTTP\\Message\:\:setBody\(\) expects string, array\\|string\> given\.$#' - count: 2 - path: ../../tests/system/HTTP/ResponseTest.php - - - - message: '#^Parameter \#3 \$expire of method CodeIgniter\\HTTP\\Response\:\:setCookie\(\) expects int, string given\.$#' - count: 2 - path: ../../tests/system/HTTP/ResponseTest.php - - - - message: '#^Parameter \#4 \$scheme of class CodeIgniter\\HTTP\\SiteURI constructor expects ''http''\|''https''\|null, '''' given\.$#' - count: 1 - path: ../../tests/system/HTTP/SiteURITest.php - - message: '#^Parameter \#1 \$num of function number_to_size expects int\|string, float given\.$#' count: 1 @@ -132,11 +107,6 @@ parameters: count: 1 path: ../../tests/system/HomeTest.php - - - message: '#^Parameter \#1 \$data of method CodeIgniter\\HTTP\\Message\:\:setBody\(\) expects string, null given\.$#' - count: 1 - path: ../../tests/system/Honeypot/HoneypotTest.php - - message: '#^Parameter \#2 \$message of method CodeIgniter\\Log\\Handlers\\ChromeLoggerHandler\:\:handle\(\) expects string, stdClass given\.$#' count: 1 @@ -157,16 +127,6 @@ parameters: count: 1 path: ../../tests/system/Test/FeatureTestTraitTest.php - - - message: '#^Parameter \#1 \$body of method CodeIgniter\\HTTP\\Response\:\:setJSON\(\) expects array\|object\|string, false given\.$#' - count: 1 - path: ../../tests/system/Test/TestResponseTest.php - - - - message: '#^Parameter \#1 \$body of method CodeIgniter\\HTTP\\Response\:\:setJSON\(\) expects array\|object\|string, true given\.$#' - count: 1 - path: ../../tests/system/Test/TestResponseTest.php - - message: '#^Parameter \#2 \$context of method CodeIgniter\\View\\Parser\:\:setData\(\) expects ''attr''\|''css''\|''html''\|''js''\|''raw''\|''url''\|null, ''unknown'' given\.$#' count: 3 @@ -191,3 +151,13 @@ parameters: message: '#^Parameter \#2 \$resultID of class CodeIgniter\\View\\DBResultDummy constructor expects mysqli_result, null given\.$#' count: 2 path: ../../tests/system/View/TableTest.php + + - + message: '#^Parameter \#1 \$body of method CodeIgniter\\HTTP\\Response\:\:setJSON\(\) expects array\\|object\|string, false given\.$#' + count: 1 + path: ../../tests/system/Test/TestResponseTest.php + + - + message: '#^Parameter \#1 \$body of method CodeIgniter\\HTTP\\Response\:\:setJSON\(\) expects array\\|object\|string, true given\.$#' + count: 1 + path: ../../tests/system/Test/TestResponseTest.php diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 1aabccca5c16..afe95d3ef7fb 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 1062 errors +# total 895 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/method.childReturnType.neon b/utils/phpstan-baseline/method.childReturnType.neon index 6e6a320a6177..1be112b559f7 100644 --- a/utils/phpstan-baseline/method.childReturnType.neon +++ b/utils/phpstan-baseline/method.childReturnType.neon @@ -3,11 +3,11 @@ parameters: ignoreErrors: - - message: '#^Return type \(bool\) of method CodeIgniter\\HTTP\\Files\\UploadedFile\:\:move\(\) should be compatible with return type \(CodeIgniter\\Files\\File\) of method CodeIgniter\\Files\\File\:\:move\(\)$#' + message: '#^Return type \(CodeIgniter\\HTTP\\URI\|null\) of method CodeIgniter\\HTTP\\OutgoingRequest\:\:getUri\(\) should be covariant with return type \(CodeIgniter\\HTTP\\URI\) of method CodeIgniter\\HTTP\\OutgoingRequestInterface\:\:getUri\(\)$#' count: 1 - path: ../../system/HTTP/Files/UploadedFile.php + path: ../../system/HTTP/OutgoingRequest.php - - message: '#^Return type \(CodeIgniter\\HTTP\\URI\|null\) of method CodeIgniter\\HTTP\\OutgoingRequest\:\:getUri\(\) should be covariant with return type \(CodeIgniter\\HTTP\\URI\) of method CodeIgniter\\HTTP\\OutgoingRequestInterface\:\:getUri\(\)$#' + message: '#^Return type \(bool\) of method CodeIgniter\\HTTP\\Files\\UploadedFile\:\:move\(\) should be compatible with return type \(CodeIgniter\\Files\\File\) of method CodeIgniter\\Files\\File\:\:move\(\)$#' count: 1 - path: ../../system/HTTP/OutgoingRequest.php + path: ../../system/HTTP/Files/UploadedFile.php diff --git a/utils/phpstan-baseline/method.notFound.neon b/utils/phpstan-baseline/method.notFound.neon index f0497ca809ae..f201cc43dad7 100644 --- a/utils/phpstan-baseline/method.notFound.neon +++ b/utils/phpstan-baseline/method.notFound.neon @@ -1,4 +1,4 @@ -# total 33 errors +# total 32 errors parameters: ignoreErrors: @@ -42,11 +42,6 @@ parameters: count: 3 path: ../../tests/system/Debug/ExceptionHandlerTest.php - - - message: '#^Call to an undefined method CodeIgniter\\HTTP\\URI\:\:getRoutePath\(\)\.$#' - count: 1 - path: ../../tests/system/HTTP/URITest.php - - message: '#^Call to an undefined method org\\bovigo\\vfs\\visitor\\vfsStreamVisitor\:\:getStructure\(\)\.$#' count: 2 @@ -77,11 +72,6 @@ parameters: count: 1 path: ../../tests/system/RESTful/ResourceControllerTest.php - - - message: '#^Call to an undefined method CodeIgniter\\Test\\TestResponse\:\:ohno\(\)\.$#' - count: 1 - path: ../../tests/system/Test/ControllerTestTraitTest.php - - message: '#^Call to an undefined method CodeIgniter\\View\\Table\:\:compileTemplate\(\)\.$#' count: 2 @@ -106,3 +96,8 @@ parameters: message: '#^Call to an undefined method CodeIgniter\\View\\Table\:\:setFromDBResult\(\)\.$#' count: 1 path: ../../tests/system/View/TableTest.php + + - + message: '#^Call to an undefined method CodeIgniter\\Test\\TestResponse\:\:ohno\(\)\.$#' + count: 1 + path: ../../tests/system/Test/ControllerTestTraitTest.php diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 94b09bc195b7..4c57cf7f24d2 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 812 errors +# total 660 errors parameters: ignoreErrors: @@ -2112,631 +2112,6 @@ parameters: count: 1 path: ../../system/Filters/PerformanceMetrics.php - - - message: '#^Method CodeIgniter\\HTTP\\CLIRequest\:\:getArgs\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CLIRequest\:\:getCookie\(\) has parameter \$index with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CLIRequest\:\:getCookie\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CLIRequest\:\:getGetPost\(\) has parameter \$flags with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CLIRequest\:\:getGetPost\(\) has parameter \$index with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CLIRequest\:\:getGetPost\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CLIRequest\:\:getGet\(\) has parameter \$flags with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CLIRequest\:\:getGet\(\) has parameter \$index with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CLIRequest\:\:getGet\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CLIRequest\:\:getOptions\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CLIRequest\:\:getPostGet\(\) has parameter \$flags with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CLIRequest\:\:getPostGet\(\) has parameter \$index with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CLIRequest\:\:getPostGet\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CLIRequest\:\:getPost\(\) has parameter \$flags with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CLIRequest\:\:getPost\(\) has parameter \$index with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CLIRequest\:\:getPost\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CLIRequest\:\:getSegments\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CLIRequest\:\:returnNullOrEmptyArray\(\) has parameter \$index with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CLIRequest\:\:returnNullOrEmptyArray\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - - - message: '#^Property CodeIgniter\\HTTP\\CLIRequest\:\:\$args type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - - - message: '#^Property CodeIgniter\\HTTP\\CLIRequest\:\:\$options type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - - - message: '#^Property CodeIgniter\\HTTP\\CLIRequest\:\:\$segments type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CLIRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CURLRequest\:\:applyBody\(\) has parameter \$curlOptions with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CURLRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CURLRequest\:\:applyBody\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CURLRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CURLRequest\:\:applyMethod\(\) has parameter \$curlOptions with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CURLRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CURLRequest\:\:applyMethod\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CURLRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CURLRequest\:\:applyRequestHeaders\(\) has parameter \$curlOptions with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CURLRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CURLRequest\:\:applyRequestHeaders\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CURLRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CURLRequest\:\:delete\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CURLRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CURLRequest\:\:get\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CURLRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CURLRequest\:\:head\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CURLRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CURLRequest\:\:options\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CURLRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CURLRequest\:\:parseOptions\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CURLRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CURLRequest\:\:patch\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CURLRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CURLRequest\:\:post\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CURLRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CURLRequest\:\:put\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CURLRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CURLRequest\:\:request\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CURLRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CURLRequest\:\:setCURLOptions\(\) has parameter \$config with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CURLRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CURLRequest\:\:setCURLOptions\(\) has parameter \$curlOptions with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CURLRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CURLRequest\:\:setCURLOptions\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CURLRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CURLRequest\:\:setForm\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CURLRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\CURLRequest\:\:setResponseHeaders\(\) has parameter \$headers with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CURLRequest.php - - - - message: '#^Property CodeIgniter\\HTTP\\CURLRequest\:\:\$config type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CURLRequest.php - - - - message: '#^Property CodeIgniter\\HTTP\\CURLRequest\:\:\$defaultConfig type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CURLRequest.php - - - - message: '#^Property CodeIgniter\\HTTP\\CURLRequest\:\:\$defaultOptions type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CURLRequest.php - - - - message: '#^Property CodeIgniter\\HTTP\\CURLRequest\:\:\$redirectDefaults type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/CURLRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\Files\\FileCollection\:\:all\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Files/FileCollection.php - - - - message: '#^Method CodeIgniter\\HTTP\\Files\\FileCollection\:\:createFileObject\(\) has parameter \$array with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Files/FileCollection.php - - - - message: '#^Method CodeIgniter\\HTTP\\Files\\FileCollection\:\:fixFilesArray\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Files/FileCollection.php - - - - message: '#^Method CodeIgniter\\HTTP\\Files\\FileCollection\:\:fixFilesArray\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Files/FileCollection.php - - - - message: '#^Method CodeIgniter\\HTTP\\Files\\FileCollection\:\:getValueDotNotationSyntax\(\) has parameter \$index with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Files/FileCollection.php - - - - message: '#^Method CodeIgniter\\HTTP\\Files\\FileCollection\:\:getValueDotNotationSyntax\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Files/FileCollection.php - - - - message: '#^Property CodeIgniter\\HTTP\\Files\\FileCollection\:\:\$files type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Files/FileCollection.php - - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getCookie\(\) has parameter \$flags with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getCookie\(\) has parameter \$index with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getFileMultiple\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getFiles\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getGetPost\(\) has parameter \$flags with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getGetPost\(\) has parameter \$index with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getGet\(\) has parameter \$flags with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getGet\(\) has parameter \$index with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getJsonVar\(\) has parameter \$flags with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getJsonVar\(\) has parameter \$index with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getOldInput\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getPostGet\(\) has parameter \$flags with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getPostGet\(\) has parameter \$index with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getPost\(\) has parameter \$flags with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getPost\(\) has parameter \$index with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getRawInputVar\(\) has parameter \$flags with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getRawInputVar\(\) has parameter \$index with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getRawInput\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getVar\(\) has parameter \$flags with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:getVar\(\) has parameter \$index with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:negotiate\(\) has parameter \$supported with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\IncomingRequest\:\:setValidLocales\(\) has parameter \$locales with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^Property CodeIgniter\\HTTP\\IncomingRequest\:\:\$oldInput type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^Property CodeIgniter\\HTTP\\IncomingRequest\:\:\$validLocales type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\Message\:\:getHeader\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Message.php - - - - message: '#^Method CodeIgniter\\HTTP\\Message\:\:setHeader\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Message.php - - - - message: '#^Property CodeIgniter\\HTTP\\Message\:\:\$headerMap type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Message.php - - - - message: '#^Property CodeIgniter\\HTTP\\Message\:\:\$validProtocolVersions type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Message.php - - - - message: '#^Method CodeIgniter\\HTTP\\MessageInterface\:\:setHeader\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/MessageInterface.php - - - - message: '#^Method CodeIgniter\\HTTP\\Negotiate\:\:charset\(\) has parameter \$supported with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Negotiate.php - - - - message: '#^Method CodeIgniter\\HTTP\\Negotiate\:\:encoding\(\) has parameter \$supported with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Negotiate.php - - - - message: '#^Method CodeIgniter\\HTTP\\Negotiate\:\:getBestMatch\(\) has parameter \$supported with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Negotiate.php - - - - message: '#^Method CodeIgniter\\HTTP\\Negotiate\:\:language\(\) has parameter \$supported with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Negotiate.php - - - - message: '#^Method CodeIgniter\\HTTP\\Negotiate\:\:matchLocales\(\) has parameter \$acceptable with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Negotiate.php - - - - message: '#^Method CodeIgniter\\HTTP\\Negotiate\:\:matchLocales\(\) has parameter \$supported with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Negotiate.php - - - - message: '#^Method CodeIgniter\\HTTP\\Negotiate\:\:matchParameters\(\) has parameter \$acceptable with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Negotiate.php - - - - message: '#^Method CodeIgniter\\HTTP\\Negotiate\:\:matchParameters\(\) has parameter \$supported with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Negotiate.php - - - - message: '#^Method CodeIgniter\\HTTP\\Negotiate\:\:matchTypes\(\) has parameter \$acceptable with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Negotiate.php - - - - message: '#^Method CodeIgniter\\HTTP\\Negotiate\:\:matchTypes\(\) has parameter \$supported with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Negotiate.php - - - - message: '#^Method CodeIgniter\\HTTP\\Negotiate\:\:match\(\) has parameter \$acceptable with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Negotiate.php - - - - message: '#^Method CodeIgniter\\HTTP\\Negotiate\:\:media\(\) has parameter \$supported with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Negotiate.php - - - - message: '#^Method CodeIgniter\\HTTP\\Negotiate\:\:parseHeader\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Negotiate.php - - - - message: '#^Method CodeIgniter\\HTTP\\OutgoingRequest\:\:__construct\(\) has parameter \$headers with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/OutgoingRequest.php - - - - message: '#^Method CodeIgniter\\HTTP\\RedirectResponse\:\:route\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/RedirectResponse.php - - - - message: '#^Method CodeIgniter\\HTTP\\RedirectResponse\:\:with\(\) has parameter \$message with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/RedirectResponse.php - - - - message: '#^Method CodeIgniter\\HTTP\\Request\:\:fetchGlobal\(\) has parameter \$flags with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Request.php - - - - message: '#^Method CodeIgniter\\HTTP\\Request\:\:fetchGlobal\(\) has parameter \$index with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Request.php - - - - message: '#^Method CodeIgniter\\HTTP\\Request\:\:getEnv\(\) has parameter \$flags with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Request.php - - - - message: '#^Method CodeIgniter\\HTTP\\Request\:\:getEnv\(\) has parameter \$index with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Request.php - - - - message: '#^Method CodeIgniter\\HTTP\\Request\:\:getServer\(\) has parameter \$flags with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Request.php - - - - message: '#^Method CodeIgniter\\HTTP\\Request\:\:getServer\(\) has parameter \$index with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Request.php - - - - message: '#^Property CodeIgniter\\HTTP\\Request\:\:\$globals type has no value type specified in iterable type array\.$#' - count: 5 - path: ../../system/HTTP/Request.php - - - - message: '#^Method CodeIgniter\\HTTP\\RequestInterface\:\:getServer\(\) has parameter \$index with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/RequestInterface.php - - - - message: '#^Method CodeIgniter\\HTTP\\Response\:\:doSetCookie\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Response.php - - - - message: '#^Method CodeIgniter\\HTTP\\Response\:\:doSetRawCookie\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Response.php - - - - message: '#^Method CodeIgniter\\HTTP\\Response\:\:formatBody\(\) has parameter \$body with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Response.php - - - - message: '#^Method CodeIgniter\\HTTP\\Response\:\:setCache\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Response.php - - - - message: '#^Method CodeIgniter\\HTTP\\Response\:\:setCookie\(\) has parameter \$name with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Response.php - - - - message: '#^Method CodeIgniter\\HTTP\\Response\:\:setJSON\(\) has parameter \$body with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Response.php - - - - message: '#^Method CodeIgniter\\HTTP\\Response\:\:setXML\(\) has parameter \$body with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Response.php - - - - message: '#^Property CodeIgniter\\HTTP\\Response\:\:\$statusCodes type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/Response.php - - - - message: '#^Method CodeIgniter\\HTTP\\ResponseInterface\:\:setCache\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/ResponseInterface.php - - - - message: '#^Method CodeIgniter\\HTTP\\ResponseInterface\:\:setCookie\(\) has parameter \$name with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/ResponseInterface.php - - - - message: '#^Method CodeIgniter\\HTTP\\ResponseInterface\:\:setJSON\(\) has parameter \$body with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/ResponseInterface.php - - - - message: '#^Method CodeIgniter\\HTTP\\ResponseInterface\:\:setXML\(\) has parameter \$body with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/ResponseInterface.php - - - - message: '#^Method CodeIgniter\\HTTP\\SiteURI\:\:baseUrl\(\) has parameter \$relativePath with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/SiteURI.php - - - - message: '#^Method CodeIgniter\\HTTP\\SiteURI\:\:convertToSegments\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/SiteURI.php - - - - message: '#^Method CodeIgniter\\HTTP\\SiteURI\:\:parseRelativePath\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/SiteURI.php - - - - message: '#^Method CodeIgniter\\HTTP\\SiteURI\:\:siteUrl\(\) has parameter \$relativePath with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/SiteURI.php - - - - message: '#^Method CodeIgniter\\HTTP\\SiteURI\:\:stringifyRelativePath\(\) has parameter \$relativePath with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/SiteURI.php - - - - message: '#^Property CodeIgniter\\HTTP\\SiteURI\:\:\$baseSegments type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/SiteURI.php - - - - message: '#^Method CodeIgniter\\HTTP\\URI\:\:setQueryArray\(\) has parameter \$query with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HTTP/URI.php - - message: '#^Method CodeIgniter\\HotReloader\\DirectoryHasher\:\:hashApp\(\) return type has no value type specified in iterable type array\.$#' count: 1 @@ -3337,121 +2712,6 @@ parameters: count: 1 path: ../../tests/system/Filters/InvalidCharsTest.php - - - message: '#^Method CodeIgniter\\HTTP\\MessageTest\:\:provideArrayHeaderValue\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/HTTP/MessageTest.php - - - - message: '#^Method CodeIgniter\\HTTP\\MessageTest\:\:testSetHeaderWithExistingArrayValuesAppendArrayValue\(\) has parameter \$arrayHeaderValue with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/HTTP/MessageTest.php - - - - message: '#^Method CodeIgniter\\HTTP\\MessageTest\:\:testSetHeaderWithExistingArrayValuesAppendStringValue\(\) has parameter \$arrayHeaderValue with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/HTTP/MessageTest.php - - - - message: '#^Property CodeIgniter\\HTTP\\ResponseCookieTest\:\:\$defaults type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/HTTP/ResponseCookieTest.php - - - - message: '#^Method CodeIgniter\\HTTP\\ResponseTest\:\:provideRedirectWithIIS\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/HTTP/ResponseTest.php - - - - message: '#^Method CodeIgniter\\HTTP\\ResponseTest\:\:provideRedirect\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/HTTP/ResponseTest.php - - - - message: '#^Property CodeIgniter\\HTTP\\ResponseTest\:\:\$server type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/HTTP/ResponseTest.php - - - - message: '#^Method CodeIgniter\\HTTP\\SiteURIFactoryDetectRoutePathTest\:\:createSiteURIFactory\(\) has parameter \$server with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php - - - - message: '#^Method CodeIgniter\\HTTP\\SiteURIFactoryDetectRoutePathTest\:\:provideExtensionPHP\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php - - - - message: '#^Method CodeIgniter\\HTTP\\SiteURIFactoryTest\:\:provideCreateFromStringWithIndexPage\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/HTTP/SiteURIFactoryTest.php - - - - message: '#^Method CodeIgniter\\HTTP\\SiteURIFactoryTest\:\:provideCreateFromStringWithoutIndexPage\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/HTTP/SiteURIFactoryTest.php - - - - message: '#^Method CodeIgniter\\HTTP\\SiteURITest\:\:provideConstructor\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/HTTP/SiteURITest.php - - - - message: '#^Method CodeIgniter\\HTTP\\SiteURITest\:\:provideRelativePathWithQueryOrFragment\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/HTTP/SiteURITest.php - - - - message: '#^Method CodeIgniter\\HTTP\\SiteURITest\:\:provideSetPath\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/HTTP/SiteURITest.php - - - - message: '#^Method CodeIgniter\\HTTP\\SiteURITest\:\:testConstructor\(\) has parameter \$expectedSegments with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/HTTP/SiteURITest.php - - - - message: '#^Method CodeIgniter\\HTTP\\SiteURITest\:\:testSetPath\(\) has parameter \$expectedSegments with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/HTTP/SiteURITest.php - - - - message: '#^Method CodeIgniter\\HTTP\\URITest\:\:defaultResolutions\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/HTTP/URITest.php - - - - message: '#^Method CodeIgniter\\HTTP\\URITest\:\:provideAuthorityRemovesDefaultPorts\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/HTTP/URITest.php - - - - message: '#^Method CodeIgniter\\HTTP\\URITest\:\:provideAuthorityReturnsExceptedValues\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/HTTP/URITest.php - - - - message: '#^Method CodeIgniter\\HTTP\\URITest\:\:providePathGetsFiltered\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/HTTP/URITest.php - - - - message: '#^Method CodeIgniter\\HTTP\\URITest\:\:provideRemoveDotSegments\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/HTTP/URITest.php - - - - message: '#^Method CodeIgniter\\HTTP\\URITest\:\:provideSetPath\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/HTTP/URITest.php - - - - message: '#^Method CodeIgniter\\HTTP\\URITest\:\:provideSimpleUri\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/HTTP/URITest.php - - message: '#^Property CodeIgniter\\Helpers\\Array\\ArrayHelperDotKeyExistsTest\:\:\$array type has no value type specified in iterable type array\.$#' count: 1 @@ -3852,16 +3112,6 @@ parameters: count: 1 path: ../../tests/system/Test/ControllerTestTraitTest.php - - - message: '#^Method CodeIgniter\\Test\\DOMParserTest\:\:provideText\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Test/DOMParserTest.php - - - - message: '#^Property CodeIgniter\\Test\\FabricatorTest\:\:\$formatters type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/FabricatorTest.php - - message: '#^Method CodeIgniter\\Test\\FeatureTestAutoRoutingImprovedTest\:\:call\(\) has parameter \$params with no value type specified in iterable type array\.$#' count: 1 @@ -3942,11 +3192,6 @@ parameters: count: 1 path: ../../tests/system/Test/FeatureTestTraitTest.php - - - message: '#^Method CodeIgniter\\Test\\FeatureTestTraitTest\:\:provideOpenCliRoutesFromHttpGot404\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestTraitTest.php - - message: '#^Method CodeIgniter\\Test\\FeatureTestTraitTest\:\:put\(\) has parameter \$params with no value type specified in iterable type array\.$#' count: 1 @@ -3967,26 +3212,6 @@ parameters: count: 1 path: ../../tests/system/Test/IniTestTraitTest.php - - - message: '#^Method CodeIgniter\\Test\\TestLoggerTest\:\:provideDidLogMethod\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Test/TestLoggerTest.php - - - - message: '#^Method CodeIgniter\\Test\\TestResponseTest\:\:getTestResponse\(\) has parameter \$headers with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/TestResponseTest.php - - - - message: '#^Method CodeIgniter\\Test\\TestResponseTest\:\:getTestResponse\(\) has parameter \$responseOptions with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/TestResponseTest.php - - - - message: '#^Method CodeIgniter\\Test\\TestResponseTest\:\:provideHttpStatusCodes\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Test/TestResponseTest.php - - message: '#^Method CodeIgniter\\Throttle\\ThrottleTest\:\:provideTokenTimeCalculationUCs\(\) return type has no value type specified in iterable type iterable\.$#' count: 1 @@ -4031,3 +3256,38 @@ parameters: message: '#^Method CodeIgniter\\View\\TableTest\:\:testGenerateOrderedColumns\(\) has parameter \$row with no value type specified in iterable type array\.$#' count: 1 path: ../../tests/system/View/TableTest.php + + - + message: '#^Method CodeIgniter\\Test\\DOMParserTest\:\:provideText\(\) return type has no value type specified in iterable type iterable\.$#' + count: 1 + path: ../../tests/system/Test/DOMParserTest.php + + - + message: '#^Method CodeIgniter\\Test\\FeatureTestTraitTest\:\:provideOpenCliRoutesFromHttpGot404\(\) return type has no value type specified in iterable type iterable\.$#' + count: 1 + path: ../../tests/system/Test/FeatureTestTraitTest.php + + - + message: '#^Method CodeIgniter\\Test\\TestLoggerTest\:\:provideDidLogMethod\(\) return type has no value type specified in iterable type iterable\.$#' + count: 1 + path: ../../tests/system/Test/TestLoggerTest.php + + - + message: '#^Method CodeIgniter\\Test\\TestResponseTest\:\:getTestResponse\(\) has parameter \$headers with no value type specified in iterable type array\.$#' + count: 1 + path: ../../tests/system/Test/TestResponseTest.php + + - + message: '#^Method CodeIgniter\\Test\\TestResponseTest\:\:getTestResponse\(\) has parameter \$responseOptions with no value type specified in iterable type array\.$#' + count: 1 + path: ../../tests/system/Test/TestResponseTest.php + + - + message: '#^Method CodeIgniter\\Test\\TestResponseTest\:\:provideHttpStatusCodes\(\) return type has no value type specified in iterable type iterable\.$#' + count: 1 + path: ../../tests/system/Test/TestResponseTest.php + + - + message: '#^Property CodeIgniter\\Test\\FabricatorTest\:\:\$formatters type has no value type specified in iterable type array\.$#' + count: 1 + path: ../../tests/system/Test/FabricatorTest.php diff --git a/utils/phpstan-baseline/missingType.parameter.neon b/utils/phpstan-baseline/missingType.parameter.neon index 0e1decdda219..0a04e771a32d 100644 --- a/utils/phpstan-baseline/missingType.parameter.neon +++ b/utils/phpstan-baseline/missingType.parameter.neon @@ -1,4 +1,4 @@ -# total 30 errors +# total 26 errors parameters: ignoreErrors: @@ -52,26 +52,6 @@ parameters: count: 1 path: ../../tests/system/Filters/FiltersTest.php - - - message: '#^Function CodeIgniter\\HTTP\\Files\\is_uploaded_file\(\) has parameter \$filename with no type specified\.$#' - count: 1 - path: ../../tests/system/HTTP/Files/FileMovingTest.php - - - - message: '#^Function CodeIgniter\\HTTP\\Files\\move_uploaded_file\(\) has parameter \$destination with no type specified\.$#' - count: 1 - path: ../../tests/system/HTTP/Files/FileMovingTest.php - - - - message: '#^Function CodeIgniter\\HTTP\\Files\\move_uploaded_file\(\) has parameter \$filename with no type specified\.$#' - count: 1 - path: ../../tests/system/HTTP/Files/FileMovingTest.php - - - - message: '#^Function CodeIgniter\\HTTP\\Files\\rrmdir\(\) has parameter \$src with no type specified\.$#' - count: 1 - path: ../../tests/system/HTTP/Files/FileMovingTest.php - - message: '#^Method CodeIgniter\\Helpers\\URLHelper\\CurrentUrlTest\:\:createRequest\(\) has parameter \$body with no type specified\.$#' count: 1 diff --git a/utils/phpstan-baseline/property.phpDocType.neon b/utils/phpstan-baseline/property.phpDocType.neon index a6812dfb728d..745a740bd8dc 100644 --- a/utils/phpstan-baseline/property.phpDocType.neon +++ b/utils/phpstan-baseline/property.phpDocType.neon @@ -1,4 +1,4 @@ -# total 38 errors +# total 36 errors parameters: ignoreErrors: @@ -122,21 +122,6 @@ parameters: count: 1 path: ../../system/Exceptions/PageNotFoundException.php - - - message: '#^PHPDoc type int of property CodeIgniter\\HTTP\\Exceptions\\RedirectException\:\:\$code is not the same as PHPDoc type mixed of overridden property Exception\:\:\$code\.$#' - count: 1 - path: ../../system/HTTP/Exceptions/RedirectException.php - - - - message: '#^PHPDoc type string of property CodeIgniter\\HTTP\\Files\\UploadedFile\:\:\$originalMimeType is not the same as PHPDoc type string\|null of overridden property CodeIgniter\\Files\\File\:\:\$originalMimeType\.$#' - count: 1 - path: ../../system/HTTP/Files/UploadedFile.php - - - - message: '#^PHPDoc type CodeIgniter\\HTTP\\URI of property CodeIgniter\\HTTP\\IncomingRequest\:\:\$uri is not the same as PHPDoc type CodeIgniter\\HTTP\\URI\|null of overridden property CodeIgniter\\HTTP\\OutgoingRequest\:\:\$uri\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - message: '#^PHPDoc type string of property CodeIgniter\\Session\\Handlers\\FileHandler\:\:\$savePath is not the same as PHPDoc type array\\|string of overridden property CodeIgniter\\Session\\Handlers\\BaseHandler\:\:\$savePath\.$#' count: 1 @@ -172,11 +157,6 @@ parameters: count: 1 path: ../../tests/system/Database/Live/SQLite3/GetIndexDataTest.php - - - message: '#^PHPDoc type CodeIgniter\\Router\\RouteCollection of property CodeIgniter\\HTTP\\RedirectResponseTest\:\:\$routes is not the same as PHPDoc type CodeIgniter\\Router\\RouteCollection\|null of overridden property CodeIgniter\\Test\\CIUnitTestCase\:\:\$routes\.$#' - count: 1 - path: ../../tests/system/HTTP/RedirectResponseTest.php - - message: '#^PHPDoc type Tests\\Support\\Models\\EventModel of property CodeIgniter\\Models\\EventsModelTest\:\:\$model is not the same as PHPDoc type CodeIgniter\\Model of overridden property CodeIgniter\\Models\\LiveModelTestCase\:\:\$model\.$#' count: 1 @@ -191,3 +171,13 @@ parameters: message: '#^PHPDoc type CodeIgniter\\Router\\RouteCollection of property CodeIgniter\\RESTful\\ResourcePresenterTest\:\:\$routes is not the same as PHPDoc type CodeIgniter\\Router\\RouteCollection\|null of overridden property CodeIgniter\\Test\\CIUnitTestCase\:\:\$routes\.$#' count: 1 path: ../../tests/system/RESTful/ResourcePresenterTest.php + + - + message: '#^PHPDoc type CodeIgniter\\HTTP\\URI of property CodeIgniter\\HTTP\\IncomingRequest\:\:\$uri is not the same as PHPDoc type CodeIgniter\\HTTP\\URI\|null of overridden property CodeIgniter\\HTTP\\OutgoingRequest\:\:\$uri\.$#' + count: 1 + path: ../../system/HTTP/IncomingRequest.php + + - + message: '#^PHPDoc type int of property CodeIgniter\\HTTP\\Exceptions\\RedirectException\:\:\$code is not the same as PHPDoc type mixed of overridden property Exception\:\:\$code\.$#' + count: 1 + path: ../../system/HTTP/Exceptions/RedirectException.php From 2f6f61afae3e786f400a16a5637c78e14620116a Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Tue, 18 Aug 2026 19:08:12 +0800 Subject: [PATCH 062/163] refactor: fix phpstan errors in `BaseBuilder` (#10466) --- system/Database/BaseBuilder.php | 165 ++++--- system/Database/OCI8/Builder.php | 2 +- system/Database/Postgre/Builder.php | 4 +- system/Database/SQLSRV/Builder.php | 4 +- system/Database/SQLite3/Builder.php | 2 +- tests/system/Database/Builder/UnionTest.php | 5 - tests/system/Database/Builder/WhenTest.php | 5 - tests/system/Database/Builder/WhereTest.php | 11 +- .../Live/SQLite3/GetIndexDataTest.php | 6 - utils/phpstan-baseline/argument.type.neon | 57 +-- .../phpstan-baseline/assign.propertyType.neon | 7 +- utils/phpstan-baseline/loader.neon | 2 +- .../method.childReturnType.neon | 8 +- utils/phpstan-baseline/method.notFound.neon | 10 +- .../missingType.iterableValue.neon | 457 ++---------------- .../phpstan-baseline/property.phpDocType.neon | 42 +- 16 files changed, 170 insertions(+), 617 deletions(-) diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 1aebd7ae0d9b..5b03b0510b5f 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -57,35 +57,35 @@ class BaseBuilder /** * QB FROM data * - * @var array + * @var list */ protected $QBFrom = []; /** * QB JOIN data * - * @var array + * @var list */ protected $QBJoin = []; /** * QB WHERE data * - * @var array + * @var list */ protected $QBWhere = []; /** * QB GROUP BY data * - * @var array + * @var list */ public $QBGroupBy = []; /** * QB HAVING data * - * @var array + * @var list */ protected $QBHaving = []; @@ -114,7 +114,7 @@ class BaseBuilder /** * QB ORDER BY data * - * @var array|string|null + * @var list|string|null */ public $QBOrderBy = []; @@ -168,10 +168,10 @@ class BaseBuilder * and is reset by resetWrite() * * @var array{ - * updateFieldsAdditional?: array, + * updateFieldsAdditional?: array, * tableIdentity?: string, - * updateFields?: array, - * constraints?: array, + * updateFields?: array, + * constraints?: array, * setQueryAsData?: string, * sql?: string, * alias?: string, @@ -204,7 +204,7 @@ class BaseBuilder /** * ORDER BY random keyword * - * @var array + * @var list */ protected $randomKeyword = [ 'RAND()', @@ -226,7 +226,7 @@ class BaseBuilder * their values for later binding * in the Query object. * - * @var array + * @var array */ protected $binds = []; @@ -234,7 +234,7 @@ class BaseBuilder * Collects the key count for named parameters * in the Query object. * - * @var array + * @var array */ protected $bindsKeyCount = []; @@ -272,7 +272,7 @@ class BaseBuilder /** * Tables relation types * - * @var array + * @var list */ protected $joinTypes = [ 'LEFT', @@ -300,7 +300,8 @@ class BaseBuilder /** * Constructor * - * @param array|string|TableName $tableName tablename or tablenames with or without aliases + * @param array|string|TableName $tableName tablename or tablenames with or without aliases + * @param array|null $options * * Examples of $tableName: `mytable`, `jobs j`, `jobs j, users u`, `['jobs j','users u']` * @@ -371,6 +372,8 @@ public function getTable(): string /** * Returns an array of bind values and their * named parameters for binding in the Query object later. + * + * @return array */ public function getBinds(): array { @@ -578,7 +581,7 @@ public function distinct(bool $val = true) /** * Generates the FROM portion of the query * - * @param array|string $from + * @param array|string|null $from */ public function from($from, bool $overwrite = false): self { @@ -708,8 +711,8 @@ public function join(string $table, $cond, string $type = '', ?bool $escape = nu * Generates the WHERE portion of the query. * Separates multiple calls with 'AND'. * - * @param array|RawSql|string $key - * @param mixed $value + * @param array|RawSql|string $key + * @param mixed $value * * @return $this */ @@ -724,8 +727,8 @@ public function where($key, $value = null, ?bool $escape = null) * Generates the WHERE portion of the query. * Separates multiple calls with 'OR'. * - * @param array|RawSql|string $key - * @param mixed $value + * @param array|RawSql|string $key + * @param mixed $value * * @return $this */ @@ -740,8 +743,8 @@ public function orWhere($key, $value = null, ?bool $escape = null) * @used-by having() * @used-by orHaving() * - * @param array|RawSql|string $key - * @param mixed $value + * @param array|RawSql|string $key + * @param mixed $value * * @return $this */ @@ -843,7 +846,7 @@ protected function whereHaving(string $qbKey, $key, $value = null, string $type * Generates a WHERE field IN('item', 'item') SQL query, * joined with 'AND' if appropriate. * - * @param array|BaseBuilder|(Closure(BaseBuilder): BaseBuilder)|null $values The values searched on, or anonymous function with subquery + * @param array|BaseBuilder|(Closure(BaseBuilder): BaseBuilder)|null $values The values searched on, or anonymous function with subquery * * @return $this */ @@ -856,7 +859,7 @@ public function whereIn(?string $key = null, $values = null, ?bool $escape = nul * Generates a WHERE field IN('item', 'item') SQL query, * joined with 'OR' if appropriate. * - * @param array|BaseBuilder|(Closure(BaseBuilder): BaseBuilder)|null $values The values searched on, or anonymous function with subquery + * @param array|BaseBuilder|(Closure(BaseBuilder): BaseBuilder)|null $values The values searched on, or anonymous function with subquery * * @return $this */ @@ -869,7 +872,7 @@ public function orWhereIn(?string $key = null, $values = null, ?bool $escape = n * Generates a WHERE field NOT IN('item', 'item') SQL query, * joined with 'AND' if appropriate. * - * @param array|BaseBuilder|(Closure(BaseBuilder): BaseBuilder)|null $values The values searched on, or anonymous function with subquery + * @param array|BaseBuilder|(Closure(BaseBuilder): BaseBuilder)|null $values The values searched on, or anonymous function with subquery * * @return $this */ @@ -882,7 +885,7 @@ public function whereNotIn(?string $key = null, $values = null, ?bool $escape = * Generates a WHERE field NOT IN('item', 'item') SQL query, * joined with 'OR' if appropriate. * - * @param array|BaseBuilder|(Closure(BaseBuilder): BaseBuilder)|null $values The values searched on, or anonymous function with subquery + * @param array|BaseBuilder|(Closure(BaseBuilder): BaseBuilder)|null $values The values searched on, or anonymous function with subquery * * @return $this */ @@ -895,7 +898,7 @@ public function orWhereNotIn(?string $key = null, $values = null, ?bool $escape * Generates a HAVING field IN('item', 'item') SQL query, * joined with 'AND' if appropriate. * - * @param array|BaseBuilder|(Closure(BaseBuilder): BaseBuilder)|null $values The values searched on, or anonymous function with subquery + * @param array|BaseBuilder|(Closure(BaseBuilder): BaseBuilder)|null $values The values searched on, or anonymous function with subquery * * @return $this */ @@ -908,7 +911,7 @@ public function havingIn(?string $key = null, $values = null, ?bool $escape = nu * Generates a HAVING field IN('item', 'item') SQL query, * joined with 'OR' if appropriate. * - * @param array|BaseBuilder|(Closure(BaseBuilder): BaseBuilder)|null $values The values searched on, or anonymous function with subquery + * @param array|BaseBuilder|(Closure(BaseBuilder): BaseBuilder)|null $values The values searched on, or anonymous function with subquery * * @return $this */ @@ -921,7 +924,7 @@ public function orHavingIn(?string $key = null, $values = null, ?bool $escape = * Generates a HAVING field NOT IN('item', 'item') SQL query, * joined with 'AND' if appropriate. * - * @param array|BaseBuilder|(Closure(BaseBuilder):BaseBuilder)|null $values The values searched on, or anonymous function with subquery + * @param array|BaseBuilder|(Closure(BaseBuilder): BaseBuilder)|null $values The values searched on, or anonymous function with subquery * * @return $this */ @@ -934,7 +937,7 @@ public function havingNotIn(?string $key = null, $values = null, ?bool $escape = * Generates a HAVING field NOT IN('item', 'item') SQL query, * joined with 'OR' if appropriate. * - * @param array|BaseBuilder|(Closure(BaseBuilder): BaseBuilder)|null $values The values searched on, or anonymous function with subquery + * @param array|BaseBuilder|(Closure(BaseBuilder): BaseBuilder)|null $values The values searched on, or anonymous function with subquery * * @return $this */ @@ -1003,7 +1006,7 @@ protected function _whereIn(?string $key = null, $values = null, bool $not = fal * Generates a %LIKE% portion of the query. * Separates multiple calls with 'AND'. * - * @param array|RawSql|string $field + * @param array|RawSql|string $field * * @return $this */ @@ -1016,7 +1019,7 @@ public function like($field, string $match = '', string $side = 'both', ?bool $e * Generates a NOT LIKE portion of the query. * Separates multiple calls with 'AND'. * - * @param array|RawSql|string $field + * @param array|RawSql|string $field * * @return $this */ @@ -1029,7 +1032,7 @@ public function notLike($field, string $match = '', string $side = 'both', ?bool * Generates a %LIKE% portion of the query. * Separates multiple calls with 'OR'. * - * @param array|RawSql|string $field + * @param array|RawSql|string $field * * @return $this */ @@ -1042,7 +1045,7 @@ public function orLike($field, string $match = '', string $side = 'both', ?bool * Generates a NOT LIKE portion of the query. * Separates multiple calls with 'OR'. * - * @param array|RawSql|string $field + * @param array|RawSql|string $field * * @return $this */ @@ -1055,7 +1058,7 @@ public function orNotLike($field, string $match = '', string $side = 'both', ?bo * Generates a %LIKE% portion of the query. * Separates multiple calls with 'AND'. * - * @param array|RawSql|string $field + * @param array|RawSql|string $field * * @return $this */ @@ -1068,7 +1071,7 @@ public function havingLike($field, string $match = '', string $side = 'both', ?b * Generates a NOT LIKE portion of the query. * Separates multiple calls with 'AND'. * - * @param array|RawSql|string $field + * @param array|RawSql|string $field * * @return $this */ @@ -1081,7 +1084,7 @@ public function notHavingLike($field, string $match = '', string $side = 'both', * Generates a %LIKE% portion of the query. * Separates multiple calls with 'OR'. * - * @param array|RawSql|string $field + * @param array|RawSql|string $field * * @return $this */ @@ -1094,7 +1097,7 @@ public function orHavingLike($field, string $match = '', string $side = 'both', * Generates a NOT LIKE portion of the query. * Separates multiple calls with 'OR'. * - * @param array|RawSql|string $field + * @param array|RawSql|string $field * * @return $this */ @@ -1406,7 +1409,7 @@ protected function groupGetType(string $type): string } /** - * @param array|string $by + * @param array|string $by * * @return $this */ @@ -1439,8 +1442,8 @@ public function groupBy($by, ?bool $escape = null) /** * Separates multiple calls with 'AND'. * - * @param array|RawSql|string $key - * @param mixed $value + * @param array|RawSql|string $key + * @param mixed $value * * @return $this */ @@ -1452,8 +1455,8 @@ public function having($key, $value = null, ?bool $escape = null) /** * Separates multiple calls with 'OR'. * - * @param array|RawSql|string $key - * @param mixed $value + * @param array|RawSql|string $key + * @param mixed $value * * @return $this */ @@ -1562,9 +1565,9 @@ protected function _limit(string $sql, bool $offsetIgnore = false): string /** * Allows key/value pairs to be set for insert(), update() or replace(). * - * @param array|object|string $key Field name, or an array of field/value pairs, or an object - * @param mixed $value Field value, if $key is a single field - * @param bool|null $escape Whether to escape values + * @param array|object|string $key Field name, or an array of field/value pairs, or an object + * @param mixed $value Field value, if $key is a single field + * @param bool|null $escape Whether to escape values * * @return $this */ @@ -1593,6 +1596,8 @@ public function set($key, $value = '', ?bool $escape = null) /** * Returns the previously set() data, alternatively resetting it if needed. + * + * @return array|list> */ public function getSetData(bool $clean = false): array { @@ -1639,7 +1644,7 @@ protected function compileFinalQuery(string $sql): string * Compiles the select statement based on the other functions called * and runs the query * - * @return false|ResultInterface + * @return false|ResultInterface|string */ public function get(?int $limit = null, int $offset = 0, bool $reset = true) { @@ -1761,7 +1766,7 @@ public function countAllResults(bool $reset = true) /** * Compiles the set conditions and returns the sql statement * - * @return array + * @return list */ public function getCompiledQBWhere() { @@ -1771,9 +1776,9 @@ public function getCompiledQBWhere() /** * Allows the where clause, limit and offset to be added directly * - * @param array|string $where + * @param array|RawSql|string|null $where * - * @return ResultInterface + * @return false|ResultInterface|string */ public function getWhere($where = null, ?int $limit = null, ?int $offset = 0, bool $reset = true) { @@ -1861,8 +1866,8 @@ protected function batchExecute(string $renderMethod, int $batchSize = 100) /** * Allows a row or multiple rows to be set for batch inserts/upserts/updates * - * @param array|object $set - * @param string $alias alias for sql table + * @param array|object $set + * @param string $alias alias for sql table * * @return $this|null */ @@ -1944,7 +1949,7 @@ public function getCompiledUpsert() /** * Converts call to batchUpsert * - * @param array|object|null $set + * @param array|object|null $set * * @return false|int|list Number of affected rows or FALSE on failure, SQL array when testMode * @@ -1980,7 +1985,7 @@ public function upsert($set = null, ?bool $escape = null) /** * Compiles batch upsert strings and runs the queries * - * @param array|object|null $set a dataset + * @param array|object|null $set a dataset * * @return false|int|list Number of affected rows or FALSE on failure, SQL array when testMode * @@ -2069,7 +2074,7 @@ private function setAlias(string $alias): BaseBuilder * * @param list|list|string $set * @param bool $addToDefault adds update fields to the default ones - * @param array|null $ignore ignores items in set + * @param list|null $ignore ignores items in set * * @return $this */ @@ -2111,7 +2116,7 @@ public function updateFields($set, bool $addToDefault = false, ?array $ignore = /** * Sets constraints for batch upsert, update * - * @param array|RawSql|string|null $set A string of columns, key value pairs, or RawSql + * @param array|RawSql|string|null $set A string of columns, key value pairs, or RawSql * * @return $this */ @@ -2147,8 +2152,8 @@ public function onConstraint($set) /** * Sets data source as a query for insertBatch()/updateBatch()/upsertBatch()/deleteBatch() * - * @param BaseBuilder|RawSql $query - * @param array|string|null $columns an array or comma delimited string of columns + * @param BaseBuilder|RawSql $query + * @param list|string|null $columns an array or comma delimited string of columns */ public function setQueryAsData($query, ?string $alias = null, $columns = null): BaseBuilder { @@ -2192,6 +2197,8 @@ public function setQueryAsData($query, ?string $alias = null, $columns = null): /** * Gets column names from a select query + * + * @return list */ protected function fieldsFromQuery(string $sql): array { @@ -2200,6 +2207,10 @@ protected function fieldsFromQuery(string $sql): array /** * Converts value array of array to array of strings + * + * @param list> $values + * + * @return list */ protected function formatValues(array $values): array { @@ -2209,7 +2220,7 @@ protected function formatValues(array $values): array /** * Compiles batch insert strings and runs the queries * - * @param array|object|null $set a dataset + * @param array|object|null $set a dataset * * @return false|int|list Number of rows inserted or FALSE on no data to perform an insert operation, SQL array when testMode */ @@ -2320,7 +2331,7 @@ public function getCompiledInsert(bool $reset = true) /** * Compiles an insert string and runs the query * - * @param array|object|null $set + * @param array|object|null $set * * @return BaseResult|bool|Query * @@ -2416,6 +2427,8 @@ protected function _insert(string $table, array $keys, array $unescapedKeys): st /** * Compiles a replace into string and runs the query * + * @param array|null $set + * * @return BaseResult|false|Query|string * * @throws DatabaseException @@ -2489,8 +2502,8 @@ public function getCompiledUpdate(bool $reset = true) /** * Compiles an update string and runs the query. * - * @param array|object|null $set - * @param array|RawSql|string|null $where + * @param array|object|null $set + * @param array|RawSql|string|null $where * * @throws DatabaseException */ @@ -2592,8 +2605,8 @@ protected function validateUpdate(): bool /** * Sets data and calls batchExecute to run queries * - * @param array|object|null $set a dataset - * @param array|RawSql|string|null $constraints + * @param array|object|null $set a dataset + * @param array|RawSql|string|null $constraints * * @return false|int|list Number of rows affected or FALSE on failure, SQL array when testMode */ @@ -2727,7 +2740,7 @@ protected function _updateBatch(string $table, array $keys, array $values): stri /** * Allows key/value pairs to be set for batch updating * - * @param array|object $key + * @param array|object $key * * @return $this * @@ -2866,8 +2879,8 @@ public function delete($where = '', ?int $limit = null, bool $resetData = true) /** * Sets data and calls batchExecute to run queries * - * @param array|object|null $set a dataset - * @param array|RawSql|null $constraints + * @param array|object|null $set a dataset + * @param array|RawSql|null $constraints * * @return false|int|list Number of rows affected or FALSE on failure, SQL array when testMode */ @@ -2903,9 +2916,9 @@ public function deleteBatch($set = null, $constraints = null, int $batchSize = 1 * * @used-by batchExecute() * - * @param string $table Protected table name - * @param list $keys QBKeys - * @param list $values QBSet + * @param string $table Protected table name + * @param list $keys QBKeys + * @param list> $values QBSet */ protected function _deleteBatch(string $table, array $keys, array $values): string { @@ -3057,7 +3070,7 @@ protected function _delete(string $table): string /** * Used to track SQL statements written with aliased tables. * - * @param array|string $table The table to inspect + * @param array|string $table The table to inspect * * @return string|null */ @@ -3327,9 +3340,9 @@ protected function unionInjection(string $sql): string /** * Takes an object as input and converts the class variables to array key/vals * - * @param array|object $object + * @param array|object $object * - * @return array + * @return array */ protected function objectToArray($object) { @@ -3355,9 +3368,9 @@ protected function objectToArray($object) /** * Takes an object as input and converts the class variables to array key/vals * - * @param array|object $object + * @param array|object $object * - * @return array + * @return array|list> */ protected function batchObjectToArray($object) { @@ -3418,7 +3431,7 @@ public function resetQuery() /** * Resets the query builder values. Called by the get() function * - * @param array $qbResetItems An array of fields to reset + * @param array $qbResetItems An array of fields to reset * * @return void */ @@ -3495,7 +3508,7 @@ protected function hasOperator(string $str): bool /** * Returns the SQL string operator * - * @return array|false|string + * @return false|list|string */ protected function getOperator(string $str, bool $list = false) { diff --git a/system/Database/OCI8/Builder.php b/system/Database/OCI8/Builder.php index 9f3fa28a95a0..118f237f597e 100644 --- a/system/Database/OCI8/Builder.php +++ b/system/Database/OCI8/Builder.php @@ -34,7 +34,7 @@ class Builder extends BaseBuilder /** * ORDER BY random keyword * - * @var array + * @var list */ protected $randomKeyword = [ '"DBMS_RANDOM"."RANDOM"', diff --git a/system/Database/Postgre/Builder.php b/system/Database/Postgre/Builder.php index 452390a8f4ab..ee0a59c63bb1 100644 --- a/system/Database/Postgre/Builder.php +++ b/system/Database/Postgre/Builder.php @@ -30,7 +30,7 @@ class Builder extends BaseBuilder /** * ORDER BY random keyword * - * @var array + * @var list */ protected $randomKeyword = [ 'RANDOM()', @@ -140,7 +140,7 @@ public function decrement(string $column, int $value = 1) * we simply do a DELETE and an INSERT on the first key/value * combo, assuming that it's either the primary key or a unique key. * - * @param array|null $set An associative array of insert values + * @param array|null $set An associative array of insert values * * @return BaseResult|false|Query|string * diff --git a/system/Database/SQLSRV/Builder.php b/system/Database/SQLSRV/Builder.php index ebc656ddad4d..d98cbe0d9d0b 100644 --- a/system/Database/SQLSRV/Builder.php +++ b/system/Database/SQLSRV/Builder.php @@ -36,7 +36,7 @@ class Builder extends BaseBuilder /** * ORDER BY random keyword * - * @var array + * @var list */ protected $randomKeyword = [ 'NEWID()', @@ -644,7 +644,7 @@ protected function compileSelect($selectOverride = false): string * Compiles the select statement based on the other functions called * and runs the query * - * @return ResultInterface + * @return false|ResultInterface|string */ public function get(?int $limit = null, int $offset = 0, bool $reset = true) { diff --git a/system/Database/SQLite3/Builder.php b/system/Database/SQLite3/Builder.php index 57dc1ae6c3ac..d31cfb493967 100644 --- a/system/Database/SQLite3/Builder.php +++ b/system/Database/SQLite3/Builder.php @@ -44,7 +44,7 @@ class Builder extends BaseBuilder /** * ORDER BY random keyword * - * @var array + * @var list */ protected $randomKeyword = [ 'RANDOM()', diff --git a/tests/system/Database/Builder/UnionTest.php b/tests/system/Database/Builder/UnionTest.php index 5711c6c3cbf7..c6d01273e2e5 100644 --- a/tests/system/Database/Builder/UnionTest.php +++ b/tests/system/Database/Builder/UnionTest.php @@ -25,11 +25,6 @@ #[Group('Others')] final class UnionTest extends CIUnitTestCase { - /** - * @var MockConnection - */ - protected $db; - protected function setUp(): void { parent::setUp(); diff --git a/tests/system/Database/Builder/WhenTest.php b/tests/system/Database/Builder/WhenTest.php index 2fc4966accb8..e902d8471fd2 100644 --- a/tests/system/Database/Builder/WhenTest.php +++ b/tests/system/Database/Builder/WhenTest.php @@ -25,11 +25,6 @@ #[Group('Others')] final class WhenTest extends CIUnitTestCase { - /** - * @var MockConnection - */ - protected $db; - protected function setUp(): void { parent::setUp(); diff --git a/tests/system/Database/Builder/WhereTest.php b/tests/system/Database/Builder/WhereTest.php index 2d0c2a3e1233..65baa097a832 100644 --- a/tests/system/Database/Builder/WhereTest.php +++ b/tests/system/Database/Builder/WhereTest.php @@ -30,11 +30,6 @@ #[Group('Others')] final class WhereTest extends CIUnitTestCase { - /** - * @var MockConnection - */ - protected $db; - protected function setUp(): void { parent::setUp(); @@ -407,6 +402,9 @@ public function testWhereInvalidKeyThrowInvalidArgumentException($key): void $builder->whereIn($key, ['Politician', 'Accountant']); } + /** + * @return iterable + */ public static function provideWhereInvalidKeyThrowInvalidArgumentException(): iterable { return [ @@ -427,6 +425,9 @@ public function testWhereInEmptyValuesThrowInvalidArgumentException($values): vo $builder->whereIn('name', $values); } + /** + * @return iterable + */ public static function provideWhereInEmptyValuesThrowInvalidArgumentException(): iterable { return [ diff --git a/tests/system/Database/Live/SQLite3/GetIndexDataTest.php b/tests/system/Database/Live/SQLite3/GetIndexDataTest.php index 5ef439644417..ac5bd5a299d8 100644 --- a/tests/system/Database/Live/SQLite3/GetIndexDataTest.php +++ b/tests/system/Database/Live/SQLite3/GetIndexDataTest.php @@ -13,7 +13,6 @@ namespace CodeIgniter\Database\Live\SQLite3; -use CodeIgniter\Database\SQLite3\Connection; use CodeIgniter\Database\SQLite3\Forge; use CodeIgniter\Test\CIUnitTestCase; use Config\Database; @@ -26,11 +25,6 @@ #[Group('DatabaseLive')] final class GetIndexDataTest extends CIUnitTestCase { - /** - * @var Connection - */ - protected $db; - private Forge $forge; protected function setUp(): void diff --git a/utils/phpstan-baseline/argument.type.neon b/utils/phpstan-baseline/argument.type.neon index b24fbeb6d8b7..4d117cc62429 100644 --- a/utils/phpstan-baseline/argument.type.neon +++ b/utils/phpstan-baseline/argument.type.neon @@ -1,27 +1,7 @@ -# total 54 errors +# total 33 errors parameters: ignoreErrors: - - - message: '#^Parameter \#3 \.\.\.\$arrays of function array_map expects array, int\|string given\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Parameter \#3 \.\.\.\$arrays of function array_map expects array, int\|string given\.$#' - count: 1 - path: ../../system/Database/OCI8/Builder.php - - - - message: '#^Parameter \#3 \.\.\.\$arrays of function array_map expects array, int\|string given\.$#' - count: 1 - path: ../../system/Database/Postgre/Builder.php - - - - message: '#^Parameter \#3 \.\.\.\$arrays of function array_map expects array, int\|string given\.$#' - count: 1 - path: ../../system/Database/SQLite3/Builder.php - - message: '#^Parameter \#2 \$to of method CodeIgniter\\Router\\RouteCollection\:\:add\(\) expects array\\|\(Closure\(mixed \.\.\.\)\: \(CodeIgniter\\HTTP\\ResponseInterface\|string\|void\)\)\|string, Closure\(mixed\)\: CodeIgniter\\HTTP\\ResponseInterface given\.$#' count: 1 @@ -52,21 +32,6 @@ parameters: count: 2 path: ../../tests/system/Config/FactoriesTest.php - - - message: '#^Parameter \#1 \$from of method CodeIgniter\\Database\\BaseBuilder\\:\:from\(\) expects array\|string, null given\.$#' - count: 1 - path: ../../tests/system/Database/Builder/FromTest.php - - - - message: '#^Parameter \#3 \$subject of function str_replace expects array\\|string, CodeIgniter\\Database\\ResultInterface given\.$#' - count: 10 - path: ../../tests/system/Database/Builder/GetTest.php - - - - message: '#^Parameter \#3 \$subject of function str_replace expects array\\|string, CodeIgniter\\Database\\ResultInterface\|false given\.$#' - count: 6 - path: ../../tests/system/Database/Builder/GetTest.php - - message: '#^Parameter \#1 \$fields of method CodeIgniter\\Database\\Forge\\:\:addField\(\) expects array\\|string, array\ given\.$#' count: 2 @@ -127,6 +92,16 @@ parameters: count: 1 path: ../../tests/system/Test/FeatureTestTraitTest.php + - + message: '#^Parameter \#1 \$body of method CodeIgniter\\HTTP\\Response\:\:setJSON\(\) expects array\\|object\|string, false given\.$#' + count: 1 + path: ../../tests/system/Test/TestResponseTest.php + + - + message: '#^Parameter \#1 \$body of method CodeIgniter\\HTTP\\Response\:\:setJSON\(\) expects array\\|object\|string, true given\.$#' + count: 1 + path: ../../tests/system/Test/TestResponseTest.php + - message: '#^Parameter \#2 \$context of method CodeIgniter\\View\\Parser\:\:setData\(\) expects ''attr''\|''css''\|''html''\|''js''\|''raw''\|''url''\|null, ''unknown'' given\.$#' count: 3 @@ -151,13 +126,3 @@ parameters: message: '#^Parameter \#2 \$resultID of class CodeIgniter\\View\\DBResultDummy constructor expects mysqli_result, null given\.$#' count: 2 path: ../../tests/system/View/TableTest.php - - - - message: '#^Parameter \#1 \$body of method CodeIgniter\\HTTP\\Response\:\:setJSON\(\) expects array\\|object\|string, false given\.$#' - count: 1 - path: ../../tests/system/Test/TestResponseTest.php - - - - message: '#^Parameter \#1 \$body of method CodeIgniter\\HTTP\\Response\:\:setJSON\(\) expects array\\|object\|string, true given\.$#' - count: 1 - path: ../../tests/system/Test/TestResponseTest.php diff --git a/utils/phpstan-baseline/assign.propertyType.neon b/utils/phpstan-baseline/assign.propertyType.neon index e782ea998087..3fb69d14d267 100644 --- a/utils/phpstan-baseline/assign.propertyType.neon +++ b/utils/phpstan-baseline/assign.propertyType.neon @@ -1,4 +1,4 @@ -# total 26 errors +# total 24 errors parameters: ignoreErrors: @@ -22,11 +22,6 @@ parameters: count: 1 path: ../../tests/system/Database/Live/SQLite3/AlterTableTest.php - - - message: '#^Property CodeIgniter\\Database\\Live\\SQLite3\\GetIndexDataTest\:\:\$db \(CodeIgniter\\Database\\SQLite3\\Connection\) does not accept CodeIgniter\\Database\\BaseConnection\.$#' - count: 2 - path: ../../tests/system/Database/Live/SQLite3/GetIndexDataTest.php - - message: '#^Property CodeIgniter\\Database\\Live\\SQLite3\\GetIndexDataTest\:\:\$forge \(CodeIgniter\\Database\\SQLite3\\Forge\) does not accept CodeIgniter\\Database\\Forge\.$#' count: 1 diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index afe95d3ef7fb..5c9064dc52fe 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 895 errors +# total 789 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/method.childReturnType.neon b/utils/phpstan-baseline/method.childReturnType.neon index 1be112b559f7..6e6a320a6177 100644 --- a/utils/phpstan-baseline/method.childReturnType.neon +++ b/utils/phpstan-baseline/method.childReturnType.neon @@ -3,11 +3,11 @@ parameters: ignoreErrors: - - message: '#^Return type \(CodeIgniter\\HTTP\\URI\|null\) of method CodeIgniter\\HTTP\\OutgoingRequest\:\:getUri\(\) should be covariant with return type \(CodeIgniter\\HTTP\\URI\) of method CodeIgniter\\HTTP\\OutgoingRequestInterface\:\:getUri\(\)$#' + message: '#^Return type \(bool\) of method CodeIgniter\\HTTP\\Files\\UploadedFile\:\:move\(\) should be compatible with return type \(CodeIgniter\\Files\\File\) of method CodeIgniter\\Files\\File\:\:move\(\)$#' count: 1 - path: ../../system/HTTP/OutgoingRequest.php + path: ../../system/HTTP/Files/UploadedFile.php - - message: '#^Return type \(bool\) of method CodeIgniter\\HTTP\\Files\\UploadedFile\:\:move\(\) should be compatible with return type \(CodeIgniter\\Files\\File\) of method CodeIgniter\\Files\\File\:\:move\(\)$#' + message: '#^Return type \(CodeIgniter\\HTTP\\URI\|null\) of method CodeIgniter\\HTTP\\OutgoingRequest\:\:getUri\(\) should be covariant with return type \(CodeIgniter\\HTTP\\URI\) of method CodeIgniter\\HTTP\\OutgoingRequestInterface\:\:getUri\(\)$#' count: 1 - path: ../../system/HTTP/Files/UploadedFile.php + path: ../../system/HTTP/OutgoingRequest.php diff --git a/utils/phpstan-baseline/method.notFound.neon b/utils/phpstan-baseline/method.notFound.neon index f201cc43dad7..432b04269975 100644 --- a/utils/phpstan-baseline/method.notFound.neon +++ b/utils/phpstan-baseline/method.notFound.neon @@ -72,6 +72,11 @@ parameters: count: 1 path: ../../tests/system/RESTful/ResourceControllerTest.php + - + message: '#^Call to an undefined method CodeIgniter\\Test\\TestResponse\:\:ohno\(\)\.$#' + count: 1 + path: ../../tests/system/Test/ControllerTestTraitTest.php + - message: '#^Call to an undefined method CodeIgniter\\View\\Table\:\:compileTemplate\(\)\.$#' count: 2 @@ -96,8 +101,3 @@ parameters: message: '#^Call to an undefined method CodeIgniter\\View\\Table\:\:setFromDBResult\(\)\.$#' count: 1 path: ../../tests/system/View/TableTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\Test\\TestResponse\:\:ohno\(\)\.$#' - count: 1 - path: ../../tests/system/Test/ControllerTestTraitTest.php diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 4c57cf7f24d2..aabd40269d62 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 660 errors +# total 581 errors parameters: ignoreErrors: @@ -317,341 +317,6 @@ parameters: count: 1 path: ../../system/DataConverter/DataConverter.php - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:__construct\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:__construct\(\) has parameter \$tableName with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:batchObjectToArray\(\) has parameter \$object with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:batchObjectToArray\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:deleteBatch\(\) has parameter \$constraints with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:deleteBatch\(\) has parameter \$set with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:fieldsFromQuery\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:formatValues\(\) has parameter \$values with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:formatValues\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:from\(\) has parameter \$from with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:getBinds\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:getCompiledQBWhere\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:getOperator\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:getSetData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:getWhere\(\) has parameter \$where with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:groupBy\(\) has parameter \$by with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:havingIn\(\) has parameter \$values with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:havingLike\(\) has parameter \$field with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:havingNotIn\(\) has parameter \$values with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:having\(\) has parameter \$key with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:insertBatch\(\) has parameter \$set with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:insert\(\) has parameter \$set with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:like\(\) has parameter \$field with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:notHavingLike\(\) has parameter \$field with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:notLike\(\) has parameter \$field with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:objectToArray\(\) has parameter \$object with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:objectToArray\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:onConstraint\(\) has parameter \$set with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:orHavingIn\(\) has parameter \$values with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:orHavingLike\(\) has parameter \$field with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:orHavingNotIn\(\) has parameter \$values with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:orHaving\(\) has parameter \$key with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:orLike\(\) has parameter \$field with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:orNotHavingLike\(\) has parameter \$field with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:orNotLike\(\) has parameter \$field with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:orWhereIn\(\) has parameter \$values with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:orWhereNotIn\(\) has parameter \$values with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:orWhere\(\) has parameter \$key with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:replace\(\) has parameter \$set with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:resetRun\(\) has parameter \$qbResetItems with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:setData\(\) has parameter \$set with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:setQueryAsData\(\) has parameter \$columns with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:setUpdateBatch\(\) has parameter \$key with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:set\(\) has parameter \$key with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:trackAliases\(\) has parameter \$table with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:updateBatch\(\) has parameter \$constraints with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:updateBatch\(\) has parameter \$set with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:updateFields\(\) has parameter \$ignore with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:update\(\) has parameter \$set with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:update\(\) has parameter \$where with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:upsertBatch\(\) has parameter \$set with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:upsert\(\) has parameter \$set with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:whereHaving\(\) has parameter \$key with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:whereIn\(\) has parameter \$values with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:whereNotIn\(\) has parameter \$values with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Method CodeIgniter\\Database\\BaseBuilder\:\:where\(\) has parameter \$key with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Property CodeIgniter\\Database\\BaseBuilder\:\:\$QBFrom type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Property CodeIgniter\\Database\\BaseBuilder\:\:\$QBGroupBy type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Property CodeIgniter\\Database\\BaseBuilder\:\:\$QBHaving type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Property CodeIgniter\\Database\\BaseBuilder\:\:\$QBJoin type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Property CodeIgniter\\Database\\BaseBuilder\:\:\$QBOptions type has no value type specified in iterable type array\.$#' - count: 3 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Property CodeIgniter\\Database\\BaseBuilder\:\:\$QBOrderBy type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Property CodeIgniter\\Database\\BaseBuilder\:\:\$QBWhere type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Property CodeIgniter\\Database\\BaseBuilder\:\:\$binds type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Property CodeIgniter\\Database\\BaseBuilder\:\:\$bindsKeyCount type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Property CodeIgniter\\Database\\BaseBuilder\:\:\$joinTypes type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - - - message: '#^Property CodeIgniter\\Database\\BaseBuilder\:\:\$randomKeyword type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseBuilder.php - - message: '#^Class CodeIgniter\\Database\\BaseConnection has PHPDoc tag @property\-read for property \$aliasedTables with no value type specified in iterable type array\.$#' count: 1 @@ -1182,16 +847,6 @@ parameters: count: 1 path: ../../system/Database/MySQLi/Utils.php - - - message: '#^Method CodeIgniter\\Database\\OCI8\\Builder\:\:fieldsFromQuery\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/OCI8/Builder.php - - - - message: '#^Property CodeIgniter\\Database\\OCI8\\Builder\:\:\$randomKeyword type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/OCI8/Builder.php - - message: '#^Method CodeIgniter\\Database\\OCI8\\Connection\:\:bindParams\(\) has parameter \$params with no value type specified in iterable type array\.$#' count: 1 @@ -1272,16 +927,6 @@ parameters: count: 1 path: ../../system/Database/OCI8/Utils.php - - - message: '#^Method CodeIgniter\\Database\\Postgre\\Builder\:\:replace\(\) has parameter \$set with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Postgre/Builder.php - - - - message: '#^Property CodeIgniter\\Database\\Postgre\\Builder\:\:\$randomKeyword type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Postgre/Builder.php - - message: '#^Method CodeIgniter\\Database\\Postgre\\Connection\:\:escape\(\) return type has no value type specified in iterable type array\.$#' count: 1 @@ -1452,21 +1097,6 @@ parameters: count: 1 path: ../../system/Database/ResultInterface.php - - - message: '#^Method CodeIgniter\\Database\\SQLSRV\\Builder\:\:fieldsFromQuery\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLSRV/Builder.php - - - - message: '#^Method CodeIgniter\\Database\\SQLSRV\\Builder\:\:replace\(\) has parameter \$set with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLSRV/Builder.php - - - - message: '#^Property CodeIgniter\\Database\\SQLSRV\\Builder\:\:\$randomKeyword type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLSRV/Builder.php - - message: '#^Method CodeIgniter\\Database\\SQLSRV\\Connection\:\:__construct\(\) has parameter \$params with no value type specified in iterable type array\.$#' count: 1 @@ -1557,11 +1187,6 @@ parameters: count: 1 path: ../../system/Database/SQLSRV/Utils.php - - - message: '#^Property CodeIgniter\\Database\\SQLite3\\Builder\:\:\$randomKeyword type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLite3/Builder.php - - message: '#^Method CodeIgniter\\Database\\SQLite3\\Forge\:\:_alterTable\(\) has parameter \$processedFields with no value type specified in iterable type array\.$#' count: 1 @@ -2612,16 +2237,6 @@ parameters: count: 1 path: ../../tests/system/Database/BaseQueryTest.php - - - message: '#^Method CodeIgniter\\Database\\Builder\\WhereTest\:\:provideWhereInEmptyValuesThrowInvalidArgumentException\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Database/Builder/WhereTest.php - - - - message: '#^Method CodeIgniter\\Database\\Builder\\WhereTest\:\:provideWhereInvalidKeyThrowInvalidArgumentException\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Database/Builder/WhereTest.php - - message: '#^Method CodeIgniter\\Database\\ConfigTest\:\:provideConvertDSN\(\) return type has no value type specified in iterable type iterable\.$#' count: 1 @@ -3112,6 +2727,16 @@ parameters: count: 1 path: ../../tests/system/Test/ControllerTestTraitTest.php + - + message: '#^Method CodeIgniter\\Test\\DOMParserTest\:\:provideText\(\) return type has no value type specified in iterable type iterable\.$#' + count: 1 + path: ../../tests/system/Test/DOMParserTest.php + + - + message: '#^Property CodeIgniter\\Test\\FabricatorTest\:\:\$formatters type has no value type specified in iterable type array\.$#' + count: 1 + path: ../../tests/system/Test/FabricatorTest.php + - message: '#^Method CodeIgniter\\Test\\FeatureTestAutoRoutingImprovedTest\:\:call\(\) has parameter \$params with no value type specified in iterable type array\.$#' count: 1 @@ -3192,6 +2817,11 @@ parameters: count: 1 path: ../../tests/system/Test/FeatureTestTraitTest.php + - + message: '#^Method CodeIgniter\\Test\\FeatureTestTraitTest\:\:provideOpenCliRoutesFromHttpGot404\(\) return type has no value type specified in iterable type iterable\.$#' + count: 1 + path: ../../tests/system/Test/FeatureTestTraitTest.php + - message: '#^Method CodeIgniter\\Test\\FeatureTestTraitTest\:\:put\(\) has parameter \$params with no value type specified in iterable type array\.$#' count: 1 @@ -3212,6 +2842,26 @@ parameters: count: 1 path: ../../tests/system/Test/IniTestTraitTest.php + - + message: '#^Method CodeIgniter\\Test\\TestLoggerTest\:\:provideDidLogMethod\(\) return type has no value type specified in iterable type iterable\.$#' + count: 1 + path: ../../tests/system/Test/TestLoggerTest.php + + - + message: '#^Method CodeIgniter\\Test\\TestResponseTest\:\:getTestResponse\(\) has parameter \$headers with no value type specified in iterable type array\.$#' + count: 1 + path: ../../tests/system/Test/TestResponseTest.php + + - + message: '#^Method CodeIgniter\\Test\\TestResponseTest\:\:getTestResponse\(\) has parameter \$responseOptions with no value type specified in iterable type array\.$#' + count: 1 + path: ../../tests/system/Test/TestResponseTest.php + + - + message: '#^Method CodeIgniter\\Test\\TestResponseTest\:\:provideHttpStatusCodes\(\) return type has no value type specified in iterable type iterable\.$#' + count: 1 + path: ../../tests/system/Test/TestResponseTest.php + - message: '#^Method CodeIgniter\\Throttle\\ThrottleTest\:\:provideTokenTimeCalculationUCs\(\) return type has no value type specified in iterable type iterable\.$#' count: 1 @@ -3256,38 +2906,3 @@ parameters: message: '#^Method CodeIgniter\\View\\TableTest\:\:testGenerateOrderedColumns\(\) has parameter \$row with no value type specified in iterable type array\.$#' count: 1 path: ../../tests/system/View/TableTest.php - - - - message: '#^Method CodeIgniter\\Test\\DOMParserTest\:\:provideText\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Test/DOMParserTest.php - - - - message: '#^Method CodeIgniter\\Test\\FeatureTestTraitTest\:\:provideOpenCliRoutesFromHttpGot404\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestTraitTest.php - - - - message: '#^Method CodeIgniter\\Test\\TestLoggerTest\:\:provideDidLogMethod\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Test/TestLoggerTest.php - - - - message: '#^Method CodeIgniter\\Test\\TestResponseTest\:\:getTestResponse\(\) has parameter \$headers with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/TestResponseTest.php - - - - message: '#^Method CodeIgniter\\Test\\TestResponseTest\:\:getTestResponse\(\) has parameter \$responseOptions with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/TestResponseTest.php - - - - message: '#^Method CodeIgniter\\Test\\TestResponseTest\:\:provideHttpStatusCodes\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Test/TestResponseTest.php - - - - message: '#^Property CodeIgniter\\Test\\FabricatorTest\:\:\$formatters type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/FabricatorTest.php diff --git a/utils/phpstan-baseline/property.phpDocType.neon b/utils/phpstan-baseline/property.phpDocType.neon index 745a740bd8dc..31a1387605af 100644 --- a/utils/phpstan-baseline/property.phpDocType.neon +++ b/utils/phpstan-baseline/property.phpDocType.neon @@ -1,4 +1,4 @@ -# total 36 errors +# total 32 errors parameters: ignoreErrors: @@ -123,39 +123,29 @@ parameters: path: ../../system/Exceptions/PageNotFoundException.php - - message: '#^PHPDoc type string of property CodeIgniter\\Session\\Handlers\\FileHandler\:\:\$savePath is not the same as PHPDoc type array\\|string of overridden property CodeIgniter\\Session\\Handlers\\BaseHandler\:\:\$savePath\.$#' - count: 1 - path: ../../system/Session/Handlers/FileHandler.php - - - - message: '#^PHPDoc type CodeIgniter\\Test\\Mock\\MockConnection of property CodeIgniter\\Database\\Builder\\InsertTest\:\:\$db is not the same as PHPDoc type CodeIgniter\\Database\\BaseConnection of overridden property CodeIgniter\\Test\\CIUnitTestCase\:\:\$db\.$#' - count: 1 - path: ../../tests/system/Database/Builder/InsertTest.php - - - - message: '#^PHPDoc type CodeIgniter\\Test\\Mock\\MockConnection of property CodeIgniter\\Database\\Builder\\UnionTest\:\:\$db is not the same as PHPDoc type CodeIgniter\\Database\\BaseConnection of overridden property CodeIgniter\\Test\\CIUnitTestCase\:\:\$db\.$#' + message: '#^PHPDoc type int of property CodeIgniter\\HTTP\\Exceptions\\RedirectException\:\:\$code is not the same as PHPDoc type mixed of overridden property Exception\:\:\$code\.$#' count: 1 - path: ../../tests/system/Database/Builder/UnionTest.php + path: ../../system/HTTP/Exceptions/RedirectException.php - - message: '#^PHPDoc type CodeIgniter\\Test\\Mock\\MockConnection of property CodeIgniter\\Database\\Builder\\UpdateTest\:\:\$db is not the same as PHPDoc type CodeIgniter\\Database\\BaseConnection of overridden property CodeIgniter\\Test\\CIUnitTestCase\:\:\$db\.$#' + message: '#^PHPDoc type CodeIgniter\\HTTP\\URI of property CodeIgniter\\HTTP\\IncomingRequest\:\:\$uri is not the same as PHPDoc type CodeIgniter\\HTTP\\URI\|null of overridden property CodeIgniter\\HTTP\\OutgoingRequest\:\:\$uri\.$#' count: 1 - path: ../../tests/system/Database/Builder/UpdateTest.php + path: ../../system/HTTP/IncomingRequest.php - - message: '#^PHPDoc type CodeIgniter\\Test\\Mock\\MockConnection of property CodeIgniter\\Database\\Builder\\WhenTest\:\:\$db is not the same as PHPDoc type CodeIgniter\\Database\\BaseConnection of overridden property CodeIgniter\\Test\\CIUnitTestCase\:\:\$db\.$#' + message: '#^PHPDoc type string of property CodeIgniter\\Session\\Handlers\\FileHandler\:\:\$savePath is not the same as PHPDoc type array\\|string of overridden property CodeIgniter\\Session\\Handlers\\BaseHandler\:\:\$savePath\.$#' count: 1 - path: ../../tests/system/Database/Builder/WhenTest.php + path: ../../system/Session/Handlers/FileHandler.php - - message: '#^PHPDoc type CodeIgniter\\Test\\Mock\\MockConnection of property CodeIgniter\\Database\\Builder\\WhereTest\:\:\$db is not the same as PHPDoc type CodeIgniter\\Database\\BaseConnection of overridden property CodeIgniter\\Test\\CIUnitTestCase\:\:\$db\.$#' + message: '#^PHPDoc type CodeIgniter\\Test\\Mock\\MockConnection of property CodeIgniter\\Database\\Builder\\InsertTest\:\:\$db is not the same as PHPDoc type CodeIgniter\\Database\\BaseConnection of overridden property CodeIgniter\\Test\\CIUnitTestCase\:\:\$db\.$#' count: 1 - path: ../../tests/system/Database/Builder/WhereTest.php + path: ../../tests/system/Database/Builder/InsertTest.php - - message: '#^PHPDoc type CodeIgniter\\Database\\SQLite3\\Connection of property CodeIgniter\\Database\\Live\\SQLite3\\GetIndexDataTest\:\:\$db is not the same as PHPDoc type CodeIgniter\\Database\\BaseConnection of overridden property CodeIgniter\\Test\\CIUnitTestCase\:\:\$db\.$#' + message: '#^PHPDoc type CodeIgniter\\Test\\Mock\\MockConnection of property CodeIgniter\\Database\\Builder\\UpdateTest\:\:\$db is not the same as PHPDoc type CodeIgniter\\Database\\BaseConnection of overridden property CodeIgniter\\Test\\CIUnitTestCase\:\:\$db\.$#' count: 1 - path: ../../tests/system/Database/Live/SQLite3/GetIndexDataTest.php + path: ../../tests/system/Database/Builder/UpdateTest.php - message: '#^PHPDoc type Tests\\Support\\Models\\EventModel of property CodeIgniter\\Models\\EventsModelTest\:\:\$model is not the same as PHPDoc type CodeIgniter\\Model of overridden property CodeIgniter\\Models\\LiveModelTestCase\:\:\$model\.$#' @@ -171,13 +161,3 @@ parameters: message: '#^PHPDoc type CodeIgniter\\Router\\RouteCollection of property CodeIgniter\\RESTful\\ResourcePresenterTest\:\:\$routes is not the same as PHPDoc type CodeIgniter\\Router\\RouteCollection\|null of overridden property CodeIgniter\\Test\\CIUnitTestCase\:\:\$routes\.$#' count: 1 path: ../../tests/system/RESTful/ResourcePresenterTest.php - - - - message: '#^PHPDoc type CodeIgniter\\HTTP\\URI of property CodeIgniter\\HTTP\\IncomingRequest\:\:\$uri is not the same as PHPDoc type CodeIgniter\\HTTP\\URI\|null of overridden property CodeIgniter\\HTTP\\OutgoingRequest\:\:\$uri\.$#' - count: 1 - path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^PHPDoc type int of property CodeIgniter\\HTTP\\Exceptions\\RedirectException\:\:\$code is not the same as PHPDoc type mixed of overridden property Exception\:\:\$code\.$#' - count: 1 - path: ../../system/HTTP/Exceptions/RedirectException.php From befd9973a6ddd8bd9b8a8feb3f84c484a61b196c Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Wed, 19 Aug 2026 14:27:07 +0800 Subject: [PATCH 063/163] style: apply latest changes of `binary_operator_spaces` fixer (#10468) --- tests/system/Validation/RulesTest.php | 6 +++--- tests/system/Validation/ValidationTest.php | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/system/Validation/RulesTest.php b/tests/system/Validation/RulesTest.php index 27806ac0fdbc..d4b71bd0a5f6 100644 --- a/tests/system/Validation/RulesTest.php +++ b/tests/system/Validation/RulesTest.php @@ -823,8 +823,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']], @@ -854,7 +854,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/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php index 6c7cb20de0e9..b43bb057b52f 100644 --- a/tests/system/Validation/ValidationTest.php +++ b/tests/system/Validation/ValidationTest.php @@ -605,13 +605,13 @@ public function testSetRuleWithCustomErrors(): void $this->validation->setRule( 'foo', 'Foo', - ['foo' => 'is_numeric'], + ['foo' => 'is_numeric'], ['is_numeric' => 'Nope. Not a number.'], ); $this->validation->setRule( 'bar', 'Bar', - ['bar' => 'is_numeric'], + ['bar' => 'is_numeric'], ['is_numeric' => 'Nope. Not a number.'], ); $result = $this->validation->run($data); @@ -1497,13 +1497,13 @@ public static function provideIfExistRuleWithAsterisk(): iterable yield 'dot-on-end-fail' => [ false, ['status.*' => 'if_exist|in_list[status_1,status_2]'], - ['status' => ['bad-status']], + ['status' => ['bad-status']], ]; yield 'dot-on-end-pass' => [ true, ['status.*' => 'if_exist|in_list[status_1,status_2]'], - ['status' => ['status_1']], + ['status' => ['status_1']], ]; yield 'dot-on-middle-fail' => [ From 7d9f7f4a4832f13baa5cd02c7d27c285983c83fb Mon Sep 17 00:00:00 2001 From: Micah Tongen <90456737+soccerlover29@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:35:09 -0400 Subject: [PATCH 064/163] refactor: add precise parameter types to Router test fixtures (#10464) --- .../Router/Controllers/BlogController.php | 2 +- tests/system/Router/Controllers/Index.php | 2 +- .../Router/Controllers/Mycontroller.php | 2 +- tests/system/Router/Controllers/Remap.php | 2 +- .../Controllers/SubDir/BlogController.php | 2 +- .../Router/Controllers/Subfolder/Home.php | 2 +- .../Subfolder/Sub/BlogController.php | 2 +- .../Router/DefinedRouteCollectorTest.php | 4 +- .../RouteCollectionReverseRouteTest.php | 4 +- tests/system/Router/RouteCollectionTest.php | 4 +- .../missingType.parameter.neon | 57 +------------------ 11 files changed, 14 insertions(+), 69 deletions(-) diff --git a/tests/system/Router/Controllers/BlogController.php b/tests/system/Router/Controllers/BlogController.php index 2287d8ed4dec..cd22bc25357b 100644 --- a/tests/system/Router/Controllers/BlogController.php +++ b/tests/system/Router/Controllers/BlogController.php @@ -21,7 +21,7 @@ public function getIndex(): void { } - public function getSomeMethod($first = ''): void + public function getSomeMethod(string $first = ''): void { } } diff --git a/tests/system/Router/Controllers/Index.php b/tests/system/Router/Controllers/Index.php index 8b1c42ec9997..c17b9b086722 100644 --- a/tests/system/Router/Controllers/Index.php +++ b/tests/system/Router/Controllers/Index.php @@ -17,7 +17,7 @@ class Index extends Controller { - public function getIndex($p1 = ''): void + public function getIndex(string $p1 = ''): void { } diff --git a/tests/system/Router/Controllers/Mycontroller.php b/tests/system/Router/Controllers/Mycontroller.php index 75ad1eae026e..a568390188cf 100644 --- a/tests/system/Router/Controllers/Mycontroller.php +++ b/tests/system/Router/Controllers/Mycontroller.php @@ -21,7 +21,7 @@ public function getIndex(): void { } - public function getSomemethod($first = ''): void + public function getSomemethod(string $first = ''): void { } } diff --git a/tests/system/Router/Controllers/Remap.php b/tests/system/Router/Controllers/Remap.php index 35a7f4b577ac..ef570c423a65 100644 --- a/tests/system/Router/Controllers/Remap.php +++ b/tests/system/Router/Controllers/Remap.php @@ -18,7 +18,7 @@ class Remap extends Controller { - public function _remap(string $method, ...$params): string + public function _remap(string $method, mixed ...$params): string { $method = 'process_' . $method; diff --git a/tests/system/Router/Controllers/SubDir/BlogController.php b/tests/system/Router/Controllers/SubDir/BlogController.php index 78bfcdef0f1d..29a84a5dc467 100644 --- a/tests/system/Router/Controllers/SubDir/BlogController.php +++ b/tests/system/Router/Controllers/SubDir/BlogController.php @@ -21,7 +21,7 @@ public function getIndex(): void { } - public function getSomeMethod($first = ''): void + public function getSomeMethod(string $first = ''): void { } } diff --git a/tests/system/Router/Controllers/Subfolder/Home.php b/tests/system/Router/Controllers/Subfolder/Home.php index 386a86d81c66..5451aec202ea 100644 --- a/tests/system/Router/Controllers/Subfolder/Home.php +++ b/tests/system/Router/Controllers/Subfolder/Home.php @@ -17,7 +17,7 @@ class Home extends Controller { - public function getIndex($p1 = null, $p2 = null): void + public function getIndex(?string $p1 = null, ?string $p2 = null): void { } } diff --git a/tests/system/Router/Controllers/Subfolder/Sub/BlogController.php b/tests/system/Router/Controllers/Subfolder/Sub/BlogController.php index 9481c75050d8..3d2a733c25fe 100644 --- a/tests/system/Router/Controllers/Subfolder/Sub/BlogController.php +++ b/tests/system/Router/Controllers/Subfolder/Sub/BlogController.php @@ -21,7 +21,7 @@ public function getIndex(): void { } - public function getSomeMethod($first = ''): void + public function getSomeMethod(string $first = ''): void { } } diff --git a/tests/system/Router/DefinedRouteCollectorTest.php b/tests/system/Router/DefinedRouteCollectorTest.php index 2ec354c88893..329c582991bf 100644 --- a/tests/system/Router/DefinedRouteCollectorTest.php +++ b/tests/system/Router/DefinedRouteCollectorTest.php @@ -25,7 +25,7 @@ #[Group('Others')] final class DefinedRouteCollectorTest extends CIUnitTestCase { - private function createRouteCollection(array $config = [], $moduleConfig = null): RouteCollection + private function createRouteCollection(array $config = [], ?Modules $moduleConfig = null): RouteCollection { $defaults = [ 'Config' => APPPATH . 'Config', @@ -37,7 +37,7 @@ private function createRouteCollection(array $config = [], $moduleConfig = null) $loader = service('locator'); - if ($moduleConfig === null) { + if (! $moduleConfig instanceof Modules) { $moduleConfig = new Modules(); $moduleConfig->enabled = false; } diff --git a/tests/system/Router/RouteCollectionReverseRouteTest.php b/tests/system/Router/RouteCollectionReverseRouteTest.php index b468c021c87d..66428a13de99 100644 --- a/tests/system/Router/RouteCollectionReverseRouteTest.php +++ b/tests/system/Router/RouteCollectionReverseRouteTest.php @@ -35,7 +35,7 @@ protected function setUp(): void $this->resetFactories(); } - protected function getCollector(array $config = [], array $files = [], $moduleConfig = null): RouteCollection + protected function getCollector(array $config = [], array $files = [], ?Modules $moduleConfig = null): RouteCollection { $defaults = [ 'Config' => APPPATH . 'Config', @@ -47,7 +47,7 @@ protected function getCollector(array $config = [], array $files = [], $moduleCo $loader = service('locator'); - if ($moduleConfig === null) { + if (! $moduleConfig instanceof Modules) { $moduleConfig = new Modules(); $moduleConfig->enabled = false; } diff --git a/tests/system/Router/RouteCollectionTest.php b/tests/system/Router/RouteCollectionTest.php index bf35a3f6fe63..e61f8ec472a9 100644 --- a/tests/system/Router/RouteCollectionTest.php +++ b/tests/system/Router/RouteCollectionTest.php @@ -44,7 +44,7 @@ protected function setUp(): void Services::injectMock('superglobals', new Superglobals()); } - protected function getCollector(array $config = [], array $files = [], $moduleConfig = null): RouteCollection + protected function getCollector(array $config = [], array $files = [], ?Modules $moduleConfig = null): RouteCollection { $defaults = [ 'Config' => APPPATH . 'Config', @@ -56,7 +56,7 @@ protected function getCollector(array $config = [], array $files = [], $moduleCo $loader = service('locator'); - if ($moduleConfig === null) { + if (! $moduleConfig instanceof Modules) { $moduleConfig = new Modules(); $moduleConfig->enabled = false; } diff --git a/utils/phpstan-baseline/missingType.parameter.neon b/utils/phpstan-baseline/missingType.parameter.neon index 0a04e771a32d..5efe82a4824a 100644 --- a/utils/phpstan-baseline/missingType.parameter.neon +++ b/utils/phpstan-baseline/missingType.parameter.neon @@ -1,4 +1,4 @@ -# total 26 errors +# total 15 errors parameters: ignoreErrors: @@ -67,61 +67,6 @@ parameters: count: 1 path: ../../tests/system/Helpers/URLHelper/SiteUrlTest.php - - - message: '#^Method CodeIgniter\\Router\\Controllers\\BlogController\:\:getSomeMethod\(\) has parameter \$first with no type specified\.$#' - count: 1 - path: ../../tests/system/Router/Controllers/BlogController.php - - - - message: '#^Method CodeIgniter\\Router\\Controllers\\Index\:\:getIndex\(\) has parameter \$p1 with no type specified\.$#' - count: 1 - path: ../../tests/system/Router/Controllers/Index.php - - - - message: '#^Method CodeIgniter\\Router\\Controllers\\Mycontroller\:\:getSomemethod\(\) has parameter \$first with no type specified\.$#' - count: 1 - path: ../../tests/system/Router/Controllers/Mycontroller.php - - - - message: '#^Method CodeIgniter\\Router\\Controllers\\Remap\:\:_remap\(\) has parameter \$params with no type specified\.$#' - count: 1 - path: ../../tests/system/Router/Controllers/Remap.php - - - - message: '#^Method CodeIgniter\\Router\\Controllers\\SubDir\\BlogController\:\:getSomeMethod\(\) has parameter \$first with no type specified\.$#' - count: 1 - path: ../../tests/system/Router/Controllers/SubDir/BlogController.php - - - - message: '#^Method CodeIgniter\\Router\\Controllers\\Subfolder\\Home\:\:getIndex\(\) has parameter \$p1 with no type specified\.$#' - count: 1 - path: ../../tests/system/Router/Controllers/Subfolder/Home.php - - - - message: '#^Method CodeIgniter\\Router\\Controllers\\Subfolder\\Home\:\:getIndex\(\) has parameter \$p2 with no type specified\.$#' - count: 1 - path: ../../tests/system/Router/Controllers/Subfolder/Home.php - - - - message: '#^Method CodeIgniter\\Router\\Controllers\\Subfolder\\Sub\\BlogController\:\:getSomeMethod\(\) has parameter \$first with no type specified\.$#' - count: 1 - path: ../../tests/system/Router/Controllers/Subfolder/Sub/BlogController.php - - - - message: '#^Method CodeIgniter\\Router\\DefinedRouteCollectorTest\:\:createRouteCollection\(\) has parameter \$moduleConfig with no type specified\.$#' - count: 1 - path: ../../tests/system/Router/DefinedRouteCollectorTest.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollectionReverseRouteTest\:\:getCollector\(\) has parameter \$moduleConfig with no type specified\.$#' - count: 1 - path: ../../tests/system/Router/RouteCollectionReverseRouteTest.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollectionTest\:\:getCollector\(\) has parameter \$moduleConfig with no type specified\.$#' - count: 1 - path: ../../tests/system/Router/RouteCollectionTest.php - - message: '#^Method CodeIgniter\\Security\\SecurityCSRFSessionRandomizeTokenTest\:\:createSession\(\) has parameter \$options with no type specified\.$#' count: 1 From fda6e259b9ceb066b0bf77c8e30b6c77dcf974c4 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Fri, 21 Aug 2026 23:51:49 +0700 Subject: [PATCH 065/163] Bump StructArmed to ^0.16 (#10472) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b80db0577efe..7fbbd44313bc 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "^0.15", + "boundwize/structarmed": "^0.16", "codeigniter/phpstan-codeigniter": "^2.1", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", From 6df2fe39aa6f595daf75c2c68f8346ed865df4e3 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sun, 23 Aug 2026 19:32:04 +0800 Subject: [PATCH 066/163] refactor: fix phpstan errors in `Forge` (#10470) --- system/Database/Forge.php | 55 ++- system/Database/MySQLi/Forge.php | 25 +- system/Database/OCI8/Forge.php | 26 -- system/Database/Postgre/Forge.php | 12 +- system/Database/SQLSRV/Forge.php | 35 -- system/Database/SQLite3/Forge.php | 13 +- utils/phpstan-baseline/argument.type.neon | 7 +- utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 317 +----------------- .../phpstan-baseline/property.phpDocType.neon | 52 +-- 10 files changed, 50 insertions(+), 494 deletions(-) diff --git a/system/Database/Forge.php b/system/Database/Forge.php index a5d950d9620c..7eefba484bc1 100644 --- a/system/Database/Forge.php +++ b/system/Database/Forge.php @@ -50,7 +50,7 @@ class Forge /** * List of unique keys. * - * @var array + * @var list */ protected $uniqueKeys = []; @@ -64,7 +64,7 @@ class Forge /** * List of foreign keys. * - * @var array + * @var list, referenceTable: string, referenceField: list, onDelete: string, onUpdate: string, fkName: string}> */ protected $foreignKeys = []; @@ -146,7 +146,7 @@ class Forge /** * UNSIGNED support * - * @var array|bool + * @var array|bool */ protected $unsigned = true; @@ -183,7 +183,7 @@ class Forge /** * Foreign Key Allowed Actions * - * @var array + * @var list */ protected $fkAllowActions = ['CASCADE', 'SET NULL', 'NO ACTION', 'RESTRICT', 'SET DEFAULT']; @@ -324,7 +324,7 @@ public function dropDatabase(string $dbName): bool /** * Add Key * - * @param array|string $key + * @param list|string $key * * @return Forge */ @@ -346,7 +346,7 @@ public function addKey($key, bool $primary = false, bool $unique = false, string /** * Add Primary Key * - * @param array|string $key + * @param list|string $key * * @return Forge */ @@ -358,7 +358,7 @@ public function addPrimaryKey($key, string $keyName = '') /** * Add Unique Key * - * @param array|string $key + * @param list|string $key * * @return Forge */ @@ -370,7 +370,7 @@ public function addUniqueKey($key, string $keyName = '') /** * Add Field * - * @param array|string $fields Field array or Field string + * @param array|string>|string $fields Field array or Field string * * @return Forge */ @@ -544,7 +544,7 @@ public function dropForeignKey(string $table, string $foreignName) } /** - * @param array $attributes Table attributes + * @param array $attributes Table attributes * * @return bool * @@ -590,7 +590,7 @@ public function createTable(string $table, bool $ifNotExists = false, array $att } /** - * @param array $attributes Table attributes + * @param array $attributes Table attributes * * @return string SQL string * @@ -626,6 +626,9 @@ protected function _createTable(string $table, bool $ifNotExists, array $attribu ); } + /** + * @param array $attributes + */ protected function _createTableAttributes(array $attributes): string { $sql = ''; @@ -746,7 +749,7 @@ public function renameTable(string $tableName, string $newTableName) } /** - * @param array|string $fields Field array or Field string + * @param array|string>|string $fields Field array or Field string * * @throws DatabaseException */ @@ -804,7 +807,7 @@ public function dropColumn(string $table, $columnNames) } /** - * @param array|string $fields Field array or Field string + * @param array|string>|string $fields Field array or Field string * * @throws DatabaseException */ @@ -846,9 +849,9 @@ public function modifyColumn(string $table, $fields): bool } /** - * @param 'ADD'|'CHANGE'|'DROP' $alterType - * @param array|string $processedFields Processed column definitions - * or column names to DROP + * @param 'ADD'|'CHANGE'|'DROP' $alterType + * @param list>|list|string $processedFields Processed column definitions + * or column names to DROP * * @return ($alterType is 'DROP' ? string : false|list|null) */ @@ -884,6 +887,8 @@ protected function _alterTable(string $alterType, string $table, $processedField /** * Returns $processedFields array from $this->fields data. + * + * @return list> */ protected function _processFields(bool $createTable = false): array { @@ -973,6 +978,8 @@ protected function _processFields(bool $createTable = false): array /** * Converts $processedField array to field definition string. + * + * @param array $processedField */ protected function _processColumn(array $processedField): string { @@ -988,6 +995,8 @@ protected function _processColumn(array $processedField): string /** * Performs a data type mapping between different databases. * + * @param array $attributes + * * @return void */ protected function _attributeType(array &$attributes) @@ -1005,6 +1014,9 @@ protected function _attributeType(array &$attributes) * - array(TYPE => UTYPE) will change $field['type'], * from TYPE to UTYPE in case of a match * + * @param array $attributes + * @param array $field + * * @return void */ protected function _attributeUnsigned(array &$attributes, array &$field) @@ -1038,6 +1050,9 @@ protected function _attributeUnsigned(array &$attributes, array &$field) } /** + * @param array $attributes + * @param array $field + * * @return void */ protected function _attributeDefault(array &$attributes, array &$field) @@ -1062,6 +1077,9 @@ protected function _attributeDefault(array &$attributes, array &$field) } /** + * @param array $attributes + * @param array $field + * * @return void */ protected function _attributeUnique(array &$attributes, array &$field) @@ -1072,6 +1090,9 @@ protected function _attributeUnique(array &$attributes, array &$field) } /** + * @param array $attributes + * @param array $field + * * @return void */ protected function _attributeAutoIncrement(array &$attributes, array &$field) @@ -1164,6 +1185,8 @@ public function processIndexes(string $table): bool * Generates SQL to add indexes * * @param bool $asQuery When true returns stand alone SQL, else partial SQL used with CREATE TABLE + * + * @return list */ protected function _processIndexes(string $table, bool $asQuery = false): array { @@ -1210,6 +1233,8 @@ protected function _processIndexes(string $table, bool $asQuery = false): array * Generates SQL to add foreign keys * * @param bool $asQuery When true returns stand alone SQL, else partial SQL used with CREATE TABLE + * + * @return list */ protected function _processForeignKeys(string $table, bool $asQuery = false): array { diff --git a/system/Database/MySQLi/Forge.php b/system/Database/MySQLi/Forge.php index d433efd335db..0cbe85a3d470 100644 --- a/system/Database/MySQLi/Forge.php +++ b/system/Database/MySQLi/Forge.php @@ -24,8 +24,6 @@ class Forge extends BaseForge { /** * CREATE DATABASE statement - * - * @var string */ protected $createDatabaseStr = 'CREATE DATABASE %s CHARACTER SET %s COLLATE %s'; @@ -38,8 +36,6 @@ class Forge extends BaseForge /** * DROP CONSTRAINT statement - * - * @var string */ protected $dropConstraintStr = 'ALTER TABLE %s DROP FOREIGN KEY %s'; @@ -56,7 +52,7 @@ class Forge extends BaseForge /** * UNSIGNED support * - * @var array + * @var list */ protected $_unsigned = [ 'TINYINT', @@ -76,7 +72,7 @@ class Forge extends BaseForge /** * Table Options list which required to be quoted * - * @var array + * @var list */ protected $_quoted_table_options = [ 'COMMENT', @@ -100,7 +96,7 @@ class Forge extends BaseForge /** * CREATE TABLE attributes * - * @param array $attributes Associative array of table attributes + * @param array $attributes Associative array of table attributes */ protected function _createTableAttributes(array $attributes): string { @@ -129,16 +125,6 @@ protected function _createTableAttributes(array $attributes): string return $sql; } - /** - * ALTER TABLE - * - * @param string $alterType ALTER type - * @param string $table Table name - * @param array|string $processedFields Processed column definitions - * or column names to DROP - * - * @return ($alterType is 'DROP' ? string : list) - */ protected function _alterTable(string $alterType, string $table, $processedFields) { if ($alterType === 'DROP') { @@ -187,11 +173,6 @@ protected function _processColumn(array $processedField): string . $extraClause; } - /** - * Generates SQL to add indexes - * - * @param bool $asQuery When true returns stand alone SQL, else partial SQL used with CREATE TABLE - */ protected function _processIndexes(string $table, bool $asQuery = false): array { $sqls = ['']; diff --git a/system/Database/OCI8/Forge.php b/system/Database/OCI8/Forge.php index 0cf8274e78ef..8b7b7ddfa3ce 100644 --- a/system/Database/OCI8/Forge.php +++ b/system/Database/OCI8/Forge.php @@ -31,38 +31,28 @@ class Forge extends BaseForge /** * CREATE DATABASE statement - * - * @var false */ protected $createDatabaseStr = false; /** * CREATE TABLE IF statement * - * @var false - * * @deprecated This is no longer used. */ protected $createTableIfStr = false; /** * DROP TABLE IF EXISTS statement - * - * @var false */ protected $dropTableIfStr = false; /** * DROP DATABASE statement - * - * @var false */ protected $dropDatabaseStr = false; /** * UNSIGNED support - * - * @var array|bool */ protected $unsigned = false; @@ -75,35 +65,19 @@ class Forge extends BaseForge /** * RENAME TABLE statement - * - * @var string */ protected $renameTableStr = 'ALTER TABLE %s RENAME TO %s'; /** * DROP CONSTRAINT statement - * - * @var string */ protected $dropConstraintStr = 'ALTER TABLE %s DROP CONSTRAINT %s'; /** * Foreign Key Allowed Actions - * - * @var array */ protected $fkAllowActions = ['CASCADE', 'SET NULL', 'NO ACTION']; - /** - * ALTER TABLE - * - * @param string $alterType ALTER type - * @param string $table Table name - * @param array|string $processedFields Processed column definitions - * or column names to DROP - * - * @return ($alterType is 'DROP' ? string : list) - */ protected function _alterTable(string $alterType, string $table, $processedFields) { $sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table); diff --git a/system/Database/Postgre/Forge.php b/system/Database/Postgre/Forge.php index 0423d6d54cd1..3f2c2321d352 100644 --- a/system/Database/Postgre/Forge.php +++ b/system/Database/Postgre/Forge.php @@ -31,8 +31,6 @@ class Forge extends BaseForge /** * DROP CONSTRAINT statement - * - * @var string */ protected $dropConstraintStr = 'ALTER TABLE %s DROP CONSTRAINT %s'; @@ -46,7 +44,7 @@ class Forge extends BaseForge /** * UNSIGNED support * - * @var array + * @var array */ protected $_unsigned = [ 'INT2' => 'INTEGER', @@ -72,19 +70,13 @@ class Forge extends BaseForge /** * CREATE TABLE attributes * - * @param array $attributes Associative array of table attributes + * @param array $attributes Associative array of table attributes */ protected function _createTableAttributes(array $attributes): string { return ''; } - /** - * @param array|string $processedFields Processed column definitions - * or column names to DROP - * - * @return ($alterType is 'DROP' ? string : false|list) - */ protected function _alterTable(string $alterType, string $table, $processedFields) { if (in_array($alterType, ['DROP', 'ADD'], true)) { diff --git a/system/Database/SQLSRV/Forge.php b/system/Database/SQLSRV/Forge.php index c9ae7557cc65..fa29fe5dbd75 100644 --- a/system/Database/SQLSRV/Forge.php +++ b/system/Database/SQLSRV/Forge.php @@ -27,8 +27,6 @@ class Forge extends BaseForge { /** * DROP CONSTRAINT statement - * - * @var string */ protected $dropConstraintStr; @@ -52,8 +50,6 @@ class Forge extends BaseForge * CREATE DATABASE IF statement * * @todo missing charset & collat - * - * @var string */ protected $createDatabaseStr = 'CREATE DATABASE %s '; @@ -72,15 +68,11 @@ class Forge extends BaseForge * * @see https://docs.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-rename-transact-sql?view=sql-server-2017 * 'EXEC sp_rename %s , %s ;' - * - * @var string */ protected $renameTableStr; /** * UNSIGNED support - * - * @var array */ protected $unsigned = [ 'TINYINT' => 'SMALLINT', @@ -91,24 +83,18 @@ class Forge extends BaseForge /** * Foreign Key Allowed Actions - * - * @var array */ protected $fkAllowActions = ['CASCADE', 'SET NULL', 'NO ACTION', 'RESTRICT', 'SET DEFAULT']; /** * CREATE TABLE IF statement * - * @var string - * * @deprecated This is no longer used. */ protected $createTableIfStr; /** * CREATE TABLE statement - * - * @var string */ protected $createTableStr; @@ -123,13 +109,6 @@ public function __construct(BaseConnection $db) $this->dropIndexStr = 'DROP INDEX %s ON ' . $this->db->escapeIdentifiers($this->db->schema) . '.%s'; } - /** - * Create database - * - * @param bool $ifNotExists Whether to add IF NOT EXISTS condition - * - * @throws DatabaseException - */ public function createDatabase(string $dbName, bool $ifNotExists = false): bool { if ($ifNotExists) { @@ -189,20 +168,11 @@ public function dropDatabase(string $dbName): bool return parent::dropDatabase($dbName); } - /** - * CREATE TABLE attributes - */ protected function _createTableAttributes(array $attributes): string { return ''; } - /** - * @param array|string $processedFields Processed column definitions - * or column names to DROP - * - * @return ($alterType is 'DROP' ? string : false|list) - */ protected function _alterTable(string $alterType, string $table, $processedFields) { // Handle DROP here @@ -336,11 +306,6 @@ protected function _dropIndex(string $table, object $indexData) return $this->db->simpleQuery($sql); } - /** - * Generates SQL to add indexes - * - * @param bool $asQuery When true returns stand alone SQL, else partial SQL used with CREATE TABLE - */ protected function _processIndexes(string $table, bool $asQuery = false): array { $sqls = []; diff --git a/system/Database/SQLite3/Forge.php b/system/Database/SQLite3/Forge.php index 688e0af559fb..2ca26bba105e 100644 --- a/system/Database/SQLite3/Forge.php +++ b/system/Database/SQLite3/Forge.php @@ -34,7 +34,7 @@ class Forge extends BaseForge /** * UNSIGNED support * - * @var array|bool + * @var array|bool */ protected $_unsigned = false; @@ -59,11 +59,6 @@ public function __construct(BaseConnection $db) } } - /** - * Create database - * - * @param bool $ifNotExists Whether to add IF NOT EXISTS condition - */ public function createDatabase(string $dbName, bool $ifNotExists = false): bool { // In SQLite, a database is created when you connect to the database. @@ -132,12 +127,6 @@ public function dropColumn(string $table, $columnNames): bool return $result; } - /** - * @param array|string $processedFields Processed column definitions - * or column names to DROP - * - * @return ($alterType is 'DROP' ? string : list|null) - */ protected function _alterTable(string $alterType, string $table, $processedFields) { switch ($alterType) { diff --git a/utils/phpstan-baseline/argument.type.neon b/utils/phpstan-baseline/argument.type.neon index 4d117cc62429..5e3cd39db63c 100644 --- a/utils/phpstan-baseline/argument.type.neon +++ b/utils/phpstan-baseline/argument.type.neon @@ -1,4 +1,4 @@ -# total 33 errors +# total 31 errors parameters: ignoreErrors: @@ -32,11 +32,6 @@ parameters: count: 2 path: ../../tests/system/Config/FactoriesTest.php - - - message: '#^Parameter \#1 \$fields of method CodeIgniter\\Database\\Forge\\:\:addField\(\) expects array\\|string, array\ given\.$#' - count: 2 - path: ../../tests/system/Database/Live/ForgeTest.php - - message: '#^Parameter \#1 \$db of class CodeIgniter\\Database\\SQLite3\\Table constructor expects CodeIgniter\\Database\\SQLite3\\Connection, CodeIgniter\\Database\\BaseConnection given\.$#' count: 1 diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 5c9064dc52fe..a5cf84a90e35 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 789 errors +# total 703 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index aabd40269d62..1078edd6ed71 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 581 errors +# total 518 errors parameters: ignoreErrors: @@ -612,141 +612,6 @@ parameters: count: 1 path: ../../system/Database/Database.php - - - message: '#^Method CodeIgniter\\Database\\Forge\:\:_alterTable\(\) has parameter \$processedFields with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\Forge\:\:_attributeAutoIncrement\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\Forge\:\:_attributeAutoIncrement\(\) has parameter \$field with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\Forge\:\:_attributeDefault\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\Forge\:\:_attributeDefault\(\) has parameter \$field with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\Forge\:\:_attributeType\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\Forge\:\:_attributeUnique\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\Forge\:\:_attributeUnique\(\) has parameter \$field with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\Forge\:\:_attributeUnsigned\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\Forge\:\:_attributeUnsigned\(\) has parameter \$field with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\Forge\:\:_createTableAttributes\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\Forge\:\:_createTable\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\Forge\:\:_processColumn\(\) has parameter \$processedField with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\Forge\:\:_processFields\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\Forge\:\:_processForeignKeys\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\Forge\:\:_processIndexes\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\Forge\:\:addColumn\(\) has parameter \$fields with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\Forge\:\:addField\(\) has parameter \$fields with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\Forge\:\:addKey\(\) has parameter \$key with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\Forge\:\:addPrimaryKey\(\) has parameter \$key with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\Forge\:\:addUniqueKey\(\) has parameter \$key with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\Forge\:\:createTable\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\Forge\:\:modifyColumn\(\) has parameter \$fields with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Forge.php - - - - message: '#^Property CodeIgniter\\Database\\Forge\:\:\$fkAllowActions type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Forge.php - - - - message: '#^Property CodeIgniter\\Database\\Forge\:\:\$foreignKeys type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Forge.php - - - - message: '#^Property CodeIgniter\\Database\\Forge\:\:\$uniqueKeys type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Forge.php - - - - message: '#^Property CodeIgniter\\Database\\Forge\:\:\$unsigned type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Forge.php - - message: '#^Method CodeIgniter\\Database\\MigrationRunner\:\:__construct\(\) has parameter \$db with no value type specified in iterable type array\.$#' count: 1 @@ -787,36 +652,6 @@ parameters: count: 1 path: ../../system/Database/MigrationRunner.php - - - message: '#^Method CodeIgniter\\Database\\MySQLi\\Forge\:\:_alterTable\(\) has parameter \$processedFields with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/MySQLi/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\MySQLi\\Forge\:\:_createTableAttributes\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/MySQLi/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\MySQLi\\Forge\:\:_processColumn\(\) has parameter \$processedField with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/MySQLi/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\MySQLi\\Forge\:\:_processIndexes\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/MySQLi/Forge.php - - - - message: '#^Property CodeIgniter\\Database\\MySQLi\\Forge\:\:\$_quoted_table_options type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/MySQLi/Forge.php - - - - message: '#^Property CodeIgniter\\Database\\MySQLi\\Forge\:\:\$_unsigned type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/MySQLi/Forge.php - - message: '#^Method CodeIgniter\\Database\\MySQLi\\PreparedQuery\:\:_execute\(\) has parameter \$data with no value type specified in iterable type array\.$#' count: 1 @@ -862,41 +697,6 @@ parameters: count: 1 path: ../../system/Database/OCI8/Connection.php - - - message: '#^Method CodeIgniter\\Database\\OCI8\\Forge\:\:_alterTable\(\) has parameter \$processedFields with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/OCI8/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\OCI8\\Forge\:\:_attributeAutoIncrement\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/OCI8/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\OCI8\\Forge\:\:_attributeAutoIncrement\(\) has parameter \$field with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/OCI8/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\OCI8\\Forge\:\:_attributeType\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/OCI8/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\OCI8\\Forge\:\:_processColumn\(\) has parameter \$processedField with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/OCI8/Forge.php - - - - message: '#^Property CodeIgniter\\Database\\OCI8\\Forge\:\:\$fkAllowActions type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/OCI8/Forge.php - - - - message: '#^Property CodeIgniter\\Database\\OCI8\\Forge\:\:\$unsigned type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/OCI8/Forge.php - - message: '#^Method CodeIgniter\\Database\\OCI8\\PreparedQuery\:\:_execute\(\) has parameter \$data with no value type specified in iterable type array\.$#' count: 1 @@ -932,41 +732,6 @@ parameters: count: 1 path: ../../system/Database/Postgre/Connection.php - - - message: '#^Method CodeIgniter\\Database\\Postgre\\Forge\:\:_alterTable\(\) has parameter \$processedFields with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Postgre/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\Postgre\\Forge\:\:_attributeAutoIncrement\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Postgre/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\Postgre\\Forge\:\:_attributeAutoIncrement\(\) has parameter \$field with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Postgre/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\Postgre\\Forge\:\:_attributeType\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Postgre/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\Postgre\\Forge\:\:_createTableAttributes\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Postgre/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\Postgre\\Forge\:\:_processColumn\(\) has parameter \$processedField with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Postgre/Forge.php - - - - message: '#^Property CodeIgniter\\Database\\Postgre\\Forge\:\:\$_unsigned type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Postgre/Forge.php - - message: '#^Method CodeIgniter\\Database\\Postgre\\PreparedQuery\:\:_execute\(\) has parameter \$data with no value type specified in iterable type array\.$#' count: 1 @@ -1102,51 +867,6 @@ parameters: count: 1 path: ../../system/Database/SQLSRV/Connection.php - - - message: '#^Method CodeIgniter\\Database\\SQLSRV\\Forge\:\:_alterTable\(\) has parameter \$processedFields with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLSRV/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\SQLSRV\\Forge\:\:_attributeAutoIncrement\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLSRV/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\SQLSRV\\Forge\:\:_attributeAutoIncrement\(\) has parameter \$field with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLSRV/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\SQLSRV\\Forge\:\:_attributeType\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLSRV/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\SQLSRV\\Forge\:\:_createTableAttributes\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLSRV/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\SQLSRV\\Forge\:\:_processColumn\(\) has parameter \$processedField with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLSRV/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\SQLSRV\\Forge\:\:_processIndexes\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLSRV/Forge.php - - - - message: '#^Property CodeIgniter\\Database\\SQLSRV\\Forge\:\:\$fkAllowActions type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLSRV/Forge.php - - - - message: '#^Property CodeIgniter\\Database\\SQLSRV\\Forge\:\:\$unsigned type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLSRV/Forge.php - - message: '#^Method CodeIgniter\\Database\\SQLSRV\\PreparedQuery\:\:_execute\(\) has parameter \$data with no value type specified in iterable type array\.$#' count: 1 @@ -1187,41 +907,6 @@ parameters: count: 1 path: ../../system/Database/SQLSRV/Utils.php - - - message: '#^Method CodeIgniter\\Database\\SQLite3\\Forge\:\:_alterTable\(\) has parameter \$processedFields with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLite3/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\SQLite3\\Forge\:\:_attributeAutoIncrement\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLite3/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\SQLite3\\Forge\:\:_attributeAutoIncrement\(\) has parameter \$field with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLite3/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\SQLite3\\Forge\:\:_attributeType\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLite3/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\SQLite3\\Forge\:\:_processColumn\(\) has parameter \$processedField with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLite3/Forge.php - - - - message: '#^Method CodeIgniter\\Database\\SQLite3\\Forge\:\:_processForeignKeys\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLite3/Forge.php - - - - message: '#^Property CodeIgniter\\Database\\SQLite3\\Forge\:\:\$_unsigned type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLite3/Forge.php - - message: '#^Method CodeIgniter\\Database\\SQLite3\\PreparedQuery\:\:_execute\(\) has parameter \$data with no value type specified in iterable type array\.$#' count: 1 diff --git a/utils/phpstan-baseline/property.phpDocType.neon b/utils/phpstan-baseline/property.phpDocType.neon index 31a1387605af..5b7e09269507 100644 --- a/utils/phpstan-baseline/property.phpDocType.neon +++ b/utils/phpstan-baseline/property.phpDocType.neon @@ -1,4 +1,4 @@ -# total 32 errors +# total 22 errors parameters: ignoreErrors: @@ -7,11 +7,6 @@ parameters: count: 1 path: ../../system/Database/MySQLi/Connection.php - - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\MySQLi\\Forge\:\:\$createDatabaseStr is not the same as PHPDoc type string\|false of overridden property CodeIgniter\\Database\\Forge\\:\:\$createDatabaseStr\.$#' - count: 1 - path: ../../system/Database/MySQLi/Forge.php - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\MySQLi\\Utils\:\:\$listDatabases is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\BaseUtils\:\:\$listDatabases\.$#' count: 1 @@ -27,31 +22,6 @@ parameters: count: 1 path: ../../system/Database/OCI8/Connection.php - - - message: '#^PHPDoc type false of property CodeIgniter\\Database\\OCI8\\Forge\:\:\$createDatabaseStr is not the same as PHPDoc type string\|false of overridden property CodeIgniter\\Database\\Forge\\:\:\$createDatabaseStr\.$#' - count: 1 - path: ../../system/Database/OCI8/Forge.php - - - - message: '#^PHPDoc type false of property CodeIgniter\\Database\\OCI8\\Forge\:\:\$createTableIfStr is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\Forge\\:\:\$createTableIfStr\.$#' - count: 1 - path: ../../system/Database/OCI8/Forge.php - - - - message: '#^PHPDoc type false of property CodeIgniter\\Database\\OCI8\\Forge\:\:\$dropDatabaseStr is not the same as PHPDoc type string\|false of overridden property CodeIgniter\\Database\\Forge\\:\:\$dropDatabaseStr\.$#' - count: 1 - path: ../../system/Database/OCI8/Forge.php - - - - message: '#^PHPDoc type false of property CodeIgniter\\Database\\OCI8\\Forge\:\:\$dropTableIfStr is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\Forge\\:\:\$dropTableIfStr\.$#' - count: 1 - path: ../../system/Database/OCI8/Forge.php - - - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\OCI8\\Forge\:\:\$renameTableStr is not the same as PHPDoc type string\|false of overridden property CodeIgniter\\Database\\Forge\\:\:\$renameTableStr\.$#' - count: 1 - path: ../../system/Database/OCI8/Forge.php - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\OCI8\\Utils\:\:\$listDatabases is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\BaseUtils\:\:\$listDatabases\.$#' count: 1 @@ -77,26 +47,6 @@ parameters: count: 1 path: ../../system/Database/SQLSRV/Connection.php - - - message: '#^PHPDoc type array of property CodeIgniter\\Database\\SQLSRV\\Forge\:\:\$unsigned is not the same as PHPDoc type array\|bool of overridden property CodeIgniter\\Database\\Forge\\:\:\$unsigned\.$#' - count: 1 - path: ../../system/Database/SQLSRV/Forge.php - - - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\SQLSRV\\Forge\:\:\$createDatabaseStr is not the same as PHPDoc type string\|false of overridden property CodeIgniter\\Database\\Forge\\:\:\$createDatabaseStr\.$#' - count: 1 - path: ../../system/Database/SQLSRV/Forge.php - - - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\SQLSRV\\Forge\:\:\$createTableIfStr is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\Forge\\:\:\$createTableIfStr\.$#' - count: 1 - path: ../../system/Database/SQLSRV/Forge.php - - - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\SQLSRV\\Forge\:\:\$renameTableStr is not the same as PHPDoc type string\|false of overridden property CodeIgniter\\Database\\Forge\\:\:\$renameTableStr\.$#' - count: 1 - path: ../../system/Database/SQLSRV/Forge.php - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\SQLSRV\\Utils\:\:\$listDatabases is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\BaseUtils\:\:\$listDatabases\.$#' count: 1 From 9ec51382cd27a3b20acdf49cd95833aca08d15f9 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Sun, 23 Aug 2026 20:00:06 +0700 Subject: [PATCH 067/163] chore: Bump Rector to 2.6.3, clean up deprecated rules, and re-run it (#10474) * Bump Rector to 2.6.3, clean up deprecated rules, and re-run it * fix return type --- composer.json | 2 +- rector.php | 10 ---------- system/CLI/InputOutput.php | 2 +- system/Config/DotEnv.php | 2 +- system/Database/Postgre/Builder.php | 2 +- system/Files/File.php | 2 +- system/HTTP/ResponseTrait.php | 4 ++-- system/HTTP/SiteURI.php | 4 ++-- system/Helpers/url_helper.php | 2 +- system/Honeypot/Honeypot.php | 2 +- system/I18n/TimeTrait.php | 2 +- system/Router/RouteCollection.php | 6 ++++-- system/Test/Mock/MockConnection.php | 2 +- 13 files changed, 17 insertions(+), 25 deletions(-) diff --git a/composer.json b/composer.json index 7fbbd44313bc..046faefcf83a 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "phpunit/phpcov": "^9.0.2 || ^10.0", "phpunit/phpunit": "^10.5.16 || ^11.2", "predis/predis": "^3.0", - "rector/rector": "2.6.1", + "rector/rector": "2.6.3", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { diff --git a/rector.php b/rector.php index f1bc1ae27374..4cb10eecfa10 100644 --- a/rector.php +++ b/rector.php @@ -25,10 +25,6 @@ use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodRector; use Rector\DeadCode\Rector\MethodCall\RemoveNullArgOnNullDefaultParamRector; use Rector\DeadCode\Rector\Property\RemoveDefaultValueFromAssignedPropertyRector; -use Rector\EarlyReturn\Rector\Foreach_\ChangeNestedForeachIfsToEarlyContinueRector; -use Rector\EarlyReturn\Rector\If_\ChangeIfElseValueAssignToEarlyReturnRector; -use Rector\EarlyReturn\Rector\If_\RemoveAlwaysElseRector; -use Rector\EarlyReturn\Rector\Return_\PreparedValueToEarlyReturnRector; use Rector\Php70\Rector\FuncCall\RandomFunctionRector; use Rector\Php71\Rector\FuncCall\RemoveExtraParametersRector; use Rector\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector; @@ -37,7 +33,6 @@ use Rector\Privatization\Rector\Class_\FinalizeTestCaseClassRector; use Rector\Privatization\Rector\Property\PrivatizeFinalClassPropertyRector; use Rector\Renaming\Rector\ConstFetch\RenameConstantRector; -use Rector\Strict\Rector\Empty_\DisallowedEmptyRuleFixerRector; use Rector\TypeDeclaration\Rector\ArrowFunction\AddArrowFunctionReturnTypeRector; use Rector\TypeDeclaration\Rector\ClassMethod\AddMethodCallBasedStrictParamTypeRector; use Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector; @@ -179,15 +174,10 @@ DeclareStrictTypesRector::class, UnderscoreToCamelCaseVariableNameRector::class, SimplifyUselessVariableRector::class, - RemoveAlwaysElseRector::class, PassStrictParameterToFunctionParameterRector::class, - ChangeNestedForeachIfsToEarlyContinueRector::class, - ChangeIfElseValueAssignToEarlyReturnRector::class, - PreparedValueToEarlyReturnRector::class, RemoveErrorSuppressInTryCatchStmtsRector::class, FuncGetArgsToVariadicParamRector::class, MakeInheritedMethodVisibilitySameAsParentRector::class, - DisallowedEmptyRuleFixerRector::class, PrivatizeFinalClassPropertyRector::class, VersionCompareFuncCallToConstantRector::class, AddClosureVoidReturnTypeWhereNoReturnRector::class, diff --git a/system/CLI/InputOutput.php b/system/CLI/InputOutput.php index b69c19e2eee1..6ed3b8f36138 100644 --- a/system/CLI/InputOutput.php +++ b/system/CLI/InputOutput.php @@ -51,7 +51,7 @@ public function input(?string $prefix = null): string $input = fgets(fopen('php://stdin', 'rb')); if ($input === false) { - $input = ''; + return ''; } return $input; diff --git a/system/Config/DotEnv.php b/system/Config/DotEnv.php index a820f42824f3..17c74d380c5c 100644 --- a/system/Config/DotEnv.php +++ b/system/Config/DotEnv.php @@ -204,7 +204,7 @@ protected function sanitizeValue(string $value): string protected function resolveNestedVariables(string $value): string { if (str_contains($value, '$')) { - $value = preg_replace_callback( + return preg_replace_callback( '/\${([a-zA-Z0-9_\.]+)}/', function ($matchedPatterns) { $nestedVariable = $this->getVariable($matchedPatterns[1]); diff --git a/system/Database/Postgre/Builder.php b/system/Database/Postgre/Builder.php index ee0a59c63bb1..4723a58e7cc8 100644 --- a/system/Database/Postgre/Builder.php +++ b/system/Database/Postgre/Builder.php @@ -57,7 +57,7 @@ protected function compileIgnore(string $statement) $sql = parent::compileIgnore($statement); if ($sql !== '') { - $sql = ' ' . trim($sql); + return ' ' . trim($sql); } return $sql; diff --git a/system/Files/File.php b/system/Files/File.php index 38eae2ebb6de..feda0cd5780d 100644 --- a/system/Files/File.php +++ b/system/Files/File.php @@ -218,7 +218,7 @@ private function getSizeByUnitInternal(int $fileSizeBase, FileSizeUnit $unit, in $size = $this->getSize() / $divider; if ($unit !== FileSizeUnit::B) { - $size = number_format($size, $precision); + return number_format($size, $precision); } return $size; diff --git a/system/HTTP/ResponseTrait.php b/system/HTTP/ResponseTrait.php index d67a05765e0e..95e58b01a05d 100644 --- a/system/HTTP/ResponseTrait.php +++ b/system/HTTP/ResponseTrait.php @@ -224,7 +224,7 @@ public function getXML() $body = $this->body; if ($this->bodyFormat !== 'xml') { - $body = service('format')->getFormatter('application/xml')->format($body); + return service('format')->getFormatter('application/xml')->format($body); } return $body; @@ -249,7 +249,7 @@ protected function formatBody($body, string $format) // Nothing much to do for a string... if (! is_string($body) || $format === 'json-unencoded') { - $body = service('format')->getFormatter($mime)->format($body); + return service('format')->getFormatter($mime)->format($body); } return $body; diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php index 7d07eff4aeeb..403269008e56 100644 --- a/system/HTTP/SiteURI.php +++ b/system/HTTP/SiteURI.php @@ -188,7 +188,7 @@ private function getIndexPageRoutePath(string $routePath): string $indexPageRoutePath = $indexPage . $routePath; if ($indexPageRoutePath === '/') { - $indexPageRoutePath = ''; + return ''; } return $indexPageRoutePath; @@ -420,7 +420,7 @@ public function baseUrl($relativePath = '', ?string $scheme = null): string private function stringifyRelativePath($relativePath): string { if (is_array($relativePath)) { - $relativePath = implode('/', $relativePath); + return implode('/', $relativePath); } return $relativePath; diff --git a/system/Helpers/url_helper.php b/system/Helpers/url_helper.php index 0b3fa5f72c9c..7ccab202e4ce 100644 --- a/system/Helpers/url_helper.php +++ b/system/Helpers/url_helper.php @@ -423,7 +423,7 @@ function prep_url(string $str = '', bool $secure = false): string // force replace http:// with https:// if ($secure) { - $str = preg_replace('/^(?:http):/i', 'https:', $str); + return preg_replace('/^(?:http):/i', 'https:', $str); } return $str; diff --git a/system/Honeypot/Honeypot.php b/system/Honeypot/Honeypot.php index badf1d719b53..4a40a8ec232a 100644 --- a/system/Honeypot/Honeypot.php +++ b/system/Honeypot/Honeypot.php @@ -111,7 +111,7 @@ protected function prepareTemplate(string $template): string $template = str_ireplace('{name}', $this->config->name, $template); if ($this->config->hidden) { - $template = str_ireplace('{template}', $template, $this->config->container); + return str_ireplace('{template}', $template, $this->config->container); } return $template; diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index 3189c2076f0a..588162b6f093 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -1157,7 +1157,7 @@ public function getUTCObject($time, ?string $timezone = null) } if ($time instanceof DateTime || $time instanceof DateTimeImmutable) { - $time = $time->setTimezone(new DateTimeZone('UTC')); + return $time->setTimezone(new DateTimeZone('UTC')); } return $time; diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index b5bd4ee76597..198168675fad 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -1455,8 +1455,10 @@ private function checkHostname($hostname): bool /** * @param array $to + * + * @return array|string */ - private function processArrayCallableSyntax(string $from, array $to): string + private function processArrayCallableSyntax(string $from, array $to): array|string { // [classname, method] // eg, [Home::class, 'index'] @@ -1474,7 +1476,7 @@ private function processArrayCallableSyntax(string $from, array $to): string && is_callable($to[0], true, $callableName) && is_string($to[1]) ) { - $to = '\\' . $callableName . '/' . $to[1]; + return '\\' . $callableName . '/' . $to[1]; } return $to; diff --git a/system/Test/Mock/MockConnection.php b/system/Test/Mock/MockConnection.php index e19e635568ea..18a3eda45d63 100644 --- a/system/Test/Mock/MockConnection.php +++ b/system/Test/Mock/MockConnection.php @@ -128,7 +128,7 @@ public function connect(bool $persistent = false) if (is_array($return)) { // By removing the top item here, we can // get a different value for, say, testing failover connections. - $return = array_shift($this->returnValues['connect']); + return array_shift($this->returnValues['connect']); } return $return; From 22a7e4ed8b5c3affec1f0493f3437419b2ead1ba Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Mon, 24 Aug 2026 04:19:15 +0800 Subject: [PATCH 068/163] refactor: fix phpstan errors in `Result` (#10473) --- system/Database/BaseResult.php | 143 +---------- system/Database/MySQLi/Connection.php | 2 +- system/Database/MySQLi/Result.php | 40 +-- system/Database/OCI8/Result.php | 33 +-- system/Database/Postgre/Result.php | 39 +-- system/Database/ResultInterface.php | 30 ++- system/Database/SQLSRV/Result.php | 38 +-- system/Database/SQLite3/Result.php | 35 +-- system/Test/Mock/MockResult.php | 35 +-- system/View/Table.php | 2 +- tests/system/Database/BaseResultTest.php | 4 +- tests/system/View/DBResultDummy.php | 2 +- utils/phpstan-baseline/loader.neon | 2 +- .../method.childParameterType.neon | 7 +- .../missingType.iterableValue.neon | 242 +----------------- 15 files changed, 43 insertions(+), 611 deletions(-) diff --git a/system/Database/BaseResult.php b/system/Database/BaseResult.php index 1b8fcdbd0cf8..5855eb064c86 100644 --- a/system/Database/BaseResult.php +++ b/system/Database/BaseResult.php @@ -25,42 +25,32 @@ abstract class BaseResult implements ResultInterface { /** - * Connection ID - * * @var TConnection */ public $connID; /** - * Result ID - * * @var false|TResult */ public $resultID; /** - * Result Array - * * @var list */ public $resultArray = []; /** - * Result Object - * * @var list */ public $resultObject = []; /** - * Custom Result Object - * - * @var array + * @var array> */ public $customResultObject = []; /** - * Current Row index + * Current row index * * @var int */ @@ -74,15 +64,11 @@ abstract class BaseResult implements ResultInterface protected $numRows; /** - * Row data - * - * @var array|null + * @var array|null */ public $rowData; /** - * Constructor - * * @param TConnection $connID * @param TResult $resultID */ @@ -92,13 +78,6 @@ public function __construct(&$connID, &$resultID) $this->resultID = $resultID; } - /** - * Retrieve the results of the query. Typically an array of - * individual data rows, which can be either an 'array', an - * 'object', or a custom class name. - * - * @param string $type The row type. Either 'array', 'object', or a class name to use - */ public function getResult(string $type = 'object'): array { if ($type === 'array') { @@ -112,13 +91,6 @@ public function getResult(string $type = 'object'): array return $this->getCustomResultObject($type); } - /** - * Returns the results as an array of custom objects. - * - * @param class-string $className - * - * @return array - */ public function getCustomResultObject(string $className) { if (isset($this->customResultObject[$className])) { @@ -165,11 +137,6 @@ public function getCustomResultObject(string $className) return $this->customResultObject[$className]; } - /** - * Returns the results as an array of arrays. - * - * If no results, an empty array is returned. - */ public function getResultArray(): array { if ($this->resultArray !== []) { @@ -202,13 +169,6 @@ public function getResultArray(): array return $this->resultArray; } - /** - * Returns the results as an array of objects. - * - * If no results, an empty array is returned. - * - * @return list - */ public function getResultObject(): array { if ($this->resultObject !== []) { @@ -245,19 +205,6 @@ public function getResultObject(): array return $this->resultObject; } - /** - * Wrapper object to return a row as either an array, an object, or - * a custom class. - * - * If the row doesn't exist, returns null. - * - * @template T of object - * - * @param int|string $n The index of the results to return, or column name. - * @param 'array'|'object'|class-string $type The type of result object. 'array', 'object' or class name. - * - * @return ($n is string ? float|int|string|null : ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null))) - */ public function getRow($n = 0, string $type = 'object') { // $n is a column name. @@ -286,18 +233,6 @@ public function getRow($n = 0, string $type = 'object') return $this->getCustomRowObject($n, $type); } - /** - * Returns a row as a custom class instance. - * - * If the row doesn't exist, returns null. - * - * @template T of object - * - * @param int $n The index of the results to return. - * @param class-string $className - * - * @return T|null - */ public function getCustomRowObject(int $n, string $className) { if (! isset($this->customResultObject[$className])) { @@ -315,13 +250,6 @@ public function getCustomRowObject(int $n, string $className) return $this->customResultObject[$className][$this->currentRow] ?? null; } - /** - * Returns a single row from the results as an array. - * - * If row doesn't exist, returns null. - * - * @return array|null - */ public function getRowArray(int $n = 0) { $result = $this->getResultArray(); @@ -336,13 +264,6 @@ public function getRowArray(int $n = 0) return $result[$this->currentRow] ?? null; } - /** - * Returns a single row from the results as an object. - * - * If row doesn't exist, returns null. - * - * @return object|stdClass|null - */ public function getRowObject(int $n = 0) { $result = $this->getResultObject(); @@ -357,14 +278,6 @@ public function getRowObject(int $n = 0) return $result[$this->currentRow] ?? null; } - /** - * Assigns an item into a particular column slot. - * - * @param array|string $key - * @param array|object|stdClass|null $value - * - * @return void - */ public function setRow($key, $value = null) { // We cache the row data for subsequent uses @@ -385,9 +298,6 @@ public function setRow($key, $value = null) } } - /** - * Returns the "first" row of the current results. - */ public function getFirstRow(string $type = 'object') { $result = $this->getResult($type); @@ -395,9 +305,6 @@ public function getFirstRow(string $type = 'object') return ($result === []) ? null : $result[0]; } - /** - * Returns the "last" row of the current results. - */ public function getLastRow(string $type = 'object') { $result = $this->getResult($type); @@ -405,9 +312,6 @@ public function getLastRow(string $type = 'object') return ($result === []) ? null : $result[count($result) - 1]; } - /** - * Returns the "next" row of the current results. - */ public function getNextRow(string $type = 'object') { $result = $this->getResult($type); @@ -418,9 +322,6 @@ public function getNextRow(string $type = 'object') return isset($result[$this->currentRow + 1]) ? $result[++$this->currentRow] : null; } - /** - * Returns the "previous" row of the current results. - */ public function getPreviousRow(string $type = 'object') { $result = $this->getResult($type); @@ -435,11 +336,6 @@ public function getPreviousRow(string $type = 'object') return $result[$this->currentRow] ?? null; } - /** - * Returns an unbuffered row and move the pointer to the next row. - * - * @return array|object|null - */ public function getUnbufferedRow(string $type = 'object') { if ($type === 'array') { @@ -478,43 +374,12 @@ private function isValidResultId(): bool return is_resource($this->resultID) || is_object($this->resultID); } - /** - * Gets the number of fields in the result set. - */ - abstract public function getFieldCount(): int; - - /** - * Generates an array of column names in the result set. - */ - abstract public function getFieldNames(): array; - - /** - * Generates an array of objects representing field meta-data. - */ - abstract public function getFieldData(): array; - - /** - * Frees the current result. - * - * @return void - */ - abstract public function freeResult(); - - /** - * Moves the internal pointer to the desired offset. This is called - * internally before fetching results to make sure the result set - * starts at zero. - * - * @return bool - */ - abstract public function dataSeek(int $n = 0); - /** * Returns the result set as an array. * * Overridden by driver classes. * - * @return array|false|null + * @return array|false|null */ abstract protected function fetchAssoc(); diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index 5a6daa5a692e..29cb80b168c5 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -481,7 +481,7 @@ protected function _indexData(string $table): array $type = 'PRIMARY'; } elseif ($index['Index_type'] === 'FULLTEXT') { $type = 'FULLTEXT'; - } elseif ($index['Non_unique']) { + } elseif ((bool) $index['Non_unique']) { $type = $index['Index_type'] === 'SPATIAL' ? 'SPATIAL' : 'INDEX'; } else { $type = 'UNIQUE'; diff --git a/system/Database/MySQLi/Result.php b/system/Database/MySQLi/Result.php index 05d07bb69078..956a4e0a705c 100644 --- a/system/Database/MySQLi/Result.php +++ b/system/Database/MySQLi/Result.php @@ -26,17 +26,11 @@ */ class Result extends BaseResult { - /** - * Gets the number of fields in the result set. - */ public function getFieldCount(): int { return $this->resultID->field_count; } - /** - * Generates an array of column names in the result set. - */ public function getFieldNames(): array { $fieldNames = []; @@ -49,9 +43,6 @@ public function getFieldNames(): array return $fieldNames; } - /** - * Generates an array of objects representing field meta-data. - */ public function getFieldData(): array { static $dataTypes = [ @@ -103,11 +94,6 @@ public function getFieldData(): array return $retVal; } - /** - * Frees the current result. - * - * @return void - */ public function freeResult() { if (is_object($this->resultID)) { @@ -116,38 +102,17 @@ public function freeResult() } } - /** - * Moves the internal pointer to the desired offset. This is called - * internally before fetching results to make sure the result set - * starts at zero. - * - * @return bool - */ public function dataSeek(int $n = 0) { return $this->resultID->data_seek($n); } - /** - * Returns the result set as an array. - * - * Overridden by driver classes. - * - * @return array|false|null - */ protected function fetchAssoc() { return $this->resultID->fetch_assoc(); } - /** - * Returns the result set as an object. - * - * Overridden by child classes. - * - * @return Entity|false|object|stdClass - */ - protected function fetchObject(string $className = 'stdClass') + protected function fetchObject(string $className = stdClass::class) { if (is_subclass_of($className, Entity::class)) { $data = $this->fetchAssoc(); @@ -158,9 +123,6 @@ protected function fetchObject(string $className = 'stdClass') return $this->resultID->fetch_object($className); } - /** - * Returns the number of rows in the resultID (i.e., mysqli_result object) - */ public function getNumRows(): int { if (! is_int($this->numRows)) { diff --git a/system/Database/OCI8/Result.php b/system/Database/OCI8/Result.php index 1049847b7af0..6882bff00f45 100644 --- a/system/Database/OCI8/Result.php +++ b/system/Database/OCI8/Result.php @@ -24,25 +24,16 @@ */ class Result extends BaseResult { - /** - * Gets the number of fields in the result set. - */ public function getFieldCount(): int { return oci_num_fields($this->resultID); } - /** - * Generates an array of column names in the result set. - */ public function getFieldNames(): array { return array_map(fn ($fieldIndex): false|string => oci_field_name($this->resultID, $fieldIndex), range(1, $this->getFieldCount())); } - /** - * Generates an array of objects representing field meta-data. - */ public function getFieldData(): array { return array_map(fn ($fieldIndex) => (object) [ @@ -52,11 +43,6 @@ public function getFieldData(): array ], range(1, $this->getFieldCount())); } - /** - * Frees the current result. - * - * @return void - */ public function freeResult() { if (is_resource($this->resultID)) { @@ -66,10 +52,6 @@ public function freeResult() } /** - * Moves the internal pointer to the desired offset. This is called - * internally before fetching results to make sure the result set - * starts at zero. - * * @return false */ public function dataSeek(int $n = 0) @@ -79,25 +61,14 @@ public function dataSeek(int $n = 0) } /** - * Returns the result set as an array. - * - * Overridden by driver classes. - * - * @return array|false + * @return array|false */ protected function fetchAssoc() { return oci_fetch_assoc($this->resultID); } - /** - * Returns the result set as an object. - * - * Overridden by child classes. - * - * @return Entity|false|object|stdClass - */ - protected function fetchObject(string $className = 'stdClass') + protected function fetchObject(string $className = stdClass::class) { $row = oci_fetch_object($this->resultID); diff --git a/system/Database/Postgre/Result.php b/system/Database/Postgre/Result.php index fee628296173..d071ff27f6f5 100644 --- a/system/Database/Postgre/Result.php +++ b/system/Database/Postgre/Result.php @@ -26,17 +26,11 @@ */ class Result extends BaseResult { - /** - * Gets the number of fields in the result set. - */ public function getFieldCount(): int { return pg_num_fields($this->resultID); } - /** - * Generates an array of column names in the result set. - */ public function getFieldNames(): array { $fieldNames = []; @@ -48,9 +42,6 @@ public function getFieldNames(): array return $fieldNames; } - /** - * Generates an array of objects representing field meta-data. - */ public function getFieldData(): array { $retVal = []; @@ -69,11 +60,6 @@ public function getFieldData(): array return $retVal; } - /** - * Frees the current result. - * - * @return void - */ public function freeResult() { if ($this->resultID !== false) { @@ -82,38 +68,20 @@ public function freeResult() } } - /** - * Moves the internal pointer to the desired offset. This is called - * internally before fetching results to make sure the result set - * starts at zero. - * - * @return bool - */ public function dataSeek(int $n = 0) { return pg_result_seek($this->resultID, $n); } /** - * Returns the result set as an array. - * - * Overridden by driver classes. - * - * @return array|false + * @return array|false */ protected function fetchAssoc() { return pg_fetch_assoc($this->resultID); } - /** - * Returns the result set as an object. - * - * Overridden by child classes. - * - * @return Entity|false|object|stdClass - */ - protected function fetchObject(string $className = 'stdClass') + protected function fetchObject(string $className = stdClass::class) { if (is_subclass_of($className, Entity::class)) { $data = $this->fetchAssoc(); @@ -124,9 +92,6 @@ protected function fetchObject(string $className = 'stdClass') return pg_fetch_object($this->resultID, null, $className); } - /** - * Returns the number of rows in the resultID (i.e., PostgreSQL query result resource) - */ public function getNumRows(): int { if (! is_int($this->numRows)) { diff --git a/system/Database/ResultInterface.php b/system/Database/ResultInterface.php index 0157a1df8149..48de9f01c3d4 100644 --- a/system/Database/ResultInterface.php +++ b/system/Database/ResultInterface.php @@ -27,6 +27,8 @@ interface ResultInterface * 'object', or a custom class name. * * @param string $type The row type. Either 'array', 'object', or a class name to use + * + * @return ($type is 'array' ? list> : ($type is 'object' ? list : list)) */ public function getResult(string $type = 'object'): array; @@ -35,7 +37,7 @@ public function getResult(string $type = 'object'): array; * * @param string $className The name of the class to use. * - * @return array + * @return list */ public function getCustomResultObject(string $className); @@ -43,6 +45,8 @@ public function getCustomResultObject(string $className); * Returns the results as an array of arrays. * * If no results, an empty array is returned. + * + * @return list> */ public function getResultArray(): array; @@ -50,6 +54,8 @@ public function getResultArray(): array; * Returns the results as an array of objects. * * If no results, an empty array is returned. + * + * @return list */ public function getResultObject(): array; @@ -64,7 +70,7 @@ public function getResultObject(): array; * @param int|string $n The index of the results to return, or column name. * @param 'array'|'object'|class-string $type The type of result object. 'array', 'object' or class name. * - * @return ($n is string ? float|int|string|null : ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null))) + * @return ($n is string ? float|int|string|null : ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null))) */ public function getRow($n = 0, string $type = 'object'); @@ -87,7 +93,7 @@ public function getCustomRowObject(int $n, string $className); * * If row doesn't exist, returns null. * - * @return array|null + * @return array|null */ public function getRowArray(int $n = 0); @@ -103,8 +109,8 @@ public function getRowObject(int $n = 0); /** * Assigns an item into a particular column slot. * - * @param array|string $key - * @param array|object|stdClass|null $value + * @param array|string $key + * @param array|object|null $value * * @return void */ @@ -117,7 +123,7 @@ public function setRow($key, $value = null); * * @param 'array'|'object'|class-string $type The type of result object. 'array', 'object' or class name. * - * @return ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null)) + * @return ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null)) */ public function getFirstRow(string $type = 'object'); @@ -128,7 +134,7 @@ public function getFirstRow(string $type = 'object'); * * @param 'array'|'object'|class-string $type The type of result object. 'array', 'object' or class name. * - * @return ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null)) + * @return ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null)) */ public function getLastRow(string $type = 'object'); @@ -139,7 +145,7 @@ public function getLastRow(string $type = 'object'); * * @param 'array'|'object'|class-string $type The type of result object. 'array', 'object' or class name. * - * @return ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null)) + * @return ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null)) */ public function getNextRow(string $type = 'object'); @@ -150,7 +156,7 @@ public function getNextRow(string $type = 'object'); * * @param 'array'|'object'|class-string $type The type of result object. 'array', 'object' or class name. * - * @return ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null)) + * @return ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null)) */ public function getPreviousRow(string $type = 'object'); @@ -162,7 +168,7 @@ public function getNumRows(): int; /** * Returns an unbuffered row and move the pointer to the next row. * - * @return array|object|null + * @return array|object|null */ public function getUnbufferedRow(string $type = 'object'); @@ -173,11 +179,15 @@ public function getFieldCount(): int; /** * Generates an array of column names in the result set. + * + * @return list */ public function getFieldNames(): array; /** * Generates an array of objects representing field meta-data. + * + * @return list */ public function getFieldData(): array; diff --git a/system/Database/SQLSRV/Result.php b/system/Database/SQLSRV/Result.php index 27e3ee1e5256..5a5f3beed997 100644 --- a/system/Database/SQLSRV/Result.php +++ b/system/Database/SQLSRV/Result.php @@ -24,17 +24,11 @@ */ class Result extends BaseResult { - /** - * Gets the number of fields in the result set. - */ public function getFieldCount(): int { return @sqlsrv_num_fields($this->resultID); } - /** - * Generates an array of column names in the result set. - */ public function getFieldNames(): array { $fieldNames = []; @@ -46,9 +40,6 @@ public function getFieldNames(): array return $fieldNames; } - /** - * Generates an array of objects representing field meta-data. - */ public function getFieldData(): array { static $dataTypes = [ @@ -103,11 +94,6 @@ public function getFieldData(): array return $retVal; } - /** - * Frees the current result. - * - * @return void - */ public function freeResult() { if (is_resource($this->resultID)) { @@ -116,13 +102,6 @@ public function freeResult() } } - /** - * Moves the internal pointer to the desired offset. This is called - * internally before fetching results to make sure the result set - * starts at zero. - * - * @return bool - */ public function dataSeek(int $n = 0) { if ($n > 0) { @@ -136,24 +115,12 @@ public function dataSeek(int $n = 0) return true; } - /** - * Returns the result set as an array. - * - * Overridden by driver classes. - * - * @return array|false|null - */ protected function fetchAssoc() { return sqlsrv_fetch_array($this->resultID, SQLSRV_FETCH_ASSOC); } - /** - * Returns the result set as an object. - * - * @return Entity|false|object|stdClass - */ - protected function fetchObject(string $className = 'stdClass') + protected function fetchObject(string $className = stdClass::class) { if (is_subclass_of($className, Entity::class)) { $data = $this->fetchAssoc(); @@ -164,9 +131,6 @@ protected function fetchObject(string $className = 'stdClass') return sqlsrv_fetch_object($this->resultID, $className); } - /** - * Returns the number of rows in the resultID (i.e., SQLSRV query result resource) - */ public function getNumRows(): int { if (! is_int($this->numRows)) { diff --git a/system/Database/SQLite3/Result.php b/system/Database/SQLite3/Result.php index aa5abf28682a..a3c8556fccfe 100644 --- a/system/Database/SQLite3/Result.php +++ b/system/Database/SQLite3/Result.php @@ -28,17 +28,11 @@ */ class Result extends BaseResult { - /** - * Gets the number of fields in the result set. - */ public function getFieldCount(): int { return $this->resultID->numColumns(); } - /** - * Generates an array of column names in the result set. - */ public function getFieldNames(): array { $fieldNames = []; @@ -50,9 +44,6 @@ public function getFieldNames(): array return $fieldNames; } - /** - * Generates an array of objects representing field meta-data. - */ public function getFieldData(): array { static $dataTypes = [ @@ -80,11 +71,6 @@ public function getFieldData(): array return $retVal; } - /** - * Frees the current result. - * - * @return void - */ public function freeResult() { if (is_object($this->resultID)) { @@ -94,12 +80,6 @@ public function freeResult() } /** - * Moves the internal pointer to the desired offset. This is called - * internally before fetching results to make sure the result set - * starts at zero. - * - * @return bool - * * @throws DatabaseException */ public function dataSeek(int $n = 0) @@ -112,25 +92,14 @@ public function dataSeek(int $n = 0) } /** - * Returns the result set as an array. - * - * Overridden by driver classes. - * - * @return array|false + * @return array|false */ protected function fetchAssoc() { return $this->resultID->fetchArray(SQLITE3_ASSOC); } - /** - * Returns the result set as an object. - * - * Overridden by child classes. - * - * @return Entity|false|object|stdClass - */ - protected function fetchObject(string $className = 'stdClass') + protected function fetchObject(string $className = stdClass::class) { // No native support for fetching rows as objects if (($row = $this->fetchAssoc()) === false) { diff --git a/system/Test/Mock/MockResult.php b/system/Test/Mock/MockResult.php index bc5cd8ac2054..28147af9cd9e 100644 --- a/system/Test/Mock/MockResult.php +++ b/system/Test/Mock/MockResult.php @@ -21,17 +21,12 @@ */ class MockResult extends BaseResult { - /** - * Gets the number of fields in the result set. - */ public function getFieldCount(): int { return 0; } /** - * Generates an array of column names in the result set. - * * @return array{} */ public function getFieldNames(): array @@ -40,8 +35,6 @@ public function getFieldNames(): array } /** - * Generates an array of objects representing field meta-data. - * * @return array{} */ public function getFieldData(): array @@ -49,34 +42,19 @@ public function getFieldData(): array return []; } - /** - * Frees the current result. - * - * @return void - */ public function freeResult() { } /** - * Moves the internal pointer to the desired offset. This is called - * internally before fetching results to make sure the result set - * starts at zero. - * - * @param int $n - * - * @return bool + * @return true */ - public function dataSeek($n = 0) + public function dataSeek(int $n = 0) { return true; } /** - * Returns the result set as an array. - * - * Overridden by driver classes. - * * @return array{} */ protected function fetchAssoc() @@ -85,20 +63,13 @@ protected function fetchAssoc() } /** - * Returns the result set as an object. - * - * @param class-string $className - * * @return object */ - protected function fetchObject($className = stdClass::class) + protected function fetchObject(string $className = stdClass::class) { return new $className(); } - /** - * Gets the number of fields in the result set. - */ public function getNumRows(): int { return 0; diff --git a/system/View/Table.php b/system/View/Table.php index ee6057811615..42ee5402adff 100644 --- a/system/View/Table.php +++ b/system/View/Table.php @@ -260,7 +260,7 @@ public function setSyncRowsWithHeading(bool $orderByKey) * * Ensures a standard associative array format for all cell data * - * @param array $args + * @param array $args * * @return array>|list> */ diff --git a/tests/system/Database/BaseResultTest.php b/tests/system/Database/BaseResultTest.php index 901faee6a565..aa14f54ec8fb 100644 --- a/tests/system/Database/BaseResultTest.php +++ b/tests/system/Database/BaseResultTest.php @@ -67,7 +67,7 @@ public function getFieldNames(): array } /** - * @return list + * @return list */ public function getFieldData(): array { @@ -84,7 +84,7 @@ public function dataSeek(int $n = 0): bool } /** - * @return false|list>|null + * @return array|false|null */ protected function fetchAssoc(): array|bool|null { diff --git a/tests/system/View/DBResultDummy.php b/tests/system/View/DBResultDummy.php index fb42f41b04b6..62ebb298b853 100644 --- a/tests/system/View/DBResultDummy.php +++ b/tests/system/View/DBResultDummy.php @@ -27,7 +27,7 @@ public function getFieldNames(): array } /** - * @return array> + * @return list> */ public function getResultArray(): array { diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index a5cf84a90e35..b3302ee9448e 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 703 errors +# total 654 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/method.childParameterType.neon b/utils/phpstan-baseline/method.childParameterType.neon index aa135b562c3e..d237e221ebf5 100644 --- a/utils/phpstan-baseline/method.childParameterType.neon +++ b/utils/phpstan-baseline/method.childParameterType.neon @@ -1,4 +1,4 @@ -# total 6 errors +# total 5 errors parameters: ignoreErrors: @@ -7,11 +7,6 @@ parameters: count: 1 path: ../../system/Cookie/Cookie.php - - - message: '#^Parameter \#1 \$className \(class\-string\) of method CodeIgniter\\Database\\BaseResult\:\:getCustomResultObject\(\) should be contravariant with parameter \$className \(string\) of method CodeIgniter\\Database\\ResultInterface\\:\:getCustomResultObject\(\)$#' - count: 1 - path: ../../system/Database/BaseResult.php - - message: '#^Parameter \#1 \$data \(array\{date\: string, timezone\: string, timezone_type\: int\}\) of method CodeIgniter\\I18n\\Time\:\:__unserialize\(\) should be contravariant with parameter \$data \(array\) of method DateTimeImmutable\:\:__unserialize\(\)$#' count: 1 diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 1078edd6ed71..6777cc2472c3 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 518 errors +# total 470 errors parameters: ignoreErrors: @@ -442,91 +442,6 @@ parameters: count: 1 path: ../../system/Database/BasePreparedQuery.php - - - message: '#^Method CodeIgniter\\Database\\BaseResult\:\:fetchAssoc\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseResult.php - - - - message: '#^Method CodeIgniter\\Database\\BaseResult\:\:getCustomResultObject\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseResult.php - - - - message: '#^Method CodeIgniter\\Database\\BaseResult\:\:getFieldData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseResult.php - - - - message: '#^Method CodeIgniter\\Database\\BaseResult\:\:getFieldNames\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseResult.php - - - - message: '#^Method CodeIgniter\\Database\\BaseResult\:\:getFirstRow\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseResult.php - - - - message: '#^Method CodeIgniter\\Database\\BaseResult\:\:getLastRow\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseResult.php - - - - message: '#^Method CodeIgniter\\Database\\BaseResult\:\:getNextRow\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseResult.php - - - - message: '#^Method CodeIgniter\\Database\\BaseResult\:\:getPreviousRow\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseResult.php - - - - message: '#^Method CodeIgniter\\Database\\BaseResult\:\:getResultArray\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseResult.php - - - - message: '#^Method CodeIgniter\\Database\\BaseResult\:\:getResult\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseResult.php - - - - message: '#^Method CodeIgniter\\Database\\BaseResult\:\:getRowArray\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseResult.php - - - - message: '#^Method CodeIgniter\\Database\\BaseResult\:\:getRow\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseResult.php - - - - message: '#^Method CodeIgniter\\Database\\BaseResult\:\:getUnbufferedRow\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseResult.php - - - - message: '#^Method CodeIgniter\\Database\\BaseResult\:\:setRow\(\) has parameter \$key with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseResult.php - - - - message: '#^Method CodeIgniter\\Database\\BaseResult\:\:setRow\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseResult.php - - - - message: '#^Property CodeIgniter\\Database\\BaseResult\:\:\$customResultObject type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseResult.php - - - - message: '#^Property CodeIgniter\\Database\\BaseResult\:\:\$rowData type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseResult.php - - message: '#^Method CodeIgniter\\Database\\BaseUtils\:\:_backup\(\) has parameter \$prefs with no value type specified in iterable type array\.$#' count: 1 @@ -662,21 +577,6 @@ parameters: count: 1 path: ../../system/Database/MySQLi/PreparedQuery.php - - - message: '#^Method CodeIgniter\\Database\\MySQLi\\Result\:\:fetchAssoc\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/MySQLi/Result.php - - - - message: '#^Method CodeIgniter\\Database\\MySQLi\\Result\:\:getFieldData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/MySQLi/Result.php - - - - message: '#^Method CodeIgniter\\Database\\MySQLi\\Result\:\:getFieldNames\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/MySQLi/Result.php - - message: '#^Method CodeIgniter\\Database\\MySQLi\\Utils\:\:_backup\(\) has parameter \$prefs with no value type specified in iterable type array\.$#' count: 1 @@ -707,21 +607,6 @@ parameters: count: 1 path: ../../system/Database/OCI8/PreparedQuery.php - - - message: '#^Method CodeIgniter\\Database\\OCI8\\Result\:\:fetchAssoc\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/OCI8/Result.php - - - - message: '#^Method CodeIgniter\\Database\\OCI8\\Result\:\:getFieldData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/OCI8/Result.php - - - - message: '#^Method CodeIgniter\\Database\\OCI8\\Result\:\:getFieldNames\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/OCI8/Result.php - - message: '#^Method CodeIgniter\\Database\\OCI8\\Utils\:\:_backup\(\) has parameter \$prefs with no value type specified in iterable type array\.$#' count: 1 @@ -742,21 +627,6 @@ parameters: count: 1 path: ../../system/Database/Postgre/PreparedQuery.php - - - message: '#^Method CodeIgniter\\Database\\Postgre\\Result\:\:fetchAssoc\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Postgre/Result.php - - - - message: '#^Method CodeIgniter\\Database\\Postgre\\Result\:\:getFieldData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Postgre/Result.php - - - - message: '#^Method CodeIgniter\\Database\\Postgre\\Result\:\:getFieldNames\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Postgre/Result.php - - message: '#^Method CodeIgniter\\Database\\Postgre\\Utils\:\:_backup\(\) has parameter \$prefs with no value type specified in iterable type array\.$#' count: 1 @@ -787,81 +657,6 @@ parameters: count: 1 path: ../../system/Database/Query.php - - - message: '#^Method CodeIgniter\\Database\\ResultInterface\:\:getCustomResultObject\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/ResultInterface.php - - - - message: '#^Method CodeIgniter\\Database\\ResultInterface\:\:getFieldData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/ResultInterface.php - - - - message: '#^Method CodeIgniter\\Database\\ResultInterface\:\:getFieldNames\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/ResultInterface.php - - - - message: '#^Method CodeIgniter\\Database\\ResultInterface\:\:getFirstRow\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/ResultInterface.php - - - - message: '#^Method CodeIgniter\\Database\\ResultInterface\:\:getLastRow\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/ResultInterface.php - - - - message: '#^Method CodeIgniter\\Database\\ResultInterface\:\:getNextRow\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/ResultInterface.php - - - - message: '#^Method CodeIgniter\\Database\\ResultInterface\:\:getPreviousRow\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/ResultInterface.php - - - - message: '#^Method CodeIgniter\\Database\\ResultInterface\:\:getResultArray\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/ResultInterface.php - - - - message: '#^Method CodeIgniter\\Database\\ResultInterface\:\:getResultObject\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/ResultInterface.php - - - - message: '#^Method CodeIgniter\\Database\\ResultInterface\:\:getResult\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/ResultInterface.php - - - - message: '#^Method CodeIgniter\\Database\\ResultInterface\:\:getRowArray\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/ResultInterface.php - - - - message: '#^Method CodeIgniter\\Database\\ResultInterface\:\:getRow\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/ResultInterface.php - - - - message: '#^Method CodeIgniter\\Database\\ResultInterface\:\:getUnbufferedRow\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/ResultInterface.php - - - - message: '#^Method CodeIgniter\\Database\\ResultInterface\:\:setRow\(\) has parameter \$key with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/ResultInterface.php - - - - message: '#^Method CodeIgniter\\Database\\ResultInterface\:\:setRow\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/ResultInterface.php - - message: '#^Method CodeIgniter\\Database\\SQLSRV\\Connection\:\:__construct\(\) has parameter \$params with no value type specified in iterable type array\.$#' count: 1 @@ -887,21 +682,6 @@ parameters: count: 1 path: ../../system/Database/SQLSRV/PreparedQuery.php - - - message: '#^Method CodeIgniter\\Database\\SQLSRV\\Result\:\:fetchAssoc\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLSRV/Result.php - - - - message: '#^Method CodeIgniter\\Database\\SQLSRV\\Result\:\:getFieldData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLSRV/Result.php - - - - message: '#^Method CodeIgniter\\Database\\SQLSRV\\Result\:\:getFieldNames\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLSRV/Result.php - - message: '#^Method CodeIgniter\\Database\\SQLSRV\\Utils\:\:_backup\(\) has parameter \$prefs with no value type specified in iterable type array\.$#' count: 1 @@ -917,21 +697,6 @@ parameters: count: 1 path: ../../system/Database/SQLite3/PreparedQuery.php - - - message: '#^Method CodeIgniter\\Database\\SQLite3\\Result\:\:fetchAssoc\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLite3/Result.php - - - - message: '#^Method CodeIgniter\\Database\\SQLite3\\Result\:\:getFieldData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLite3/Result.php - - - - message: '#^Method CodeIgniter\\Database\\SQLite3\\Result\:\:getFieldNames\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLite3/Result.php - - message: '#^Method CodeIgniter\\Database\\SQLite3\\Table\:\:addForeignKey\(\) has parameter \$foreignKeys with no value type specified in iterable type array\.$#' count: 1 @@ -2557,11 +2322,6 @@ parameters: count: 1 path: ../../tests/system/Throttle/ThrottleTest.php - - - message: '#^Method CodeIgniter\\View\\DBResultDummy\:\:getFieldNames\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/View/DBResultDummy.php - - message: '#^Method CodeIgniter\\View\\ParserTest\:\:provideEscHandling\(\) return type has no value type specified in iterable type iterable\.$#' count: 1 From 9a4acaae31b7611d8ea1029256b2625638e09dd2 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Mon, 24 Aug 2026 05:14:23 +0800 Subject: [PATCH 069/163] refactor: fix phpstan errors in `Connection` (#10475) --- system/Database/BaseConnection.php | 125 +++++++------- system/Database/ConnectionInterface.php | 6 +- system/Database/MySQLi/Connection.php | 37 +---- system/Database/OCI8/Connection.php | 54 +------ system/Database/Postgre/Connection.php | 47 +----- system/Database/SQLSRV/Connection.php | 42 +---- system/Database/SQLite3/Connection.php | 39 +---- tests/system/Database/Live/EscapeTest.php | 3 + tests/system/Database/Live/GetVersionTest.php | 2 + .../Database/Live/MySQLi/FoundRowsTest.php | 3 + .../Database/Live/MySQLi/NumberNativeTest.php | 3 + .../Live/OCI8/CallStoredProcedureTest.php | 7 + tests/system/Database/Live/OrderTest.php | 3 + .../Database/Live/PreparedQueryTest.php | 3 + utils/phpstan-baseline/loader.neon | 2 +- utils/phpstan-baseline/method.notFound.neon | 17 +- .../missingType.iterableValue.neon | 152 +----------------- utils/phpstan-baseline/property.notFound.neon | 27 +--- .../phpstan-baseline/property.phpDocType.neon | 27 +--- 19 files changed, 103 insertions(+), 496 deletions(-) diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index f872ab2bdc06..98ec9df3e2b9 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -25,35 +25,35 @@ use Throwable; /** - * @property-read array $aliasedTables - * @property-read string $charset - * @property-read bool $compress - * @property-read float $connectDuration - * @property-read float $connectTime - * @property-read string $database - * @property-read array $dateFormat - * @property-read string $DBCollat - * @property-read bool $DBDebug - * @property-read string $DBDriver - * @property-read string $DBPrefix - * @property-read string $DSN - * @property-read array|bool $encrypt - * @property-read array $failover - * @property-read string $hostname - * @property-read Query $lastQuery - * @property-read string $password - * @property-read bool $pConnect - * @property-read int|string $port - * @property-read bool $pretend - * @property-read string $queryClass - * @property-read array $reservedIdentifiers - * @property-read bool $strictOn - * @property-read string $subdriver - * @property-read string $swapPre - * @property-read int $transDepth - * @property-read bool $transFailure - * @property-read bool $transStatus - * @property-read string $username + * @property-read list $aliasedTables + * @property-read string $charset + * @property-read bool $compress + * @property-read float $connectDuration + * @property-read float $connectTime + * @property-read string $database + * @property-read array $dateFormat + * @property-read string $DBCollat + * @property-read bool $DBDebug + * @property-read string $DBDriver + * @property-read string $DBPrefix + * @property-read string $DSN + * @property-read array|bool $encrypt + * @property-read list> $failover + * @property-read string $hostname + * @property-read Query $lastQuery + * @property-read string $password + * @property-read bool $pConnect + * @property-read int|string $port + * @property-read bool $pretend + * @property-read string $queryClass + * @property-read list $reservedIdentifiers + * @property-read bool $strictOn + * @property-read string $subdriver + * @property-read string $swapPre + * @property-read int $transDepth + * @property-read bool $transFailure + * @property-read bool $transStatus + * @property-read string $username * * @template TConnection * @template TResult @@ -177,7 +177,7 @@ abstract class BaseConnection implements ConnectionInterface /** * Encryption flag/data * - * @var array|bool + * @var array|bool */ protected $encrypt = false; @@ -202,7 +202,7 @@ abstract class BaseConnection implements ConnectionInterface /** * Settings for a failover connection. * - * @var array + * @var list> */ protected $failover = []; @@ -240,14 +240,14 @@ abstract class BaseConnection implements ConnectionInterface * * Identifiers that must NOT be escaped. * - * @var array + * @var list */ protected $reservedIdentifiers = ['*']; /** * Identifier escape character * - * @var array|string + * @var list|string */ public $escapeChar = '"'; @@ -268,15 +268,12 @@ abstract class BaseConnection implements ConnectionInterface /** * RegExp used to escape identifiers * - * @var array + * @var list */ protected $pregEscapeChar = []; /** - * Holds previously looked up data - * for performance reasons. - * - * @var array + * @var array */ public $dataCache = []; @@ -374,7 +371,7 @@ abstract class BaseConnection implements ConnectionInterface ]; /** - * Saves our connection settings. + * @param array $params */ public function __construct(array $params) { @@ -739,6 +736,8 @@ public function getPlatform(): string * collected during use of the Builder, and set here * so queries are built correctly. * + * @param list $aliases + * * @return $this */ public function setAliasedTables(array $aliases) @@ -1111,7 +1110,7 @@ abstract protected function _transRollback(): bool; /** * Returns a non-shared new instance of the query builder for this connection. * - * @param array|string|TableName $tableName + * @param array|string|TableName $tableName * * @return BaseBuilder * @@ -1156,6 +1155,7 @@ public function newQuery(): BaseBuilder * }) * * @param Closure(BaseConnection): mixed $func + * @param array $options * * @return BasePreparedQuery|null */ @@ -1242,12 +1242,12 @@ public function getConnectDuration(int $decimals = 6): string * insert the table prefix (if it exists) in the proper position, and escape only * the correct identifiers. * - * @param array|int|string|TableName $item - * @param bool $prefixSingle Prefix a table name with no segments? - * @param bool $protectIdentifiers Protect table or column names? - * @param bool $fieldExists Supplied $item contains a column name? + * @param array|int|string|TableName $item + * @param bool $prefixSingle Prefix a table name with no segments? + * @param bool $protectIdentifiers Protect table or column names? + * @param bool $fieldExists Supplied $item contains a column name? * - * @return ($item is array ? array : string) + * @return ($item is array ? array : string) */ public function protectIdentifiers($item, bool $prefixSingle = false, ?bool $protectIdentifiers = null, bool $fieldExists = true) { @@ -1448,9 +1448,9 @@ private function escapeTableName(TableName $tableName): string * * This function escapes column and table names * - * @param array|string $item + * @param array|string $item * - * @return ($item is array ? array : string) + * @return ($item is array ? array : string) */ public function escapeIdentifiers($item) { @@ -1534,7 +1534,7 @@ abstract public function affectedRows(): int; * * @param mixed $str * - * @return ($str is array ? array : float|int|string) + * @return ($str is array ? array : float|int|string) */ public function escape($str) { @@ -1638,7 +1638,7 @@ protected function _escapeString(string $str): string * This function enables you to call PHP database functions that are not natively included * in CodeIgniter, in a platform independent manner. * - * @param array ...$params + * @param mixed ...$params * * @throws DatabaseException */ @@ -1682,7 +1682,7 @@ protected function getDriverFunctionPrefix(): string */ public function listTables(bool $constrainByPrefix = false) { - if (isset($this->dataCache['table_names']) && $this->dataCache['table_names']) { + if (($this->dataCache['table_names'] ?? []) !== []) { $tables = $constrainByPrefix ? preg_grep("/^{$this->DBPrefix}/", $this->dataCache['table_names']) : $this->dataCache['table_names']; @@ -1847,21 +1847,18 @@ public function getForeignKeyData(string $table) /** * Converts array of arrays generated by _foreignKeyData() to array of objects * - * @return array + * @param array, + * foreign_table_name: string, + * foreign_column_name: list, + * on_delete: string, + * on_update: string|null, + * match: string|null, + * }> $data * - * array[ - * {constraint_name} => - * stdClass[ - * 'constraint_name' => string, - * 'table_name' => string, - * 'column_name' => string[], - * 'foreign_table_name' => string, - * 'foreign_column_name' => string[], - * 'on_delete' => string, - * 'on_update' => string, - * 'match' => string - * ] - * ] + * @return array */ protected function foreignKeyDataToObjects(array $data) { diff --git a/system/Database/ConnectionInterface.php b/system/Database/ConnectionInterface.php index d1206155125c..0474736b5cd2 100644 --- a/system/Database/ConnectionInterface.php +++ b/system/Database/ConnectionInterface.php @@ -118,7 +118,7 @@ public function simpleQuery(string $sql); /** * Returns an instance of the query builder for this connection. * - * @param array|string $tableName Table name. + * @param array|string $tableName Table name. * * @return BaseBuilder Builder. */ @@ -139,7 +139,7 @@ public function getLastQuery(); * * @param mixed $str * - * @return ($str is array ? array : float|int|string) + * @return ($str is array ? array : float|int|string) */ public function escape($str); @@ -147,7 +147,7 @@ public function escape($str); * Allows for custom calls to the database engine that are not * supported through our database layer. * - * @param array ...$params + * @param mixed ...$params * * @return mixed */ diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index 29cb80b168c5..10e56e944dd4 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -15,7 +15,6 @@ use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\Exceptions\DatabaseException; -use CodeIgniter\Database\TableName; use CodeIgniter\Exceptions\LogicException; use mysqli; use mysqli_result; @@ -51,7 +50,7 @@ class Connection extends BaseConnection /** * Identifier escape character * - * @var string + * @var list|string */ public $escapeChar = '`'; @@ -234,11 +233,6 @@ public function connect(bool $persistent = false) return false; } - /** - * Close the database connection. - * - * @return void - */ protected function _close() { $this->connID->close(); @@ -379,10 +373,7 @@ public function escapeLikeStringDirect($str) } /** - * Generates the SQL for listing tables in a platform-dependent manner. * Uses escapeLikeStringDirect(). - * - * @param string|null $tableName If $tableName is provided will return only this table if exists. */ protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string { @@ -399,11 +390,6 @@ protected function _listTables(bool $prefixLimit = false, ?string $tableName = n return $sql; } - /** - * Generates a platform-specific query string so that the column names can be fetched. - * - * @param string|TableName $table - */ protected function _listColumns($table = ''): string { $tableName = $this->protectIdentifiers( @@ -417,10 +403,6 @@ protected function _listColumns($table = ''): string } /** - * Returns an array of objects with field data - * - * @return list - * * @throws DatabaseException */ protected function _fieldData(string $table): array @@ -449,10 +431,6 @@ protected function _fieldData(string $table): array } /** - * Returns an array of objects with index data - * - * @return array - * * @throws DatabaseException * @throws LogicException */ @@ -497,10 +475,6 @@ protected function _indexData(string $table): array } /** - * Returns an array of objects with Foreign key data - * - * @return array - * * @throws DatabaseException */ protected function _foreignKeyData(string $table): array @@ -598,25 +572,16 @@ public function insertID(): int return $this->connID->insert_id; } - /** - * Begin Transaction - */ protected function _transBegin(): bool { return $this->connID->begin_transaction(); } - /** - * Commit Transaction - */ protected function _transCommit(): bool { return $this->connID->commit(); } - /** - * Rollback Transaction - */ protected function _transRollback(): bool { return $this->connID->rollback(); diff --git a/system/Database/OCI8/Connection.php b/system/Database/OCI8/Connection.php index 6f3d0dd7794c..0d700ef095cf 100644 --- a/system/Database/OCI8/Connection.php +++ b/system/Database/OCI8/Connection.php @@ -37,7 +37,7 @@ class Connection extends BaseConnection /** * Identifier escape character * - * @var string + * @var list|string */ public $escapeChar = '"'; @@ -46,7 +46,7 @@ class Connection extends BaseConnection * * Identifiers that must NOT be escaped. * - * @var array + * @var list */ protected $reservedIdentifiers = [ '*', @@ -155,11 +155,6 @@ public function connect(bool $persistent = false) : $func($this->username, $this->password, $this->DSN, $this->charset); } - /** - * Close the database connection. - * - * @return void - */ protected function _close() { if (is_resource($this->cursorId)) { @@ -283,11 +278,6 @@ public function affectedRows(): int return oci_num_rows($this->stmtId); } - /** - * Generates the SQL for listing tables in a platform-dependent manner. - * - * @param string|null $tableName If $tableName is provided will return only this table if exists. - */ protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string { $sql = 'SELECT "TABLE_NAME" FROM "USER_TABLES"'; @@ -304,11 +294,6 @@ protected function _listTables(bool $prefixLimit = false, ?string $tableName = n return $sql; } - /** - * Generates a platform-specific query string so that the column names can be fetched. - * - * @param string|TableName $table - */ protected function _listColumns($table = ''): string { if ($table instanceof TableName) { @@ -328,10 +313,6 @@ protected function _listColumns($table = ''): string } /** - * Returns an array of objects with field data - * - * @return list - * * @throws DatabaseException */ protected function _fieldData(string $table): array @@ -385,10 +366,6 @@ private function normalizeDefault(?string $default): ?string } /** - * Returns an array of objects with index data - * - * @return array - * * @throws DatabaseException */ protected function _indexData(string $table): array @@ -434,10 +411,6 @@ protected function _indexData(string $table): array } /** - * Returns an array of objects with Foreign key data - * - * @return array - * * @throws DatabaseException */ protected function _foreignKeyData(string $table): array @@ -544,14 +517,8 @@ public function getCursor() /** * Executes a stored procedure * - * @param string $procedureName procedure name to execute - * @param array $params params array keys - * KEY OPTIONAL NOTES - * name no the name of the parameter should be in : format - * value no the value of the parameter. If this is an OUT or IN OUT parameter, - * this should be a reference to a variable - * type yes the type of the parameter - * length yes the max size of the parameter + * @param string $procedureName Procedure name to execute + * @param list $params `name` must be in `:` format. `value` is bound by reference. * * @return bool|Query|Result */ @@ -565,7 +532,7 @@ public function storedProcedure(string $procedureName, array $params) $sql = sprintf( 'BEGIN %s (' . substr(str_repeat(',%s', count($params)), 1) . '); END;', $procedureName, - ...array_map(static fn ($row) => $row['name'], $params), + ...array_map(static fn (array $row): string => $row['name'], $params), ); $this->resetStmtId = false; @@ -580,7 +547,7 @@ public function storedProcedure(string $procedureName, array $params) /** * Bind parameters * - * @param array $params + * @param list $params * * @return void */ @@ -734,9 +701,6 @@ protected function buildDSN() $this->DSN = ''; } - /** - * Begin Transaction - */ protected function _transBegin(): bool { $this->commitMode = OCI_NO_AUTO_COMMIT; @@ -744,9 +708,6 @@ protected function _transBegin(): bool return true; } - /** - * Commit Transaction - */ protected function _transCommit(): bool { $this->commitMode = OCI_COMMIT_ON_SUCCESS; @@ -754,9 +715,6 @@ protected function _transCommit(): bool return oci_commit($this->connID); } - /** - * Rollback Transaction - */ protected function _transRollback(): bool { $this->commitMode = OCI_COMMIT_ON_SUCCESS; diff --git a/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php index fc5421a894ea..a152b90aa539 100644 --- a/system/Database/Postgre/Connection.php +++ b/system/Database/Postgre/Connection.php @@ -47,7 +47,7 @@ class Connection extends BaseConnection /** * Identifier escape character * - * @var string + * @var list|string */ public $escapeChar = '"'; @@ -157,11 +157,6 @@ private function convertDSN() $this->DSN = $output; } - /** - * Close the database connection. - * - * @return void - */ protected function _close() { pg_close($this->connID); @@ -251,15 +246,6 @@ public function affectedRows(): int return pg_affected_rows($this->resultID); } - /** - * "Smart" Escape String - * - * Escapes data based on type - * - * @param mixed $str - * - * @return ($str is array ? array : float|int|string) - */ public function escape($str) { if (! $this->connID) { @@ -297,11 +283,6 @@ protected function _escapeString(string $str): string return pg_escape_string($this->connID, $str); } - /** - * Generates the SQL for listing tables in a platform-dependent manner. - * - * @param string|null $tableName If $tableName is provided will return only this table if exists. - */ protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string { $sql = 'SELECT "table_name" FROM "information_schema"."tables" WHERE "table_schema" = \'' . $this->schema . "'"; @@ -319,11 +300,6 @@ protected function _listTables(bool $prefixLimit = false, ?string $tableName = n return $sql; } - /** - * Generates a platform-specific query string so that the column names can be fetched. - * - * @param string|TableName $table - */ protected function _listColumns($table = ''): string { if ($table instanceof TableName) { @@ -339,10 +315,6 @@ protected function _listColumns($table = ''): string } /** - * Returns an array of objects with field data - * - * @return list - * * @throws DatabaseException */ protected function _fieldData(string $table): array @@ -374,10 +346,6 @@ protected function _fieldData(string $table): array } /** - * Returns an array of objects with index data - * - * @return array - * * @throws DatabaseException */ protected function _indexData(string $table): array @@ -413,10 +381,6 @@ protected function _indexData(string $table): array } /** - * Returns an array of objects with Foreign key data - * - * @return array - * * @throws DatabaseException */ protected function _foreignKeyData(string $table): array @@ -593,25 +557,16 @@ protected function setClientEncoding(string $charset): bool return pg_set_client_encoding($this->connID, $charset) === 0; } - /** - * Begin Transaction - */ protected function _transBegin(): bool { return (bool) pg_query($this->connID, 'BEGIN'); } - /** - * Commit Transaction - */ protected function _transCommit(): bool { return (bool) pg_query($this->connID, 'COMMIT'); } - /** - * Rollback Transaction - */ protected function _transRollback(): bool { return (bool) pg_query($this->connID, 'ROLLBACK'); diff --git a/system/Database/SQLSRV/Connection.php b/system/Database/SQLSRV/Connection.php index ce1a210341d3..5e82e7b2abf3 100644 --- a/system/Database/SQLSRV/Connection.php +++ b/system/Database/SQLSRV/Connection.php @@ -54,7 +54,7 @@ class Connection extends BaseConnection /** * Identifier escape character * - * @var string + * @var list|string */ public $escapeChar = '"'; @@ -84,9 +84,6 @@ class Connection extends BaseConnection */ protected $_reserved_identifiers = ['*']; - /** - * Class constructor - */ public function __construct(array $params) { parent::__construct($params); @@ -166,11 +163,6 @@ public function getAllErrorMessages(): string return implode("\n", $errors); } - /** - * Close the database connection. - * - * @return void - */ protected function _close() { sqlsrv_close($this->connID); @@ -192,11 +184,6 @@ public function insertID(): int return (int) ($this->query('SELECT SCOPE_IDENTITY() AS insert_id')->getRow()->insert_id ?? 0); } - /** - * Generates the SQL for listing tables in a platform-dependent manner. - * - * @param string|null $tableName If $tableName is provided will return only this table if exists. - */ protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string { $sql = 'SELECT [TABLE_NAME] AS "name"' @@ -216,11 +203,6 @@ protected function _listTables(bool $prefixLimit = false, ?string $tableName = n return $sql; } - /** - * Generates a platform-specific query string so that the column names can be fetched. - * - * @param string|TableName $table - */ protected function _listColumns($table = ''): string { if ($table instanceof TableName) { @@ -236,10 +218,6 @@ protected function _listColumns($table = ''): string } /** - * Returns an array of objects with index data - * - * @return array - * * @throws DatabaseException */ protected function _indexData(string $table): array @@ -273,11 +251,6 @@ protected function _indexData(string $table): array } /** - * Returns an array of objects with Foreign key data - * referenced_object_id parent_object_id - * - * @return array - * * @throws DatabaseException */ protected function _foreignKeyData(string $table): array @@ -340,10 +313,6 @@ protected function _enableForeignKeyChecks() } /** - * Returns an array of objects with field data - * - * @return list - * * @throws DatabaseException */ protected function _fieldData(string $table): array @@ -412,25 +381,16 @@ private function normalizeDefault(?string $default): ?string return $default; } - /** - * Begin Transaction - */ protected function _transBegin(): bool { return sqlsrv_begin_transaction($this->connID); } - /** - * Commit Transaction - */ protected function _transCommit(): bool { return sqlsrv_commit($this->connID); } - /** - * Rollback Transaction - */ protected function _transRollback(): bool { return sqlsrv_rollback($this->connID); diff --git a/system/Database/SQLite3/Connection.php b/system/Database/SQLite3/Connection.php index a78fca8c8b65..23ca0eaa5c35 100644 --- a/system/Database/SQLite3/Connection.php +++ b/system/Database/SQLite3/Connection.php @@ -39,7 +39,7 @@ class Connection extends BaseConnection /** * Identifier escape character * - * @var string + * @var list|string */ public $escapeChar = '`'; @@ -119,11 +119,6 @@ public function connect(bool $persistent = false) } } - /** - * Close the database connection. - * - * @return void - */ protected function _close() { $this->connID->close(); @@ -198,11 +193,6 @@ protected function _escapeString(string $str): string return $this->connID->escapeString($str); } - /** - * Generates the SQL for listing tables in a platform-dependent manner. - * - * @param string|null $tableName If $tableName is provided will return only this table if exists. - */ protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string { if ((string) $tableName !== '') { @@ -218,11 +208,6 @@ protected function _listTables(bool $prefixLimit = false, ?string $tableName = n : ''); } - /** - * Generates a platform-specific query string so that the column names can be fetched. - * - * @param string|TableName $table - */ protected function _listColumns($table = ''): string { if ($table instanceof TableName) { @@ -281,10 +266,6 @@ public function getFieldNames($tableName) } /** - * Returns an array of objects with field data - * - * @return list - * * @throws DatabaseException */ protected function _fieldData(string $table): array @@ -319,10 +300,6 @@ protected function _fieldData(string $table): array } /** - * Returns an array of objects with index data - * - * @return array - * * @throws DatabaseException */ protected function _indexData(string $table): array @@ -375,11 +352,6 @@ protected function _indexData(string $table): array return $retVal; } - /** - * Returns an array of objects with Foreign key data - * - * @return array - */ protected function _foreignKeyData(string $table): array { if (! $this->supportsForeignKeys()) { @@ -446,25 +418,16 @@ public function insertID(): int return $this->connID->lastInsertRowID(); } - /** - * Begin Transaction - */ protected function _transBegin(): bool { return $this->connID->exec('BEGIN TRANSACTION'); } - /** - * Commit Transaction - */ protected function _transCommit(): bool { return $this->connID->exec('END TRANSACTION'); } - /** - * Rollback Transaction - */ protected function _transRollback(): bool { return $this->connID->exec('ROLLBACK'); diff --git a/tests/system/Database/Live/EscapeTest.php b/tests/system/Database/Live/EscapeTest.php index adc5d414e34e..b3c27601ac42 100644 --- a/tests/system/Database/Live/EscapeTest.php +++ b/tests/system/Database/Live/EscapeTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Database\Live; +use CodeIgniter\Database\MySQLi\Connection as MySQLiConnection; use CodeIgniter\Database\RawSql; use CodeIgniter\I18n\Time; use CodeIgniter\Test\CIUnitTestCase; @@ -100,6 +101,8 @@ public function testEscapeLikeStringStringable(): void public function testEscapeLikeStringDirect(): void { if ($this->db->DBDriver === 'MySQLi') { + $this->assertInstanceOf(MySQLiConnection::class, $this->db); + $expected = "SHOW COLUMNS FROM brands WHERE column LIKE 'wild\\_chars%'"; $sql = "SHOW COLUMNS FROM brands WHERE column LIKE '" . $this->db->escapeLikeStringDirect('wild_chars') . "%'"; diff --git a/tests/system/Database/Live/GetVersionTest.php b/tests/system/Database/Live/GetVersionTest.php index 93678e3b8356..cb4e5c1f5386 100644 --- a/tests/system/Database/Live/GetVersionTest.php +++ b/tests/system/Database/Live/GetVersionTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Database\Live; +use CodeIgniter\Database\MySQLi\Connection as MySQLiConnection; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\DatabaseTestTrait; use PHPUnit\Framework\Attributes\Group; @@ -30,6 +31,7 @@ final class GetVersionTest extends CIUnitTestCase public function testGetVersion(): void { if ($this->db->DBDriver === 'MySQLi') { + $this->assertInstanceOf(MySQLiConnection::class, $this->db); $this->db->mysqli = false; } diff --git a/tests/system/Database/Live/MySQLi/FoundRowsTest.php b/tests/system/Database/Live/MySQLi/FoundRowsTest.php index b39f8999085d..122af23b24cd 100644 --- a/tests/system/Database/Live/MySQLi/FoundRowsTest.php +++ b/tests/system/Database/Live/MySQLi/FoundRowsTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Database\Live\MySQLi; +use CodeIgniter\Database\MySQLi\Connection as MySQLiConnection; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\DatabaseTestTrait; use Config\Database; @@ -56,6 +57,7 @@ public function testEnableFoundRows(): void $db1 = Database::connect($this->tests); + $this->assertInstanceOf(MySQLiConnection::class, $db1); $this->assertTrue($db1->foundRows); } @@ -65,6 +67,7 @@ public function testDisableFoundRows(): void $db1 = Database::connect($this->tests); + $this->assertInstanceOf(MySQLiConnection::class, $db1); $this->assertFalse($db1->foundRows); } diff --git a/tests/system/Database/Live/MySQLi/NumberNativeTest.php b/tests/system/Database/Live/MySQLi/NumberNativeTest.php index 3834c4c3f9b0..c419cd6e8e01 100644 --- a/tests/system/Database/Live/MySQLi/NumberNativeTest.php +++ b/tests/system/Database/Live/MySQLi/NumberNativeTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Database\Live\MySQLi; +use CodeIgniter\Database\MySQLi\Connection as MySQLiConnection; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\DatabaseTestTrait; use Config\Database; @@ -54,6 +55,7 @@ public function testEnableNumberNative(): void $this->markTestSkipped('Only MySQLi can complete this test.'); } + $this->assertInstanceOf(MySQLiConnection::class, $db1); $this->assertTrue($db1->numberNative); } @@ -67,6 +69,7 @@ public function testDisableNumberNative(): void $this->markTestSkipped('Only MySQLi can complete this test.'); } + $this->assertInstanceOf(MySQLiConnection::class, $db1); $this->assertFalse($db1->numberNative); } diff --git a/tests/system/Database/Live/OCI8/CallStoredProcedureTest.php b/tests/system/Database/Live/OCI8/CallStoredProcedureTest.php index 32c9efbf0624..8404e2378ae2 100644 --- a/tests/system/Database/Live/OCI8/CallStoredProcedureTest.php +++ b/tests/system/Database/Live/OCI8/CallStoredProcedureTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Database\Live\OCI8; +use CodeIgniter\Database\OCI8\Connection as OCI8Connection; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\DatabaseTestTrait; use PHPUnit\Framework\Attributes\Group; @@ -38,6 +39,8 @@ protected function setUp(): void public function testCallPackageProcedure(): void { + $this->assertInstanceOf(OCI8Connection::class, $this->db); + $result = '0'; $this->db->storedProcedure('calculator.plus', [ @@ -60,6 +63,8 @@ public function testCallPackageProcedure(): void public function testCallStoredProcedure(): void { + $this->assertInstanceOf(OCI8Connection::class, $this->db); + $result = '0'; $this->db->storedProcedure('plus', [ @@ -82,6 +87,8 @@ public function testCallStoredProcedure(): void public function testCallStoredProcedureForCursor(): void { + $this->assertInstanceOf(OCI8Connection::class, $this->db); + $result = $this->db->getCursor(); $this->db->storedProcedure('one', [ diff --git a/tests/system/Database/Live/OrderTest.php b/tests/system/Database/Live/OrderTest.php index 1451222b5ceb..521078e79e18 100644 --- a/tests/system/Database/Live/OrderTest.php +++ b/tests/system/Database/Live/OrderTest.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Database\Live; +use CodeIgniter\Database\SQLSRV\Connection as SQLSRVConnection; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\DatabaseTestTrait; use PHPUnit\Framework\Attributes\Group; @@ -84,6 +85,8 @@ public function testOrderRandom(): void if ($this->db->DBDriver === 'MySQLi') { $key = 'RAND()'; } elseif ($this->db->DBDriver === 'SQLSRV') { + $this->assertInstanceOf(SQLSRVConnection::class, $this->db); + $key = 'NEWID()'; $table = '"' . $this->db->getDatabase() . '"."' . $this->db->schema . '".' . $table; } elseif ($this->db->DBDriver === 'OCI8') { diff --git a/tests/system/Database/Live/PreparedQueryTest.php b/tests/system/Database/Live/PreparedQueryTest.php index 304269969a2d..5561b310a814 100644 --- a/tests/system/Database/Live/PreparedQueryTest.php +++ b/tests/system/Database/Live/PreparedQueryTest.php @@ -17,6 +17,7 @@ use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\Query; use CodeIgniter\Database\ResultInterface; +use CodeIgniter\Database\SQLSRV\Connection as SQLSRVConnection; use CodeIgniter\Exceptions\BadMethodCallException; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\DatabaseTestTrait; @@ -76,6 +77,8 @@ public function testPrepareReturnsPreparedQuery(): void } if ($this->db->DBDriver === 'SQLSRV') { + $this->assertInstanceOf(SQLSRVConnection::class, $this->db); + $database = $this->db->getDatabase(); $expected = "INSERT INTO {$ec}{$database}{$ec}.{$ec}{$this->db->schema}{$ec}.{$ec}{$pre}user{$ec} ({$ec}name{$ec},{$ec}email{$ec},{$ec}country{$ec}) VALUES ({$placeholders})"; } else { diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index b3302ee9448e..a6bf6b71991c 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 654 errors +# total 607 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/method.notFound.neon b/utils/phpstan-baseline/method.notFound.neon index 432b04269975..5fbb5963bf85 100644 --- a/utils/phpstan-baseline/method.notFound.neon +++ b/utils/phpstan-baseline/method.notFound.neon @@ -1,4 +1,4 @@ -# total 32 errors +# total 27 errors parameters: ignoreErrors: @@ -22,21 +22,6 @@ parameters: count: 2 path: ../../tests/system/Commands/Utilities/Routes/FilterFinderTest.php - - - message: '#^Call to an undefined method CodeIgniter\\Database\\BaseConnection\:\:escapeLikeStringDirect\(\)\.$#' - count: 1 - path: ../../tests/system/Database/Live/EscapeTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\Database\\BaseConnection\:\:getCursor\(\)\.$#' - count: 1 - path: ../../tests/system/Database/Live/OCI8/CallStoredProcedureTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\Database\\BaseConnection\:\:storedProcedure\(\)\.$#' - count: 3 - path: ../../tests/system/Database/Live/OCI8/CallStoredProcedureTest.php - - message: '#^Call to an undefined method CodeIgniter\\HTTP\\ResponseInterface\:\:pretend\(\)\.$#' count: 3 diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 6777cc2472c3..a70bd3c72915 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 470 errors +# total 440 errors parameters: ignoreErrors: @@ -317,116 +317,6 @@ parameters: count: 1 path: ../../system/DataConverter/DataConverter.php - - - message: '#^Class CodeIgniter\\Database\\BaseConnection has PHPDoc tag @property\-read for property \$aliasedTables with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseConnection.php - - - - message: '#^Class CodeIgniter\\Database\\BaseConnection has PHPDoc tag @property\-read for property \$dateFormat with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseConnection.php - - - - message: '#^Class CodeIgniter\\Database\\BaseConnection has PHPDoc tag @property\-read for property \$encrypt with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseConnection.php - - - - message: '#^Class CodeIgniter\\Database\\BaseConnection has PHPDoc tag @property\-read for property \$failover with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseConnection.php - - - - message: '#^Class CodeIgniter\\Database\\BaseConnection has PHPDoc tag @property\-read for property \$reservedIdentifiers with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseConnection.php - - - - message: '#^Method CodeIgniter\\Database\\BaseConnection\:\:__construct\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseConnection.php - - - - message: '#^Method CodeIgniter\\Database\\BaseConnection\:\:callFunction\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseConnection.php - - - - message: '#^Method CodeIgniter\\Database\\BaseConnection\:\:escapeIdentifiers\(\) has parameter \$item with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseConnection.php - - - - message: '#^Method CodeIgniter\\Database\\BaseConnection\:\:escapeIdentifiers\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseConnection.php - - - - message: '#^Method CodeIgniter\\Database\\BaseConnection\:\:escape\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseConnection.php - - - - message: '#^Method CodeIgniter\\Database\\BaseConnection\:\:foreignKeyDataToObjects\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseConnection.php - - - - message: '#^Method CodeIgniter\\Database\\BaseConnection\:\:prepare\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseConnection.php - - - - message: '#^Method CodeIgniter\\Database\\BaseConnection\:\:protectIdentifiers\(\) has parameter \$item with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseConnection.php - - - - message: '#^Method CodeIgniter\\Database\\BaseConnection\:\:protectIdentifiers\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseConnection.php - - - - message: '#^Method CodeIgniter\\Database\\BaseConnection\:\:setAliasedTables\(\) has parameter \$aliases with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseConnection.php - - - - message: '#^Method CodeIgniter\\Database\\BaseConnection\:\:table\(\) has parameter \$tableName with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseConnection.php - - - - message: '#^Property CodeIgniter\\Database\\BaseConnection\:\:\$dataCache type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseConnection.php - - - - message: '#^Property CodeIgniter\\Database\\BaseConnection\:\:\$encrypt type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseConnection.php - - - - message: '#^Property CodeIgniter\\Database\\BaseConnection\:\:\$escapeChar type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseConnection.php - - - - message: '#^Property CodeIgniter\\Database\\BaseConnection\:\:\$failover type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseConnection.php - - - - message: '#^Property CodeIgniter\\Database\\BaseConnection\:\:\$pregEscapeChar type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseConnection.php - - - - message: '#^Property CodeIgniter\\Database\\BaseConnection\:\:\$reservedIdentifiers type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseConnection.php - - message: '#^Method CodeIgniter\\Database\\BasePreparedQuery\:\:_execute\(\) has parameter \$data with no value type specified in iterable type array\.$#' count: 1 @@ -487,21 +377,6 @@ parameters: count: 1 path: ../../system/Database/Config.php - - - message: '#^Method CodeIgniter\\Database\\ConnectionInterface\:\:callFunction\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/ConnectionInterface.php - - - - message: '#^Method CodeIgniter\\Database\\ConnectionInterface\:\:escape\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/ConnectionInterface.php - - - - message: '#^Method CodeIgniter\\Database\\ConnectionInterface\:\:table\(\) has parameter \$tableName with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/ConnectionInterface.php - - message: '#^Method CodeIgniter\\Database\\Database\:\:initDriver\(\) has parameter \$argument with no value type specified in iterable type array\.$#' count: 1 @@ -582,21 +457,6 @@ parameters: count: 1 path: ../../system/Database/MySQLi/Utils.php - - - message: '#^Method CodeIgniter\\Database\\OCI8\\Connection\:\:bindParams\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/OCI8/Connection.php - - - - message: '#^Method CodeIgniter\\Database\\OCI8\\Connection\:\:storedProcedure\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/OCI8/Connection.php - - - - message: '#^Property CodeIgniter\\Database\\OCI8\\Connection\:\:\$reservedIdentifiers type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/OCI8/Connection.php - - message: '#^Method CodeIgniter\\Database\\OCI8\\PreparedQuery\:\:_execute\(\) has parameter \$data with no value type specified in iterable type array\.$#' count: 1 @@ -612,11 +472,6 @@ parameters: count: 1 path: ../../system/Database/OCI8/Utils.php - - - message: '#^Method CodeIgniter\\Database\\Postgre\\Connection\:\:escape\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Postgre/Connection.php - - message: '#^Method CodeIgniter\\Database\\Postgre\\PreparedQuery\:\:_execute\(\) has parameter \$data with no value type specified in iterable type array\.$#' count: 1 @@ -657,11 +512,6 @@ parameters: count: 1 path: ../../system/Database/Query.php - - - message: '#^Method CodeIgniter\\Database\\SQLSRV\\Connection\:\:__construct\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLSRV/Connection.php - - message: '#^Method CodeIgniter\\Database\\SQLSRV\\PreparedQuery\:\:_execute\(\) has parameter \$data with no value type specified in iterable type array\.$#' count: 1 diff --git a/utils/phpstan-baseline/property.notFound.neon b/utils/phpstan-baseline/property.notFound.neon index ebd40c7f58a3..13dd11a84ac9 100644 --- a/utils/phpstan-baseline/property.notFound.neon +++ b/utils/phpstan-baseline/property.notFound.neon @@ -1,32 +1,7 @@ -# total 28 errors +# total 21 errors parameters: ignoreErrors: - - - message: '#^Access to an undefined property CodeIgniter\\Database\\BaseConnection\:\:\$mysqli\.$#' - count: 1 - path: ../../tests/system/Database/Live/GetVersionTest.php - - - - message: '#^Access to an undefined property CodeIgniter\\Database\\BaseConnection\:\:\$foundRows\.$#' - count: 2 - path: ../../tests/system/Database/Live/MySQLi/FoundRowsTest.php - - - - message: '#^Access to an undefined property CodeIgniter\\Database\\BaseConnection\:\:\$numberNative\.$#' - count: 2 - path: ../../tests/system/Database/Live/MySQLi/NumberNativeTest.php - - - - message: '#^Access to an undefined property CodeIgniter\\Database\\BaseConnection\:\:\$schema\.$#' - count: 1 - path: ../../tests/system/Database/Live/OrderTest.php - - - - message: '#^Access to an undefined property CodeIgniter\\Database\\BaseConnection\:\:\$schema\.$#' - count: 1 - path: ../../tests/system/Database/Live/PreparedQueryTest.php - - message: '#^Access to an undefined property CodeIgniter\\Database\\BaseConnection\:\:\$schema\.$#' count: 2 diff --git a/utils/phpstan-baseline/property.phpDocType.neon b/utils/phpstan-baseline/property.phpDocType.neon index 5b7e09269507..275166c25508 100644 --- a/utils/phpstan-baseline/property.phpDocType.neon +++ b/utils/phpstan-baseline/property.phpDocType.neon @@ -1,12 +1,7 @@ -# total 22 errors +# total 17 errors parameters: ignoreErrors: - - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\MySQLi\\Connection\:\:\$escapeChar is not the same as PHPDoc type array\|string of overridden property CodeIgniter\\Database\\BaseConnection\\:\:\$escapeChar\.$#' - count: 1 - path: ../../system/Database/MySQLi/Connection.php - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\MySQLi\\Utils\:\:\$listDatabases is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\BaseUtils\:\:\$listDatabases\.$#' count: 1 @@ -17,21 +12,11 @@ parameters: count: 1 path: ../../system/Database/MySQLi/Utils.php - - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\OCI8\\Connection\:\:\$escapeChar is not the same as PHPDoc type array\|string of overridden property CodeIgniter\\Database\\BaseConnection\\:\:\$escapeChar\.$#' - count: 1 - path: ../../system/Database/OCI8/Connection.php - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\OCI8\\Utils\:\:\$listDatabases is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\BaseUtils\:\:\$listDatabases\.$#' count: 1 path: ../../system/Database/OCI8/Utils.php - - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\Postgre\\Connection\:\:\$escapeChar is not the same as PHPDoc type array\|string of overridden property CodeIgniter\\Database\\BaseConnection\\:\:\$escapeChar\.$#' - count: 1 - path: ../../system/Database/Postgre/Connection.php - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\Postgre\\Utils\:\:\$listDatabases is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\BaseUtils\:\:\$listDatabases\.$#' count: 1 @@ -42,11 +27,6 @@ parameters: count: 1 path: ../../system/Database/Postgre/Utils.php - - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\SQLSRV\\Connection\:\:\$escapeChar is not the same as PHPDoc type array\|string of overridden property CodeIgniter\\Database\\BaseConnection\\:\:\$escapeChar\.$#' - count: 1 - path: ../../system/Database/SQLSRV/Connection.php - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\SQLSRV\\Utils\:\:\$listDatabases is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\BaseUtils\:\:\$listDatabases\.$#' count: 1 @@ -57,11 +37,6 @@ parameters: count: 1 path: ../../system/Database/SQLSRV/Utils.php - - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\SQLite3\\Connection\:\:\$escapeChar is not the same as PHPDoc type array\|string of overridden property CodeIgniter\\Database\\BaseConnection\\:\:\$escapeChar\.$#' - count: 1 - path: ../../system/Database/SQLite3/Connection.php - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\SQLite3\\Utils\:\:\$optimizeTable is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\BaseUtils\:\:\$optimizeTable\.$#' count: 1 From 68883aecd58fb25d086f021570c7504cad12af61 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Mon, 24 Aug 2026 13:08:56 +0800 Subject: [PATCH 070/163] refactor: fix phpstan errors in `PreparedQuery` and `Utils` (#10476) --- system/Database/BasePreparedQuery.php | 35 +---- system/Database/BaseUtils.php | 20 +-- system/Database/MySQLi/PreparedQuery.php | 19 --- system/Database/MySQLi/Utils.php | 12 +- system/Database/OCI8/PreparedQuery.php | 19 --- system/Database/OCI8/Utils.php | 8 +- system/Database/Postgre/PreparedQuery.php | 18 --- system/Database/Postgre/Utils.php | 12 +- system/Database/PreparedQueryInterface.php | 6 +- system/Database/SQLSRV/PreparedQuery.php | 25 +--- system/Database/SQLSRV/Utils.php | 12 +- system/Database/SQLite3/PreparedQuery.php | 19 --- system/Database/SQLite3/Utils.php | 8 +- utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 127 +----------------- .../missingType.parameter.neon | 12 +- .../phpstan-baseline/property.phpDocType.neon | 42 +----- 17 files changed, 45 insertions(+), 351 deletions(-) diff --git a/system/Database/BasePreparedQuery.php b/system/Database/BasePreparedQuery.php index e770a8d35f11..723b6b8f12e6 100644 --- a/system/Database/BasePreparedQuery.php +++ b/system/Database/BasePreparedQuery.php @@ -70,15 +70,6 @@ public function __construct(BaseConnection $db) $this->db = $db; } - /** - * Prepares the query against the database, and saves the connection - * info necessary to execute the query later. - * - * NOTE: This version is based on SQL code. Child classes should - * override this method. - * - * @return $this - */ public function prepare(string $sql, array $options = [], string $queryClass = Query::class) { // We only support positional placeholders (?), so convert @@ -101,18 +92,13 @@ public function prepare(string $sql, array $options = [], string $queryClass = Q } /** - * The database-dependent portion of the prepare statement. + * @param array $options Passed to the connection's prepare statement. Only the SQLSRV driver uses it. * * @return $this */ abstract public function _prepare(string $sql, array $options = []); /** - * Takes a new set of data and runs it against the currently - * prepared query. Upon success, will return a Results object. - * - * @return bool|ResultInterface - * * @throws DatabaseException */ public function execute(...$data) @@ -186,7 +172,7 @@ public function execute(...$data) } /** - * The database dependant version of the execute method. + * @param list $data */ abstract public function _execute(array $data): bool; @@ -197,11 +183,6 @@ abstract public function _execute(array $data): bool; */ abstract public function _getResult(); - /** - * Explicitly closes the prepared statement. - * - * @throws BadMethodCallException - */ public function close(): bool { if (! isset($this->statement)) { @@ -215,14 +196,8 @@ public function close(): bool } } - /** - * The database-dependent version of the close method. - */ abstract protected function _close(): bool; - /** - * Returns the SQL that has been prepared. - */ public function getQueryString(): string { if (! $this->query instanceof QueryInterface) { @@ -240,17 +215,11 @@ public function hasError(): bool return $this->errorString !== ''; } - /** - * Returns the error code created while executing this statement. - */ public function getErrorCode(): int { return $this->errorCode; } - /** - * Returns the error message created while executing this statement. - */ public function getErrorMessage(): string { return $this->errorString; diff --git a/system/Database/BaseUtils.php b/system/Database/BaseUtils.php index ddea4301c556..cf42bb7f7184 100644 --- a/system/Database/BaseUtils.php +++ b/system/Database/BaseUtils.php @@ -16,14 +16,12 @@ use CodeIgniter\Database\Exceptions\DatabaseException; /** - * Class BaseUtils + * @template TDb of BaseConnection */ abstract class BaseUtils { /** - * Database object - * - * @var object + * @var TDb */ protected $db; @@ -49,7 +47,7 @@ abstract class BaseUtils protected $repairTable = false; /** - * Class constructor + * @param TDb $db */ public function __construct(ConnectionInterface $db) { @@ -59,7 +57,7 @@ public function __construct(ConnectionInterface $db) /** * List databases * - * @return array|bool + * @return bool|list * * @throws DatabaseException */ @@ -229,6 +227,8 @@ public function getCSVFromResult(ResultInterface $query, string $delim = ',', st /** * Generate XML data from a query result object + * + * @param array $params */ public function getXMLFromResult(ResultInterface $query, array $params = []): string { @@ -264,9 +264,9 @@ public function getXMLFromResult(ResultInterface $query, array $params = []): st /** * Database Backup * - * @param array|string $params + * @param array|string $params * - * @return false|never|string + * @return false|string * * @throws DatabaseException */ @@ -320,9 +320,9 @@ public function backup($params = []) } /** - * Platform dependent version of the backup function. + * @param array|null $prefs * - * @return false|never|string + * @return false|string */ abstract public function _backup(?array $prefs = null); } diff --git a/system/Database/MySQLi/PreparedQuery.php b/system/Database/MySQLi/PreparedQuery.php index d018250bfc48..4f40a358386c 100644 --- a/system/Database/MySQLi/PreparedQuery.php +++ b/system/Database/MySQLi/PreparedQuery.php @@ -28,16 +28,6 @@ */ class PreparedQuery extends BasePreparedQuery { - /** - * Prepares the query against the database, and saves the connection - * info necessary to execute the query later. - * - * NOTE: This version is based on SQL code. Child classes should - * override this method. - * - * @param array $options Passed to the connection's prepare statement. - * Unused in the MySQLi driver. - */ public function _prepare(string $sql, array $options = []): PreparedQuery { // Mysqli driver doesn't like statements @@ -56,10 +46,6 @@ public function _prepare(string $sql, array $options = []): PreparedQuery return $this; } - /** - * Takes a new set of data and runs it against the currently - * prepared query. Upon success, will return a Results object. - */ public function _execute(array $data): bool { if (! isset($this->statement)) { @@ -104,8 +90,6 @@ public function _execute(array $data): bool } /** - * Returns the result object for the prepared query or false on failure. - * * @return false|mysqli_result */ public function _getResult() @@ -113,9 +97,6 @@ public function _getResult() return $this->statement->get_result(); } - /** - * Deallocate prepared statements. - */ protected function _close(): bool { return $this->statement->close(); diff --git a/system/Database/MySQLi/Utils.php b/system/Database/MySQLi/Utils.php index be68367361fd..16f0d0f98ec7 100644 --- a/system/Database/MySQLi/Utils.php +++ b/system/Database/MySQLi/Utils.php @@ -18,26 +18,22 @@ /** * Utils for MySQLi + * + * @extends BaseUtils */ class Utils extends BaseUtils { /** - * List databases statement - * - * @var string + * @var bool|string */ protected $listDatabases = 'SHOW DATABASES'; /** - * OPTIMIZE TABLE statement - * - * @var string + * @var bool|string */ protected $optimizeTable = 'OPTIMIZE TABLE %s'; /** - * Platform dependent version of the backup function. - * * @return never */ public function _backup(?array $prefs = null) diff --git a/system/Database/OCI8/PreparedQuery.php b/system/Database/OCI8/PreparedQuery.php index bd027bcb1bc3..38c24ff26b99 100644 --- a/system/Database/OCI8/PreparedQuery.php +++ b/system/Database/OCI8/PreparedQuery.php @@ -30,16 +30,6 @@ class PreparedQuery extends BasePreparedQuery */ private ?string $lastInsertTableName = null; - /** - * Prepares the query against the database, and saves the connection - * info necessary to execute the query later. - * - * NOTE: This version is based on SQL code. Child classes should - * override this method. - * - * @param array $options Passed to the connection's prepare statement. - * Unused in the OCI8 driver. - */ public function _prepare(string $sql, array $options = []): PreparedQuery { if (! $this->statement = oci_parse($this->db->connID, $this->parameterize($sql))) { @@ -57,10 +47,6 @@ public function _prepare(string $sql, array $options = []): PreparedQuery return $this; } - /** - * Takes a new set of data and runs it against the currently - * prepared query. Upon success, will return a Results object. - */ public function _execute(array $data): bool { if (! isset($this->statement)) { @@ -93,8 +79,6 @@ public function _execute(array $data): bool } /** - * Returns the statement resource for the prepared query or false when preparing failed. - * * @return resource|null */ public function _getResult() @@ -102,9 +86,6 @@ public function _getResult() return $this->statement; } - /** - * Deallocate prepared statements. - */ protected function _close(): bool { return oci_free_statement($this->statement); diff --git a/system/Database/OCI8/Utils.php b/system/Database/OCI8/Utils.php index c6e97963facf..4995446076fb 100644 --- a/system/Database/OCI8/Utils.php +++ b/system/Database/OCI8/Utils.php @@ -18,19 +18,17 @@ /** * Utils for OCI8 + * + * @extends BaseUtils */ class Utils extends BaseUtils { /** - * List databases statement - * - * @var string + * @var bool|string */ protected $listDatabases = 'SELECT TABLESPACE_NAME FROM USER_TABLESPACES'; /** - * Platform dependent version of the backup function. - * * @return never */ public function _backup(?array $prefs = null) diff --git a/system/Database/Postgre/PreparedQuery.php b/system/Database/Postgre/PreparedQuery.php index 646c55d91406..418259db7013 100644 --- a/system/Database/Postgre/PreparedQuery.php +++ b/system/Database/Postgre/PreparedQuery.php @@ -44,15 +44,6 @@ class PreparedQuery extends BasePreparedQuery protected $result; /** - * Prepares the query against the database, and saves the connection - * info necessary to execute the query later. - * - * NOTE: This version is based on SQL code. Child classes should - * override this method. - * - * @param array $options Passed to the connection's prepare statement. - * Unused in the MySQLi driver. - * * @throws Exception */ public function _prepare(string $sql, array $options = []): PreparedQuery @@ -77,10 +68,6 @@ public function _prepare(string $sql, array $options = []): PreparedQuery return $this; } - /** - * Takes a new set of data and runs it against the currently - * prepared query. Upon success, will return a Results object. - */ public function _execute(array $data): bool { if (! isset($this->statement)) { @@ -99,8 +86,6 @@ public function _execute(array $data): bool } /** - * Returns the result object for the prepared query or false on failure. - * * @return PgSqlResult|null */ public function _getResult() @@ -108,9 +93,6 @@ public function _getResult() return $this->result; } - /** - * Deallocate prepared statements. - */ protected function _close(): bool { return pg_query($this->db->connID, 'DEALLOCATE "' . $this->db->escapeIdentifiers($this->name) . '"') !== false; diff --git a/system/Database/Postgre/Utils.php b/system/Database/Postgre/Utils.php index c3b7b82d1681..ab2e3bd6d056 100644 --- a/system/Database/Postgre/Utils.php +++ b/system/Database/Postgre/Utils.php @@ -18,26 +18,22 @@ /** * Utils for Postgre + * + * @extends BaseUtils */ class Utils extends BaseUtils { /** - * List databases statement - * - * @var string + * @var bool|string */ protected $listDatabases = 'SELECT datname FROM pg_database'; /** - * OPTIMIZE TABLE statement - * - * @var string + * @var bool|string */ protected $optimizeTable = 'REINDEX TABLE %s'; /** - * Platform dependent version of the backup function. - * * @return never */ public function _backup(?array $prefs = null) diff --git a/system/Database/PreparedQueryInterface.php b/system/Database/PreparedQueryInterface.php index c49c2e7bb551..fa2c2a9aac28 100644 --- a/system/Database/PreparedQueryInterface.php +++ b/system/Database/PreparedQueryInterface.php @@ -26,6 +26,8 @@ interface PreparedQueryInterface * Takes a new set of data and runs it against the currently * prepared query. Upon success, will return a Results object. * + * @param mixed ...$data + * * @return bool|ResultInterface */ public function execute(...$data); @@ -34,12 +36,14 @@ public function execute(...$data); * Prepares the query against the database, and saves the connection * info necessary to execute the query later. * + * @param array $options + * * @return $this */ public function prepare(string $sql, array $options = []); /** - * Explicity closes the statement. + * Explicitly closes the statement. * * @throws BadMethodCallException */ diff --git a/system/Database/SQLSRV/PreparedQuery.php b/system/Database/SQLSRV/PreparedQuery.php index 0cdc842b1196..9e21eed19000 100644 --- a/system/Database/SQLSRV/PreparedQuery.php +++ b/system/Database/SQLSRV/PreparedQuery.php @@ -18,7 +18,7 @@ use CodeIgniter\Exceptions\BadMethodCallException; /** - * Prepared query for Postgre + * Prepared query for SQLSRV * * @extends BasePreparedQuery */ @@ -27,7 +27,7 @@ class PreparedQuery extends BasePreparedQuery /** * Parameters array used to store the dynamic variables. * - * @var array + * @var array */ protected $parameters = []; @@ -37,13 +37,7 @@ public function __construct(Connection $db) } /** - * Prepares the query against the database, and saves the connection - * info necessary to execute the query later. - * - * NOTE: This version is based on SQL code. Child classes should - * override this method. - * - * @param array $options Options takes an associative array; + * @param array $options Positional array of bind values, keyed by placeholder index. * * @throws DatabaseException */ @@ -70,10 +64,6 @@ public function _prepare(string $sql, array $options = []): PreparedQuery return $this; } - /** - * Takes a new set of data and runs it against the currently - * prepared query. - */ public function _execute(array $data): bool { if (! isset($this->statement)) { @@ -94,8 +84,6 @@ public function _execute(array $data): bool } /** - * Returns the statement resource for the prepared query or false when preparing failed. - * * @return resource|null */ public function _getResult() @@ -103,9 +91,6 @@ public function _getResult() return $this->statement; } - /** - * Deallocate prepared statements. - */ protected function _close(): bool { return sqlsrv_free_stmt($this->statement); @@ -114,7 +99,9 @@ protected function _close(): bool /** * Handle parameters. * - * @param array $options + * @param array $options + * + * @return list */ protected function parameterize(string $queryString, array $options): array { diff --git a/system/Database/SQLSRV/Utils.php b/system/Database/SQLSRV/Utils.php index f74ac39f7a88..1e7fe45730a2 100644 --- a/system/Database/SQLSRV/Utils.php +++ b/system/Database/SQLSRV/Utils.php @@ -19,20 +19,18 @@ /** * Utils for SQLSRV + * + * @extends BaseUtils */ class Utils extends BaseUtils { /** - * List databases statement - * - * @var string + * @var bool|string */ protected $listDatabases = 'EXEC sp_helpdb'; // Can also be: EXEC sp_databases /** - * OPTIMIZE TABLE statement - * - * @var string + * @var bool|string */ protected $optimizeTable = 'ALTER INDEX all ON %s REORGANIZE'; @@ -44,8 +42,6 @@ public function __construct(ConnectionInterface $db) } /** - * Platform dependent version of the backup function. - * * @return never */ public function _backup(?array $prefs = null) diff --git a/system/Database/SQLite3/PreparedQuery.php b/system/Database/SQLite3/PreparedQuery.php index 5a5153a5f7a3..1986961f648a 100644 --- a/system/Database/SQLite3/PreparedQuery.php +++ b/system/Database/SQLite3/PreparedQuery.php @@ -35,16 +35,6 @@ class PreparedQuery extends BasePreparedQuery */ protected $result; - /** - * Prepares the query against the database, and saves the connection - * info necessary to execute the query later. - * - * NOTE: This version is based on SQL code. Child classes should - * override this method. - * - * @param array $options Passed to the connection's prepare statement. - * Unused in the MySQLi driver. - */ public function _prepare(string $sql, array $options = []): PreparedQuery { if (! ($this->statement = $this->db->connID->prepare($sql))) { @@ -59,10 +49,6 @@ public function _prepare(string $sql, array $options = []): PreparedQuery return $this; } - /** - * Takes a new set of data and runs it against the currently - * prepared query. Upon success, will return a Results object. - */ public function _execute(array $data): bool { if (! isset($this->statement)) { @@ -99,8 +85,6 @@ public function _execute(array $data): bool } /** - * Returns the result object for the prepared query or false on failure. - * * @return false|SQLite3Result */ public function _getResult() @@ -108,9 +92,6 @@ public function _getResult() return $this->result; } - /** - * Deallocate prepared statements. - */ protected function _close(): bool { return $this->statement->close(); diff --git a/system/Database/SQLite3/Utils.php b/system/Database/SQLite3/Utils.php index 93f7f0e00db7..c2102e6accab 100644 --- a/system/Database/SQLite3/Utils.php +++ b/system/Database/SQLite3/Utils.php @@ -18,19 +18,17 @@ /** * Utils for SQLite3 + * + * @extends BaseUtils */ class Utils extends BaseUtils { /** - * OPTIMIZE TABLE statement - * - * @var string + * @var bool|string */ protected $optimizeTable = 'REINDEX %s'; /** - * Platform dependent version of the backup function. - * * @return never */ public function _backup(?array $prefs = null) diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index a6bf6b71991c..e2eed4892221 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 607 errors +# total 572 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index a70bd3c72915..f8c8995deaa0 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 440 errors +# total 415 errors parameters: ignoreErrors: @@ -317,41 +317,6 @@ parameters: count: 1 path: ../../system/DataConverter/DataConverter.php - - - message: '#^Method CodeIgniter\\Database\\BasePreparedQuery\:\:_execute\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BasePreparedQuery.php - - - - message: '#^Method CodeIgniter\\Database\\BasePreparedQuery\:\:_prepare\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BasePreparedQuery.php - - - - message: '#^Method CodeIgniter\\Database\\BasePreparedQuery\:\:prepare\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BasePreparedQuery.php - - - - message: '#^Method CodeIgniter\\Database\\BaseUtils\:\:_backup\(\) has parameter \$prefs with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseUtils.php - - - - message: '#^Method CodeIgniter\\Database\\BaseUtils\:\:backup\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseUtils.php - - - - message: '#^Method CodeIgniter\\Database\\BaseUtils\:\:getXMLFromResult\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseUtils.php - - - - message: '#^Method CodeIgniter\\Database\\BaseUtils\:\:listDatabases\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/BaseUtils.php - - message: '#^Method CodeIgniter\\Database\\Config\:\:connect\(\) has parameter \$group with no value type specified in iterable type array\.$#' count: 1 @@ -442,56 +407,6 @@ parameters: count: 1 path: ../../system/Database/MigrationRunner.php - - - message: '#^Method CodeIgniter\\Database\\MySQLi\\PreparedQuery\:\:_execute\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/MySQLi/PreparedQuery.php - - - - message: '#^Method CodeIgniter\\Database\\MySQLi\\PreparedQuery\:\:_prepare\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/MySQLi/PreparedQuery.php - - - - message: '#^Method CodeIgniter\\Database\\MySQLi\\Utils\:\:_backup\(\) has parameter \$prefs with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/MySQLi/Utils.php - - - - message: '#^Method CodeIgniter\\Database\\OCI8\\PreparedQuery\:\:_execute\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/OCI8/PreparedQuery.php - - - - message: '#^Method CodeIgniter\\Database\\OCI8\\PreparedQuery\:\:_prepare\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/OCI8/PreparedQuery.php - - - - message: '#^Method CodeIgniter\\Database\\OCI8\\Utils\:\:_backup\(\) has parameter \$prefs with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/OCI8/Utils.php - - - - message: '#^Method CodeIgniter\\Database\\Postgre\\PreparedQuery\:\:_execute\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Postgre/PreparedQuery.php - - - - message: '#^Method CodeIgniter\\Database\\Postgre\\PreparedQuery\:\:_prepare\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Postgre/PreparedQuery.php - - - - message: '#^Method CodeIgniter\\Database\\Postgre\\Utils\:\:_backup\(\) has parameter \$prefs with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Postgre/Utils.php - - - - message: '#^Method CodeIgniter\\Database\\PreparedQueryInterface\:\:prepare\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/PreparedQueryInterface.php - - message: '#^Method CodeIgniter\\Database\\Query\:\:matchNamedBinds\(\) has parameter \$binds with no value type specified in iterable type array\.$#' count: 1 @@ -512,41 +427,6 @@ parameters: count: 1 path: ../../system/Database/Query.php - - - message: '#^Method CodeIgniter\\Database\\SQLSRV\\PreparedQuery\:\:_execute\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLSRV/PreparedQuery.php - - - - message: '#^Method CodeIgniter\\Database\\SQLSRV\\PreparedQuery\:\:_prepare\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLSRV/PreparedQuery.php - - - - message: '#^Method CodeIgniter\\Database\\SQLSRV\\PreparedQuery\:\:parameterize\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLSRV/PreparedQuery.php - - - - message: '#^Property CodeIgniter\\Database\\SQLSRV\\PreparedQuery\:\:\$parameters type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLSRV/PreparedQuery.php - - - - message: '#^Method CodeIgniter\\Database\\SQLSRV\\Utils\:\:_backup\(\) has parameter \$prefs with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLSRV/Utils.php - - - - message: '#^Method CodeIgniter\\Database\\SQLite3\\PreparedQuery\:\:_execute\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLite3/PreparedQuery.php - - - - message: '#^Method CodeIgniter\\Database\\SQLite3\\PreparedQuery\:\:_prepare\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLite3/PreparedQuery.php - - message: '#^Method CodeIgniter\\Database\\SQLite3\\Table\:\:addForeignKey\(\) has parameter \$foreignKeys with no value type specified in iterable type array\.$#' count: 1 @@ -577,11 +457,6 @@ parameters: count: 1 path: ../../system/Database/SQLite3/Table.php - - - message: '#^Method CodeIgniter\\Database\\SQLite3\\Utils\:\:_backup\(\) has parameter \$prefs with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLite3/Utils.php - - message: '#^Method CodeIgniter\\Debug\\BaseExceptionHandler\:\:collectVars\(\) return type has no value type specified in iterable type array\.$#' count: 1 diff --git a/utils/phpstan-baseline/missingType.parameter.neon b/utils/phpstan-baseline/missingType.parameter.neon index 5efe82a4824a..baa8330cffd4 100644 --- a/utils/phpstan-baseline/missingType.parameter.neon +++ b/utils/phpstan-baseline/missingType.parameter.neon @@ -1,17 +1,7 @@ -# total 15 errors +# total 13 errors parameters: ignoreErrors: - - - message: '#^Method CodeIgniter\\Database\\BasePreparedQuery\:\:execute\(\) has parameter \$data with no type specified\.$#' - count: 1 - path: ../../system/Database/BasePreparedQuery.php - - - - message: '#^Method CodeIgniter\\Database\\PreparedQueryInterface\:\:execute\(\) has parameter \$data with no type specified\.$#' - count: 1 - path: ../../system/Database/PreparedQueryInterface.php - - message: '#^Method CodeIgniter\\Commands\\Utilities\\Routes\\AutoRouterImproved\\Controllers\\Dash_folder\\Dash_controller\:\:getDash_method\(\) has parameter \$p1 with no type specified\.$#' count: 1 diff --git a/utils/phpstan-baseline/property.phpDocType.neon b/utils/phpstan-baseline/property.phpDocType.neon index 275166c25508..fbbec53d281e 100644 --- a/utils/phpstan-baseline/property.phpDocType.neon +++ b/utils/phpstan-baseline/property.phpDocType.neon @@ -1,47 +1,7 @@ -# total 17 errors +# total 9 errors parameters: ignoreErrors: - - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\MySQLi\\Utils\:\:\$listDatabases is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\BaseUtils\:\:\$listDatabases\.$#' - count: 1 - path: ../../system/Database/MySQLi/Utils.php - - - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\MySQLi\\Utils\:\:\$optimizeTable is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\BaseUtils\:\:\$optimizeTable\.$#' - count: 1 - path: ../../system/Database/MySQLi/Utils.php - - - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\OCI8\\Utils\:\:\$listDatabases is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\BaseUtils\:\:\$listDatabases\.$#' - count: 1 - path: ../../system/Database/OCI8/Utils.php - - - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\Postgre\\Utils\:\:\$listDatabases is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\BaseUtils\:\:\$listDatabases\.$#' - count: 1 - path: ../../system/Database/Postgre/Utils.php - - - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\Postgre\\Utils\:\:\$optimizeTable is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\BaseUtils\:\:\$optimizeTable\.$#' - count: 1 - path: ../../system/Database/Postgre/Utils.php - - - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\SQLSRV\\Utils\:\:\$listDatabases is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\BaseUtils\:\:\$listDatabases\.$#' - count: 1 - path: ../../system/Database/SQLSRV/Utils.php - - - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\SQLSRV\\Utils\:\:\$optimizeTable is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\BaseUtils\:\:\$optimizeTable\.$#' - count: 1 - path: ../../system/Database/SQLSRV/Utils.php - - - - message: '#^PHPDoc type string of property CodeIgniter\\Database\\SQLite3\\Utils\:\:\$optimizeTable is not the same as PHPDoc type bool\|string of overridden property CodeIgniter\\Database\\BaseUtils\:\:\$optimizeTable\.$#' - count: 1 - path: ../../system/Database/SQLite3/Utils.php - - message: '#^PHPDoc type int of property CodeIgniter\\Exceptions\\PageNotFoundException\:\:\$code is not the same as PHPDoc type mixed of overridden property Exception\:\:\$code\.$#' count: 1 From d9ee179e2c5004ec1d84e6f0eaac1977e4c61251 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Mon, 24 Aug 2026 13:57:10 +0800 Subject: [PATCH 071/163] refactor: fix phpstan errors in `Config`, `Database`, `Query`, `MigrationRunner`, and `SQLite3\Table` (#10477) --- system/Database/Config.php | 14 +- system/Database/Database.php | 14 +- system/Database/MigrationRunner.php | 16 +- system/Database/Query.php | 10 +- system/Database/SQLite3/Table.php | 14 +- .../Database/Live/SQLite3/AlterTableTest.php | 11 +- .../Live/SQLite3/GetIndexDataTest.php | 7 +- .../Migrations/MigrationRunnerTest.php | 7 + .../phpstan-baseline/assign.propertyType.neon | 12 +- utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 142 +----------------- .../missingType.parameter.neon | 7 +- utils/phpstan-baseline/property.notFound.neon | 7 +- 13 files changed, 75 insertions(+), 188 deletions(-) diff --git a/system/Database/Config.php b/system/Database/Config.php index 413d0b3b5de8..790c2fb63b12 100644 --- a/system/Database/Config.php +++ b/system/Database/Config.php @@ -26,7 +26,7 @@ class Config extends BaseConfig * Cache for instance of any connections that * have been requested as a "shared" instance. * - * @var array + * @var array */ protected static $instances = []; @@ -41,9 +41,9 @@ class Config extends BaseConfig /** * Returns the database connection * - * @param array|BaseConnection|non-empty-string|null $group The name of the connection group to use, - * or an array of configuration settings. - * @param bool $getShared Whether to return a shared instance of the connection. + * @param array|BaseConnection|non-empty-string|null $group The name of the connection group to use, + * or an array of configuration settings. + * @param bool $getShared Whether to return a shared instance of the connection. * * @return BaseConnection */ @@ -90,6 +90,8 @@ public static function connect($group = null, bool $getShared = true) /** * Returns an array of all db connections currently made. + * + * @return array */ public static function getConnections(): array { @@ -100,7 +102,7 @@ public static function getConnections(): array * Loads and returns an instance of the Forge for the specified * database group, and loads the group if it hasn't been loaded yet. * - * @param array|ConnectionInterface|string|null $group + * @param array|ConnectionInterface|string|null $group * * @return Forge */ @@ -114,7 +116,7 @@ public static function forge($group = null) /** * Returns a new instance of the Database Utilities class. * - * @param array|string|null $group + * @param array|string|null $group * * @return BaseUtils */ diff --git a/system/Database/Database.php b/system/Database/Database.php index 2955ce0d37c5..4cb5dfaf1627 100644 --- a/system/Database/Database.php +++ b/system/Database/Database.php @@ -31,13 +31,15 @@ class Database * Helps to keep track of all open connections for performance * monitoring, logging, etc. * - * @var array + * @var array */ protected $connections = []; /** * Parses the connection binds and creates a Database Connection instance. * + * @param array $params + * * @return BaseConnection * * @throws InvalidArgumentException @@ -94,6 +96,10 @@ public function loadUtils(ConnectionInterface $db): BaseUtils /** * Parses universal DSN string * + * @param array $params + * + * @return array + * * @throws InvalidArgumentException */ protected function parseDSN(array $params): array @@ -132,9 +138,9 @@ protected function parseDSN(array $params): array /** * Creates a database object. * - * @param string $driver Driver name. FQCN can be used. - * @param string $class 'Connection'|'Forge'|'Utils' - * @param array|ConnectionInterface $argument The constructor parameter or DB connection + * @param string $driver Driver name. FQCN can be used. + * @param string $class 'Connection'|'Forge'|'Utils' + * @param array|ConnectionInterface $argument The constructor parameter or DB connection * * @return BaseConnection|BaseUtils|Forge */ diff --git a/system/Database/MigrationRunner.php b/system/Database/MigrationRunner.php index 809fd84a718d..0b3459ba076e 100644 --- a/system/Database/MigrationRunner.php +++ b/system/Database/MigrationRunner.php @@ -90,7 +90,7 @@ class MigrationRunner /** * used to return messages for CLI. * - * @var array + * @var list */ protected $cliMessages = []; @@ -139,7 +139,7 @@ class MigrationRunner * default DB group so that it creates the `migrations` table in the default * DB group. Therefore, passing $db is for testing purposes only. * - * @param array|ConnectionInterface|string|null $db DB group. For testing purposes only. + * @param array|ConnectionInterface|string|null $db DB group. For testing purposes only. * * @throws ConfigException */ @@ -440,7 +440,7 @@ public function force(string $path, string $namespace, ?string $group = null) /** * Retrieves list of available migration scripts * - * @return array List of all located migrations by their UID + * @return array List of all located migrations by their UID */ public function findMigrations(): array { @@ -465,6 +465,8 @@ public function findMigrations(): array /** * Retrieves a list of available migration scripts for one namespace + * + * @return list */ public function findNamespaceMigrations(string $namespace): array { @@ -610,6 +612,8 @@ public function getObjectUid($object): string /** * Retrieves messages formatted for CLI output + * + * @return list */ public function getCliMessages(): array { @@ -693,6 +697,8 @@ protected function removeHistory($history) /** * Grabs the full migration history from the database for a group + * + * @return list */ public function getHistory(string $group = 'default'): array { @@ -719,6 +725,8 @@ public function getHistory(string $group = 'default'): array * Returns the migration history for a single batch. * * @param string $order + * + * @return list */ public function getBatchHistory(int $batch, $order = 'asc'): array { @@ -734,6 +742,8 @@ public function getBatchHistory(int $batch, $order = 'asc'): array /** * Returns all the batches from the database history in order + * + * @return list */ public function getBatches(): array { diff --git a/system/Database/Query.php b/system/Database/Query.php index dbefaff6a09d..294f143e57e8 100644 --- a/system/Database/Query.php +++ b/system/Database/Query.php @@ -44,7 +44,7 @@ class Query implements QueryInterface, Stringable /** * The binds and their values used for binding. * - * @var array + * @var array */ protected $binds = []; @@ -129,6 +129,8 @@ public function setQuery(string $sql, mixed $binds = null, bool $setEscape = tru /** * Will store the variables to bind into the query later. * + * @param array $binds + * * @return $this */ public function setBinds(array $binds, bool $setEscape = true) @@ -266,6 +268,9 @@ protected function compileBinds() } } + /** + * @param array $binds + */ protected function matchNamedBinds(string $sql, array $binds): string { $replacers = []; @@ -287,6 +292,9 @@ protected function matchNamedBinds(string $sql, array $binds): string return strtr($sql, $replacers); } + /** + * @param array $binds + */ protected function matchSimpleBinds(string $sql, array $binds, int $bindCount, int $ml): string { if ($c = preg_match_all("/'[^']*'/", $sql, $matches) >= 1) { diff --git a/system/Database/SQLite3/Table.php b/system/Database/SQLite3/Table.php index 48e081ed906b..1884d8c3ed44 100644 --- a/system/Database/SQLite3/Table.php +++ b/system/Database/SQLite3/Table.php @@ -35,14 +35,14 @@ class Table /** * All of the unique/primary keys in the table. * - * @var array + * @var array, type: string}> */ protected $keys = []; /** * All of the foreign keys in the table. * - * @var array + * @var array */ protected $foreignKeys = []; @@ -230,6 +230,8 @@ public function dropForeignKey(string $foreignName) /** * Adds primary key + * + * @param array{fields?: list} $fields */ public function addPrimaryKey(array $fields): Table { @@ -254,6 +256,8 @@ public function addPrimaryKey(array $fields): Table /** * Add a foreign key * + * @param list, referenceTable: string, referenceField: list, onDelete: string, onUpdate: string, fkName: string}> $foreignKeys + * * @return $this */ public function addForeignKey(array $foreignKeys) @@ -374,9 +378,9 @@ protected function copyData() * Converts fields retrieved from the database to * the format needed for creating fields with Forge. * - * @param array|bool $fields + * @param bool|list $fields * - * @return ($fields is array ? array : mixed) + * @return ($fields is array ? array> : mixed) */ protected function formatFields($fields) { @@ -452,7 +456,7 @@ private function isNumericType(string $type): bool * * @param array $keys * - * @return array + * @return array, type: string}> */ protected function formatKeys($keys) { diff --git a/tests/system/Database/Live/SQLite3/AlterTableTest.php b/tests/system/Database/Live/SQLite3/AlterTableTest.php index 46806b9618fb..61bf7ad44f6b 100644 --- a/tests/system/Database/Live/SQLite3/AlterTableTest.php +++ b/tests/system/Database/Live/SQLite3/AlterTableTest.php @@ -14,6 +14,7 @@ namespace CodeIgniter\Database\Live\SQLite3; use CodeIgniter\Database\Exceptions\DataException; +use CodeIgniter\Database\SQLite3\Connection; use CodeIgniter\Database\SQLite3\Forge; use CodeIgniter\Database\SQLite3\Table; use CodeIgniter\Test\CIUnitTestCase; @@ -54,8 +55,14 @@ protected function setUp(): void 'database' => ':memory:', 'DBDebug' => true, ]; - $this->db = db_connect($config); - $this->forge = Database::forge($config); + $db = db_connect($config); + $this->assertInstanceOf(Connection::class, $db); + $this->db = $db; + + $forge = Database::forge($config); + $this->assertInstanceOf(Forge::class, $forge); + $this->forge = $forge; + $this->table = new Table($this->db, $this->forge); $this->dropTables(); diff --git a/tests/system/Database/Live/SQLite3/GetIndexDataTest.php b/tests/system/Database/Live/SQLite3/GetIndexDataTest.php index ac5bd5a299d8..854d247daf22 100644 --- a/tests/system/Database/Live/SQLite3/GetIndexDataTest.php +++ b/tests/system/Database/Live/SQLite3/GetIndexDataTest.php @@ -41,8 +41,11 @@ protected function setUp(): void 'database' => 'database.db', 'DBDebug' => true, ]; - $this->db = db_connect($config, false); - $this->forge = Database::forge($config); + $this->db = db_connect($config, false); + + $forge = Database::forge($config); + $this->assertInstanceOf(Forge::class, $forge); + $this->forge = $forge; } public function testGetIndexData(): void diff --git a/tests/system/Database/Migrations/MigrationRunnerTest.php b/tests/system/Database/Migrations/MigrationRunnerTest.php index 510c8169fa34..79ad1cf39330 100644 --- a/tests/system/Database/Migrations/MigrationRunnerTest.php +++ b/tests/system/Database/Migrations/MigrationRunnerTest.php @@ -14,7 +14,9 @@ namespace CodeIgniter\Database\Migrations; use CodeIgniter\Database\BaseConnection; +use CodeIgniter\Database\ConnectionInterface; use CodeIgniter\Database\MigrationRunner; +use CodeIgniter\Database\SQLSRV\Connection as SQLSRVConnection; use CodeIgniter\Events\Events; use CodeIgniter\Exceptions\ConfigException; use CodeIgniter\Test\CIUnitTestCase; @@ -121,6 +123,7 @@ public function testGetHistory(): void ]; if ($this->db->DBDriver === 'SQLSRV') { + $this->assertInstanceOf(SQLSRVConnection::class, $this->db); $this->db->simpleQuery('SET IDENTITY_INSERT ' . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->prefixTable('migrations') . ' ON'); } @@ -138,6 +141,7 @@ public function testGetHistory(): void $this->assertSame($expected, $history); if ($this->db->DBDriver === 'SQLSRV') { + $this->assertInstanceOf(SQLSRVConnection::class, $this->db); $this->db->simpleQuery('SET IDENTITY_INSERT ' . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->prefixTable('migrations') . ' OFF'); $db = $this->getPrivateProperty($runner, 'db'); $db->table('migrations')->delete(['id' => 4]); @@ -477,6 +481,9 @@ public function testMigrationUsesSameConnectionAsMigrationRunner(): void } } + /** + * @param array|ConnectionInterface|string|null $db + */ protected function resetTables($db = null): void { $forge = Database::forge($db); diff --git a/utils/phpstan-baseline/assign.propertyType.neon b/utils/phpstan-baseline/assign.propertyType.neon index 3fb69d14d267..735faa6341d8 100644 --- a/utils/phpstan-baseline/assign.propertyType.neon +++ b/utils/phpstan-baseline/assign.propertyType.neon @@ -1,4 +1,4 @@ -# total 24 errors +# total 22 errors parameters: ignoreErrors: @@ -17,16 +17,6 @@ parameters: count: 1 path: ../../tests/system/Commands/Utilities/Routes/FilterFinderTest.php - - - message: '#^Property CodeIgniter\\Database\\Live\\SQLite3\\AlterTableTest\:\:\$forge \(CodeIgniter\\Database\\SQLite3\\Forge\) does not accept CodeIgniter\\Database\\Forge\.$#' - count: 1 - path: ../../tests/system/Database/Live/SQLite3/AlterTableTest.php - - - - message: '#^Property CodeIgniter\\Database\\Live\\SQLite3\\GetIndexDataTest\:\:\$forge \(CodeIgniter\\Database\\SQLite3\\Forge\) does not accept CodeIgniter\\Database\\Forge\.$#' - count: 1 - path: ../../tests/system/Database/Live/SQLite3/GetIndexDataTest.php - - message: '#^Property CodeIgniter\\Filters\\CSRFTest\:\:\$response \(CodeIgniter\\HTTP\\Response\|null\) does not accept CodeIgniter\\HTTP\\ResponseInterface\.$#' count: 2 diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index e2eed4892221..947f86255805 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 572 errors +# total 539 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index f8c8995deaa0..51c988f3992a 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 415 errors +# total 387 errors parameters: ignoreErrors: @@ -317,146 +317,6 @@ parameters: count: 1 path: ../../system/DataConverter/DataConverter.php - - - message: '#^Method CodeIgniter\\Database\\Config\:\:connect\(\) has parameter \$group with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Config.php - - - - message: '#^Method CodeIgniter\\Database\\Config\:\:forge\(\) has parameter \$group with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Config.php - - - - message: '#^Method CodeIgniter\\Database\\Config\:\:getConnections\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Config.php - - - - message: '#^Method CodeIgniter\\Database\\Config\:\:utils\(\) has parameter \$group with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Config.php - - - - message: '#^Property CodeIgniter\\Database\\Config\:\:\$instances type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Config.php - - - - message: '#^Method CodeIgniter\\Database\\Database\:\:initDriver\(\) has parameter \$argument with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Database.php - - - - message: '#^Method CodeIgniter\\Database\\Database\:\:load\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Database.php - - - - message: '#^Method CodeIgniter\\Database\\Database\:\:parseDSN\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Database.php - - - - message: '#^Method CodeIgniter\\Database\\Database\:\:parseDSN\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Database.php - - - - message: '#^Property CodeIgniter\\Database\\Database\:\:\$connections type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Database.php - - - - message: '#^Method CodeIgniter\\Database\\MigrationRunner\:\:__construct\(\) has parameter \$db with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/MigrationRunner.php - - - - message: '#^Method CodeIgniter\\Database\\MigrationRunner\:\:findMigrations\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/MigrationRunner.php - - - - message: '#^Method CodeIgniter\\Database\\MigrationRunner\:\:findNamespaceMigrations\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/MigrationRunner.php - - - - message: '#^Method CodeIgniter\\Database\\MigrationRunner\:\:getBatchHistory\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/MigrationRunner.php - - - - message: '#^Method CodeIgniter\\Database\\MigrationRunner\:\:getBatches\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/MigrationRunner.php - - - - message: '#^Method CodeIgniter\\Database\\MigrationRunner\:\:getCliMessages\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/MigrationRunner.php - - - - message: '#^Method CodeIgniter\\Database\\MigrationRunner\:\:getHistory\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/MigrationRunner.php - - - - message: '#^Property CodeIgniter\\Database\\MigrationRunner\:\:\$cliMessages type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/MigrationRunner.php - - - - message: '#^Method CodeIgniter\\Database\\Query\:\:matchNamedBinds\(\) has parameter \$binds with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Query.php - - - - message: '#^Method CodeIgniter\\Database\\Query\:\:matchSimpleBinds\(\) has parameter \$binds with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Query.php - - - - message: '#^Method CodeIgniter\\Database\\Query\:\:setBinds\(\) has parameter \$binds with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Query.php - - - - message: '#^Property CodeIgniter\\Database\\Query\:\:\$binds type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/Query.php - - - - message: '#^Method CodeIgniter\\Database\\SQLite3\\Table\:\:addForeignKey\(\) has parameter \$foreignKeys with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLite3/Table.php - - - - message: '#^Method CodeIgniter\\Database\\SQLite3\\Table\:\:addPrimaryKey\(\) has parameter \$fields with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLite3/Table.php - - - - message: '#^Method CodeIgniter\\Database\\SQLite3\\Table\:\:formatFields\(\) has parameter \$fields with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLite3/Table.php - - - - message: '#^Method CodeIgniter\\Database\\SQLite3\\Table\:\:formatFields\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLite3/Table.php - - - - message: '#^Property CodeIgniter\\Database\\SQLite3\\Table\:\:\$foreignKeys type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLite3/Table.php - - - - message: '#^Property CodeIgniter\\Database\\SQLite3\\Table\:\:\$keys type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Database/SQLite3/Table.php - - message: '#^Method CodeIgniter\\Debug\\BaseExceptionHandler\:\:collectVars\(\) return type has no value type specified in iterable type array\.$#' count: 1 diff --git a/utils/phpstan-baseline/missingType.parameter.neon b/utils/phpstan-baseline/missingType.parameter.neon index baa8330cffd4..54d2e4a72c64 100644 --- a/utils/phpstan-baseline/missingType.parameter.neon +++ b/utils/phpstan-baseline/missingType.parameter.neon @@ -1,4 +1,4 @@ -# total 13 errors +# total 12 errors parameters: ignoreErrors: @@ -27,11 +27,6 @@ parameters: count: 1 path: ../../tests/system/Config/FactoriesTest.php - - - message: '#^Method CodeIgniter\\Database\\Migrations\\MigrationRunnerTest\:\:resetTables\(\) has parameter \$db with no type specified\.$#' - count: 1 - path: ../../tests/system/Database/Migrations/MigrationRunnerTest.php - - message: '#^Method CodeIgniter\\Entity\\EntityTest\:\:getCastEntity\(\) has parameter \$data with no type specified\.$#' count: 1 diff --git a/utils/phpstan-baseline/property.notFound.neon b/utils/phpstan-baseline/property.notFound.neon index 13dd11a84ac9..f55cee7b3de4 100644 --- a/utils/phpstan-baseline/property.notFound.neon +++ b/utils/phpstan-baseline/property.notFound.neon @@ -1,12 +1,7 @@ -# total 21 errors +# total 19 errors parameters: ignoreErrors: - - - message: '#^Access to an undefined property CodeIgniter\\Database\\BaseConnection\:\:\$schema\.$#' - count: 2 - path: ../../tests/system/Database/Migrations/MigrationRunnerTest.php - - message: '#^Access to an undefined property CodeIgniter\\Encryption\\EncrypterInterface\:\:\$key\.$#' count: 1 From 92008b339e9fc2051190654059b3a85997829b87 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Mon, 24 Aug 2026 14:20:26 +0800 Subject: [PATCH 072/163] refactor: fix phpstan errors in `Debug` (#10478) --- system/Debug/BaseExceptionHandler.php | 20 +- system/Debug/Exceptions.php | 14 +- system/Debug/Iterator.php | 4 +- system/Debug/Timer.php | 4 +- system/Debug/Toolbar.php | 16 +- .../Toolbar/Collectors/BaseCollector.php | 22 +- system/Debug/Toolbar/Collectors/Config.php | 11 + system/Debug/Toolbar/Collectors/Database.php | 23 +- system/Debug/Toolbar/Collectors/Events.php | 4 + system/Debug/Toolbar/Collectors/Files.php | 2 + system/Debug/Toolbar/Collectors/History.php | 22 +- system/Debug/Toolbar/Collectors/Timers.php | 2 + system/Debug/Toolbar/Collectors/Views.php | 12 +- tests/system/Debug/ExceptionHandlerTest.php | 6 +- tests/system/Debug/TimerTest.php | 2 +- user_guide_src/source/changelogs/v4.7.5.rst | 2 + utils/phpstan-baseline/argument.type.neon | 7 +- utils/phpstan-baseline/loader.neon | 2 +- utils/phpstan-baseline/method.notFound.neon | 17 +- .../missingType.iterableValue.neon | 237 +----------------- 20 files changed, 151 insertions(+), 278 deletions(-) diff --git a/system/Debug/BaseExceptionHandler.php b/system/Debug/BaseExceptionHandler.php index b9180a98bd59..6adaa8eaaca9 100644 --- a/system/Debug/BaseExceptionHandler.php +++ b/system/Debug/BaseExceptionHandler.php @@ -70,6 +70,16 @@ abstract public function handle( /** * Gathers the variables that will be made available to the view. + * + * @return array{ + * title: string, + * type: string, + * code: int, + * message: string, + * file: string, + * line: int, + * trace: list>, + * } */ protected function collectVars(Throwable $exception, int $statusCode): array { @@ -99,6 +109,11 @@ protected function collectVars(Throwable $exception, int $statusCode): array /** * Mask sensitive data in the trace. + * + * @param list> $trace + * @param list $keysToMask + * + * @return list> */ protected function maskSensitiveData(array $trace, array $keysToMask, string $path = ''): array { @@ -110,9 +125,10 @@ protected function maskSensitiveData(array $trace, array $keysToMask, string $pa } /** - * @param array|object $args + * @param array|object $args + * @param list $keysToMask * - * @return array|object + * @return array|object */ private function maskData($args, array $keysToMask, string $path = '') { diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index 3c8b1f006ea7..74092b627963 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -355,6 +355,8 @@ protected function render(Throwable $exception, int $statusCode) /** * Gathers the variables that will be made available to the view. * + * @return array{title: string, type: string, code: int, message: string, file: string, line: int, trace: list>} + * * @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler. */ protected function collectVars(Throwable $exception, int $statusCode): array @@ -386,9 +388,10 @@ protected function collectVars(Throwable $exception, int $statusCode): array /** * Mask sensitive data in the trace. * - * @param array $trace + * @param list> $trace + * @param list $keysToMask * - * @return array + * @return list> * * @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler. */ @@ -402,9 +405,10 @@ protected function maskSensitiveData($trace, array $keysToMask, string $path = ' } /** - * @param array|object $args + * @param array|object $args + * @param list $keysToMask * - * @return array|object + * @return array|object * * @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler. */ @@ -441,6 +445,8 @@ private function maskData($args, array $keysToMask, string $path = '') /** * Determines the HTTP status code and the exit status code for this request. + * + * @return array{int, int} */ protected function determineCodes(Throwable $exception): array { diff --git a/system/Debug/Iterator.php b/system/Debug/Iterator.php index 55ee0f3bce8b..07a6d6d3689f 100644 --- a/system/Debug/Iterator.php +++ b/system/Debug/Iterator.php @@ -23,14 +23,14 @@ class Iterator /** * Stores the tests that we are to run. * - * @var array + * @var array */ protected $tests = []; /** * Stores the results of each of the tests. * - * @var array + * @var array */ protected $results = []; diff --git a/system/Debug/Timer.php b/system/Debug/Timer.php index 45be554ea7ce..be4b56ce7cc8 100644 --- a/system/Debug/Timer.php +++ b/system/Debug/Timer.php @@ -28,7 +28,7 @@ class Timer /** * List of all timers. * - * @var array + * @var array */ protected $timers = []; @@ -107,6 +107,8 @@ public function getElapsedTime(string $name, int $decimals = 4) * Returns the array of timers, with the duration pre-calculated for you. * * @param int $decimals Number of decimal places + * + * @return array */ public function getTimers(int $decimals = 4): array { diff --git a/system/Debug/Toolbar.php b/system/Debug/Toolbar.php index 311a41a2bf9d..d38cfcb20e4e 100644 --- a/system/Debug/Toolbar.php +++ b/system/Debug/Toolbar.php @@ -190,6 +190,9 @@ public function run(float $startTime, float $totalTime, RequestInterface $reques /** * Called within the view to display the timeline itself. + * + * @param list>}> $collectors + * @param array $styles */ protected function renderTimeline(array $collectors, float $startTime, int $segmentCount, int $segmentDuration, array &$styles): string { @@ -202,6 +205,9 @@ protected function renderTimeline(array $collectors, float $startTime, int $segm /** * Recursively renders timeline elements and their children. + * + * @param list> $rows + * @param array $styles */ protected function renderTimelineRecursive(array $rows, float $startTime, int $segmentCount, int $segmentDuration, array &$styles, int &$styleCount, int $level = 0, bool $isChild = false): string { @@ -268,7 +274,9 @@ protected function renderTimelineRecursive(array $rows, float $startTime, int $s /** * Returns a sorted array of timeline data arrays from the collectors. * - * @param array $collectors + * @param list>}> $collectors + * + * @return list> */ protected function collectTimelineData($collectors): array { @@ -305,6 +313,10 @@ protected function collectTimelineData($collectors): array /** * Arranges the already sorted timeline data into a parent => child structure. + * + * @param list> $elements + * + * @return list> */ protected function structureTimelineData(array $elements): array { @@ -333,6 +345,8 @@ protected function structureTimelineData(array $elements): array /** * Returns an array of data from all of the modules * that should be displayed in the 'Vars' tab. + * + * @return array */ protected function collectVarData(): array { diff --git a/system/Debug/Toolbar/Collectors/BaseCollector.php b/system/Debug/Toolbar/Collectors/BaseCollector.php index 81cc631f98e7..05fdbf71a95e 100644 --- a/system/Debug/Toolbar/Collectors/BaseCollector.php +++ b/system/Debug/Toolbar/Collectors/BaseCollector.php @@ -105,6 +105,8 @@ public function hasTimelineData(): bool /** * Grabs the data for the timeline, properly formatted, * or returns an empty array. + * + * @return list> */ public function timelineData(): array { @@ -140,7 +142,7 @@ public function hasVarData(): bool * ], * ]; * - * @return array|null + * @return array|null */ public function getVarData() { @@ -159,6 +161,8 @@ public function getVarData() * 'start' => 10 // milliseconds * 'duration' => 15 // milliseconds * ] + * + * @return list> */ protected function formatTimelineData(): array { @@ -168,7 +172,7 @@ protected function formatTimelineData(): array /** * Returns the data of this collector to be formatted in the toolbar * - * @return array|string + * @return array|string */ public function display() { @@ -218,6 +222,20 @@ public function icon(): string /** * Return settings as an array. + * + * @return array{ + * title: string, + * titleSafe: string, + * titleDetails: string, + * display: array|string, + * badgeValue: int|null, + * isEmpty: bool, + * hasTabContent: bool, + * hasLabel: bool, + * icon: string, + * hasTimelineData: bool, + * timelineData: list>, + * } */ public function getAsArray(): array { diff --git a/system/Debug/Toolbar/Collectors/Config.php b/system/Debug/Toolbar/Collectors/Config.php index 80673f979f6c..d806858728dc 100644 --- a/system/Debug/Toolbar/Collectors/Config.php +++ b/system/Debug/Toolbar/Collectors/Config.php @@ -23,6 +23,17 @@ class Config { /** * Return toolbar config values as an array. + * + * @return array{ + * ciVersion: string, + * phpVersion: string, + * phpSAPI: string, + * environment: string, + * baseURL: string, + * timezone: string, + * locale: string, + * cspEnabled: bool, + * } */ public static function display(): array { diff --git a/system/Debug/Toolbar/Collectors/Database.php b/system/Debug/Toolbar/Collectors/Database.php index 4d5a06f40486..6ab1094c2918 100644 --- a/system/Debug/Toolbar/Collectors/Database.php +++ b/system/Debug/Toolbar/Collectors/Database.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Debug\Toolbar\Collectors; +use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\Query; use CodeIgniter\I18n\Time; use Config\Toolbar; @@ -55,7 +56,7 @@ class Database extends BaseCollector /** * Array of database connections. * - * @var array + * @var array */ protected $connections; @@ -63,7 +64,7 @@ class Database extends BaseCollector * The query instances that have been collected * through the DBQuery Event. * - * @var array + * @var list>}> */ protected static $queries = []; @@ -113,7 +114,13 @@ public static function collect(Query $query) /** * Returns timeline data formatted for the toolbar. * - * @return array The formatted data or an empty array. + * @return list */ protected function formatTimelineData(): array { @@ -144,6 +151,16 @@ protected function formatTimelineData(): array /** * Returns the data of this collector to be formatted in the toolbar + * + * @return array{queries: list>, + * trace-file: string, + * qid: string + * }>} */ public function display(): array { diff --git a/system/Debug/Toolbar/Collectors/Events.php b/system/Debug/Toolbar/Collectors/Events.php index 173c71a32733..46d20083c0bd 100644 --- a/system/Debug/Toolbar/Collectors/Events.php +++ b/system/Debug/Toolbar/Collectors/Events.php @@ -53,6 +53,8 @@ class Events extends BaseCollector /** * Child classes should implement this to return the timeline data * formatted for correct usage. + * + * @return list */ protected function formatTimelineData(): array { @@ -74,6 +76,8 @@ protected function formatTimelineData(): array /** * Returns the data of this collector to be formatted in the toolbar + * + * @return array{events: array} */ public function display(): array { diff --git a/system/Debug/Toolbar/Collectors/Files.php b/system/Debug/Toolbar/Collectors/Files.php index a573388f960e..35e345c04fc4 100644 --- a/system/Debug/Toolbar/Collectors/Files.php +++ b/system/Debug/Toolbar/Collectors/Files.php @@ -52,6 +52,8 @@ public function getTitleDetails(): string /** * Returns the data of this collector to be formatted in the toolbar + * + * @return array{coreFiles: list, userFiles: list} */ public function display(): array { diff --git a/system/Debug/Toolbar/Collectors/History.php b/system/Debug/Toolbar/Collectors/History.php index 583b350cb4b6..a0575feba9e8 100644 --- a/system/Debug/Toolbar/Collectors/History.php +++ b/system/Debug/Toolbar/Collectors/History.php @@ -55,7 +55,16 @@ class History extends BaseCollector protected $title = 'History'; /** - * @var array History files + * @var list */ protected $files = []; @@ -111,6 +120,17 @@ public function setFiles(string $current, int $limit = 20) /** * Returns the data of this collector to be formatted in the toolbar + * + * @return array{files: list} */ public function display(): array { diff --git a/system/Debug/Toolbar/Collectors/Timers.php b/system/Debug/Toolbar/Collectors/Timers.php index d450cc5c306a..9e86af2c8d9d 100644 --- a/system/Debug/Toolbar/Collectors/Timers.php +++ b/system/Debug/Toolbar/Collectors/Timers.php @@ -45,6 +45,8 @@ class Timers extends BaseCollector /** * Child classes should implement this to return the timeline data * formatted for correct usage. + * + * @return list */ protected function formatTimelineData(): array { diff --git a/system/Debug/Toolbar/Collectors/Views.php b/system/Debug/Toolbar/Collectors/Views.php index a63e172c3b8a..bae742a588a2 100644 --- a/system/Debug/Toolbar/Collectors/Views.php +++ b/system/Debug/Toolbar/Collectors/Views.php @@ -14,6 +14,7 @@ namespace CodeIgniter\Debug\Toolbar\Collectors; use CodeIgniter\View\RendererInterface; +use CodeIgniter\View\View; /** * Views collector @@ -70,10 +71,15 @@ class Views extends BaseCollector /** * Views counter * - * @var array + * @var array{} + * + * @deprecated 4.7.5 Unused. */ protected $views = []; + /** + * @phpstan-assert View $this->viewer + */ private function initViewer(): void { $this->viewer ??= service('renderer'); @@ -82,6 +88,8 @@ private function initViewer(): void /** * Child classes should implement this to return the timeline data * formatted for correct usage. + * + * @return list */ protected function formatTimelineData(): array { @@ -118,6 +126,8 @@ protected function formatTimelineData(): array * 'bar' => 'baz' * ], * ]; + * + * @return array> */ public function getVarData(): array { diff --git a/tests/system/Debug/ExceptionHandlerTest.php b/tests/system/Debug/ExceptionHandlerTest.php index 43086860b0d6..59eaa8c203d6 100644 --- a/tests/system/Debug/ExceptionHandlerTest.php +++ b/tests/system/Debug/ExceptionHandlerTest.php @@ -16,6 +16,7 @@ use App\Controllers\Home; use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Exceptions\RuntimeException; +use CodeIgniter\HTTP\Response; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\IniTestTrait; use CodeIgniter\Test\StreamFilterTrait; @@ -110,7 +111,8 @@ public function testHandleWebPageNotFoundExceptionDoNotAcceptHTML(): void { $exception = PageNotFoundException::forControllerNotFound('Foo', 'bar'); - $request = service('incomingrequest', null, false); + $request = service('incomingrequest', null, false); + /** @var Response $response */ $response = service('response', null, false); $response->pretend(); @@ -131,6 +133,7 @@ public function testHandleWebPageNotFoundExceptionAcceptHTML(): void $request = service('incomingrequest', null, false); $request->setHeader('accept', 'text/html'); + /** @var Response $response */ $response = service('response', null, false); $response->pretend(); @@ -147,6 +150,7 @@ public function testHandleCLIPageNotFoundException(): void $request = Services::clirequest(null, false); $request->setHeader('accept', 'text/html'); + /** @var Response $response */ $response = service('response', null, false); $response->pretend(); diff --git a/tests/system/Debug/TimerTest.php b/tests/system/Debug/TimerTest.php index 756c2ce50340..0f28e7dba69b 100644 --- a/tests/system/Debug/TimerTest.php +++ b/tests/system/Debug/TimerTest.php @@ -176,7 +176,7 @@ public function testRecordThrowsErrorOnCallableWithParams(): void $this->expectException(ArgumentCountError::class); $timer = new Timer(); - $timer->record('error', 'strlen'); + $timer->record('error', 'strlen'); // @phpstan-ignore argument.type (Needed for testing) } public function testCommonNoNameExpectTimer(): void diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index 9495108b28a6..b5f11bc83f79 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -26,6 +26,8 @@ Changes Deprecations ************ +- **Debug Toolbar:** Deprecated the unused ``CodeIgniter\Debug\Toolbar\Collectors\Views::$views`` property. + ********** Bugs Fixed ********** diff --git a/utils/phpstan-baseline/argument.type.neon b/utils/phpstan-baseline/argument.type.neon index 5e3cd39db63c..e72c009d6298 100644 --- a/utils/phpstan-baseline/argument.type.neon +++ b/utils/phpstan-baseline/argument.type.neon @@ -1,4 +1,4 @@ -# total 31 errors +# total 30 errors parameters: ignoreErrors: @@ -52,11 +52,6 @@ parameters: count: 2 path: ../../tests/system/Database/Live/UpsertTest.php - - - message: '#^Parameter \#2 \$callable of method CodeIgniter\\Debug\\Timer\:\:record\(\) expects callable\(\)\: mixed, ''strlen'' given\.$#' - count: 1 - path: ../../tests/system/Debug/TimerTest.php - - message: '#^Parameter \#1 \$num of function number_to_size expects int\|string, float given\.$#' count: 1 diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 947f86255805..d017c3f9b903 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 539 errors +# total 485 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/method.notFound.neon b/utils/phpstan-baseline/method.notFound.neon index 5fbb5963bf85..97c2353c3ea4 100644 --- a/utils/phpstan-baseline/method.notFound.neon +++ b/utils/phpstan-baseline/method.notFound.neon @@ -1,17 +1,7 @@ -# total 27 errors +# total 21 errors parameters: ignoreErrors: - - - message: '#^Call to an undefined method CodeIgniter\\View\\RendererInterface\:\:getData\(\)\.$#' - count: 1 - path: ../../system/Debug/Toolbar/Collectors/Views.php - - - - message: '#^Call to an undefined method CodeIgniter\\View\\RendererInterface\:\:getPerformanceData\(\)\.$#' - count: 2 - path: ../../system/Debug/Toolbar/Collectors/Views.php - - message: '#^Call to an undefined method CodeIgniter\\HTTP\\ResponseInterface\:\:pretend\(\)\.$#' count: 1 @@ -22,11 +12,6 @@ parameters: count: 2 path: ../../tests/system/Commands/Utilities/Routes/FilterFinderTest.php - - - message: '#^Call to an undefined method CodeIgniter\\HTTP\\ResponseInterface\:\:pretend\(\)\.$#' - count: 3 - path: ../../tests/system/Debug/ExceptionHandlerTest.php - - message: '#^Call to an undefined method org\\bovigo\\vfs\\visitor\\vfsStreamVisitor\:\:getStructure\(\)\.$#' count: 2 diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 51c988f3992a..fee6f02c8294 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 387 errors +# total 340 errors parameters: ignoreErrors: @@ -317,241 +317,6 @@ parameters: count: 1 path: ../../system/DataConverter/DataConverter.php - - - message: '#^Method CodeIgniter\\Debug\\BaseExceptionHandler\:\:collectVars\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/BaseExceptionHandler.php - - - - message: '#^Method CodeIgniter\\Debug\\BaseExceptionHandler\:\:maskData\(\) has parameter \$args with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/BaseExceptionHandler.php - - - - message: '#^Method CodeIgniter\\Debug\\BaseExceptionHandler\:\:maskData\(\) has parameter \$keysToMask with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/BaseExceptionHandler.php - - - - message: '#^Method CodeIgniter\\Debug\\BaseExceptionHandler\:\:maskData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/BaseExceptionHandler.php - - - - message: '#^Method CodeIgniter\\Debug\\BaseExceptionHandler\:\:maskSensitiveData\(\) has parameter \$keysToMask with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/BaseExceptionHandler.php - - - - message: '#^Method CodeIgniter\\Debug\\BaseExceptionHandler\:\:maskSensitiveData\(\) has parameter \$trace with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/BaseExceptionHandler.php - - - - message: '#^Method CodeIgniter\\Debug\\BaseExceptionHandler\:\:maskSensitiveData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/BaseExceptionHandler.php - - - - message: '#^Method CodeIgniter\\Debug\\Exceptions\:\:collectVars\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Exceptions.php - - - - message: '#^Method CodeIgniter\\Debug\\Exceptions\:\:determineCodes\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Exceptions.php - - - - message: '#^Method CodeIgniter\\Debug\\Exceptions\:\:maskData\(\) has parameter \$args with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Exceptions.php - - - - message: '#^Method CodeIgniter\\Debug\\Exceptions\:\:maskData\(\) has parameter \$keysToMask with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Exceptions.php - - - - message: '#^Method CodeIgniter\\Debug\\Exceptions\:\:maskData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Exceptions.php - - - - message: '#^Method CodeIgniter\\Debug\\Exceptions\:\:maskSensitiveData\(\) has parameter \$keysToMask with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Exceptions.php - - - - message: '#^Method CodeIgniter\\Debug\\Exceptions\:\:maskSensitiveData\(\) has parameter \$trace with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Exceptions.php - - - - message: '#^Method CodeIgniter\\Debug\\Exceptions\:\:maskSensitiveData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Exceptions.php - - - - message: '#^Property CodeIgniter\\Debug\\Iterator\:\:\$results type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Iterator.php - - - - message: '#^Property CodeIgniter\\Debug\\Iterator\:\:\$tests type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Iterator.php - - - - message: '#^Method CodeIgniter\\Debug\\Timer\:\:getTimers\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Timer.php - - - - message: '#^Property CodeIgniter\\Debug\\Timer\:\:\$timers type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Timer.php - - - - message: '#^Method CodeIgniter\\Debug\\Toolbar\:\:collectTimelineData\(\) has parameter \$collectors with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar.php - - - - message: '#^Method CodeIgniter\\Debug\\Toolbar\:\:collectTimelineData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar.php - - - - message: '#^Method CodeIgniter\\Debug\\Toolbar\:\:collectVarData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar.php - - - - message: '#^Method CodeIgniter\\Debug\\Toolbar\:\:renderTimelineRecursive\(\) has parameter \$rows with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar.php - - - - message: '#^Method CodeIgniter\\Debug\\Toolbar\:\:renderTimelineRecursive\(\) has parameter \$styles with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar.php - - - - message: '#^Method CodeIgniter\\Debug\\Toolbar\:\:renderTimeline\(\) has parameter \$collectors with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar.php - - - - message: '#^Method CodeIgniter\\Debug\\Toolbar\:\:renderTimeline\(\) has parameter \$styles with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar.php - - - - message: '#^Method CodeIgniter\\Debug\\Toolbar\:\:structureTimelineData\(\) has parameter \$elements with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar.php - - - - message: '#^Method CodeIgniter\\Debug\\Toolbar\:\:structureTimelineData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar.php - - - - message: '#^Method CodeIgniter\\Debug\\Toolbar\\Collectors\\BaseCollector\:\:display\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar/Collectors/BaseCollector.php - - - - message: '#^Method CodeIgniter\\Debug\\Toolbar\\Collectors\\BaseCollector\:\:formatTimelineData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar/Collectors/BaseCollector.php - - - - message: '#^Method CodeIgniter\\Debug\\Toolbar\\Collectors\\BaseCollector\:\:getAsArray\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar/Collectors/BaseCollector.php - - - - message: '#^Method CodeIgniter\\Debug\\Toolbar\\Collectors\\BaseCollector\:\:getVarData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar/Collectors/BaseCollector.php - - - - message: '#^Method CodeIgniter\\Debug\\Toolbar\\Collectors\\BaseCollector\:\:timelineData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar/Collectors/BaseCollector.php - - - - message: '#^Method CodeIgniter\\Debug\\Toolbar\\Collectors\\Config\:\:display\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar/Collectors/Config.php - - - - message: '#^Method CodeIgniter\\Debug\\Toolbar\\Collectors\\Database\:\:display\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar/Collectors/Database.php - - - - message: '#^Method CodeIgniter\\Debug\\Toolbar\\Collectors\\Database\:\:formatTimelineData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar/Collectors/Database.php - - - - message: '#^Property CodeIgniter\\Debug\\Toolbar\\Collectors\\Database\:\:\$connections type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar/Collectors/Database.php - - - - message: '#^Property CodeIgniter\\Debug\\Toolbar\\Collectors\\Database\:\:\$queries type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar/Collectors/Database.php - - - - message: '#^Method CodeIgniter\\Debug\\Toolbar\\Collectors\\Events\:\:display\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar/Collectors/Events.php - - - - message: '#^Method CodeIgniter\\Debug\\Toolbar\\Collectors\\Events\:\:formatTimelineData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar/Collectors/Events.php - - - - message: '#^Method CodeIgniter\\Debug\\Toolbar\\Collectors\\Files\:\:display\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar/Collectors/Files.php - - - - message: '#^Method CodeIgniter\\Debug\\Toolbar\\Collectors\\History\:\:display\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar/Collectors/History.php - - - - message: '#^Property CodeIgniter\\Debug\\Toolbar\\Collectors\\History\:\:\$files type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar/Collectors/History.php - - - - message: '#^Method CodeIgniter\\Debug\\Toolbar\\Collectors\\Timers\:\:formatTimelineData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar/Collectors/Timers.php - - - - message: '#^Method CodeIgniter\\Debug\\Toolbar\\Collectors\\Views\:\:formatTimelineData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar/Collectors/Views.php - - - - message: '#^Method CodeIgniter\\Debug\\Toolbar\\Collectors\\Views\:\:getVarData\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar/Collectors/Views.php - - - - message: '#^Property CodeIgniter\\Debug\\Toolbar\\Collectors\\Views\:\:\$views type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Debug/Toolbar/Collectors/Views.php - - message: '#^Method CodeIgniter\\Email\\Email\:\:__construct\(\) has parameter \$config with no value type specified in iterable type array\.$#' count: 1 From 67d42a90e0d08c3c25c88710849e9745575a66c1 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Mon, 24 Aug 2026 14:51:51 +0800 Subject: [PATCH 073/163] refactor: fix phpstan errors in `Test` (#10479) --- system/Test/Constraints/SeeInDatabase.php | 4 +- system/Test/ControllerTestTrait.php | 4 +- system/Test/DOMParser.php | 2 + system/Test/Fabricator.php | 34 +- system/Test/FeatureTestTrait.php | 28 +- system/Test/IniTestTrait.php | 6 + system/Test/TestResponse.php | 4 +- tests/system/Test/ControllerTestTraitTest.php | 2 +- tests/system/Test/DOMParserTest.php | 3 + tests/system/Test/FabricatorTest.php | 2 + tests/system/Test/FeatureTestTraitTest.php | 3 + tests/system/Test/TestLoggerTest.php | 3 + tests/system/Test/TestResponseTest.php | 10 +- utils/phpstan-baseline/argument.type.neon | 27 +- .../phpstan-baseline/assign.propertyType.neon | 7 +- utils/phpstan-baseline/loader.neon | 2 +- utils/phpstan-baseline/method.notFound.neon | 7 +- .../missingType.iterableValue.neon | 292 +----------------- 18 files changed, 82 insertions(+), 358 deletions(-) diff --git a/system/Test/Constraints/SeeInDatabase.php b/system/Test/Constraints/SeeInDatabase.php index d1025bc13d08..ef795274ed5d 100644 --- a/system/Test/Constraints/SeeInDatabase.php +++ b/system/Test/Constraints/SeeInDatabase.php @@ -34,12 +34,14 @@ class SeeInDatabase extends Constraint /** * Data used to compare results against. * - * @var array + * @var array */ protected $data; /** * SeeInDatabase constructor. + * + * @param array $data */ public function __construct(ConnectionInterface $db, array $data) { diff --git a/system/Test/ControllerTestTrait.php b/system/Test/ControllerTestTrait.php index 2a7be526383d..01c42458969e 100644 --- a/system/Test/ControllerTestTrait.php +++ b/system/Test/ControllerTestTrait.php @@ -76,7 +76,7 @@ trait ControllerTestTrait /** * URI of this request. * - * @var string + * @var string|URI */ protected $uri = 'http://example.com'; @@ -144,7 +144,7 @@ public function controller(string $name) /** * Runs the specified method on the controller and returns the results. * - * @param array $params + * @param mixed ...$params * * @return TestResponse * diff --git a/system/Test/DOMParser.php b/system/Test/DOMParser.php index 52765a4d656b..0afddfa4425e 100644 --- a/system/Test/DOMParser.php +++ b/system/Test/DOMParser.php @@ -198,6 +198,8 @@ public function dontSeeXPath(string $path): bool /** * Search the DOM using an XPath expression. * + * @param list $paths + * * @return DOMNodeList|false */ protected function doXPath(?string $search, string $element, array $paths = []) diff --git a/system/Test/Fabricator.php b/system/Test/Fabricator.php index b366464d5a26..c7cf97780fdc 100644 --- a/system/Test/Fabricator.php +++ b/system/Test/Fabricator.php @@ -37,7 +37,7 @@ class Fabricator /** * Array of counts for fabricated items * - * @var array + * @var array */ protected static $tableCounts = []; @@ -65,28 +65,28 @@ class Fabricator /** * Map of properties and their formatter to use * - * @var array|null + * @var array|null */ protected $formatters; /** * Date fields present in the model * - * @var array + * @var list */ protected $dateFields = []; /** * Array of data to add or override faked versions * - * @var array + * @var array */ protected $overrides = []; /** * Array of single-use data to override faked versions * - * @var array|null + * @var array|null */ protected $tempOverrides; @@ -111,9 +111,9 @@ class Fabricator /** * Store the model instance and initialize Faker to the locale. * - * @param object|string $model Instance or classname of the model to use - * @param array|null $formatters Array of property => formatter - * @param string|null $locale Locale for Faker provider + * @param object|string $model Instance or classname of the model to use + * @param array|null $formatters Array of property => formatter + * @param string|null $locale Locale for Faker provider * * @throws InvalidArgumentException */ @@ -238,6 +238,8 @@ public function getFaker(): Generator /** * Return and reset tempOverrides + * + * @return array */ public function getOverrides(): array { @@ -251,8 +253,8 @@ public function getOverrides(): array /** * Set the overrides, once or persistent * - * @param array $overrides Array of [field => value] - * @param bool $persist Whether these overrides should persist through the next operation + * @param array $overrides Array of [field => value] + * @param bool $persist Whether these overrides should persist through the next operation */ public function setOverrides(array $overrides = [], $persist = true): self { @@ -307,6 +309,8 @@ public function setValid(string $field, ?Closure $validator = null, int $maxRetr /** * Returns the current formatters + * + * @return array|null */ public function getFormatters(): ?array { @@ -316,7 +320,7 @@ public function getFormatters(): ?array /** * Set the formatters to use. Will attempt to autodetect if none are available. * - * @param array|null $formatters Array of [field => formatter], or null to detect + * @param array|null $formatters Array of [field => formatter], or null to detect */ public function setFormatters(?array $formatters = null): self { @@ -399,7 +403,7 @@ protected function guessFormatter($field): string * * @param int|null $count Optional number to create a collection * - * @return array|object An array or object (based on returnType), or an array of returnTypes + * @return array|list|object>|object An array or object (based on returnType), or an array of returnTypes */ public function make(?int $count = null) { @@ -424,7 +428,7 @@ public function make(?int $count = null) /** * Generate an array of faked data * - * @return array An array of faked data + * @return array An array of faked data * * @throws RuntimeException */ @@ -534,7 +538,7 @@ public function makeObject(?string $className = null): object * @param int|null $count Optional number to create a collection * @param bool $mock Whether to execute or mock the insertion * - * @return array|object An array or object (based on returnType), or an array of returnTypes + * @return array|list|object>|object|null An array or object (based on returnType), or an array of returnTypes * * @throws FrameworkException */ @@ -572,7 +576,7 @@ public function create(?int $count = null, bool $mock = false) * * @param int|null $count Optional number to create a collection * - * @return array|object An array or object (based on returnType), or an array of returnTypes + * @return array|list|object>|object An array or object (based on returnType), or an array of returnTypes */ protected function createMock(?int $count = null) { diff --git a/system/Test/FeatureTestTrait.php b/system/Test/FeatureTestTrait.php index ce115761c117..6067aa660928 100644 --- a/system/Test/FeatureTestTrait.php +++ b/system/Test/FeatureTestTrait.php @@ -179,7 +179,8 @@ public function skipEvents() * Calls a single URI, executes it, and returns a TestResponse * instance that can be used to run many assertions against. * - * @param string $method HTTP verb + * @param string $method HTTP verb + * @param array|null $params * * @return TestResponse */ @@ -242,7 +243,8 @@ public function call(string $method, string $path, ?array $params = null) /** * Performs a GET request. * - * @param string $path URI path relative to baseURL. May include query. + * @param string $path URI path relative to baseURL. May include query. + * @param array|null $params * * @return TestResponse * @@ -257,6 +259,8 @@ public function get(string $path, ?array $params = null) /** * Performs a POST request. * + * @param array|null $params + * * @return TestResponse * * @throws RedirectException @@ -270,6 +274,8 @@ public function post(string $path, ?array $params = null) /** * Performs a PUT request * + * @param array|null $params + * * @return TestResponse * * @throws RedirectException @@ -283,6 +289,8 @@ public function put(string $path, ?array $params = null) /** * Performss a PATCH request * + * @param array|null $params + * * @return TestResponse * * @throws RedirectException @@ -296,6 +304,8 @@ public function patch(string $path, ?array $params = null) /** * Performs a DELETE request. * + * @param array|null $params + * * @return TestResponse * * @throws RedirectException @@ -309,6 +319,8 @@ public function delete(string $path, ?array $params = null) /** * Performs an OPTIONS request. * + * @param array|null $params + * * @return TestResponse * * @throws RedirectException @@ -379,10 +391,11 @@ protected function setupHeaders(IncomingRequest $request) * * Always populate the GET vars based on the URI. * - * @param string $name Superglobal name (lowercase) - * @param non-empty-array|null $params + * @param string $name Superglobal name (lowercase) + * @param IncomingRequest $request + * @param non-empty-array|null $params * - * @return Request + * @return IncomingRequest * * @throws ReflectionException */ @@ -418,7 +431,10 @@ protected function populateGlobals(string $name, Request $request, ?array $param * This allows the body to be formatted in a way that the controller is going to * expect as in the case of testing a JSON or XML API. * - * @param array|null $params The parameters to be formatted and put in the body. + * @param IncomingRequest $request + * @param array|null $params The parameters to be formatted and put in the body. + * + * @return IncomingRequest */ protected function setRequestBody(Request $request, ?array $params = null): Request { diff --git a/system/Test/IniTestTrait.php b/system/Test/IniTestTrait.php index 01f8c89a0675..9093854bf225 100644 --- a/system/Test/IniTestTrait.php +++ b/system/Test/IniTestTrait.php @@ -15,8 +15,14 @@ trait IniTestTrait { + /** + * @var array + */ private array $iniSettings = []; + /** + * @param list $keys + */ private function backupIniValues(array $keys): void { foreach ($keys as $key) { diff --git a/system/Test/TestResponse.php b/system/Test/TestResponse.php index 13b1df3abaf0..580f938d77af 100644 --- a/system/Test/TestResponse.php +++ b/system/Test/TestResponse.php @@ -357,6 +357,8 @@ public function getJSON() /** * Test that the response contains a matching JSON fragment. + * + * @param array $fragment */ public function assertJSONFragment(array $fragment, bool $strict = false): void { @@ -378,7 +380,7 @@ public function assertJSONFragment(array $fragment, bool $strict = false): void * Asserts that the JSON exactly matches the passed in data. * If the value being passed in is a string, it must be a json_encoded string. * - * @param array|object|string $test + * @param array|object|string $test */ public function assertJSONExact($test): void { diff --git a/tests/system/Test/ControllerTestTraitTest.php b/tests/system/Test/ControllerTestTraitTest.php index 44301afa376d..5662c773d4cd 100644 --- a/tests/system/Test/ControllerTestTraitTest.php +++ b/tests/system/Test/ControllerTestTraitTest.php @@ -222,7 +222,7 @@ public function testFailsForward(): void ->execute('index'); // won't fail, but doesn't do anything - $this->assertNull($result->ohno('Hi')); + $this->assertNull($result->ohno('Hi')); // @phpstan-ignore method.notFound (Testing TestResponse::__call() fallback) } /** diff --git a/tests/system/Test/DOMParserTest.php b/tests/system/Test/DOMParserTest.php index ce43f2863cc6..58d4e332ce40 100644 --- a/tests/system/Test/DOMParserTest.php +++ b/tests/system/Test/DOMParserTest.php @@ -144,6 +144,9 @@ public function testSeeElement($text): void $this->assertTrue($dom->see($text, 'h1')); } + /** + * @return iterable + */ public static function provideText(): iterable { return [ diff --git a/tests/system/Test/FabricatorTest.php b/tests/system/Test/FabricatorTest.php index 0af1329aad8a..7eaa81863cfd 100644 --- a/tests/system/Test/FabricatorTest.php +++ b/tests/system/Test/FabricatorTest.php @@ -31,6 +31,8 @@ final class FabricatorTest extends CIUnitTestCase { /** * Default formatters to use for UserModel. Should match detected version. + * + * @var array */ private array $formatters = [ 'name' => 'name', diff --git a/tests/system/Test/FeatureTestTraitTest.php b/tests/system/Test/FeatureTestTraitTest.php index 79a00104a079..563815314a6c 100644 --- a/tests/system/Test/FeatureTestTraitTest.php +++ b/tests/system/Test/FeatureTestTraitTest.php @@ -358,6 +358,9 @@ public function testOpenCliRoutesFromHttpGot404(string $from, string $to, string $this->get($httpGet); } + /** + * @return iterable + */ public static function provideOpenCliRoutesFromHttpGot404(): iterable { return [ diff --git a/tests/system/Test/TestLoggerTest.php b/tests/system/Test/TestLoggerTest.php index 4c9f38df0dad..7999ee5364bc 100644 --- a/tests/system/Test/TestLoggerTest.php +++ b/tests/system/Test/TestLoggerTest.php @@ -34,6 +34,9 @@ public function testDidLogMethod(bool $expected, string $level, string $message, ); } + /** + * @return iterable + */ public static function provideDidLogMethod(): iterable { yield 'exact' => [ diff --git a/tests/system/Test/TestResponseTest.php b/tests/system/Test/TestResponseTest.php index 315e1f8db6bd..2ec981e786e5 100644 --- a/tests/system/Test/TestResponseTest.php +++ b/tests/system/Test/TestResponseTest.php @@ -40,6 +40,8 @@ public function testIsOK(int $code, bool $isOk): void /** * Provides status codes and their expected "OK" + * + * @return iterable */ public static function provideHttpStatusCodes(): iterable { @@ -312,7 +314,7 @@ public function testGetJSONEmptyJSON(): void public function testGetJSONFalseJSON(): void { $this->getTestResponse('

Hello World

'); - $this->response->setJSON(false, true); + $this->response->setJSON(false, true); // @phpstan-ignore argument.type (Needed for testing) // this should be FALSE - json_encode(false) $this->assertSame('false', $this->testResponse->getJSON()); @@ -321,7 +323,7 @@ public function testGetJSONFalseJSON(): void public function testGetJSONTrueJSON(): void { $this->getTestResponse('

Hello World

'); - $this->response->setJSON(true, true); + $this->response->setJSON(true, true); // @phpstan-ignore argument.type (Needed for testing) // this should be TRUE - json_encode(true) $this->assertSame('true', $this->testResponse->getJSON()); @@ -427,6 +429,10 @@ public function testAssertJsonExactString(): void $this->testResponse->assertJSONExact($formatter->format($data)); } + /** + * @param array $responseOptions + * @param array $headers + */ protected function getTestResponse(?string $body = null, array $responseOptions = [], array $headers = []): void { $this->response = new Response(new App()); diff --git a/utils/phpstan-baseline/argument.type.neon b/utils/phpstan-baseline/argument.type.neon index e72c009d6298..4451b62f6419 100644 --- a/utils/phpstan-baseline/argument.type.neon +++ b/utils/phpstan-baseline/argument.type.neon @@ -1,4 +1,4 @@ -# total 30 errors +# total 25 errors parameters: ignoreErrors: @@ -57,11 +57,6 @@ parameters: count: 1 path: ../../tests/system/Helpers/NumberHelperTest.php - - - message: '#^Parameter \#1 \$request of method CodeIgniter\\CodeIgniter\:\:setRequest\(\) expects CodeIgniter\\HTTP\\CLIRequest\|CodeIgniter\\HTTP\\IncomingRequest, CodeIgniter\\HTTP\\Request given\.$#' - count: 1 - path: ../../tests/system/HomeTest.php - - message: '#^Parameter \#2 \$message of method CodeIgniter\\Log\\Handlers\\ChromeLoggerHandler\:\:handle\(\) expects string, stdClass given\.$#' count: 1 @@ -72,26 +67,6 @@ parameters: count: 1 path: ../../tests/system/RESTful/ResourceControllerTest.php - - - message: '#^Parameter \#1 \$request of method CodeIgniter\\CodeIgniter\:\:setRequest\(\) expects CodeIgniter\\HTTP\\CLIRequest\|CodeIgniter\\HTTP\\IncomingRequest, CodeIgniter\\HTTP\\Request given\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestAutoRoutingImprovedTest.php - - - - message: '#^Parameter \#1 \$request of method CodeIgniter\\CodeIgniter\:\:setRequest\(\) expects CodeIgniter\\HTTP\\CLIRequest\|CodeIgniter\\HTTP\\IncomingRequest, CodeIgniter\\HTTP\\Request given\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestTraitTest.php - - - - message: '#^Parameter \#1 \$body of method CodeIgniter\\HTTP\\Response\:\:setJSON\(\) expects array\\|object\|string, false given\.$#' - count: 1 - path: ../../tests/system/Test/TestResponseTest.php - - - - message: '#^Parameter \#1 \$body of method CodeIgniter\\HTTP\\Response\:\:setJSON\(\) expects array\\|object\|string, true given\.$#' - count: 1 - path: ../../tests/system/Test/TestResponseTest.php - - message: '#^Parameter \#2 \$context of method CodeIgniter\\View\\Parser\:\:setData\(\) expects ''attr''\|''css''\|''html''\|''js''\|''raw''\|''url''\|null, ''unknown'' given\.$#' count: 3 diff --git a/utils/phpstan-baseline/assign.propertyType.neon b/utils/phpstan-baseline/assign.propertyType.neon index 735faa6341d8..a2a5ca34cc63 100644 --- a/utils/phpstan-baseline/assign.propertyType.neon +++ b/utils/phpstan-baseline/assign.propertyType.neon @@ -1,4 +1,4 @@ -# total 22 errors +# total 20 errors parameters: ignoreErrors: @@ -62,11 +62,6 @@ parameters: count: 1 path: ../../tests/system/Session/SessionTest.php - - - message: '#^Property CodeIgniter\\Test\\ControllerTestTraitTest\:\:\$uri \(string\) does not accept CodeIgniter\\HTTP\\SiteURI\.$#' - count: 2 - path: ../../tests/system/Test/ControllerTestTraitTest.php - - message: '#^Property CodeIgniter\\View\\ParserPluginTest\:\:\$validator \(CodeIgniter\\Validation\\Validation\) does not accept CodeIgniter\\Validation\\ValidationInterface\.$#' count: 1 diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index d017c3f9b903..61ebc04982b4 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 485 errors +# total 419 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/method.notFound.neon b/utils/phpstan-baseline/method.notFound.neon index 97c2353c3ea4..67fac4e02b49 100644 --- a/utils/phpstan-baseline/method.notFound.neon +++ b/utils/phpstan-baseline/method.notFound.neon @@ -1,4 +1,4 @@ -# total 21 errors +# total 20 errors parameters: ignoreErrors: @@ -42,11 +42,6 @@ parameters: count: 1 path: ../../tests/system/RESTful/ResourceControllerTest.php - - - message: '#^Call to an undefined method CodeIgniter\\Test\\TestResponse\:\:ohno\(\)\.$#' - count: 1 - path: ../../tests/system/Test/ControllerTestTraitTest.php - - message: '#^Call to an undefined method CodeIgniter\\View\\Table\:\:compileTemplate\(\)\.$#' count: 2 diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index fee6f02c8294..333eaa92ae2e 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 340 errors +# total 282 errors parameters: ignoreErrors: @@ -617,101 +617,6 @@ parameters: count: 1 path: ../../system/Publisher/Publisher.php - - - message: '#^Method CodeIgniter\\Test\\Constraints\\SeeInDatabase\:\:__construct\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Constraints/SeeInDatabase.php - - - - message: '#^Property CodeIgniter\\Test\\Constraints\\SeeInDatabase\:\:\$data type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Constraints/SeeInDatabase.php - - - - message: '#^Method CodeIgniter\\Test\\DOMParser\:\:doXPath\(\) has parameter \$paths with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/DOMParser.php - - - - message: '#^Method CodeIgniter\\Test\\Fabricator\:\:__construct\(\) has parameter \$formatters with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Fabricator.php - - - - message: '#^Method CodeIgniter\\Test\\Fabricator\:\:createMock\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Fabricator.php - - - - message: '#^Method CodeIgniter\\Test\\Fabricator\:\:create\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Fabricator.php - - - - message: '#^Method CodeIgniter\\Test\\Fabricator\:\:getFormatters\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Fabricator.php - - - - message: '#^Method CodeIgniter\\Test\\Fabricator\:\:getOverrides\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Fabricator.php - - - - message: '#^Method CodeIgniter\\Test\\Fabricator\:\:makeArray\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Fabricator.php - - - - message: '#^Method CodeIgniter\\Test\\Fabricator\:\:make\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Fabricator.php - - - - message: '#^Method CodeIgniter\\Test\\Fabricator\:\:setFormatters\(\) has parameter \$formatters with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Fabricator.php - - - - message: '#^Method CodeIgniter\\Test\\Fabricator\:\:setOverrides\(\) has parameter \$overrides with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Fabricator.php - - - - message: '#^Property CodeIgniter\\Test\\Fabricator\:\:\$dateFields type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Fabricator.php - - - - message: '#^Property CodeIgniter\\Test\\Fabricator\:\:\$formatters type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Fabricator.php - - - - message: '#^Property CodeIgniter\\Test\\Fabricator\:\:\$overrides type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Fabricator.php - - - - message: '#^Property CodeIgniter\\Test\\Fabricator\:\:\$tableCounts type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Fabricator.php - - - - message: '#^Property CodeIgniter\\Test\\Fabricator\:\:\$tempOverrides type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/Fabricator.php - - - - message: '#^Method CodeIgniter\\Test\\TestResponse\:\:assertJSONExact\(\) has parameter \$test with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/TestResponse.php - - - - message: '#^Method CodeIgniter\\Test\\TestResponse\:\:assertJSONFragment\(\) has parameter \$fragment with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Test/TestResponse.php - - message: '#^Method CodeIgniter\\Typography\\Typography\:\:protectCharacters\(\) has parameter \$match with no value type specified in iterable type array\.$#' count: 1 @@ -1077,16 +982,6 @@ parameters: count: 1 path: ../../tests/system/Database/Live/UpdateTest.php - - - message: '#^Method CodeIgniter\\Debug\\ExceptionHandlerTest\:\:backupIniValues\(\) has parameter \$keys with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Debug/ExceptionHandlerTest.php - - - - message: '#^Property CodeIgniter\\Debug\\ExceptionHandlerTest\:\:\$iniSettings type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Debug/ExceptionHandlerTest.php - - message: '#^Method CodeIgniter\\Email\\EmailTest\:\:provideEmailSendWithClearance\(\) return type has no value type specified in iterable type iterable\.$#' count: 1 @@ -1357,51 +1252,6 @@ parameters: count: 1 path: ../../tests/system/Helpers/URLHelper/SiteUrlTest.php - - - message: '#^Method CodeIgniter\\HomeTest\:\:call\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/HomeTest.php - - - - message: '#^Method CodeIgniter\\HomeTest\:\:delete\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/HomeTest.php - - - - message: '#^Method CodeIgniter\\HomeTest\:\:get\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/HomeTest.php - - - - message: '#^Method CodeIgniter\\HomeTest\:\:options\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/HomeTest.php - - - - message: '#^Method CodeIgniter\\HomeTest\:\:patch\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/HomeTest.php - - - - message: '#^Method CodeIgniter\\HomeTest\:\:populateGlobals\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/HomeTest.php - - - - message: '#^Method CodeIgniter\\HomeTest\:\:post\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/HomeTest.php - - - - message: '#^Method CodeIgniter\\HomeTest\:\:put\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/HomeTest.php - - - - message: '#^Method CodeIgniter\\HomeTest\:\:setRequestBody\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/HomeTest.php - - message: '#^Method CodeIgniter\\I18n\\TimeLegacyTest\:\:provideToStringDoesNotDependOnLocale\(\) return type has no value type specified in iterable type iterable\.$#' count: 1 @@ -1522,146 +1372,6 @@ parameters: count: 1 path: ../../tests/system/Router/RouterTest.php - - - message: '#^Method CodeIgniter\\Test\\ControllerTestTraitTest\:\:execute\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/ControllerTestTraitTest.php - - - - message: '#^Method CodeIgniter\\Test\\DOMParserTest\:\:provideText\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Test/DOMParserTest.php - - - - message: '#^Property CodeIgniter\\Test\\FabricatorTest\:\:\$formatters type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/FabricatorTest.php - - - - message: '#^Method CodeIgniter\\Test\\FeatureTestAutoRoutingImprovedTest\:\:call\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestAutoRoutingImprovedTest.php - - - - message: '#^Method CodeIgniter\\Test\\FeatureTestAutoRoutingImprovedTest\:\:delete\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestAutoRoutingImprovedTest.php - - - - message: '#^Method CodeIgniter\\Test\\FeatureTestAutoRoutingImprovedTest\:\:get\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestAutoRoutingImprovedTest.php - - - - message: '#^Method CodeIgniter\\Test\\FeatureTestAutoRoutingImprovedTest\:\:options\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestAutoRoutingImprovedTest.php - - - - message: '#^Method CodeIgniter\\Test\\FeatureTestAutoRoutingImprovedTest\:\:patch\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestAutoRoutingImprovedTest.php - - - - message: '#^Method CodeIgniter\\Test\\FeatureTestAutoRoutingImprovedTest\:\:populateGlobals\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestAutoRoutingImprovedTest.php - - - - message: '#^Method CodeIgniter\\Test\\FeatureTestAutoRoutingImprovedTest\:\:post\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestAutoRoutingImprovedTest.php - - - - message: '#^Method CodeIgniter\\Test\\FeatureTestAutoRoutingImprovedTest\:\:put\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestAutoRoutingImprovedTest.php - - - - message: '#^Method CodeIgniter\\Test\\FeatureTestAutoRoutingImprovedTest\:\:setRequestBody\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestAutoRoutingImprovedTest.php - - - - message: '#^Method CodeIgniter\\Test\\FeatureTestTraitTest\:\:call\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestTraitTest.php - - - - message: '#^Method CodeIgniter\\Test\\FeatureTestTraitTest\:\:delete\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestTraitTest.php - - - - message: '#^Method CodeIgniter\\Test\\FeatureTestTraitTest\:\:get\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestTraitTest.php - - - - message: '#^Method CodeIgniter\\Test\\FeatureTestTraitTest\:\:options\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestTraitTest.php - - - - message: '#^Method CodeIgniter\\Test\\FeatureTestTraitTest\:\:patch\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestTraitTest.php - - - - message: '#^Method CodeIgniter\\Test\\FeatureTestTraitTest\:\:populateGlobals\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestTraitTest.php - - - - message: '#^Method CodeIgniter\\Test\\FeatureTestTraitTest\:\:post\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestTraitTest.php - - - - message: '#^Method CodeIgniter\\Test\\FeatureTestTraitTest\:\:provideOpenCliRoutesFromHttpGot404\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestTraitTest.php - - - - message: '#^Method CodeIgniter\\Test\\FeatureTestTraitTest\:\:put\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestTraitTest.php - - - - message: '#^Method CodeIgniter\\Test\\FeatureTestTraitTest\:\:setRequestBody\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/FeatureTestTraitTest.php - - - - message: '#^Method CodeIgniter\\Test\\IniTestTraitTest\:\:backupIniValues\(\) has parameter \$keys with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/IniTestTraitTest.php - - - - message: '#^Property CodeIgniter\\Test\\IniTestTraitTest\:\:\$iniSettings type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/IniTestTraitTest.php - - - - message: '#^Method CodeIgniter\\Test\\TestLoggerTest\:\:provideDidLogMethod\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Test/TestLoggerTest.php - - - - message: '#^Method CodeIgniter\\Test\\TestResponseTest\:\:getTestResponse\(\) has parameter \$headers with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/TestResponseTest.php - - - - message: '#^Method CodeIgniter\\Test\\TestResponseTest\:\:getTestResponse\(\) has parameter \$responseOptions with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Test/TestResponseTest.php - - - - message: '#^Method CodeIgniter\\Test\\TestResponseTest\:\:provideHttpStatusCodes\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Test/TestResponseTest.php - - message: '#^Method CodeIgniter\\Throttle\\ThrottleTest\:\:provideTokenTimeCalculationUCs\(\) return type has no value type specified in iterable type iterable\.$#' count: 1 From 590d8ebd3b15456c926fa19fdaa6e5ecdbddc4e2 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Mon, 24 Aug 2026 15:17:04 +0800 Subject: [PATCH 074/163] refactor: widen `Response` body and `Formatter` data typing to `mixed` (#10480) --- system/Format/FormatterInterface.php | 2 +- system/Format/JSONFormatter.php | 2 +- system/Format/XMLFormatter.php | 2 +- system/HTTP/ResponseInterface.php | 4 ++-- system/HTTP/ResponseTrait.php | 8 ++++---- tests/system/Test/TestResponseTest.php | 4 ++-- user_guide_src/source/changelogs/v4.7.5.rst | 2 ++ 7 files changed, 13 insertions(+), 11 deletions(-) diff --git a/system/Format/FormatterInterface.php b/system/Format/FormatterInterface.php index 0c2492b90a77..5f6d6df3870a 100644 --- a/system/Format/FormatterInterface.php +++ b/system/Format/FormatterInterface.php @@ -21,7 +21,7 @@ interface FormatterInterface /** * Takes the given data and formats it. * - * @param array|object|string $data + * @param mixed $data * * @return false|non-empty-string */ diff --git a/system/Format/JSONFormatter.php b/system/Format/JSONFormatter.php index ac237fcc7af3..fb882a0e4eac 100644 --- a/system/Format/JSONFormatter.php +++ b/system/Format/JSONFormatter.php @@ -26,7 +26,7 @@ class JSONFormatter implements FormatterInterface /** * Takes the given data and formats it. * - * @param array|object|string $data + * @param mixed $data * * @return false|non-empty-string */ diff --git a/system/Format/XMLFormatter.php b/system/Format/XMLFormatter.php index efef70cec2cb..0f058e8a7ff7 100644 --- a/system/Format/XMLFormatter.php +++ b/system/Format/XMLFormatter.php @@ -27,7 +27,7 @@ class XMLFormatter implements FormatterInterface /** * Takes the given data and formats it. * - * @param array|object|string $data + * @param mixed $data * * @return false|non-empty-string */ diff --git a/system/HTTP/ResponseInterface.php b/system/HTTP/ResponseInterface.php index 258cbcf3c452..a1fbcb4acc06 100644 --- a/system/HTTP/ResponseInterface.php +++ b/system/HTTP/ResponseInterface.php @@ -200,7 +200,7 @@ public function setContentType(string $mime, string $charset = 'UTF-8'); /** * Converts the $body into JSON and sets the Content Type header. * - * @param array|object|string $body + * @param mixed $body * * @return $this */ @@ -218,7 +218,7 @@ public function getJSON(); /** * Converts $body into XML, and sets the correct Content-Type. * - * @param array|string $body + * @param mixed $body * * @return $this */ diff --git a/system/HTTP/ResponseTrait.php b/system/HTTP/ResponseTrait.php index 95e58b01a05d..50a8ab8c319e 100644 --- a/system/HTTP/ResponseTrait.php +++ b/system/HTTP/ResponseTrait.php @@ -169,7 +169,7 @@ public function setContentType(string $mime, string $charset = 'UTF-8') /** * Converts the $body into JSON and sets the Content Type header. * - * @param array|object|string $body + * @param mixed $body * * @return $this */ @@ -201,7 +201,7 @@ public function getJSON() /** * Converts $body into XML, and sets the correct Content-Type. * - * @param array|string $body + * @param mixed $body * * @return $this */ @@ -234,8 +234,8 @@ public function getXML() * Handles conversion of the data into the appropriate format, * and sets the correct Content-Type header for our response. * - * @param array|object|string $body - * @param string $format Valid: json, xml + * @param mixed $body + * @param string $format Valid: json, xml * * @return false|string * diff --git a/tests/system/Test/TestResponseTest.php b/tests/system/Test/TestResponseTest.php index 2ec981e786e5..c79104e43e72 100644 --- a/tests/system/Test/TestResponseTest.php +++ b/tests/system/Test/TestResponseTest.php @@ -314,7 +314,7 @@ public function testGetJSONEmptyJSON(): void public function testGetJSONFalseJSON(): void { $this->getTestResponse('

Hello World

'); - $this->response->setJSON(false, true); // @phpstan-ignore argument.type (Needed for testing) + $this->response->setJSON(false, true); // this should be FALSE - json_encode(false) $this->assertSame('false', $this->testResponse->getJSON()); @@ -323,7 +323,7 @@ public function testGetJSONFalseJSON(): void public function testGetJSONTrueJSON(): void { $this->getTestResponse('

Hello World

'); - $this->response->setJSON(true, true); // @phpstan-ignore argument.type (Needed for testing) + $this->response->setJSON(true, true); // this should be TRUE - json_encode(true) $this->assertSame('true', $this->testResponse->getJSON()); diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index b5f11bc83f79..787d67e7de9a 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -22,6 +22,8 @@ Message Changes Changes ******* +- **Response:** ``setJSON()`` and ``setXML()``, and the ``FormatterInterface::format()`` contract implemented by ``JSONFormatter`` and ``XMLFormatter``, now type their body/data parameter as ``mixed`` instead of ``array|object|string``, matching what they already accepted at runtime (e.g., scalars and ``bool``). + ************ Deprecations ************ From e650a714bc9482722dd107321d711d27162bc3d0 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Tue, 25 Aug 2026 21:12:41 +0800 Subject: [PATCH 075/163] refactor: fix remaining errors in `Helpers`' tests (#10481) --- system/Helpers/number_helper.php | 6 +- .../Array/ArrayHelperDotKeyExistsTest.php | 3 + .../Array/ArrayHelperRecursiveDiffTest.php | 3 + .../ArrayHelperSortValuesByNaturalTest.php | 7 + tests/system/Helpers/ArrayHelperTest.php | 49 +++- tests/system/Helpers/CookieHelperTest.php | 8 +- tests/system/Helpers/FilesystemHelperTest.php | 8 +- tests/system/Helpers/HTMLHelperTest.php | 3 + tests/system/Helpers/InflectorHelperTest.php | 3 + .../Helpers/URLHelper/CurrentUrlTest.php | 5 +- .../system/Helpers/URLHelper/MiscUrlTest.php | 44 +++- .../Helpers/URLHelper/SiteUrlCliTest.php | 3 + .../system/Helpers/URLHelper/SiteUrlTest.php | 5 +- utils/phpstan-baseline/argument.type.neon | 7 +- .../phpstan-baseline/assign.propertyType.neon | 7 +- utils/phpstan-baseline/loader.neon | 2 +- utils/phpstan-baseline/method.notFound.neon | 7 +- .../missingType.iterableValue.neon | 232 +----------------- .../missingType.parameter.neon | 17 +- 19 files changed, 139 insertions(+), 280 deletions(-) diff --git a/system/Helpers/number_helper.php b/system/Helpers/number_helper.php index 120e33d795cd..a737428706bd 100644 --- a/system/Helpers/number_helper.php +++ b/system/Helpers/number_helper.php @@ -19,16 +19,16 @@ /** * Formats a numbers as bytes, based on size, and adds the appropriate suffix * - * @param int|string $num Will be cast as int + * @param float|int|string $num Will be cast as int * @param non-empty-string|null $locale [optional] * * @return bool|string */ function number_to_size($num, int $precision = 1, ?string $locale = null) { - // Strip any formatting & ensure numeric input try { - // @phpstan-ignore-next-line + // Strip any formatting & ensure numeric input + // @phpstan-ignore binaryOp.invalid $num = 0 + str_replace(',', '', (string) $num); } catch (ErrorException) { // Catch "Warning: A non-numeric value encountered" diff --git a/tests/system/Helpers/Array/ArrayHelperDotKeyExistsTest.php b/tests/system/Helpers/Array/ArrayHelperDotKeyExistsTest.php index 0641c41b2bd0..432fafcf1a0c 100644 --- a/tests/system/Helpers/Array/ArrayHelperDotKeyExistsTest.php +++ b/tests/system/Helpers/Array/ArrayHelperDotKeyExistsTest.php @@ -23,6 +23,9 @@ #[Group('Others')] final class ArrayHelperDotKeyExistsTest extends CIUnitTestCase { + /** + * @var array>>> + */ private array $array = [ 'contacts' => [ 'friends' => [ diff --git a/tests/system/Helpers/Array/ArrayHelperRecursiveDiffTest.php b/tests/system/Helpers/Array/ArrayHelperRecursiveDiffTest.php index 2bb1c594def1..9c3e6f943604 100644 --- a/tests/system/Helpers/Array/ArrayHelperRecursiveDiffTest.php +++ b/tests/system/Helpers/Array/ArrayHelperRecursiveDiffTest.php @@ -22,6 +22,9 @@ #[Group('Others')] final class ArrayHelperRecursiveDiffTest extends CIUnitTestCase { + /** + * @var array + */ private array $compareWith; protected function setUp(): void diff --git a/tests/system/Helpers/Array/ArrayHelperSortValuesByNaturalTest.php b/tests/system/Helpers/Array/ArrayHelperSortValuesByNaturalTest.php index 4529b4da6918..e3c38c9030fa 100644 --- a/tests/system/Helpers/Array/ArrayHelperSortValuesByNaturalTest.php +++ b/tests/system/Helpers/Array/ArrayHelperSortValuesByNaturalTest.php @@ -22,6 +22,9 @@ #[Group('Others')] final class ArrayHelperSortValuesByNaturalTest extends CIUnitTestCase { + /** + * @var list + */ private array $arrayWithStringValues = [ 'apple10', 'banana', @@ -37,6 +40,10 @@ final class ArrayHelperSortValuesByNaturalTest extends CIUnitTestCase 'Яблоко', 'apple', ]; + + /** + * @var list> + */ private array $arrayWithArrayValues = [ ['apple', 'Banana'], ['apple10', 'Apple'], diff --git a/tests/system/Helpers/ArrayHelperTest.php b/tests/system/Helpers/ArrayHelperTest.php index 0293cc6c826e..e91d8e56cc8e 100644 --- a/tests/system/Helpers/ArrayHelperTest.php +++ b/tests/system/Helpers/ArrayHelperTest.php @@ -211,11 +211,10 @@ public function testArrayDotIgnoresLastWildcard(): void } /** - * @param int|string $key - * @param array|string|null $expected + * @param array|string|null $expected */ #[DataProvider('provideArrayDeepSearch')] - public function testArrayDeepSearch($key, $expected): void + public function testArrayDeepSearch(int|string $key, array|string|null $expected): void { $data = [ 'key1' => 'Value 1', @@ -241,6 +240,9 @@ public function testArrayDeepSearch($key, $expected): void $this->assertSame($expected, $result); } + /** + * @return iterable|string|null}> + */ public static function provideArrayDeepSearch(): iterable { return [ @@ -274,6 +276,11 @@ public function testArrayDeepSearchReturnNullEmptyArray(): void $this->assertNull(array_deep_search('key644', $data)); } + /** + * @param list $data + * @param array $sortColumns + * @param list $expected + */ #[DataProvider('provideSortByMultipleKeys')] public function testArraySortByMultipleKeysWithArray(array $data, array $sortColumns, array $expected): void { @@ -283,6 +290,11 @@ public function testArraySortByMultipleKeysWithArray(array $data, array $sortCol $this->assertSame($expected, array_column($data, 'name')); } + /** + * @param list $data + * @param array $sortColumns + * @param list $expected + */ #[DataProvider('provideSortByMultipleKeys')] public function testArraySortByMultipleKeysWithObjects(array $data, array $sortColumns, array $expected): void { @@ -297,6 +309,11 @@ public function testArraySortByMultipleKeysWithObjects(array $data, array $sortC $this->assertSame($expected, array_column($data, 'name')); } + /** + * @param list $data + * @param array $sortColumns + * @param list $expected + */ #[DataProvider('provideSortByMultipleKeys')] public function testArraySortByMultipleKeysFailsEmptyParameter(array $data, array $sortColumns, array $expected): void { @@ -334,6 +351,9 @@ public function testArraySortByMultipleKeysFailsInconsistentArraySizes($data): v array_sort_by_multiple_keys($data, $sortColumns); } + /** + * @return iterable, array, list}> + */ public static function provideSortByMultipleKeys(): iterable { $seed = [ @@ -387,12 +407,19 @@ public static function provideSortByMultipleKeys(): iterable ]; } + /** + * @param array $input + * @param array $expected + */ #[DataProvider('provideArrayFlattening')] public function testArrayFlattening(array $input, array $expected): void { $this->assertSame($expected, array_flatten_with_dots($input)); } + /** + * @return iterable, array}> + */ public static function provideArrayFlattening(): iterable { yield 'normal' => [ @@ -485,6 +512,11 @@ public static function provideArrayFlattening(): iterable ]; } + /** + * @param list $indexes + * @param array $data + * @param array $expected + */ #[DataProvider('provideArrayGroupByIncludeEmpty')] public function testArrayGroupByIncludeEmpty(array $indexes, array $data, array $expected): void { @@ -493,6 +525,9 @@ public function testArrayGroupByIncludeEmpty(array $indexes, array $data, array $this->assertSame($expected, $actual, 'array including empty not the same'); } + /** + * @return iterable, array, array}> + */ public static function provideArrayGroupByIncludeEmpty(): iterable { yield 'simple group-by test' => [ @@ -904,6 +939,11 @@ public static function provideArrayGroupByIncludeEmpty(): iterable ]; } + /** + * @param list $indexes + * @param array $data + * @param array $expected + */ #[DataProvider('provideArrayGroupByExcludeEmpty')] public function testArrayGroupByExcludeEmpty(array $indexes, array $data, array $expected): void { @@ -912,6 +952,9 @@ public function testArrayGroupByExcludeEmpty(array $indexes, array $data, array $this->assertSame($expected, $actual, 'array excluding empty not the same'); } + /** + * @return iterable, array, array}> + */ public static function provideArrayGroupByExcludeEmpty(): iterable { yield 'simple group-by test' => [ diff --git a/tests/system/Helpers/CookieHelperTest.php b/tests/system/Helpers/CookieHelperTest.php index 9b79e8d61dbc..f853d8ec7820 100644 --- a/tests/system/Helpers/CookieHelperTest.php +++ b/tests/system/Helpers/CookieHelperTest.php @@ -51,9 +51,11 @@ protected function setUp(): void $this->value = 'hello world'; $this->expire = 9999; - Services::injectMock('response', new MockResponse(new App())); - $this->response = service('response'); - $request = new IncomingRequest(new App(), new SiteURI(new App()), null, new UserAgent()); + $response = new MockResponse(new App()); + Services::injectMock('response', $response); + $this->response = $response; + + $request = new IncomingRequest(new App(), new SiteURI(new App()), null, new UserAgent()); Services::injectMock('request', $request); helper('cookie'); diff --git a/tests/system/Helpers/FilesystemHelperTest.php b/tests/system/Helpers/FilesystemHelperTest.php index 9d4bf74eb4e5..f264864ebf9a 100644 --- a/tests/system/Helpers/FilesystemHelperTest.php +++ b/tests/system/Helpers/FilesystemHelperTest.php @@ -188,13 +188,17 @@ public function testDirectoryMirrorSkipExistingFolder(): void // skips the existing folder directory_mirror($root . 'src', $root . 'dest'); - $structure = vfsStream::inspect(new vfsStreamStructureVisitor())->getStructure(); + $visitor = vfsStream::inspect(new vfsStreamStructureVisitor()); + $this->assertInstanceOf(vfsStreamStructureVisitor::class, $visitor); + $structure = $visitor->getStructure(); $this->assertSame([], $structure['root']['dest']['AnEmptyFolder']); // skips the existing folder (the same as overwrite = true) directory_mirror($root . 'src', $root . 'dest', false); - $structure = vfsStream::inspect(new vfsStreamStructureVisitor())->getStructure(); + $visitor = vfsStream::inspect(new vfsStreamStructureVisitor()); + $this->assertInstanceOf(vfsStreamStructureVisitor::class, $visitor); + $structure = $visitor->getStructure(); $this->assertSame([], $structure['root']['dest']['AnEmptyFolder']); } diff --git a/tests/system/Helpers/HTMLHelperTest.php b/tests/system/Helpers/HTMLHelperTest.php index dcce366474b9..787b100bbf2b 100644 --- a/tests/system/Helpers/HTMLHelperTest.php +++ b/tests/system/Helpers/HTMLHelperTest.php @@ -26,6 +26,9 @@ #[Group('Others')] final class HTMLHelperTest extends CIUnitTestCase { + /** + * @var list + */ private array $tracks; /** diff --git a/tests/system/Helpers/InflectorHelperTest.php b/tests/system/Helpers/InflectorHelperTest.php index 59068b8bf751..3d3079ec0880 100644 --- a/tests/system/Helpers/InflectorHelperTest.php +++ b/tests/system/Helpers/InflectorHelperTest.php @@ -252,6 +252,9 @@ public function testOrdinal(string $suffix, int $number): void $this->assertSame($suffix, ordinal($number)); } + /** + * @return iterable + */ public static function provideOrdinal(): iterable { return [ diff --git a/tests/system/Helpers/URLHelper/CurrentUrlTest.php b/tests/system/Helpers/URLHelper/CurrentUrlTest.php index 54ac56bb6a54..6058ba1698d2 100644 --- a/tests/system/Helpers/URLHelper/CurrentUrlTest.php +++ b/tests/system/Helpers/URLHelper/CurrentUrlTest.php @@ -89,7 +89,7 @@ public function testCurrentURLReturnsAllowedHostname(): void $this->assertSame('http://www.example.jp/public/index.php/', current_url()); } - private function createRequest(?App $config = null, $body = null, ?string $path = null): void + private function createRequest(?App $config = null, ?string $body = null, ?string $path = null): void { $config ??= new App(); @@ -281,6 +281,9 @@ public function testUrlIsWithSubfolder(string $currentPath, string $testPath, bo $this->assertSame($expected, url_is($testPath)); } + /** + * @return iterable + */ public static function provideUrlIs(): iterable { return [ diff --git a/tests/system/Helpers/URLHelper/MiscUrlTest.php b/tests/system/Helpers/URLHelper/MiscUrlTest.php index 422b2e01cea6..c8cefbda9c8b 100644 --- a/tests/system/Helpers/URLHelper/MiscUrlTest.php +++ b/tests/system/Helpers/URLHelper/MiscUrlTest.php @@ -146,6 +146,9 @@ public function testAnchor($expected = '', $uri = '', $title = '', $attributes = // Test anchor + /** + * @return iterable> + */ public static function provideAnchor(): iterable { return [ @@ -206,6 +209,9 @@ public function testAnchorNoindex($expected = '', $uri = '', $title = '', $attri $this->assertSame($expected, anchor($uri, $title, $attributes, $this->config)); } + /** + * @return iterable> + */ public static function provideAnchorNoindex(): iterable { return [ @@ -272,6 +278,9 @@ public function testAnchorTargetted($expected = '', $uri = '', $title = '', $att $this->assertSame($expected, anchor($uri, $title, $attributes, $this->config)); } + /** + * @return iterable> + */ public static function provideAnchorTargetted(): iterable { return [ @@ -326,6 +335,9 @@ public function testAnchorExamples($expected = '', $uri = '', $title = '', $attr $this->assertSame($expected, anchor($uri, $title, $attributes, $this->config)); } + /** + * @return iterable> + */ public static function provideAnchorExamples(): iterable { return [ @@ -371,6 +383,9 @@ public function testAnchorPopup($expected = '', $uri = '', $title = '', $attribu // Test anchor_popup + /** + * @return iterable> + */ public static function provideAnchorPopup(): iterable { return [ @@ -428,6 +443,9 @@ public function testMailto($expected = '', $email = '', $title = '', $attributes // Test mailto + /** + * @return iterable> + */ public static function provideMailto(): iterable { return [ @@ -466,6 +484,9 @@ public function testSafeMailto($expected = '', $email = '', $title = '', $attrib // Test safe_mailto + /** + * @return iterable> + */ public static function provideSafeMailto(): iterable { return [ @@ -509,6 +530,9 @@ public function testAutoLinkUrl($in, $out): void // Test auto_link + /** + * @return iterable + */ public static function provideAutoLinkUrl(): iterable { return [ @@ -561,6 +585,9 @@ public function testAutoLinkEmail($in, $out): void $this->assertSame($out, auto_link($in, 'email')); } + /** + * @return iterable + */ public static function provideAutoLinkEmail(): iterable { return [ @@ -609,6 +636,9 @@ public function testAutolinkBoth($in, $out): void $this->assertSame($out, auto_link($in)); } + /** + * @return iterable + */ public static function provideAutolinkBoth(): iterable { return [ @@ -661,6 +691,9 @@ public function testAutoLinkPopup($in, $out): void $this->assertSame($out, auto_link($in, 'url', true)); } + /** + * @return iterable + */ public static function provideAutoLinkPopup(): iterable { return [ @@ -707,6 +740,9 @@ public function testPrepUrl(string $input, string $expected, bool $secure): void // Test prep_url + /** + * @return iterable + */ public static function providePrepUrl(): iterable { // input, expected, secure @@ -843,7 +879,7 @@ public function testMbUrlTitleExtraDashes(): void } #[DataProvider('provideUrlTo')] - public function testUrlTo(string $expected, string $input, ...$args): void + public function testUrlTo(string $expected, string $input, int|string ...$args): void { service('superglobals')->setServer('HTTP_HOST', 'example.com'); @@ -856,6 +892,9 @@ public function testUrlTo(string $expected, string $input, ...$args): void $this->assertSame($expected, url_to($input, ...$args)); } + /** + * @return iterable + */ public static function provideUrlTo(): iterable { $page = config('App')->indexPage !== '' ? config('App')->indexPage . '/' : ''; @@ -884,6 +923,9 @@ public function testUrlToThrowsOnEmptyOrMissingRoute(string $route): void url_to($route); } + /** + * @return iterable + */ public static function provideUrlToThrowsOnEmptyOrMissingRoute(): iterable { return [ diff --git a/tests/system/Helpers/URLHelper/SiteUrlCliTest.php b/tests/system/Helpers/URLHelper/SiteUrlCliTest.php index ad99f46a8b2a..0d9d9b114dd8 100644 --- a/tests/system/Helpers/URLHelper/SiteUrlCliTest.php +++ b/tests/system/Helpers/URLHelper/SiteUrlCliTest.php @@ -94,6 +94,9 @@ public function testUrls( $this->assertSame($expectedBaseUrl, base_url($path, $scheme)); } + /** + * @return iterable + */ public static function provideUrls(): iterable { // baseURL, indexPage, scheme, secure, path, expectedSiteUrl, expectedBaseUrl diff --git a/tests/system/Helpers/URLHelper/SiteUrlTest.php b/tests/system/Helpers/URLHelper/SiteUrlTest.php index c8a07758e3bd..a095cf703a86 100644 --- a/tests/system/Helpers/URLHelper/SiteUrlTest.php +++ b/tests/system/Helpers/URLHelper/SiteUrlTest.php @@ -55,7 +55,7 @@ protected function tearDown(): void service('superglobals')->setServerArray([]); } - private function createRequest(?App $config = null, $body = null, ?string $path = null): void + private function createRequest(?App $config = null, ?string $body = null, ?string $path = null): void { $config ??= new App(); @@ -105,6 +105,9 @@ public function testUrls( $this->assertSame($expectedBaseUrl, base_url($path, $scheme)); } + /** + * @return iterable + */ public static function provideUrls(): iterable { // baseURL, indexPage, scheme, secure, path, expectedSiteUrl, expectedBaseUrl diff --git a/utils/phpstan-baseline/argument.type.neon b/utils/phpstan-baseline/argument.type.neon index 4451b62f6419..bd5919d3f762 100644 --- a/utils/phpstan-baseline/argument.type.neon +++ b/utils/phpstan-baseline/argument.type.neon @@ -1,4 +1,4 @@ -# total 25 errors +# total 24 errors parameters: ignoreErrors: @@ -52,11 +52,6 @@ parameters: count: 2 path: ../../tests/system/Database/Live/UpsertTest.php - - - message: '#^Parameter \#1 \$num of function number_to_size expects int\|string, float given\.$#' - count: 1 - path: ../../tests/system/Helpers/NumberHelperTest.php - - message: '#^Parameter \#2 \$message of method CodeIgniter\\Log\\Handlers\\ChromeLoggerHandler\:\:handle\(\) expects string, stdClass given\.$#' count: 1 diff --git a/utils/phpstan-baseline/assign.propertyType.neon b/utils/phpstan-baseline/assign.propertyType.neon index a2a5ca34cc63..f783f8da680e 100644 --- a/utils/phpstan-baseline/assign.propertyType.neon +++ b/utils/phpstan-baseline/assign.propertyType.neon @@ -1,4 +1,4 @@ -# total 20 errors +# total 19 errors parameters: ignoreErrors: @@ -42,11 +42,6 @@ parameters: count: 4 path: ../../tests/system/Filters/HoneypotTest.php - - - message: '#^Property CodeIgniter\\Helpers\\CookieHelperTest\:\:\$response \(CodeIgniter\\HTTP\\Response\) does not accept CodeIgniter\\HTTP\\ResponseInterface\.$#' - count: 1 - path: ../../tests/system/Helpers/CookieHelperTest.php - - message: '#^Property CodeIgniter\\Honeypot\\HoneypotTest\:\:\$response \(CodeIgniter\\HTTP\\Response\) does not accept CodeIgniter\\HTTP\\RequestInterface\|CodeIgniter\\HTTP\\ResponseInterface\|string\|null\.$#' count: 1 diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 61ebc04982b4..9954e15ad7a3 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 419 errors +# total 366 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/method.notFound.neon b/utils/phpstan-baseline/method.notFound.neon index 67fac4e02b49..9443f439a180 100644 --- a/utils/phpstan-baseline/method.notFound.neon +++ b/utils/phpstan-baseline/method.notFound.neon @@ -1,4 +1,4 @@ -# total 20 errors +# total 18 errors parameters: ignoreErrors: @@ -12,11 +12,6 @@ parameters: count: 2 path: ../../tests/system/Commands/Utilities/Routes/FilterFinderTest.php - - - message: '#^Call to an undefined method org\\bovigo\\vfs\\visitor\\vfsStreamVisitor\:\:getStructure\(\)\.$#' - count: 2 - path: ../../tests/system/Helpers/FilesystemHelperTest.php - - message: '#^Call to an undefined method CodeIgniter\\Images\\Handlers\\BaseHandler\:\:getPathname\(\)\.$#' count: 1 diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 333eaa92ae2e..a91013e3788b 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 282 errors +# total 236 errors parameters: ignoreErrors: @@ -1022,236 +1022,6 @@ parameters: count: 1 path: ../../tests/system/Filters/InvalidCharsTest.php - - - message: '#^Property CodeIgniter\\Helpers\\Array\\ArrayHelperDotKeyExistsTest\:\:\$array type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Helpers/Array/ArrayHelperDotKeyExistsTest.php - - - - message: '#^Property CodeIgniter\\Helpers\\Array\\ArrayHelperRecursiveDiffTest\:\:\$compareWith type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Helpers/Array/ArrayHelperRecursiveDiffTest.php - - - - message: '#^Property CodeIgniter\\Helpers\\Array\\ArrayHelperSortValuesByNaturalTest\:\:\$arrayWithArrayValues type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Helpers/Array/ArrayHelperSortValuesByNaturalTest.php - - - - message: '#^Property CodeIgniter\\Helpers\\Array\\ArrayHelperSortValuesByNaturalTest\:\:\$arrayWithStringValues type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Helpers/Array/ArrayHelperSortValuesByNaturalTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\ArrayHelperTest\:\:provideArrayDeepSearch\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Helpers/ArrayHelperTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\ArrayHelperTest\:\:provideArrayFlattening\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Helpers/ArrayHelperTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\ArrayHelperTest\:\:provideArrayGroupByExcludeEmpty\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Helpers/ArrayHelperTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\ArrayHelperTest\:\:provideArrayGroupByIncludeEmpty\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Helpers/ArrayHelperTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\ArrayHelperTest\:\:provideSortByMultipleKeys\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Helpers/ArrayHelperTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\ArrayHelperTest\:\:testArrayDeepSearch\(\) has parameter \$expected with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Helpers/ArrayHelperTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\ArrayHelperTest\:\:testArrayFlattening\(\) has parameter \$expected with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Helpers/ArrayHelperTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\ArrayHelperTest\:\:testArrayFlattening\(\) has parameter \$input with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Helpers/ArrayHelperTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\ArrayHelperTest\:\:testArrayGroupByExcludeEmpty\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Helpers/ArrayHelperTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\ArrayHelperTest\:\:testArrayGroupByExcludeEmpty\(\) has parameter \$expected with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Helpers/ArrayHelperTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\ArrayHelperTest\:\:testArrayGroupByExcludeEmpty\(\) has parameter \$indexes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Helpers/ArrayHelperTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\ArrayHelperTest\:\:testArrayGroupByIncludeEmpty\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Helpers/ArrayHelperTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\ArrayHelperTest\:\:testArrayGroupByIncludeEmpty\(\) has parameter \$expected with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Helpers/ArrayHelperTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\ArrayHelperTest\:\:testArrayGroupByIncludeEmpty\(\) has parameter \$indexes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Helpers/ArrayHelperTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\ArrayHelperTest\:\:testArraySortByMultipleKeysFailsEmptyParameter\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Helpers/ArrayHelperTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\ArrayHelperTest\:\:testArraySortByMultipleKeysFailsEmptyParameter\(\) has parameter \$expected with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Helpers/ArrayHelperTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\ArrayHelperTest\:\:testArraySortByMultipleKeysFailsEmptyParameter\(\) has parameter \$sortColumns with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Helpers/ArrayHelperTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\ArrayHelperTest\:\:testArraySortByMultipleKeysWithArray\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Helpers/ArrayHelperTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\ArrayHelperTest\:\:testArraySortByMultipleKeysWithArray\(\) has parameter \$expected with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Helpers/ArrayHelperTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\ArrayHelperTest\:\:testArraySortByMultipleKeysWithArray\(\) has parameter \$sortColumns with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Helpers/ArrayHelperTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\ArrayHelperTest\:\:testArraySortByMultipleKeysWithObjects\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Helpers/ArrayHelperTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\ArrayHelperTest\:\:testArraySortByMultipleKeysWithObjects\(\) has parameter \$expected with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Helpers/ArrayHelperTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\ArrayHelperTest\:\:testArraySortByMultipleKeysWithObjects\(\) has parameter \$sortColumns with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Helpers/ArrayHelperTest.php - - - - message: '#^Property CodeIgniter\\Helpers\\HTMLHelperTest\:\:\$tracks type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Helpers/HTMLHelperTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\InflectorHelperTest\:\:provideOrdinal\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Helpers/InflectorHelperTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\URLHelper\\CurrentUrlTest\:\:provideUrlIs\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Helpers/URLHelper/CurrentUrlTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\URLHelper\\MiscUrlTest\:\:provideAnchorExamples\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Helpers/URLHelper/MiscUrlTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\URLHelper\\MiscUrlTest\:\:provideAnchorNoindex\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Helpers/URLHelper/MiscUrlTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\URLHelper\\MiscUrlTest\:\:provideAnchorPopup\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Helpers/URLHelper/MiscUrlTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\URLHelper\\MiscUrlTest\:\:provideAnchorTargetted\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Helpers/URLHelper/MiscUrlTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\URLHelper\\MiscUrlTest\:\:provideAnchor\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Helpers/URLHelper/MiscUrlTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\URLHelper\\MiscUrlTest\:\:provideAutoLinkEmail\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Helpers/URLHelper/MiscUrlTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\URLHelper\\MiscUrlTest\:\:provideAutoLinkPopup\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Helpers/URLHelper/MiscUrlTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\URLHelper\\MiscUrlTest\:\:provideAutoLinkUrl\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Helpers/URLHelper/MiscUrlTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\URLHelper\\MiscUrlTest\:\:provideAutolinkBoth\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Helpers/URLHelper/MiscUrlTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\URLHelper\\MiscUrlTest\:\:provideMailto\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Helpers/URLHelper/MiscUrlTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\URLHelper\\MiscUrlTest\:\:providePrepUrl\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Helpers/URLHelper/MiscUrlTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\URLHelper\\MiscUrlTest\:\:provideSafeMailto\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Helpers/URLHelper/MiscUrlTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\URLHelper\\MiscUrlTest\:\:provideUrlToThrowsOnEmptyOrMissingRoute\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Helpers/URLHelper/MiscUrlTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\URLHelper\\MiscUrlTest\:\:provideUrlTo\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Helpers/URLHelper/MiscUrlTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\URLHelper\\SiteUrlCliTest\:\:provideUrls\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Helpers/URLHelper/SiteUrlCliTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\URLHelper\\SiteUrlTest\:\:provideUrls\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Helpers/URLHelper/SiteUrlTest.php - - message: '#^Method CodeIgniter\\I18n\\TimeLegacyTest\:\:provideToStringDoesNotDependOnLocale\(\) return type has no value type specified in iterable type iterable\.$#' count: 1 diff --git a/utils/phpstan-baseline/missingType.parameter.neon b/utils/phpstan-baseline/missingType.parameter.neon index 54d2e4a72c64..87eecd560077 100644 --- a/utils/phpstan-baseline/missingType.parameter.neon +++ b/utils/phpstan-baseline/missingType.parameter.neon @@ -1,4 +1,4 @@ -# total 12 errors +# total 9 errors parameters: ignoreErrors: @@ -37,21 +37,6 @@ parameters: count: 1 path: ../../tests/system/Filters/FiltersTest.php - - - message: '#^Method CodeIgniter\\Helpers\\URLHelper\\CurrentUrlTest\:\:createRequest\(\) has parameter \$body with no type specified\.$#' - count: 1 - path: ../../tests/system/Helpers/URLHelper/CurrentUrlTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\URLHelper\\MiscUrlTest\:\:testUrlTo\(\) has parameter \$args with no type specified\.$#' - count: 1 - path: ../../tests/system/Helpers/URLHelper/MiscUrlTest.php - - - - message: '#^Method CodeIgniter\\Helpers\\URLHelper\\SiteUrlTest\:\:createRequest\(\) has parameter \$body with no type specified\.$#' - count: 1 - path: ../../tests/system/Helpers/URLHelper/SiteUrlTest.php - - message: '#^Method CodeIgniter\\Security\\SecurityCSRFSessionRandomizeTokenTest\:\:createSession\(\) has parameter \$options with no type specified\.$#' count: 1 From af6a478465b6b0c2879cd285b4bc14316c3a7d16 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Tue, 25 Aug 2026 23:03:17 +0800 Subject: [PATCH 076/163] refactor: fix phpstan errors in `Config` (#10482) --- system/Config/BaseConfig.php | 5 +- system/Config/BaseService.php | 6 +- system/Config/DotEnv.php | 4 + system/Config/Factories.php | 33 ++-- system/Config/Factory.php | 4 +- system/Config/Services.php | 11 +- tests/system/Config/DotEnvTest.php | 3 + tests/system/Config/FactoriesTest.php | 40 +++-- tests/system/Config/MimesTest.php | 6 + tests/system/Config/ServicesTest.php | 5 +- utils/phpstan-baseline/argument.type.neon | 12 +- .../codeigniter.modelArgumentType.neon | 8 - utils/phpstan-baseline/loader.neon | 3 +- .../missingType.iterableValue.neon | 162 +----------------- .../missingType.parameter.neon | 7 +- .../staticMethod.notFound.neon | 22 +-- 16 files changed, 86 insertions(+), 245 deletions(-) delete mode 100644 utils/phpstan-baseline/codeigniter.modelArgumentType.neon diff --git a/system/Config/BaseConfig.php b/system/Config/BaseConfig.php index baa030ed7b5c..208797ce6d1c 100644 --- a/system/Config/BaseConfig.php +++ b/system/Config/BaseConfig.php @@ -37,7 +37,7 @@ class BaseConfig * An optional array of classes that will act as Registrars * for rapidly setting config class properties. * - * @var array + * @var list */ public static $registrars = []; @@ -70,6 +70,9 @@ class BaseConfig */ protected static $moduleConfig; + /** + * @param array $array + */ public static function __set_state(array $array) { static::$override = false; diff --git a/system/Config/BaseService.php b/system/Config/BaseService.php index d59332b392ef..e5f46586a305 100644 --- a/system/Config/BaseService.php +++ b/system/Config/BaseService.php @@ -134,7 +134,7 @@ * @method static Security security(App $config = null, $getShared = true) * @method static Session session(ConfigSession $config = null, $getShared = true) * @method static SiteURIFactory siteurifactory(App $config = null, Superglobals $superglobals = null, $getShared = true) - * @method static Superglobals superglobals(array $server = null, array $get = null, bool $getShared = true) + * @method static Superglobals superglobals(array $server = null, array $get = null, bool $getShared = true) * @method static Throttler throttler($getShared = true) * @method static Timer timer($getShared = true) * @method static Toolbar toolbar(ConfigToolbar $config = null, $getShared = true) @@ -178,7 +178,7 @@ class BaseService /** * A cache of other service classes we've found. * - * @var array + * @var array{} * * @deprecated 4.5.0 No longer used. */ @@ -317,6 +317,8 @@ public static function locator(bool $getShared = true) * Provides the ability to perform case-insensitive calling of service * names. * + * @param array $arguments + * * @return object|null */ public static function __callStatic(string $name, array $arguments) diff --git a/system/Config/DotEnv.php b/system/Config/DotEnv.php index 17c74d380c5c..a2f15e7c4850 100644 --- a/system/Config/DotEnv.php +++ b/system/Config/DotEnv.php @@ -51,6 +51,8 @@ public function load(): bool /** * Parse the .env file into an array of key => value + * + * @return array|null */ public function parse(): ?array { @@ -119,6 +121,8 @@ protected function setVariable(string $name, string $value = '') /** * Parses for assignment, cleans the $name and $value, and ensures * that nested variables are handled. + * + * @return array{string, string} */ public function normaliseVariable(string $name, string $value = ''): array { diff --git a/system/Config/Factories.php b/system/Config/Factories.php index ab5343d15fb8..98f15264a40f 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -27,7 +27,7 @@ * instantiation checks. * * @method static BaseConfig|null config(...$arguments) - * @method static Model|null models(string $alias, array $options = [], ?ConnectionInterface &$conn = null) + * @method static Model|null models(string $alias, array $options = [], ?ConnectionInterface &$conn = null) * @see \CodeIgniter\Config\FactoriesTest */ final class Factories @@ -127,6 +127,8 @@ public static function define(string $component, string $alias, string $classnam * Loads instances based on the method component name. Either * creates a new instance or returns an existing shared instance. * + * @param array $arguments + * * @return object|null */ public static function __callStatic(string $component, array $arguments) @@ -140,7 +142,7 @@ public static function __callStatic(string $component, array $arguments) // Determine the component-specific options $options = array_merge(self::getOptions($component), $options); - if (! $options['getShared']) { + if (! (bool) $options['getShared']) { if (isset(self::$aliases[$options['component']][$alias])) { $class = self::$aliases[$options['component']][$alias]; @@ -192,6 +194,9 @@ public static function get(string $component, string $alias): ?object /** * Gets the defined instance. If not exists, creates new one. * + * @param array $options + * @param array $arguments + * * @return object|null */ private static function getDefinedInstance(array $options, string $alias, array $arguments) @@ -230,6 +235,8 @@ private static function getDefinedInstance(array $options, string $alias, array /** * Creates the shared instance. + * + * @param array $arguments */ private static function createInstance(string $component, string $class, array $arguments): void { @@ -264,8 +271,8 @@ private static function isConfig(string $component): bool /** * Finds a component class * - * @param array $options The array of component-specific directives - * @param string $alias Class alias. See the $aliases property. + * @param array $options The array of component-specific directives + * @param string $alias Class alias. See the $aliases property. */ private static function locateClass(array $options, string $alias): ?string { @@ -341,8 +348,8 @@ private static function isNamespaced(string $alias): bool /** * Verifies that a class & config satisfy the "preferApp" option * - * @param array $options The array of component-specific directives - * @param string $alias Class alias. See the $aliases property. + * @param array $options The array of component-specific directives + * @param string $alias Class alias. See the $aliases property. */ private static function verifyPreferApp(array $options, string $alias): bool { @@ -362,8 +369,8 @@ private static function verifyPreferApp(array $options, string $alias): bool /** * Verifies that a class & config satisfy the "instanceOf" option * - * @param array $options The array of component-specific directives - * @param string $alias Class alias. See the $aliases property. + * @param array $options The array of component-specific directives + * @param string $alias Class alias. See the $aliases property. */ private static function verifyInstanceOf(array $options, string $alias): bool { @@ -408,8 +415,8 @@ public static function getOptions(string $component): array /** * Normalizes, stores, and returns the configuration for a specific component * - * @param string $component Lowercase, plural component name - * @param array $values option values + * @param string $component Lowercase, plural component name + * @param array $values Option values * * @return array The result after applying defaults and normalization */ @@ -541,6 +548,12 @@ public static function getComponentInstances(string $component): array /** * Sets component data * + * @param array{ + * options: array, + * aliases: array, + * instances: array, + * } $data + * * @internal For caching only */ public static function setComponentInstances(string $component, array $data): void diff --git a/system/Config/Factory.php b/system/Config/Factory.php index b25267791beb..9203af73ff09 100644 --- a/system/Config/Factory.php +++ b/system/Config/Factory.php @@ -28,7 +28,7 @@ class Factory extends BaseConfig * Supplies a default set of options to merge for * all unspecified factory components. * - * @var array + * @var array */ public static $default = [ 'component' => null, @@ -42,7 +42,7 @@ class Factory extends BaseConfig * Specifies that Models should always favor child * classes to allow easy extension of module Models. * - * @var array + * @var array */ public $models = [ 'preferApp' => true, diff --git a/system/Config/Services.php b/system/Config/Services.php index b465857905a1..f5a11ac19a80 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -198,6 +198,8 @@ public static function csp(?CSPConfig $config = null, bool $getShared = true) * The CURL Request class acts as a simple HTTP client for interacting * with other servers, typically through APIs. * + * @param array $options + * * @return CURLRequest */ public static function curlrequest(array $options = [], ?ResponseInterface $response = null, ?App $config = null, bool $getShared = true) @@ -220,7 +222,7 @@ public static function curlrequest(array $options = [], ?ResponseInterface $resp /** * The Email class allows you to send email via mail, sendmail, SMTP. * - * @param array|EmailConfig|null $config + * @param array|EmailConfig|null $config * * @return Email */ @@ -739,6 +741,13 @@ public static function siteurifactory( /** * Superglobals. * + * @param array|null $server + * @param array|null $get + * @param array|null $post + * @param array|null $cookie + * @param array|null $files + * @param array|null $request + * * @return Superglobals */ public static function superglobals( diff --git a/tests/system/Config/DotEnvTest.php b/tests/system/Config/DotEnvTest.php index 58e66d412e9e..732fbeac1cdc 100644 --- a/tests/system/Config/DotEnvTest.php +++ b/tests/system/Config/DotEnvTest.php @@ -73,6 +73,9 @@ public function testLoadsVars(string $expected, string $varname): void $this->assertSame($expected, getenv($varname)); } + /** + * @return iterable + */ public static function provideLoadsVars(): iterable { yield from [ diff --git a/tests/system/Config/FactoriesTest.php b/tests/system/Config/FactoriesTest.php index c87e4a135e2a..434211e638a5 100644 --- a/tests/system/Config/FactoriesTest.php +++ b/tests/system/Config/FactoriesTest.php @@ -41,7 +41,7 @@ protected function setUp(): void Factories::reset(); } - protected function getFactoriesStaticProperty(...$params): mixed + protected function getFactoriesStaticProperty(string ...$params): mixed { // First parameter is the actual property $name = array_shift($params); @@ -158,14 +158,14 @@ public function testGetsBasenameInvalid(): void public function testCreatesByBasename(): void { - $result = Factories::widgets('SomeWidget', ['getShared' => false]); + $result = Factories::widgets('SomeWidget', ['getShared' => false]); // @phpstan-ignore staticMethod.notFound $this->assertInstanceOf(SomeWidget::class, $result); } public function testCreatesByClassname(): void { - $result = Factories::widgets(SomeWidget::class, ['getShared' => false]); + $result = Factories::widgets(SomeWidget::class, ['getShared' => false]); // @phpstan-ignore staticMethod.notFound $this->assertInstanceOf(SomeWidget::class, $result); } @@ -179,22 +179,22 @@ public function testCreatesByAbsoluteClassname(): void public function testCreatesInvalid(): void { - $result = Factories::widgets('gfnusvjai', ['getShared' => false]); + $result = Factories::widgets('gfnusvjai', ['getShared' => false]); // @phpstan-ignore staticMethod.notFound $this->assertNull($result); } public function testIgnoresNonClass(): void { - $result = Factories::widgets('NopeWidget', ['getShared' => false]); + $result = Factories::widgets('NopeWidget', ['getShared' => false]); // @phpstan-ignore staticMethod.notFound $this->assertNull($result); } public function testReturnsSharedInstance(): void { - $widget1 = Factories::widgets('SomeWidget'); - $widget2 = Factories::widgets(SomeWidget::class); + $widget1 = Factories::widgets('SomeWidget'); // @phpstan-ignore staticMethod.notFound + $widget2 = Factories::widgets(SomeWidget::class); // @phpstan-ignore staticMethod.notFound $this->assertSame($widget1, $widget2); } @@ -203,7 +203,7 @@ public function testInjection(): void { Factories::injectMock('widgets', 'Banana', new stdClass()); - $result = Factories::widgets('Banana'); + $result = Factories::widgets('Banana'); // @phpstan-ignore staticMethod.notFound $this->assertInstanceOf('stdClass', $result); } @@ -212,7 +212,7 @@ public function testRespectsComponentAlias(): void { Factories::setOptions('tedwigs', ['component' => 'widgets']); - $result = Factories::tedwigs('SomeWidget'); + $result = Factories::tedwigs('SomeWidget'); // @phpstan-ignore staticMethod.notFound $this->assertInstanceOf(SomeWidget::class, $result); } @@ -228,10 +228,10 @@ public function testRespectsInstanceOf(): void { Factories::setOptions('widgets', ['instanceOf' => 'stdClass']); - $result = Factories::widgets('SomeWidget'); + $result = Factories::widgets('SomeWidget'); // @phpstan-ignore staticMethod.notFound $this->assertInstanceOf(SomeWidget::class, $result); - $result = Factories::widgets('OtherWidget'); + $result = Factories::widgets('OtherWidget'); // @phpstan-ignore staticMethod.notFound $this->assertNull($result); } @@ -239,7 +239,7 @@ public function testSharedRespectsInstanceOf(): void { Factories::injectMock('widgets', 'SomeWidget', new OtherWidget()); - $result = Factories::widgets('SomeWidget', ['instanceOf' => 'stdClass']); + $result = Factories::widgets('SomeWidget', ['instanceOf' => 'stdClass']); // @phpstan-ignore staticMethod.notFound $this->assertInstanceOf(SomeWidget::class, $result); } @@ -247,7 +247,7 @@ public function testPrioritizesParameterOptions(): void { Factories::setOptions('widgets', ['instanceOf' => 'stdClass']); - $result = Factories::widgets(OtherWidget::class, ['instanceOf' => null]); + $result = Factories::widgets(OtherWidget::class, ['instanceOf' => null]); // @phpstan-ignore staticMethod.notFound $this->assertInstanceOf(OtherWidget::class, $result); } @@ -259,7 +259,7 @@ public function testFindsAppFirst(): void class_alias(SomeWidget::class, $class); } - $result = Factories::widgets('OtherWidget'); + $result = Factories::widgets('OtherWidget'); // @phpstan-ignore staticMethod.notFound $this->assertInstanceOf(SomeWidget::class, $result); } @@ -277,6 +277,7 @@ class TestRegistrar $result = Factories::config('TestRegistrar'); + // @phpstan-ignore argument.type (Config\TestRegistrar is created at runtime by this test) $this->assertInstanceOf('Config\TestRegistrar', $result); // Delete the config class in App @@ -317,14 +318,14 @@ public function testPreferAppIsIgnored(): void class_alias(SomeWidget::class, $class); } - $result = Factories::widgets(OtherWidget::class); + $result = Factories::widgets(OtherWidget::class); // @phpstan-ignore staticMethod.notFound $this->assertInstanceOf(OtherWidget::class, $result); } public function testCanLoadTwoCellsWithSameShortName(): void { - $cell1 = Factories::cells('\\' . SampleClass::class); - $cell2 = Factories::cells('\\' . \Tests\Support\View\OtherCells\SampleClass::class); + $cell1 = Factories::cells('\\' . SampleClass::class); // @phpstan-ignore staticMethod.notFound + $cell2 = Factories::cells('\\' . \Tests\Support\View\OtherCells\SampleClass::class); // @phpstan-ignore staticMethod.notFound $this->assertNotSame($cell1, $cell2); } @@ -369,6 +370,7 @@ public function testDefineSameAliasAndSameClassTwice(): void UserModel::class, ); + // @phpstan-ignore codeigniter.modelArgumentType (aliased to Tests\Support\Models\UserModel above) $model = model('CodeIgniter\Shield\Models\UserModel'); $this->assertInstanceOf(UserModel::class, $model); @@ -382,7 +384,7 @@ public function testDefineNonExistentClass(): void Factories::define( 'models', 'CodeIgniter\Shield\Models\UserModel', - 'App\Models\UserModel', + 'App\Models\UserModel', // @phpstan-ignore argument.type (deliberately does not exist) ); } @@ -398,7 +400,7 @@ public function testDefineAfterLoading(): void Factories::define( 'models', UserModel::class, - 'App\Models\UserModel', + 'App\Models\UserModel', // @phpstan-ignore argument.type (deliberately does not exist) ); } diff --git a/tests/system/Config/MimesTest.php b/tests/system/Config/MimesTest.php index 0b22a45e5735..af6839c9da98 100644 --- a/tests/system/Config/MimesTest.php +++ b/tests/system/Config/MimesTest.php @@ -30,6 +30,9 @@ public function testGuessExtensionFromType(?string $expected, string $mime): voi $this->assertSame($expected, Mimes::guessExtensionFromType($mime)); } + /** + * @return iterable + */ public static function provideGuessExtensionFromType(): iterable { return [ @@ -62,6 +65,9 @@ public function testGuessTypeFromExtension(?string $expected, string $ext): void $this->assertSame($expected, Mimes::guessTypeFromExtension($ext)); } + /** + * @return iterable + */ public static function provideGuessTypeFromExtension(): iterable { return [ diff --git a/tests/system/Config/ServicesTest.php b/tests/system/Config/ServicesTest.php index 86228dfadd3f..66c9158e67ab 100644 --- a/tests/system/Config/ServicesTest.php +++ b/tests/system/Config/ServicesTest.php @@ -66,6 +66,9 @@ #[Group('SeparateProcess')] final class ServicesTest extends CIUnitTestCase { + /** + * @var array + */ private array $original; #[WithoutErrorHandler] @@ -459,7 +462,7 @@ public function testParser(): void public function testRedirectResponse(): void { - $result = Services::redirectResponse(); + $result = Services::redirectresponse(); $this->assertInstanceOf(RedirectResponse::class, $result); } diff --git a/utils/phpstan-baseline/argument.type.neon b/utils/phpstan-baseline/argument.type.neon index bd5919d3f762..3c70bd498fba 100644 --- a/utils/phpstan-baseline/argument.type.neon +++ b/utils/phpstan-baseline/argument.type.neon @@ -1,4 +1,4 @@ -# total 24 errors +# total 21 errors parameters: ignoreErrors: @@ -22,16 +22,6 @@ parameters: count: 1 path: ../../tests/system/CodeIgniterTest.php - - - message: '#^Parameter \#1 \$expected of method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) expects class\-string\, string given\.$#' - count: 1 - path: ../../tests/system/Config/FactoriesTest.php - - - - message: '#^Parameter \#3 \$classname of static method CodeIgniter\\Config\\Factories\:\:define\(\) expects class\-string, string given\.$#' - count: 2 - path: ../../tests/system/Config/FactoriesTest.php - - message: '#^Parameter \#1 \$db of class CodeIgniter\\Database\\SQLite3\\Table constructor expects CodeIgniter\\Database\\SQLite3\\Connection, CodeIgniter\\Database\\BaseConnection given\.$#' count: 1 diff --git a/utils/phpstan-baseline/codeigniter.modelArgumentType.neon b/utils/phpstan-baseline/codeigniter.modelArgumentType.neon deleted file mode 100644 index 0bf0d63faeb2..000000000000 --- a/utils/phpstan-baseline/codeigniter.modelArgumentType.neon +++ /dev/null @@ -1,8 +0,0 @@ -# total 1 error - -parameters: - ignoreErrors: - - - message: '#^Parameter \#1 \$name of function model expects a valid class string, ''CodeIgniter\\\\Shield\\\\Models\\\\UserModel'' given\.$#' - count: 1 - path: ../../tests/system/Config/FactoriesTest.php diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 9954e15ad7a3..d591b6fcf902 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,10 +1,9 @@ -# total 366 errors +# total 312 errors includes: - argument.type.neon - arguments.count.neon - assign.propertyType.neon - - codeigniter.modelArgumentType.neon - deadCode.unreachable.neon - function.resultUnused.neon - method.childParameterType.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index a91013e3788b..bd603a4178a1 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 236 errors +# total 204 errors parameters: ignoreErrors: @@ -127,146 +127,6 @@ parameters: count: 1 path: ../../system/Common.php - - - message: '#^Method CodeIgniter\\Config\\BaseConfig\:\:__set_state\(\) has parameter \$array with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/BaseConfig.php - - - - message: '#^Property CodeIgniter\\Config\\BaseConfig\:\:\$registrars type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/BaseConfig.php - - - - message: '#^Class CodeIgniter\\Config\\BaseService has PHPDoc tag @method for method superglobals\(\) parameter \#1 \$server with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/BaseService.php - - - - message: '#^Class CodeIgniter\\Config\\BaseService has PHPDoc tag @method for method superglobals\(\) parameter \#2 \$get with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/BaseService.php - - - - message: '#^Method CodeIgniter\\Config\\BaseService\:\:__callStatic\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/BaseService.php - - - - message: '#^Property CodeIgniter\\Config\\BaseService\:\:\$services type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/BaseService.php - - - - message: '#^Method CodeIgniter\\Config\\DotEnv\:\:normaliseVariable\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/DotEnv.php - - - - message: '#^Method CodeIgniter\\Config\\DotEnv\:\:parse\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/DotEnv.php - - - - message: '#^Class CodeIgniter\\Config\\Factories has PHPDoc tag @method for method models\(\) parameter \#2 \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/Factories.php - - - - message: '#^Method CodeIgniter\\Config\\Factories\:\:__callStatic\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/Factories.php - - - - message: '#^Method CodeIgniter\\Config\\Factories\:\:createInstance\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/Factories.php - - - - message: '#^Method CodeIgniter\\Config\\Factories\:\:getDefinedInstance\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/Factories.php - - - - message: '#^Method CodeIgniter\\Config\\Factories\:\:getDefinedInstance\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/Factories.php - - - - message: '#^Method CodeIgniter\\Config\\Factories\:\:locateClass\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/Factories.php - - - - message: '#^Method CodeIgniter\\Config\\Factories\:\:setComponentInstances\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/Factories.php - - - - message: '#^Method CodeIgniter\\Config\\Factories\:\:setOptions\(\) has parameter \$values with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/Factories.php - - - - message: '#^Method CodeIgniter\\Config\\Factories\:\:verifyInstanceOf\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/Factories.php - - - - message: '#^Method CodeIgniter\\Config\\Factories\:\:verifyPreferApp\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/Factories.php - - - - message: '#^Property CodeIgniter\\Config\\Factory\:\:\$default type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/Factory.php - - - - message: '#^Property CodeIgniter\\Config\\Factory\:\:\$models type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/Factory.php - - - - message: '#^Method CodeIgniter\\Config\\Services\:\:curlrequest\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/Services.php - - - - message: '#^Method CodeIgniter\\Config\\Services\:\:email\(\) has parameter \$config with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/Services.php - - - - message: '#^Method CodeIgniter\\Config\\Services\:\:superglobals\(\) has parameter \$cookie with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/Services.php - - - - message: '#^Method CodeIgniter\\Config\\Services\:\:superglobals\(\) has parameter \$files with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/Services.php - - - - message: '#^Method CodeIgniter\\Config\\Services\:\:superglobals\(\) has parameter \$get with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/Services.php - - - - message: '#^Method CodeIgniter\\Config\\Services\:\:superglobals\(\) has parameter \$post with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/Services.php - - - - message: '#^Method CodeIgniter\\Config\\Services\:\:superglobals\(\) has parameter \$request with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/Services.php - - - - message: '#^Method CodeIgniter\\Config\\Services\:\:superglobals\(\) has parameter \$server with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Config/Services.php - - message: '#^Method CodeIgniter\\Controller\:\:setValidator\(\) has parameter \$messages with no value type specified in iterable type array\.$#' count: 1 @@ -817,26 +677,6 @@ parameters: count: 1 path: ../../tests/system/CommonHelperTest.php - - - message: '#^Method CodeIgniter\\Config\\DotEnvTest\:\:provideLoadsVars\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Config/DotEnvTest.php - - - - message: '#^Method CodeIgniter\\Config\\MimesTest\:\:provideGuessExtensionFromType\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Config/MimesTest.php - - - - message: '#^Method CodeIgniter\\Config\\MimesTest\:\:provideGuessTypeFromExtension\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Config/MimesTest.php - - - - message: '#^Property CodeIgniter\\Config\\ServicesTest\:\:\$original type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Config/ServicesTest.php - - message: '#^Property CodeIgniter\\Cookie\\CookieStoreTest\:\:\$defaults type has no value type specified in iterable type array\.$#' count: 1 diff --git a/utils/phpstan-baseline/missingType.parameter.neon b/utils/phpstan-baseline/missingType.parameter.neon index 87eecd560077..bc058735997d 100644 --- a/utils/phpstan-baseline/missingType.parameter.neon +++ b/utils/phpstan-baseline/missingType.parameter.neon @@ -1,4 +1,4 @@ -# total 9 errors +# total 8 errors parameters: ignoreErrors: @@ -22,11 +22,6 @@ parameters: count: 1 path: ../../tests/system/Commands/Utilities/Routes/AutoRouterImproved/Controllers/SubDir/BlogController.php - - - message: '#^Method CodeIgniter\\Config\\FactoriesTest\:\:getFactoriesStaticProperty\(\) has parameter \$params with no type specified\.$#' - count: 1 - path: ../../tests/system/Config/FactoriesTest.php - - message: '#^Method CodeIgniter\\Entity\\EntityTest\:\:getCastEntity\(\) has parameter \$data with no type specified\.$#' count: 1 diff --git a/utils/phpstan-baseline/staticMethod.notFound.neon b/utils/phpstan-baseline/staticMethod.notFound.neon index 3ad7dfbbd356..09b088e3c84f 100644 --- a/utils/phpstan-baseline/staticMethod.notFound.neon +++ b/utils/phpstan-baseline/staticMethod.notFound.neon @@ -1,4 +1,4 @@ -# total 18 errors +# total 1 error parameters: ignoreErrors: @@ -6,23 +6,3 @@ parameters: message: '#^Call to an undefined static method CodeIgniter\\Config\\Factories\:\:cells\(\)\.$#' count: 1 path: ../../system/View/Cell.php - - - - message: '#^Call to an undefined static method CodeIgniter\\Config\\Factories\:\:cells\(\)\.$#' - count: 2 - path: ../../tests/system/Config/FactoriesTest.php - - - - message: '#^Call to an undefined static method CodeIgniter\\Config\\Factories\:\:tedwigs\(\)\.$#' - count: 1 - path: ../../tests/system/Config/FactoriesTest.php - - - - message: '#^Call to an undefined static method CodeIgniter\\Config\\Factories\:\:widgets\(\)\.$#' - count: 13 - path: ../../tests/system/Config/FactoriesTest.php - - - - message: '#^Call to an undefined static method Tests\\Support\\Config\\Services\:\:redirectResponse\(\)\.$#' - count: 1 - path: ../../tests/system/Config/ServicesTest.php From 18804c58475a82d07095d28413507e41c866d83b Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Wed, 26 Aug 2026 01:57:04 +0800 Subject: [PATCH 077/163] refactor: fix phpstan errors in `Commands` (#10483) --- system/Commands/ListCommands.php | 7 + .../Translation/LocalizationFinder.php | 13 +- system/Commands/Utilities/Namespaces.php | 14 +- .../AutoRouterImproved/AutoRouteCollector.php | 7 +- .../ControllerMethodReader.php | 5 +- .../Routes/ControllerMethodReader.php | 2 + .../Utilities/Routes/FilterFinder.php | 3 + .../Translation/LocalizationFinderTest.php | 9 ++ .../AutoRouteCollectorTest.php | 3 + .../Dash_folder/Dash_controller.php | 4 +- .../Controllers/SubDir/BlogController.php | 2 +- .../Utilities/Routes/FilterFinderTest.php | 15 +- .../Routes/SampleURIGeneratorTest.php | 3 + .../phpstan-baseline/assign.propertyType.neon | 7 +- utils/phpstan-baseline/loader.neon | 2 +- utils/phpstan-baseline/method.notFound.neon | 7 +- .../missingType.iterableValue.neon | 132 +----------------- .../missingType.parameter.neon | 22 +-- 18 files changed, 81 insertions(+), 176 deletions(-) diff --git a/system/Commands/ListCommands.php b/system/Commands/ListCommands.php index 8761e2bb7bbc..c976733b768d 100644 --- a/system/Commands/ListCommands.php +++ b/system/Commands/ListCommands.php @@ -15,12 +15,15 @@ use CodeIgniter\CLI\BaseCommand; use CodeIgniter\CLI\CLI; +use CodeIgniter\CLI\Commands; /** * CI Help command for the spark script. * * Lists the basic usage information for the spark script, * and provides a way to list help for other commands. + * + * @phpstan-import-type commands_list from Commands */ class ListCommands extends BaseCommand { @@ -88,6 +91,8 @@ public function run(array $params) /** * Lists the commands with accompanying info. * + * @param commands_list $commands + * * @return int */ protected function listFull(array $commands) @@ -133,6 +138,8 @@ protected function listFull(array $commands) /** * Lists the commands only. * + * @param commands_list $commands + * * @return int */ protected function listSimple(array $commands) diff --git a/system/Commands/Translation/LocalizationFinder.php b/system/Commands/Translation/LocalizationFinder.php index ca7a1fb20230..8fed993a6ecc 100644 --- a/system/Commands/Translation/LocalizationFinder.php +++ b/system/Commands/Translation/LocalizationFinder.php @@ -213,7 +213,7 @@ private function findResolvedTranslations(string $langFileName, string $currentL /** * @param SplFileInfo|string $file * - * @return array + * @return array{foundLanguageKeys: array>, badLanguageKeys: array>} */ private function findTranslationsInFile($file): array { @@ -273,6 +273,9 @@ private function isIgnoredFile(SplFileInfo $file): bool return $file->getExtension() !== 'php'; } + /** + * @param array $language + */ private function templateFile(array $language = []): string { if ($language !== []) { @@ -337,6 +340,10 @@ private function replaceArraySyntax(string $code): string /** * Create multidimensional array from another keys + * + * @param list $fromKeys + * + * @return array */ private function buildMultiArray(array $fromKeys, string $lastArrayValue = ''): array { @@ -356,6 +363,10 @@ private function buildMultiArray(array $fromKeys, string $lastArrayValue = ''): /** * Convert multi arrays to specific CLI table rows (flat array) + * + * @param array $array + * + * @return list */ private function arrayToTableRows(string $langFileName, array $array): array { diff --git a/system/Commands/Utilities/Namespaces.php b/system/Commands/Utilities/Namespaces.php index 6469d1afc97f..51bc045759ff 100644 --- a/system/Commands/Utilities/Namespaces.php +++ b/system/Commands/Utilities/Namespaces.php @@ -91,9 +91,14 @@ public function run(array $params) CLI::table($tbody, $thead); } + /** + * @param array $params + * + * @return list + */ private function outputAllNamespaces(array $params): array { - $maxLength = $params['m']; + $maxLength = (int) $params['m']; $autoloader = service('autoloader'); @@ -129,9 +134,14 @@ private function truncate(string $string, int $max): string return $string; } + /** + * @param array $params + * + * @return list + */ private function outputCINamespaces(array $params): array { - $maxLength = $params['m']; + $maxLength = (int) $params['m']; $config = new Autoload(); diff --git a/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php b/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php index 60a79011426a..a19bd5f24188 100644 --- a/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php +++ b/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php @@ -95,9 +95,9 @@ public function get(): array /** * Adding Filters * - * @param list> $routes + * @param list, before?: string, after?: string}> $routes * - * @return list> + * @return list, before: string, after: string}> */ private function addFilters(array $routes): array { @@ -134,6 +134,9 @@ private function addFilters(array $routes): array return $routes; } + /** + * @param array{method: string, route: string, route_params: string, handler: string, params: array, before?: string, after?: string} $route + */ private function generateSampleUri(array $route, bool $longest = true): string { $sampleUri = ''; diff --git a/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php b/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php index 599e90f4d2f3..0863388b8af5 100644 --- a/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php +++ b/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php @@ -45,7 +45,7 @@ public function __construct( * * @param class-string $class * - * @return list> + * @return list}> */ public function read(string $class, string $defaultController = 'Home', string $defaultMethod = 'index'): array { @@ -128,6 +128,9 @@ public function read(string $class, string $defaultController = 'Home', string $ return $output; } + /** + * @return array{array, string} + */ private function getParameters(ReflectionMethod $method): array { $params = []; diff --git a/system/Commands/Utilities/Routes/ControllerMethodReader.php b/system/Commands/Utilities/Routes/ControllerMethodReader.php index 7c8faffd16be..aa5cddaf48d1 100644 --- a/system/Commands/Utilities/Routes/ControllerMethodReader.php +++ b/system/Commands/Utilities/Routes/ControllerMethodReader.php @@ -147,6 +147,8 @@ private function getUriByClass(string $classname): string /** * Gets a route without default controller. + * + * @return list */ private function getRouteWithoutController( string $classShortname, diff --git a/system/Commands/Utilities/Routes/FilterFinder.php b/system/Commands/Utilities/Routes/FilterFinder.php index 98a1e3659eaf..6477e1d13300 100644 --- a/system/Commands/Utilities/Routes/FilterFinder.php +++ b/system/Commands/Utilities/Routes/FilterFinder.php @@ -36,6 +36,9 @@ public function __construct(?Router $router = null, ?Filters $filters = null) $this->filters = $filters ?? service('filters'); } + /** + * @return list + */ private function getRouteFilters(string $uri): array { $this->router->handle($uri); diff --git a/tests/system/Commands/Translation/LocalizationFinderTest.php b/tests/system/Commands/Translation/LocalizationFinderTest.php index bfbd00d8bf83..f8daf59ee151 100644 --- a/tests/system/Commands/Translation/LocalizationFinderTest.php +++ b/tests/system/Commands/Translation/LocalizationFinderTest.php @@ -166,6 +166,9 @@ public function testWriteSkipsKeysAlreadyTranslatedByFramework(): void $this->assertArrayNotHasKey('pageNotFound', $generatedKeys); } + /** + * @return array + */ private function getActualTranslationOneKeys(): array { return [ @@ -179,6 +182,9 @@ private function getActualTranslationOneKeys(): array ]; } + /** + * @return array + */ private function getActualTranslationThreeKeys(): array { return [ @@ -212,6 +218,9 @@ private function getActualTranslationThreeKeys(): array ]; } + /** + * @return array + */ private function getActualTranslationFourKeys(): array { return [ diff --git a/tests/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollectorTest.php b/tests/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollectorTest.php index 3413d1a83b86..e3e7369ad131 100644 --- a/tests/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollectorTest.php +++ b/tests/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollectorTest.php @@ -31,6 +31,9 @@ protected function setUp(): void $this->resetServices(true); } + /** + * @param array>> $filterConfigFilters + */ private function createAutoRouteCollector(array $filterConfigFilters): AutoRouteCollector { $routes = service('routes'); diff --git a/tests/system/Commands/Utilities/Routes/AutoRouterImproved/Controllers/Dash_folder/Dash_controller.php b/tests/system/Commands/Utilities/Routes/AutoRouterImproved/Controllers/Dash_folder/Dash_controller.php index 486c8529d082..2a1502c6d12c 100644 --- a/tests/system/Commands/Utilities/Routes/AutoRouterImproved/Controllers/Dash_folder/Dash_controller.php +++ b/tests/system/Commands/Utilities/Routes/AutoRouterImproved/Controllers/Dash_folder/Dash_controller.php @@ -17,11 +17,11 @@ class Dash_controller extends Controller { - public function getSomemethod($p1 = ''): void + public function getSomemethod(string $p1 = ''): void { } - public function getDash_method($p1, $p2 = ''): void + public function getDash_method(string $p1, string $p2 = ''): void { } } diff --git a/tests/system/Commands/Utilities/Routes/AutoRouterImproved/Controllers/SubDir/BlogController.php b/tests/system/Commands/Utilities/Routes/AutoRouterImproved/Controllers/SubDir/BlogController.php index fee7eab80416..5054ecd8e861 100644 --- a/tests/system/Commands/Utilities/Routes/AutoRouterImproved/Controllers/SubDir/BlogController.php +++ b/tests/system/Commands/Utilities/Routes/AutoRouterImproved/Controllers/SubDir/BlogController.php @@ -21,7 +21,7 @@ public function getIndex(): void { } - public function getSomeMethod($first = ''): void + public function getSomeMethod(string $first = ''): void { } } diff --git a/tests/system/Commands/Utilities/Routes/FilterFinderTest.php b/tests/system/Commands/Utilities/Routes/FilterFinderTest.php index ce813ec3631e..80f566feab75 100644 --- a/tests/system/Commands/Utilities/Routes/FilterFinderTest.php +++ b/tests/system/Commands/Utilities/Routes/FilterFinderTest.php @@ -47,13 +47,19 @@ protected function setUp(): void { parent::setUp(); - $this->request = service('request'); - $this->response = service('response'); + $this->request = service('request'); + + $response = service('response'); + $this->assertInstanceOf(Response::class, $response); + $this->response = $response; $this->moduleConfig = new Modules(); $this->moduleConfig->enabled = false; } + /** + * @param array $routes + */ private function createRouteCollection(array $routes = []): RouteCollectionInterface { $collection = new RouteCollection(service('locator'), $this->moduleConfig, new Routing()); @@ -72,6 +78,9 @@ private function createRouter(RouteCollectionInterface $collection): Router return new Router($collection, $this->request); } + /** + * @param array $config + */ private function createFilters(array $config = []): Filters { $config = ($config !== []) ? $config : [ @@ -159,6 +168,7 @@ public function testFindGlobalsAndRouteFilters(): void public function testFindGlobalsAndRouteFiltersWithArguments(): void { + /** @var RouteCollection $collection */ $collection = $this->createRouteCollection(); $collection->get('admin', ' AdminController::index', ['filter' => 'honeypot:arg1,arg2']); $router = $this->createRouter($collection); @@ -177,6 +187,7 @@ public function testFindGlobalsAndRouteFiltersWithArguments(): void public function testFindClassesGlobalsAndRouteFiltersWithArguments(): void { + /** @var RouteCollection $collection */ $collection = $this->createRouteCollection(); $collection->get('admin', ' AdminController::index', ['filter' => 'honeypot:arg1,arg2']); $router = $this->createRouter($collection); diff --git a/tests/system/Commands/Utilities/Routes/SampleURIGeneratorTest.php b/tests/system/Commands/Utilities/Routes/SampleURIGeneratorTest.php index 1d67afae1ce4..b2c976788b66 100644 --- a/tests/system/Commands/Utilities/Routes/SampleURIGeneratorTest.php +++ b/tests/system/Commands/Utilities/Routes/SampleURIGeneratorTest.php @@ -33,6 +33,9 @@ public function testGet(string $routeKey, string $expected): void $this->assertSame($expected, $uri); } + /** + * @return iterable + */ public static function provideGet(): iterable { yield from [ diff --git a/utils/phpstan-baseline/assign.propertyType.neon b/utils/phpstan-baseline/assign.propertyType.neon index f783f8da680e..d8ba9db941a4 100644 --- a/utils/phpstan-baseline/assign.propertyType.neon +++ b/utils/phpstan-baseline/assign.propertyType.neon @@ -1,4 +1,4 @@ -# total 19 errors +# total 18 errors parameters: ignoreErrors: @@ -12,11 +12,6 @@ parameters: count: 1 path: ../../system/Controller.php - - - message: '#^Property CodeIgniter\\Commands\\Utilities\\Routes\\FilterFinderTest\:\:\$response \(CodeIgniter\\HTTP\\Response\) does not accept CodeIgniter\\HTTP\\ResponseInterface\.$#' - count: 1 - path: ../../tests/system/Commands/Utilities/Routes/FilterFinderTest.php - - message: '#^Property CodeIgniter\\Filters\\CSRFTest\:\:\$response \(CodeIgniter\\HTTP\\Response\|null\) does not accept CodeIgniter\\HTTP\\ResponseInterface\.$#' count: 2 diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index d591b6fcf902..eb9a9772134c 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 312 errors +# total 279 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/method.notFound.neon b/utils/phpstan-baseline/method.notFound.neon index 9443f439a180..8453adb3b030 100644 --- a/utils/phpstan-baseline/method.notFound.neon +++ b/utils/phpstan-baseline/method.notFound.neon @@ -1,4 +1,4 @@ -# total 18 errors +# total 16 errors parameters: ignoreErrors: @@ -7,11 +7,6 @@ parameters: count: 1 path: ../../tests/system/CodeIgniterTest.php - - - message: '#^Call to an undefined method CodeIgniter\\Router\\RouteCollectionInterface\:\:get\(\)\.$#' - count: 2 - path: ../../tests/system/Commands/Utilities/Routes/FilterFinderTest.php - - message: '#^Call to an undefined method CodeIgniter\\Images\\Handlers\\BaseHandler\:\:getPathname\(\)\.$#' count: 1 diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index bd603a4178a1..21e533524975 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,102 +1,7 @@ -# total 204 errors +# total 178 errors parameters: ignoreErrors: - - - message: '#^Method CodeIgniter\\Commands\\ListCommands\:\:listFull\(\) has parameter \$commands with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Commands/ListCommands.php - - - - message: '#^Method CodeIgniter\\Commands\\ListCommands\:\:listSimple\(\) has parameter \$commands with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Commands/ListCommands.php - - - - message: '#^Method CodeIgniter\\Commands\\Translation\\LocalizationFinder\:\:arrayToTableRows\(\) has parameter \$array with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Commands/Translation/LocalizationFinder.php - - - - message: '#^Method CodeIgniter\\Commands\\Translation\\LocalizationFinder\:\:arrayToTableRows\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Commands/Translation/LocalizationFinder.php - - - - message: '#^Method CodeIgniter\\Commands\\Translation\\LocalizationFinder\:\:buildMultiArray\(\) has parameter \$fromKeys with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Commands/Translation/LocalizationFinder.php - - - - message: '#^Method CodeIgniter\\Commands\\Translation\\LocalizationFinder\:\:buildMultiArray\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Commands/Translation/LocalizationFinder.php - - - - message: '#^Method CodeIgniter\\Commands\\Translation\\LocalizationFinder\:\:findTranslationsInFile\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Commands/Translation/LocalizationFinder.php - - - - message: '#^Method CodeIgniter\\Commands\\Translation\\LocalizationFinder\:\:templateFile\(\) has parameter \$language with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Commands/Translation/LocalizationFinder.php - - - - message: '#^Method CodeIgniter\\Commands\\Utilities\\Namespaces\:\:outputAllNamespaces\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Commands/Utilities/Namespaces.php - - - - message: '#^Method CodeIgniter\\Commands\\Utilities\\Namespaces\:\:outputAllNamespaces\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Commands/Utilities/Namespaces.php - - - - message: '#^Method CodeIgniter\\Commands\\Utilities\\Namespaces\:\:outputCINamespaces\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Commands/Utilities/Namespaces.php - - - - message: '#^Method CodeIgniter\\Commands\\Utilities\\Namespaces\:\:outputCINamespaces\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Commands/Utilities/Namespaces.php - - - - message: '#^Method CodeIgniter\\Commands\\Utilities\\Routes\\AutoRouterImproved\\AutoRouteCollector\:\:addFilters\(\) has parameter \$routes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php - - - - message: '#^Method CodeIgniter\\Commands\\Utilities\\Routes\\AutoRouterImproved\\AutoRouteCollector\:\:addFilters\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php - - - - message: '#^Method CodeIgniter\\Commands\\Utilities\\Routes\\AutoRouterImproved\\AutoRouteCollector\:\:generateSampleUri\(\) has parameter \$route with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php - - - - message: '#^Method CodeIgniter\\Commands\\Utilities\\Routes\\AutoRouterImproved\\ControllerMethodReader\:\:getParameters\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php - - - - message: '#^Method CodeIgniter\\Commands\\Utilities\\Routes\\AutoRouterImproved\\ControllerMethodReader\:\:read\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php - - - - message: '#^Method CodeIgniter\\Commands\\Utilities\\Routes\\ControllerMethodReader\:\:getRouteWithoutController\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Commands/Utilities/Routes/ControllerMethodReader.php - - - - message: '#^Method CodeIgniter\\Commands\\Utilities\\Routes\\FilterFinder\:\:getRouteFilters\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Commands/Utilities/Routes/FilterFinder.php - - message: '#^Function log_message\(\) has parameter \$context with no value type specified in iterable type array\.$#' count: 1 @@ -632,41 +537,6 @@ parameters: count: 1 path: ../../tests/system/CodeIgniterTest.php - - - message: '#^Method CodeIgniter\\Commands\\Translation\\LocalizationFinderTest\:\:getActualTranslationFourKeys\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Commands/Translation/LocalizationFinderTest.php - - - - message: '#^Method CodeIgniter\\Commands\\Translation\\LocalizationFinderTest\:\:getActualTranslationOneKeys\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Commands/Translation/LocalizationFinderTest.php - - - - message: '#^Method CodeIgniter\\Commands\\Translation\\LocalizationFinderTest\:\:getActualTranslationThreeKeys\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Commands/Translation/LocalizationFinderTest.php - - - - message: '#^Method CodeIgniter\\Commands\\Utilities\\Routes\\AutoRouterImproved\\AutoRouteCollectorTest\:\:createAutoRouteCollector\(\) has parameter \$filterConfigFilters with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollectorTest.php - - - - message: '#^Method CodeIgniter\\Commands\\Utilities\\Routes\\FilterFinderTest\:\:createFilters\(\) has parameter \$config with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Commands/Utilities/Routes/FilterFinderTest.php - - - - message: '#^Method CodeIgniter\\Commands\\Utilities\\Routes\\FilterFinderTest\:\:createRouteCollection\(\) has parameter \$routes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Commands/Utilities/Routes/FilterFinderTest.php - - - - message: '#^Method CodeIgniter\\Commands\\Utilities\\Routes\\SampleURIGeneratorTest\:\:provideGet\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Commands/Utilities/Routes/SampleURIGeneratorTest.php - - message: '#^Method CodeIgniter\\CommonFunctionsTest\:\:provideCleanPathActuallyCleaningThePaths\(\) return type has no value type specified in iterable type iterable\.$#' count: 1 diff --git a/utils/phpstan-baseline/missingType.parameter.neon b/utils/phpstan-baseline/missingType.parameter.neon index bc058735997d..26a303d346d8 100644 --- a/utils/phpstan-baseline/missingType.parameter.neon +++ b/utils/phpstan-baseline/missingType.parameter.neon @@ -1,27 +1,7 @@ -# total 8 errors +# total 4 errors parameters: ignoreErrors: - - - message: '#^Method CodeIgniter\\Commands\\Utilities\\Routes\\AutoRouterImproved\\Controllers\\Dash_folder\\Dash_controller\:\:getDash_method\(\) has parameter \$p1 with no type specified\.$#' - count: 1 - path: ../../tests/system/Commands/Utilities/Routes/AutoRouterImproved/Controllers/Dash_folder/Dash_controller.php - - - - message: '#^Method CodeIgniter\\Commands\\Utilities\\Routes\\AutoRouterImproved\\Controllers\\Dash_folder\\Dash_controller\:\:getDash_method\(\) has parameter \$p2 with no type specified\.$#' - count: 1 - path: ../../tests/system/Commands/Utilities/Routes/AutoRouterImproved/Controllers/Dash_folder/Dash_controller.php - - - - message: '#^Method CodeIgniter\\Commands\\Utilities\\Routes\\AutoRouterImproved\\Controllers\\Dash_folder\\Dash_controller\:\:getSomemethod\(\) has parameter \$p1 with no type specified\.$#' - count: 1 - path: ../../tests/system/Commands/Utilities/Routes/AutoRouterImproved/Controllers/Dash_folder/Dash_controller.php - - - - message: '#^Method CodeIgniter\\Commands\\Utilities\\Routes\\AutoRouterImproved\\Controllers\\SubDir\\BlogController\:\:getSomeMethod\(\) has parameter \$first with no type specified\.$#' - count: 1 - path: ../../tests/system/Commands/Utilities/Routes/AutoRouterImproved/Controllers/SubDir/BlogController.php - - message: '#^Method CodeIgniter\\Entity\\EntityTest\:\:getCastEntity\(\) has parameter \$data with no type specified\.$#' count: 1 From eeb3c08423a622cd8e0c5cb32c9f3ba8584c300f Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Wed, 26 Aug 2026 03:02:57 +0800 Subject: [PATCH 078/163] refactor: fix phpstan errors in `View` (#10484) --- system/Config/Factories.php | 1 + system/Traits/PropertiesTrait.php | 4 + system/View/Cell.php | 16 ++-- system/View/Cells/Cell.php | 6 ++ system/View/Parser.php | 6 +- tests/system/Config/FactoriesTest.php | 4 +- tests/system/View/ParserPluginTest.php | 7 +- tests/system/View/ParserTest.php | 9 +- tests/system/View/TableTest.php | 35 +++++--- utils/phpstan-baseline/argument.type.neon | 27 +----- .../phpstan-baseline/assign.propertyType.neon | 7 +- utils/phpstan-baseline/loader.neon | 3 +- utils/phpstan-baseline/method.notFound.neon | 27 +----- .../missingType.iterableValue.neon | 89 +------------------ .../staticMethod.notFound.neon | 8 -- 15 files changed, 67 insertions(+), 182 deletions(-) delete mode 100644 utils/phpstan-baseline/staticMethod.notFound.neon diff --git a/system/Config/Factories.php b/system/Config/Factories.php index 98f15264a40f..1adbafc48e7b 100644 --- a/system/Config/Factories.php +++ b/system/Config/Factories.php @@ -26,6 +26,7 @@ * large performance boost and helps keep code clean of lengthy * instantiation checks. * + * @method static object|null cells(string $alias, array $options = []) * @method static BaseConfig|null config(...$arguments) * @method static Model|null models(string $alias, array $options = [], ?ConnectionInterface &$conn = null) * @see \CodeIgniter\Config\FactoriesTest diff --git a/system/Traits/PropertiesTrait.php b/system/Traits/PropertiesTrait.php index bf0571b2f05b..dd14e6c68d3a 100644 --- a/system/Traits/PropertiesTrait.php +++ b/system/Traits/PropertiesTrait.php @@ -27,6 +27,8 @@ trait PropertiesTrait { /** * Attempts to set the values of public class properties. + * + * @param array $params */ final public function fill(array $params): self { @@ -61,6 +63,8 @@ public function getProperties(object $obj): array /** * Get the protected and private properties of the class and return as an array. + * + * @return list */ final public function getNonPublicProperties(): array { diff --git a/system/View/Cell.php b/system/View/Cell.php index 3a3e9595e3f5..5e0eda26255b 100644 --- a/system/View/Cell.php +++ b/system/View/Cell.php @@ -78,9 +78,7 @@ public function render(string $library, $params = null, int $ttl = 0, ?string $c { [$instance, $method] = $this->determineClass($library); - $class = is_object($instance) - ? $instance::class - : null; + $class = $instance::class; $params = $this->prepareParams($params); @@ -160,6 +158,8 @@ public function prepareParams($params) /** * Given the library string, attempts to determine the class and method * to call. + * + * @return array{object, string} */ protected function determineClass(string $library): array { @@ -198,6 +198,8 @@ protected function determineClass(string $library): array /** * Renders a cell that extends the BaseCell class. + * + * @param array $params */ final protected function renderCell(BaseCell $instance, string $method, array $params): string { @@ -234,6 +236,10 @@ final protected function renderCell(BaseCell $instance, string $method, array $p * Returns the values from $params that match the parameters * for a method, in the order they are defined. This allows * them to be passed directly into the method. + * + * @param array $params + * + * @return list */ private function getMethodParams(BaseCell $instance, string $method, array $params): array { @@ -262,9 +268,9 @@ private function getMethodParams(BaseCell $instance, string $method, array $para * * @todo Determine if this can be refactored to use $this-getMethodParams(). * - * @param object $instance + * @param array $params */ - final protected function renderSimpleClass($instance, string $method, array $params, string $class): string + final protected function renderSimpleClass(object $instance, string $method, array $params, string $class): string { // Try to match up the parameter list we were provided // with the parameter name in the callback method. diff --git a/system/View/Cells/Cell.php b/system/View/Cells/Cell.php index 581499028e4b..ae1586dbcbcb 100644 --- a/system/View/Cells/Cell.php +++ b/system/View/Cells/Cell.php @@ -71,6 +71,8 @@ public function setView(string $view) * current scope and captures the output buffer instead of * relying on the view service. * + * @param array $data + * * @throws LogicException */ final protected function view(?string $view, array $data = []): string @@ -129,6 +131,10 @@ public function __toString(): string /** * Allows the developer to define computed properties * as methods with `get` prefixed to the protected/private property name. + * + * @param array $properties + * + * @return array */ private function includeComputedProperties(array $properties): array { diff --git a/system/View/Parser.php b/system/View/Parser.php index af4ebea346ac..56c3b093bc5d 100644 --- a/system/View/Parser.php +++ b/system/View/Parser.php @@ -543,9 +543,9 @@ public function setConditionalDelimiters($leftDelimiter = '{', $rightDelimiter = * Handles replacing a pseudo-variable with the actual content. Will double-check * for escaping brackets. * - * @param array|string $pattern - * @param string $content - * @param string $template + * @param array|string $pattern + * @param string $content + * @param string $template */ protected function replaceSingle($pattern, $content, $template, bool $escape = false): string { diff --git a/tests/system/Config/FactoriesTest.php b/tests/system/Config/FactoriesTest.php index 434211e638a5..abaef3997a88 100644 --- a/tests/system/Config/FactoriesTest.php +++ b/tests/system/Config/FactoriesTest.php @@ -324,8 +324,8 @@ class_alias(SomeWidget::class, $class); public function testCanLoadTwoCellsWithSameShortName(): void { - $cell1 = Factories::cells('\\' . SampleClass::class); // @phpstan-ignore staticMethod.notFound - $cell2 = Factories::cells('\\' . \Tests\Support\View\OtherCells\SampleClass::class); // @phpstan-ignore staticMethod.notFound + $cell1 = Factories::cells('\\' . SampleClass::class); + $cell2 = Factories::cells('\\' . \Tests\Support\View\OtherCells\SampleClass::class); $this->assertNotSame($cell1, $cell2); } diff --git a/tests/system/View/ParserPluginTest.php b/tests/system/View/ParserPluginTest.php index bb458a85ad04..a9312e91168c 100644 --- a/tests/system/View/ParserPluginTest.php +++ b/tests/system/View/ParserPluginTest.php @@ -36,8 +36,11 @@ protected function setUp(): void Services::reset(true); - $this->parser = service('parser'); - $this->validator = service('validation'); + $this->parser = service('parser'); + + $validator = service('validation'); + $this->assertInstanceOf(Validation::class, $validator); + $this->validator = $validator; } public function testCurrentURL(): void diff --git a/tests/system/View/ParserTest.php b/tests/system/View/ParserTest.php index fa6784445e8e..730acb6cf8c1 100644 --- a/tests/system/View/ParserTest.php +++ b/tests/system/View/ParserTest.php @@ -438,6 +438,9 @@ public function testEscHandling($value, $expected = null): void $this->assertSame($expected, \esc($value)); } + /** + * @return iterable> + */ public static function provideEscHandling(): iterable { return [ @@ -543,20 +546,20 @@ public function testNoEscapingSetData(): void { $template = '{ foo | noescape}'; - $this->parser->setData(['foo' => 'http://foo.com'], 'unknown'); + $this->parser->setData(['foo' => 'http://foo.com'], 'unknown'); // @phpstan-ignore argument.type (deliberately invalid context) $this->assertSame('http://foo.com', $this->parser->renderString($template)); } public function testAutoEscaping(): void { - $this->parser->setData(['foo' => 'http://foo.com'], 'unknown'); + $this->parser->setData(['foo' => 'http://foo.com'], 'unknown'); // @phpstan-ignore argument.type (deliberately invalid context) $this->assertSame('html', $this->parser->shouldAddEscaping('{ foo | this | that }')); } public function testAutoEscapingNot(): void { - $this->parser->setData(['foo' => 'http://foo.com'], 'unknown'); + $this->parser->setData(['foo' => 'http://foo.com'], 'unknown'); // @phpstan-ignore argument.type (deliberately invalid context) $this->assertFalse($this->parser->shouldAddEscaping('{ foo | noescape }')); } diff --git a/tests/system/View/TableTest.php b/tests/system/View/TableTest.php index b7946b059138..e09cb9e6631f 100644 --- a/tests/system/View/TableTest.php +++ b/tests/system/View/TableTest.php @@ -201,7 +201,7 @@ public function testPrepArgs(): void $this->assertSame( $expected, - $this->table->prepArgs(['name', 'color', 'size']), + $this->table->prepArgs(['name', 'color', 'size']), // @phpstan-ignore method.notFound (Testing MockTable::__call() fallback) ); // with cell attributes @@ -213,7 +213,7 @@ public function testPrepArgs(): void $this->assertSame( $expected, - $this->table->prepArgs(['name', 'color', 'size', ['data' => 'weight', 'class' => 'awesome']]), + $this->table->prepArgs(['name', 'color', 'size', ['data' => 'weight', 'class' => 'awesome']]), // @phpstan-ignore method.notFound (Testing MockTable::__call() fallback) ); } @@ -241,7 +241,7 @@ public function testDefaultTemplateKeys(): void ]; foreach ($keys as $key) { - $this->assertArrayHasKey($key, $this->table->defaultTemplate()); + $this->assertArrayHasKey($key, $this->table->defaultTemplate()); // @phpstan-ignore method.notFound (Testing MockTable::__call() fallback) } } @@ -251,14 +251,14 @@ public function testCompileTemplate(): void // non default key $this->table->setTemplate(['nonsense' => 'foo']); - $this->table->compileTemplate(); + $this->table->compileTemplate(); // @phpstan-ignore method.notFound (Testing MockTable::__call() fallback) $this->assertArrayHasKey('nonsense', $this->table->template); $this->assertSame('foo', $this->table->template['nonsense']); // override default $this->table->setTemplate(['table_close' => '']); - $this->table->compileTemplate(); + $this->table->compileTemplate(); // @phpstan-ignore method.notFound (Testing MockTable::__call() fallback) $this->assertArrayHasKey('table_close', $this->table->template); $this->assertSame('', $this->table->template['table_close']); @@ -267,9 +267,9 @@ public function testCompileTemplate(): void public function testMakeColumns(): void { // Test bogus parameters - $this->assertFalse($this->table->makeColumns('invalid_junk')); + $this->assertFalse($this->table->makeColumns('invalid_junk')); // @phpstan-ignore argument.type (deliberately wrong type) $this->assertFalse($this->table->makeColumns([])); - $this->assertFalse($this->table->makeColumns(['one', 'two'], '2.5')); + $this->assertFalse($this->table->makeColumns(['one', 'two'], '2.5')); // @phpstan-ignore argument.type (deliberately wrong type) // Now on to the actual column creation @@ -345,12 +345,12 @@ public function testSetFromArray(): void ]; $this->table->autoHeading = false; - $this->table->setFromArray($data); + $this->table->setFromArray($data); // @phpstan-ignore method.notFound (Testing MockTable::__call() fallback) $this->assertEmpty($this->table->heading); $this->table->clear(); - $this->table->setFromArray($data); + $this->table->setFromArray($data); // @phpstan-ignore method.notFound (Testing MockTable::__call() fallback) $this->assertCount(2, $this->table->rows); $expected = [ @@ -376,9 +376,9 @@ public function testSetFromObject(): void $dummy->connID = null; $dummy->resultID = null; - $DBResult = new DBResultDummy($dummy->connID, $dummy->resultID); + $DBResult = new DBResultDummy($dummy->connID, $dummy->resultID); // @phpstan-ignore argument.type, argument.type (deliberately null, exercising the fallback path) - $this->table->setFromDBResult($DBResult); + $this->table->setFromDBResult($DBResult); // @phpstan-ignore method.notFound (Testing MockTable::__call() fallback) $expected = [ ['data' => 'name'], @@ -733,7 +733,7 @@ public function testGenerateFromDBResult(): void $dummy = new stdClass(); $dummy->connID = null; $dummy->resultID = null; - $DBResult = new DBResultDummy($dummy->connID, $dummy->resultID); + $DBResult = new DBResultDummy($dummy->connID, $dummy->resultID); // @phpstan-ignore argument.type, argument.type (deliberately null, exercising the fallback path) $table = $this->table->generate($DBResult); @@ -780,6 +780,10 @@ public function testInvalidCallback(): void $this->assertStringContainsString('FredBlueSmall', $generated); } + /** + * @param array $heading + * @param array $row + */ #[DataProvider('orderedColumnUsecases')] public function testAddRowAndGenerateOrderedColumns(array $heading, array $row, string $expectContainsString): void { @@ -792,6 +796,10 @@ public function testAddRowAndGenerateOrderedColumns(array $heading, array $row, $this->assertStringContainsString($expectContainsString, $generated); } + /** + * @param array $heading + * @param array $row + */ #[DataProvider('orderedColumnUsecases')] public function testGenerateOrderedColumns(array $heading, array $row, string $expectContainsString): void { @@ -803,6 +811,9 @@ public function testGenerateOrderedColumns(array $heading, array $row, string $e $this->assertStringContainsString($expectContainsString, $generated); } + /** + * @return iterable, row: array, expectContainsString: string}> + */ public static function orderedColumnUsecases(): iterable { yield from [ diff --git a/utils/phpstan-baseline/argument.type.neon b/utils/phpstan-baseline/argument.type.neon index 3c70bd498fba..f5e14ec6a08f 100644 --- a/utils/phpstan-baseline/argument.type.neon +++ b/utils/phpstan-baseline/argument.type.neon @@ -1,4 +1,4 @@ -# total 21 errors +# total 12 errors parameters: ignoreErrors: @@ -51,28 +51,3 @@ parameters: message: '#^Parameter \#1 \$format of method CodeIgniter\\RESTful\\ResourceController\:\:setFormat\(\) expects ''json''\|''xml'', ''Nonsense'' given\.$#' count: 1 path: ../../tests/system/RESTful/ResourceControllerTest.php - - - - message: '#^Parameter \#2 \$context of method CodeIgniter\\View\\Parser\:\:setData\(\) expects ''attr''\|''css''\|''html''\|''js''\|''raw''\|''url''\|null, ''unknown'' given\.$#' - count: 3 - path: ../../tests/system/View/ParserTest.php - - - - message: '#^Parameter \#1 \$array of method CodeIgniter\\View\\Table\:\:makeColumns\(\) expects list\, ''invalid_junk'' given\.$#' - count: 1 - path: ../../tests/system/View/TableTest.php - - - - message: '#^Parameter \#1 \$connID of class CodeIgniter\\View\\DBResultDummy constructor expects mysqli, null given\.$#' - count: 2 - path: ../../tests/system/View/TableTest.php - - - - message: '#^Parameter \#2 \$columnLimit of method CodeIgniter\\View\\Table\:\:makeColumns\(\) expects int, string given\.$#' - count: 1 - path: ../../tests/system/View/TableTest.php - - - - message: '#^Parameter \#2 \$resultID of class CodeIgniter\\View\\DBResultDummy constructor expects mysqli_result, null given\.$#' - count: 2 - path: ../../tests/system/View/TableTest.php diff --git a/utils/phpstan-baseline/assign.propertyType.neon b/utils/phpstan-baseline/assign.propertyType.neon index d8ba9db941a4..73d202cb464e 100644 --- a/utils/phpstan-baseline/assign.propertyType.neon +++ b/utils/phpstan-baseline/assign.propertyType.neon @@ -1,4 +1,4 @@ -# total 18 errors +# total 17 errors parameters: ignoreErrors: @@ -51,8 +51,3 @@ parameters: message: '#^Property Config\\Cookie\:\:\$samesite \(''''\|''Lax''\|''None''\|''Strict''\) does not accept ''Invalid''\.$#' count: 1 path: ../../tests/system/Session/SessionTest.php - - - - message: '#^Property CodeIgniter\\View\\ParserPluginTest\:\:\$validator \(CodeIgniter\\Validation\\Validation\) does not accept CodeIgniter\\Validation\\ValidationInterface\.$#' - count: 1 - path: ../../tests/system/View/ParserPluginTest.php diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index eb9a9772134c..ed9db7c3f4ea 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 279 errors +# total 243 errors includes: - argument.type.neon @@ -14,4 +14,3 @@ includes: - property.defaultValue.neon - property.notFound.neon - property.phpDocType.neon - - staticMethod.notFound.neon diff --git a/utils/phpstan-baseline/method.notFound.neon b/utils/phpstan-baseline/method.notFound.neon index 8453adb3b030..72d050dcc45c 100644 --- a/utils/phpstan-baseline/method.notFound.neon +++ b/utils/phpstan-baseline/method.notFound.neon @@ -1,4 +1,4 @@ -# total 16 errors +# total 8 errors parameters: ignoreErrors: @@ -31,28 +31,3 @@ parameters: message: '#^Call to an undefined method CodeIgniter\\HTTP\\ResponseInterface\:\:pretend\(\)\.$#' count: 1 path: ../../tests/system/RESTful/ResourceControllerTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\View\\Table\:\:compileTemplate\(\)\.$#' - count: 2 - path: ../../tests/system/View/TableTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\View\\Table\:\:defaultTemplate\(\)\.$#' - count: 1 - path: ../../tests/system/View/TableTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\View\\Table\:\:prepArgs\(\)\.$#' - count: 2 - path: ../../tests/system/View/TableTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\View\\Table\:\:setFromArray\(\)\.$#' - count: 2 - path: ../../tests/system/View/TableTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\View\\Table\:\:setFromDBResult\(\)\.$#' - count: 1 - path: ../../tests/system/View/TableTest.php diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 21e533524975..95ae423936df 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 178 errors +# total 161 errors parameters: ignoreErrors: @@ -393,65 +393,10 @@ parameters: path: ../../system/Typography/Typography.php - - message: '#^Method CodeIgniter\\View\\Cell\:\:determineClass\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/View/Cell.php - - - - message: '#^Method CodeIgniter\\View\\Cell\:\:getMethodParams\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/View/Cell.php - - - - message: '#^Method CodeIgniter\\View\\Cell\:\:getMethodParams\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/View/Cell.php - - - - message: '#^Method CodeIgniter\\View\\Cell\:\:renderCell\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/View/Cell.php - - - - message: '#^Method CodeIgniter\\View\\Cell\:\:renderSimpleClass\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/View/Cell.php - - - - message: '#^Method CodeIgniter\\View\\Cells\\Cell\:\:fill\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/View/Cells/Cell.php - - - - message: '#^Method CodeIgniter\\View\\Cells\\Cell\:\:getNonPublicProperties\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/View/Cells/Cell.php - - - - message: '#^Method CodeIgniter\\View\\Cells\\Cell\:\:includeComputedProperties\(\) has parameter \$properties with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/View/Cells/Cell.php - - - - message: '#^Method CodeIgniter\\View\\Cells\\Cell\:\:includeComputedProperties\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/View/Cells/Cell.php - - - - message: '#^Method CodeIgniter\\View\\Cells\\Cell\:\:view\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/View/Cells/Cell.php - - - - message: '#^Method class@anonymous/system/Traits/PropertiesTrait\.php\:49\:\:getProperties\(\) return type has no value type specified in iterable type array\.$#' + message: '#^Method class@anonymous/system/Traits/PropertiesTrait\.php\:51\:\:getProperties\(\) return type has no value type specified in iterable type array\.$#' count: 1 path: ../../system/View/Cells/Cell.php - - - message: '#^Method CodeIgniter\\View\\Parser\:\:replaceSingle\(\) has parameter \$pattern with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/View/Parser.php - - message: '#^Method CodeIgniter\\AutoReview\\ComposerJsonTest\:\:checkConfig\(\) has parameter \$fromComponent with no value type specified in iterable type array\.$#' count: 1 @@ -861,33 +806,3 @@ parameters: message: '#^Method CodeIgniter\\Throttle\\ThrottleTest\:\:testTokenTimeCalculationUCs\(\) has parameter \$checkInputs with no value type specified in iterable type array\.$#' count: 1 path: ../../tests/system/Throttle/ThrottleTest.php - - - - message: '#^Method CodeIgniter\\View\\ParserTest\:\:provideEscHandling\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/View/ParserTest.php - - - - message: '#^Method CodeIgniter\\View\\TableTest\:\:orderedColumnUsecases\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/View/TableTest.php - - - - message: '#^Method CodeIgniter\\View\\TableTest\:\:testAddRowAndGenerateOrderedColumns\(\) has parameter \$heading with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/View/TableTest.php - - - - message: '#^Method CodeIgniter\\View\\TableTest\:\:testAddRowAndGenerateOrderedColumns\(\) has parameter \$row with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/View/TableTest.php - - - - message: '#^Method CodeIgniter\\View\\TableTest\:\:testGenerateOrderedColumns\(\) has parameter \$heading with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/View/TableTest.php - - - - message: '#^Method CodeIgniter\\View\\TableTest\:\:testGenerateOrderedColumns\(\) has parameter \$row with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/View/TableTest.php diff --git a/utils/phpstan-baseline/staticMethod.notFound.neon b/utils/phpstan-baseline/staticMethod.notFound.neon deleted file mode 100644 index 09b088e3c84f..000000000000 --- a/utils/phpstan-baseline/staticMethod.notFound.neon +++ /dev/null @@ -1,8 +0,0 @@ -# total 1 error - -parameters: - ignoreErrors: - - - message: '#^Call to an undefined static method CodeIgniter\\Config\\Factories\:\:cells\(\)\.$#' - count: 1 - path: ../../system/View/Cell.php From 0a7b317f6046bda20d3d29da78e0c7a74f36051f Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Wed, 26 Aug 2026 03:32:02 +0800 Subject: [PATCH 079/163] refactor: fix phpstan errors in `Filters` (#10485) --- system/Filters/Filters.php | 18 +-- system/Filters/ForceHTTPS.php | 4 +- system/Filters/InvalidChars.php | 8 +- system/Filters/PageCache.php | 4 +- system/Filters/PerformanceMetrics.php | 4 +- tests/system/Filters/CSRFTest.php | 14 ++- tests/system/Filters/DebugToolbarTest.php | 7 +- tests/system/Filters/FiltersTest.php | 22 +++- tests/system/Filters/HoneypotTest.php | 36 ++++-- tests/system/Filters/InvalidCharsTest.php | 6 + .../phpstan-baseline/assign.propertyType.neon | 27 +---- utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 112 +----------------- .../missingType.parameter.neon | 7 +- 14 files changed, 88 insertions(+), 183 deletions(-) diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 38bd553249bb..973b9ded7979 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -369,6 +369,8 @@ public function runRequired(string $position = 'before') * * @param 'after'|'before' $position * + * @return array{list, array>} + * * @internal */ public function getRequiredFilters(string $position = 'before'): array @@ -410,7 +412,9 @@ public function getRequiredFilters(string $position = 'before'): array * Set the toolbar filter to the last position to be executed. * * @param list $filters `after` filter array - * @param bool $remove if true, remove `toolbar` filter + * @param bool $remove If true, remove `toolbar` filter + * + * @return list */ private function setToolbarToLast(array $filters, bool $remove = false): array { @@ -853,8 +857,8 @@ protected function processAliasesToClass(string $position) /** * Check paths for match for URI * - * @param string $uri URI to test against - * @param array|string $paths The path patterns to test + * @param string $uri URI to test against + * @param array|string $paths The path patterns to test * * @return bool True if any of the paths apply to the URI */ @@ -876,8 +880,8 @@ private function pathApplies(string $uri, $paths) /** * Check except paths * - * @param string $uri URI path relative to baseURL (all lowercase) - * @param array|string $paths The except path patterns + * @param string $uri URI path relative to baseURL (all lowercase) + * @param array|string $paths The except path patterns * * @return bool True if the URI matches except paths. */ @@ -899,8 +903,8 @@ private function checkExcept(string $uri, $paths): bool /** * Check the URI path as pseudo-regex * - * @param string $uri URI path relative to baseURL (all lowercase, URL-decoded) - * @param array $paths The except path patterns + * @param string $uri URI path relative to baseURL (all lowercase, URL-decoded) + * @param array $paths The except path patterns */ private function checkPseudoRegex(string $uri, array $paths): bool { diff --git a/system/Filters/ForceHTTPS.php b/system/Filters/ForceHTTPS.php index aa9cb3716d08..6f59338d948f 100644 --- a/system/Filters/ForceHTTPS.php +++ b/system/Filters/ForceHTTPS.php @@ -30,7 +30,7 @@ class ForceHTTPS implements FilterInterface * as set the HTTP Strict Transport Security (HSTS) header for those browsers * that support it. * - * @param array|null $arguments + * @param list|null $arguments * * @return ResponseInterface|null */ @@ -56,7 +56,7 @@ public function before(RequestInterface $request, $arguments = null) /** * We don't have anything to do here. * - * @param array|null $arguments + * @param list|null $arguments */ public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { diff --git a/system/Filters/InvalidChars.php b/system/Filters/InvalidChars.php index 85e71c5d4f59..5d714b0fdf11 100644 --- a/system/Filters/InvalidChars.php +++ b/system/Filters/InvalidChars.php @@ -84,9 +84,9 @@ public function after(RequestInterface $request, ResponseInterface $response, $a /** * Check the character encoding is valid UTF-8. * - * @param array|string $value + * @param array|string $value * - * @return array|string + * @return array|string * * @throws SecurityException */ @@ -113,9 +113,9 @@ protected function checkEncoding($value) /** * Check for the presence of control characters except line breaks and tabs. * - * @param array|string $value + * @param array|string $value * - * @return array|string + * @return array|string */ protected function checkControl($value) { diff --git a/system/Filters/PageCache.php b/system/Filters/PageCache.php index ff4bd097f84c..d292ff9875c9 100644 --- a/system/Filters/PageCache.php +++ b/system/Filters/PageCache.php @@ -45,7 +45,7 @@ public function __construct(?Cache $config = null) /** * Checks page cache and return if found. * - * @param array|null $arguments + * @param list|null $arguments * * @return ResponseInterface|null */ @@ -61,7 +61,7 @@ public function before(RequestInterface $request, $arguments = null) /** * Cache the page. * - * @param array|null $arguments + * @param list|null $arguments */ public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { diff --git a/system/Filters/PerformanceMetrics.php b/system/Filters/PerformanceMetrics.php index 97582ad8dc05..259f7d46e298 100644 --- a/system/Filters/PerformanceMetrics.php +++ b/system/Filters/PerformanceMetrics.php @@ -24,7 +24,7 @@ class PerformanceMetrics implements FilterInterface /** * We don't need to do anything here. * - * @param array|null $arguments + * @param list|null $arguments */ public function before(RequestInterface $request, $arguments = null) { @@ -34,7 +34,7 @@ public function before(RequestInterface $request, $arguments = null) /** * Replaces the performance metrics. * - * @param array|null $arguments + * @param list|null $arguments */ public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { diff --git a/tests/system/Filters/CSRFTest.php b/tests/system/Filters/CSRFTest.php index 71a6dfc28e7e..d258755f4870 100644 --- a/tests/system/Filters/CSRFTest.php +++ b/tests/system/Filters/CSRFTest.php @@ -53,8 +53,11 @@ public function testDoNotCheckCliRequest(): void 'after' => [], ]; - $this->request = Services::clirequest(null, false); - $this->response = service('response'); + $this->request = Services::clirequest(null, false); + + $response = service('response'); + $this->assertInstanceOf(Response::class, $response); + $this->response = $response; $filters = new Filters($this->config, $this->request, $this->response); $uri = 'admin/foo/bar'; @@ -71,8 +74,11 @@ public function testPassGetRequest(): void 'after' => [], ]; - $this->request = service('incomingrequest', null, false); - $this->response = service('response'); + $this->request = service('incomingrequest', null, false); + + $response = service('response'); + $this->assertInstanceOf(Response::class, $response); + $this->response = $response; $filters = new Filters($this->config, $this->request, $this->response); $uri = 'admin/foo/bar'; diff --git a/tests/system/Filters/DebugToolbarTest.php b/tests/system/Filters/DebugToolbarTest.php index 905dd46cf15a..fc9f8f179776 100644 --- a/tests/system/Filters/DebugToolbarTest.php +++ b/tests/system/Filters/DebugToolbarTest.php @@ -45,8 +45,11 @@ protected function setUp(): void Services::injectMock('superglobals', new Superglobals()); - $this->request = service('request'); - $this->response = service('response'); + $this->request = service('request'); + + $response = service('response'); + $this->assertInstanceOf(Response::class, $response); + $this->response = $response; } public function testDebugToolbarFilter(): void diff --git a/tests/system/Filters/FiltersTest.php b/tests/system/Filters/FiltersTest.php index 11567f429c1d..d696498efaa2 100644 --- a/tests/system/Filters/FiltersTest.php +++ b/tests/system/Filters/FiltersTest.php @@ -24,6 +24,7 @@ use CodeIgniter\Filters\fixtures\Multiple2; use CodeIgniter\Filters\fixtures\Role; use CodeIgniter\HTTP\CLIRequest; +use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\Response; use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\Superglobals; @@ -70,10 +71,12 @@ protected function setUp(): void Services::injectMock('superglobals', new Superglobals()); - $this->response = service('response'); + $response = service('response'); + $this->assertInstanceOf(Response::class, $response); + $this->response = $response; } - private function createFilters(FiltersConfig $config, $request = null): Filters + private function createFilters(FiltersConfig $config, ?RequestInterface $request = null): Filters { $request ??= service('request'); @@ -213,10 +216,10 @@ public function testProcessMethodProcessGlobals(): void } /** - * @param array|string $except + * @param list|string $except */ #[DataProvider('provideProcessMethodProcessGlobalsWithExcept')] - public function testProcessMethodProcessGlobalsWithExcept($except): void + public function testProcessMethodProcessGlobalsWithExcept(array|string $except): void { service('superglobals')->setServer('REQUEST_METHOD', 'GET'); @@ -249,6 +252,9 @@ public function testProcessMethodProcessGlobalsWithExcept($except): void $this->assertSame($expected, $filters->initialize($uri)->getFilters()); } + /** + * @return iterable|string}> + */ public static function provideProcessMethodProcessGlobalsWithExcept(): iterable { return [ @@ -590,10 +596,11 @@ public function testOtherResult(): void } /** - * @param array|string $except + * @param list|string $except + * @param array{before: list, after: list} $expected */ #[DataProvider('provideBeforeExcept')] - public function testBeforeExcept(string $uri, $except, array $expected): void + public function testBeforeExcept(string $uri, array|string $except, array $expected): void { service('superglobals')->setServer('REQUEST_METHOD', 'GET'); @@ -619,6 +626,9 @@ public function testBeforeExcept(string $uri, $except, array $expected): void $this->assertSame($expected, $filters->initialize($uri)->getFilters()); } + /** + * @return iterable|string, array{before: list, after: list}}> + */ public static function provideBeforeExcept(): iterable { return [ diff --git a/tests/system/Filters/HoneypotTest.php b/tests/system/Filters/HoneypotTest.php index 630a882352ec..8ad3106f1a9f 100644 --- a/tests/system/Filters/HoneypotTest.php +++ b/tests/system/Filters/HoneypotTest.php @@ -65,8 +65,11 @@ public function testBeforeTriggered(): void 'after' => [], ]; - $this->request = service('request', null, false); - $this->response = service('response'); + $this->request = service('request', null, false); + + $response = service('response'); + $this->assertInstanceOf(Response::class, $response); + $this->response = $response; $filters = new Filters($this->config, $this->request, $this->response); $uri = 'admin/foo/bar'; @@ -83,8 +86,11 @@ public function testBeforeClean(): void ]; service('superglobals')->unsetPost($this->honey->name); - $this->request = service('request', null, false); - $this->response = service('response'); + $this->request = service('request', null, false); + + $response = service('response'); + $this->assertInstanceOf(Response::class, $response); + $this->response = $response; $expected = $this->request; @@ -104,14 +110,19 @@ public function testAfter(): void 'after' => ['honeypot'], ]; - $this->request = service('request', null, false); - $this->response = service('response'); + $this->request = service('request', null, false); + + $response = service('response'); + $this->assertInstanceOf(Response::class, $response); + $this->response = $response; $filters = new Filters($this->config, $this->request, $this->response); $uri = 'admin/foo/bar'; $this->response->setBody('
'); - $this->response = $filters->run($uri, 'after'); + $response = $filters->run($uri, 'after'); + $this->assertInstanceOf(Response::class, $response); + $this->response = $response; $this->assertStringContainsString($this->honey->name, (string) $this->response->getBody()); } @@ -124,14 +135,19 @@ public function testAfterNotApplicable(): void 'after' => ['honeypot'], ]; - $this->request = service('request', null, false); - $this->response = service('response'); + $this->request = service('request', null, false); + + $response = service('response'); + $this->assertInstanceOf(Response::class, $response); + $this->response = $response; $filters = new Filters($this->config, $this->request, $this->response); $uri = 'admin/foo/bar'; $this->response->setBody('
'); - $this->response = $filters->run($uri, 'after'); + $response = $filters->run($uri, 'after'); + $this->assertInstanceOf(Response::class, $response); + $this->response = $response; $this->assertStringNotContainsString($this->honey->name, (string) $this->response->getBody()); } } diff --git a/tests/system/Filters/InvalidCharsTest.php b/tests/system/Filters/InvalidCharsTest.php index 4cf112b6da91..66588b5b1a35 100644 --- a/tests/system/Filters/InvalidCharsTest.php +++ b/tests/system/Filters/InvalidCharsTest.php @@ -161,6 +161,9 @@ public function testCheckControlStringWithLineBreakAndTabReturnsTheString(string $this->invalidChars->before($this->request); } + /** + * @return iterable + */ public static function provideCheckControlStringWithLineBreakAndTabReturnsTheString(): iterable { yield from [ @@ -183,6 +186,9 @@ public function testCheckControlStringWithControlCharsCausesException(string $in $this->invalidChars->before($this->request); } + /** + * @return iterable + */ public static function provideCheckControlStringWithControlCharsCausesException(): iterable { yield from [ diff --git a/utils/phpstan-baseline/assign.propertyType.neon b/utils/phpstan-baseline/assign.propertyType.neon index 73d202cb464e..51c177310b90 100644 --- a/utils/phpstan-baseline/assign.propertyType.neon +++ b/utils/phpstan-baseline/assign.propertyType.neon @@ -1,4 +1,4 @@ -# total 17 errors +# total 7 errors parameters: ignoreErrors: @@ -12,31 +12,6 @@ parameters: count: 1 path: ../../system/Controller.php - - - message: '#^Property CodeIgniter\\Filters\\CSRFTest\:\:\$response \(CodeIgniter\\HTTP\\Response\|null\) does not accept CodeIgniter\\HTTP\\ResponseInterface\.$#' - count: 2 - path: ../../tests/system/Filters/CSRFTest.php - - - - message: '#^Property CodeIgniter\\Filters\\DebugToolbarTest\:\:\$response \(CodeIgniter\\HTTP\\Response\) does not accept CodeIgniter\\HTTP\\ResponseInterface\.$#' - count: 1 - path: ../../tests/system/Filters/DebugToolbarTest.php - - - - message: '#^Property CodeIgniter\\Filters\\FiltersTest\:\:\$response \(CodeIgniter\\HTTP\\Response\) does not accept CodeIgniter\\HTTP\\ResponseInterface\.$#' - count: 1 - path: ../../tests/system/Filters/FiltersTest.php - - - - message: '#^Property CodeIgniter\\Filters\\HoneypotTest\:\:\$response \(CodeIgniter\\HTTP\\Response\|null\) does not accept CodeIgniter\\HTTP\\RequestInterface\|CodeIgniter\\HTTP\\ResponseInterface\|string\|null\.$#' - count: 2 - path: ../../tests/system/Filters/HoneypotTest.php - - - - message: '#^Property CodeIgniter\\Filters\\HoneypotTest\:\:\$response \(CodeIgniter\\HTTP\\Response\|null\) does not accept CodeIgniter\\HTTP\\ResponseInterface\.$#' - count: 4 - path: ../../tests/system/Filters/HoneypotTest.php - - message: '#^Property CodeIgniter\\Honeypot\\HoneypotTest\:\:\$response \(CodeIgniter\\HTTP\\Response\) does not accept CodeIgniter\\HTTP\\RequestInterface\|CodeIgniter\\HTTP\\ResponseInterface\|string\|null\.$#' count: 1 diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index ed9db7c3f4ea..f4afd2ab61a8 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 243 errors +# total 210 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 95ae423936df..1927116d4675 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 161 errors +# total 139 errors parameters: ignoreErrors: @@ -227,81 +227,6 @@ parameters: count: 1 path: ../../system/Exceptions/PageNotFoundException.php - - - message: '#^Method CodeIgniter\\Filters\\Filters\:\:checkExcept\(\) has parameter \$paths with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Filters/Filters.php - - - - message: '#^Method CodeIgniter\\Filters\\Filters\:\:checkPseudoRegex\(\) has parameter \$paths with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Filters/Filters.php - - - - message: '#^Method CodeIgniter\\Filters\\Filters\:\:getRequiredFilters\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Filters/Filters.php - - - - message: '#^Method CodeIgniter\\Filters\\Filters\:\:pathApplies\(\) has parameter \$paths with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Filters/Filters.php - - - - message: '#^Method CodeIgniter\\Filters\\Filters\:\:setToolbarToLast\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Filters/Filters.php - - - - message: '#^Method CodeIgniter\\Filters\\ForceHTTPS\:\:after\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Filters/ForceHTTPS.php - - - - message: '#^Method CodeIgniter\\Filters\\ForceHTTPS\:\:before\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Filters/ForceHTTPS.php - - - - message: '#^Method CodeIgniter\\Filters\\InvalidChars\:\:checkControl\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Filters/InvalidChars.php - - - - message: '#^Method CodeIgniter\\Filters\\InvalidChars\:\:checkControl\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Filters/InvalidChars.php - - - - message: '#^Method CodeIgniter\\Filters\\InvalidChars\:\:checkEncoding\(\) has parameter \$value with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Filters/InvalidChars.php - - - - message: '#^Method CodeIgniter\\Filters\\InvalidChars\:\:checkEncoding\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Filters/InvalidChars.php - - - - message: '#^Method CodeIgniter\\Filters\\PageCache\:\:after\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Filters/PageCache.php - - - - message: '#^Method CodeIgniter\\Filters\\PageCache\:\:before\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Filters/PageCache.php - - - - message: '#^Method CodeIgniter\\Filters\\PerformanceMetrics\:\:after\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Filters/PerformanceMetrics.php - - - - message: '#^Method CodeIgniter\\Filters\\PerformanceMetrics\:\:before\(\) has parameter \$arguments with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Filters/PerformanceMetrics.php - - message: '#^Method CodeIgniter\\HotReloader\\DirectoryHasher\:\:hashApp\(\) return type has no value type specified in iterable type array\.$#' count: 1 @@ -642,41 +567,6 @@ parameters: count: 1 path: ../../tests/system/Email/EmailTest.php - - - message: '#^Method CodeIgniter\\Filters\\FiltersTest\:\:provideBeforeExcept\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Filters/FiltersTest.php - - - - message: '#^Method CodeIgniter\\Filters\\FiltersTest\:\:provideProcessMethodProcessGlobalsWithExcept\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Filters/FiltersTest.php - - - - message: '#^Method CodeIgniter\\Filters\\FiltersTest\:\:testBeforeExcept\(\) has parameter \$except with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Filters/FiltersTest.php - - - - message: '#^Method CodeIgniter\\Filters\\FiltersTest\:\:testBeforeExcept\(\) has parameter \$expected with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Filters/FiltersTest.php - - - - message: '#^Method CodeIgniter\\Filters\\FiltersTest\:\:testProcessMethodProcessGlobalsWithExcept\(\) has parameter \$except with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Filters/FiltersTest.php - - - - message: '#^Method CodeIgniter\\Filters\\InvalidCharsTest\:\:provideCheckControlStringWithControlCharsCausesException\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Filters/InvalidCharsTest.php - - - - message: '#^Method CodeIgniter\\Filters\\InvalidCharsTest\:\:provideCheckControlStringWithLineBreakAndTabReturnsTheString\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Filters/InvalidCharsTest.php - - message: '#^Method CodeIgniter\\I18n\\TimeLegacyTest\:\:provideToStringDoesNotDependOnLocale\(\) return type has no value type specified in iterable type iterable\.$#' count: 1 diff --git a/utils/phpstan-baseline/missingType.parameter.neon b/utils/phpstan-baseline/missingType.parameter.neon index 26a303d346d8..49f9dffb6507 100644 --- a/utils/phpstan-baseline/missingType.parameter.neon +++ b/utils/phpstan-baseline/missingType.parameter.neon @@ -1,4 +1,4 @@ -# total 4 errors +# total 3 errors parameters: ignoreErrors: @@ -7,11 +7,6 @@ parameters: count: 1 path: ../../tests/system/Entity/EntityTest.php - - - message: '#^Method CodeIgniter\\Filters\\FiltersTest\:\:createFilters\(\) has parameter \$request with no type specified\.$#' - count: 1 - path: ../../tests/system/Filters/FiltersTest.php - - message: '#^Method CodeIgniter\\Security\\SecurityCSRFSessionRandomizeTokenTest\:\:createSession\(\) has parameter \$options with no type specified\.$#' count: 1 From e4b057e1fd15327f43991773689db1ff031dd2d1 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Wed, 26 Aug 2026 03:49:10 +0800 Subject: [PATCH 080/163] refactor: fix phpstan errors in `Encryption` (#10486) --- system/Encryption/EncrypterInterface.php | 8 +-- system/Encryption/Encryption.php | 14 +++-- system/Encryption/Handlers/OpenSSLHandler.php | 5 +- system/Encryption/Handlers/SodiumHandler.php | 5 +- system/Encryption/KeyRotationDecorator.php | 3 + tests/system/Encryption/EncryptionTest.php | 6 +- .../Handlers/OpenSSLHandlerTest.php | 3 + .../Encryption/Handlers/SodiumHandlerTest.php | 4 +- .../Encryption/KeyRotationDecoratorTest.php | 1 + utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 62 +------------------ utils/phpstan-baseline/property.notFound.neon | 47 +------------- 12 files changed, 39 insertions(+), 121 deletions(-) diff --git a/system/Encryption/EncrypterInterface.php b/system/Encryption/EncrypterInterface.php index 9c1b0d36d1be..1c916e8b390c 100644 --- a/system/Encryption/EncrypterInterface.php +++ b/system/Encryption/EncrypterInterface.php @@ -26,8 +26,8 @@ interface EncrypterInterface /** * Encrypt - convert plaintext into ciphertext * - * @param string $data Input data - * @param array|string|null $params Overridden parameters, specifically the key + * @param string $data Input data + * @param array|string|null $params Overridden parameters, specifically the key * * @return string * @@ -38,8 +38,8 @@ public function encrypt(#[SensitiveParameter] $data, #[SensitiveParameter] $para /** * Decrypt - convert ciphertext into plaintext * - * @param string $data Encrypted data - * @param array|string|null $params Overridden parameters, specifically the key + * @param string $data Encrypted data + * @param array|string|null $params Overridden parameters, specifically the key * * @return string * diff --git a/system/Encryption/Encryption.php b/system/Encryption/Encryption.php index 47a6ec649e1d..d24a6754efab 100644 --- a/system/Encryption/Encryption.php +++ b/system/Encryption/Encryption.php @@ -70,7 +70,7 @@ class Encryption /** * Map of drivers to handler classes, in preference order * - * @var array + * @var list */ protected $drivers = [ 'OpenSSL', @@ -138,8 +138,14 @@ public function initialize(?EncryptionConfig $config = null) $handlerName = 'CodeIgniter\\Encryption\\Handlers\\' . $this->driver . 'Handler'; $this->encrypter = new $handlerName($config); - if (($config->previousKeys ?? []) !== []) { - $this->encrypter = new KeyRotationDecorator($this->encrypter, $config->previousKeys); + // (array) '' is [''], not [], so the unset default must be filtered out here. + $previousKeys = array_values(array_filter( + (array) ($config->previousKeys ?? []), + static fn ($key): bool => $key !== '', + )); + + if ($previousKeys !== []) { + $this->encrypter = new KeyRotationDecorator($this->encrypter, $previousKeys); } return $this->encrypter; @@ -162,7 +168,7 @@ public static function createKey($length = 32) * * @param string $key Property name * - * @return array|string|null + * @return list|string|null */ public function __get($key) { diff --git a/system/Encryption/Handlers/OpenSSLHandler.php b/system/Encryption/Handlers/OpenSSLHandler.php index 26c10a258536..d649a66bf060 100644 --- a/system/Encryption/Handlers/OpenSSLHandler.php +++ b/system/Encryption/Handlers/OpenSSLHandler.php @@ -19,6 +19,9 @@ /** * Encryption handling for OpenSSL library * + * @property-read string $cipher + * @property-read string $key + * * @see \CodeIgniter\Encryption\Handlers\OpenSSLHandlerTest */ class OpenSSLHandler extends BaseHandler @@ -33,7 +36,7 @@ class OpenSSLHandler extends BaseHandler /** * List of supported HMAC algorithms * - * @var array [name => digest size] + * @var array [name => digest size] */ protected array $digestSize = [ 'SHA224' => 28, diff --git a/system/Encryption/Handlers/SodiumHandler.php b/system/Encryption/Handlers/SodiumHandler.php index c759bdbf518f..0fa1bd235247 100644 --- a/system/Encryption/Handlers/SodiumHandler.php +++ b/system/Encryption/Handlers/SodiumHandler.php @@ -20,6 +20,9 @@ /** * SodiumHandler uses libsodium in encryption. * + * @property-read int $blockSize + * @property-read string|null $key + * * @see https://github.com/jedisct1/libsodium/issues/392 * @see \CodeIgniter\Encryption\Handlers\SodiumHandlerTest */ @@ -124,7 +127,7 @@ public function decrypt($data, #[SensitiveParameter] $params = null) /** * Parse the $params before doing assignment. * - * @param array|string|null $params + * @param array|string|null $params * * @return void * diff --git a/system/Encryption/KeyRotationDecorator.php b/system/Encryption/KeyRotationDecorator.php index 7202bcc06764..46846dead1bb 100644 --- a/system/Encryption/KeyRotationDecorator.php +++ b/system/Encryption/KeyRotationDecorator.php @@ -22,6 +22,9 @@ * Wraps any EncrypterInterface implementation to provide automatic * fallback to previous encryption keys during decryption. This enables * seamless key rotation without requiring re-encryption of existing data. + * + * @property-read string|null $cipher + * @property-read string|null $key */ class KeyRotationDecorator implements EncrypterInterface { diff --git a/tests/system/Encryption/EncryptionTest.php b/tests/system/Encryption/EncryptionTest.php index 48fd6dc78e73..81c4554336e0 100644 --- a/tests/system/Encryption/EncryptionTest.php +++ b/tests/system/Encryption/EncryptionTest.php @@ -15,6 +15,7 @@ use CodeIgniter\Config\Services as CodeIgniterServices; use CodeIgniter\Encryption\Exceptions\EncryptionException; +use CodeIgniter\Encryption\Handlers\OpenSSLHandler; use CodeIgniter\Superglobals; use CodeIgniter\Test\CIUnitTestCase; use Config\Encryption as EncryptionConfig; @@ -156,6 +157,7 @@ public function testServiceShared(): void $config->key = 'Abracadabra'; $encrypter = Services::encrypter($config, true); + $this->assertInstanceOf(OpenSSLHandler::class, $encrypter); $this->assertSame('anything', $encrypter->key); } @@ -166,7 +168,7 @@ public function testMagicIssetTrue(): void public function testMagicIssetFalse(): void { - $this->assertFalse(isset($this->encryption->bogus)); + $this->assertFalse(isset($this->encryption->bogus)); // @phpstan-ignore property.notFound } public function testMagicGet(): void @@ -176,7 +178,7 @@ public function testMagicGet(): void public function testMagicGetMissing(): void { - $this->assertNull($this->encryption->bogus); + $this->assertNull($this->encryption->bogus); // @phpstan-ignore property.notFound } public function testDecryptEncryptedDataByCI3AES128CBC(): void diff --git a/tests/system/Encryption/Handlers/OpenSSLHandlerTest.php b/tests/system/Encryption/Handlers/OpenSSLHandlerTest.php index e883c5b2fbc6..bac22a881f0a 100644 --- a/tests/system/Encryption/Handlers/OpenSSLHandlerTest.php +++ b/tests/system/Encryption/Handlers/OpenSSLHandlerTest.php @@ -46,6 +46,7 @@ public function testSanity(): void $params->key = 'Something other than an empty string'; $encrypter = $this->encryption->initialize($params); + $this->assertInstanceOf(OpenSSLHandler::class, $encrypter); $this->assertSame('AES-256-CTR', $encrypter->cipher); $this->assertSame('Something other than an empty string', $encrypter->key); @@ -64,6 +65,7 @@ public function testSimple(): void $params->key = '\xd0\xc9\x08\xc4\xde\x52\x12\x6e\xf8\xcc\xdb\x03\xea\xa0\x3a\x5c'; // Default state (AES-256/Rijndael-256 in CTR mode) $encrypter = $this->encryption->initialize($params); + $this->assertInstanceOf(OpenSSLHandler::class, $encrypter); // Was the key properly set? $this->assertSame($params->key, $encrypter->key); @@ -145,6 +147,7 @@ public function testInternalKeyNotModifiedByParams(): void $params->key = 'original-key-value'; $encrypter = $this->encryption->initialize($params); + $this->assertInstanceOf(OpenSSLHandler::class, $encrypter); $this->assertSame('original-key-value', $encrypter->key); diff --git a/tests/system/Encryption/Handlers/SodiumHandlerTest.php b/tests/system/Encryption/Handlers/SodiumHandlerTest.php index 09b1784e4a12..47406cef00d0 100644 --- a/tests/system/Encryption/Handlers/SodiumHandlerTest.php +++ b/tests/system/Encryption/Handlers/SodiumHandlerTest.php @@ -47,10 +47,11 @@ public function testPropertiesGetter(): void $this->config->key = sodium_crypto_secretbox_keygen(); $this->config->blockSize = 256; $encrypter = $this->encryption->initialize($this->config); + $this->assertInstanceOf(SodiumHandler::class, $encrypter); $this->assertSame($this->config->key, $encrypter->key); $this->assertSame($this->config->blockSize, $encrypter->blockSize); - $this->assertNull($encrypter->driver); + $this->assertNull($encrypter->driver); // @phpstan-ignore property.notFound } public function testEmptyKeyThrowsErrorOnInitialize(): void @@ -136,6 +137,7 @@ public function testInternalKeyNotModifiedByParams(): void $this->config->key = $originalKey; $encrypter = $this->encryption->initialize($this->config); + $this->assertInstanceOf(SodiumHandler::class, $encrypter); $this->assertSame($originalKey, $encrypter->key); diff --git a/tests/system/Encryption/KeyRotationDecoratorTest.php b/tests/system/Encryption/KeyRotationDecoratorTest.php index e94ea16bcd65..1f86220739db 100644 --- a/tests/system/Encryption/KeyRotationDecoratorTest.php +++ b/tests/system/Encryption/KeyRotationDecoratorTest.php @@ -193,6 +193,7 @@ public function testPropertyAccessDelegation(): void $params->previousKeys = ['old-key']; $encrypter = $this->encryption->initialize($params); + $this->assertInstanceOf(KeyRotationDecorator::class, $encrypter); $this->assertSame('AES-128-CBC', $encrypter->cipher); $this->assertSame('test-key-very-long', $encrypter->key); diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index f4afd2ab61a8..56afc2207507 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 210 errors +# total 185 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 1927116d4675..12b4aba4fafc 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 139 errors +# total 127 errors parameters: ignoreErrors: @@ -162,66 +162,6 @@ parameters: count: 1 path: ../../system/Email/Email.php - - - message: '#^Method CodeIgniter\\Encryption\\EncrypterInterface\:\:decrypt\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Encryption/EncrypterInterface.php - - - - message: '#^Method CodeIgniter\\Encryption\\EncrypterInterface\:\:encrypt\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Encryption/EncrypterInterface.php - - - - message: '#^Method CodeIgniter\\Encryption\\Encryption\:\:__get\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Encryption/Encryption.php - - - - message: '#^Property CodeIgniter\\Encryption\\Encryption\:\:\$drivers type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Encryption/Encryption.php - - - - message: '#^Method CodeIgniter\\Encryption\\Handlers\\OpenSSLHandler\:\:decrypt\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Encryption/Handlers/OpenSSLHandler.php - - - - message: '#^Method CodeIgniter\\Encryption\\Handlers\\OpenSSLHandler\:\:encrypt\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Encryption/Handlers/OpenSSLHandler.php - - - - message: '#^Property CodeIgniter\\Encryption\\Handlers\\OpenSSLHandler\:\:\$digestSize type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Encryption/Handlers/OpenSSLHandler.php - - - - message: '#^Method CodeIgniter\\Encryption\\Handlers\\SodiumHandler\:\:decrypt\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Encryption/Handlers/SodiumHandler.php - - - - message: '#^Method CodeIgniter\\Encryption\\Handlers\\SodiumHandler\:\:encrypt\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Encryption/Handlers/SodiumHandler.php - - - - message: '#^Method CodeIgniter\\Encryption\\Handlers\\SodiumHandler\:\:parseParams\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Encryption/Handlers/SodiumHandler.php - - - - message: '#^Method CodeIgniter\\Encryption\\KeyRotationDecorator\:\:decrypt\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Encryption/KeyRotationDecorator.php - - - - message: '#^Method CodeIgniter\\Encryption\\KeyRotationDecorator\:\:encrypt\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Encryption/KeyRotationDecorator.php - - message: '#^Method CodeIgniter\\Exceptions\\PageNotFoundException\:\:lang\(\) has parameter \$args with no value type specified in iterable type array\.$#' count: 1 diff --git a/utils/phpstan-baseline/property.notFound.neon b/utils/phpstan-baseline/property.notFound.neon index f55cee7b3de4..1a8daae7b918 100644 --- a/utils/phpstan-baseline/property.notFound.neon +++ b/utils/phpstan-baseline/property.notFound.neon @@ -1,52 +1,7 @@ -# total 19 errors +# total 6 errors parameters: ignoreErrors: - - - message: '#^Access to an undefined property CodeIgniter\\Encryption\\EncrypterInterface\:\:\$key\.$#' - count: 1 - path: ../../tests/system/Encryption/EncryptionTest.php - - - - message: '#^Access to an undefined property CodeIgniter\\Encryption\\Encryption\:\:\$bogus\.$#' - count: 2 - path: ../../tests/system/Encryption/EncryptionTest.php - - - - message: '#^Access to an undefined property CodeIgniter\\Encryption\\EncrypterInterface\:\:\$cipher\.$#' - count: 1 - path: ../../tests/system/Encryption/Handlers/OpenSSLHandlerTest.php - - - - message: '#^Access to an undefined property CodeIgniter\\Encryption\\EncrypterInterface\:\:\$key\.$#' - count: 3 - path: ../../tests/system/Encryption/Handlers/OpenSSLHandlerTest.php - - - - message: '#^Access to an undefined property CodeIgniter\\Encryption\\EncrypterInterface\:\:\$blockSize\.$#' - count: 1 - path: ../../tests/system/Encryption/Handlers/SodiumHandlerTest.php - - - - message: '#^Access to an undefined property CodeIgniter\\Encryption\\EncrypterInterface\:\:\$driver\.$#' - count: 1 - path: ../../tests/system/Encryption/Handlers/SodiumHandlerTest.php - - - - message: '#^Access to an undefined property CodeIgniter\\Encryption\\EncrypterInterface\:\:\$key\.$#' - count: 2 - path: ../../tests/system/Encryption/Handlers/SodiumHandlerTest.php - - - - message: '#^Access to an undefined property CodeIgniter\\Encryption\\EncrypterInterface\:\:\$cipher\.$#' - count: 1 - path: ../../tests/system/Encryption/KeyRotationDecoratorTest.php - - - - message: '#^Access to an undefined property CodeIgniter\\Encryption\\EncrypterInterface\:\:\$key\.$#' - count: 1 - path: ../../tests/system/Encryption/KeyRotationDecoratorTest.php - - message: '#^Access to an undefined property CodeIgniter\\I18n\\TimeLegacy\:\:\$foobar\.$#' count: 1 From 6b8e12a02ef4a46e885413f3fc1f7f1aadb7cb5e Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Wed, 26 Aug 2026 03:58:54 +0800 Subject: [PATCH 081/163] refactor: fix iterable types in `Email` (#10487) --- system/Email/Email.php | 41 +++++---- tests/system/Email/EmailTest.php | 3 + utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 87 +------------------ 4 files changed, 29 insertions(+), 104 deletions(-) diff --git a/system/Email/Email.php b/system/Email/Email.php index 71c68d9e2841..a3d948d9edfd 100644 --- a/system/Email/Email.php +++ b/system/Email/Email.php @@ -30,14 +30,14 @@ class Email /** * Properties from the last successful send. * - * @var array|null + * @var array|null */ public $archive; /** * Properties to be added to the next archive. * - * @var array + * @var array|string> */ protected $tmpArchive = []; @@ -298,7 +298,7 @@ class Email * * @see Email::printDebugger() * - * @var array + * @var list */ protected $debugMessage = []; @@ -312,35 +312,42 @@ class Email /** * Recipients * - * @var array|string + * @var list */ protected $recipients = []; /** * CC Recipients * - * @var array + * @var list */ protected $CCArray = []; /** * BCC Recipients * - * @var array + * @var list */ protected $BCCArray = []; /** * Message headers * - * @var array + * @var array */ protected $headers = []; /** * Attachment data * - * @var array + * @var list */ protected $attachments = []; @@ -405,7 +412,7 @@ class Email protected static $func_overload; /** - * @param array|\Config\Email|null $config + * @param array|\Config\Email|null $config */ public function __construct($config = null) { @@ -419,7 +426,7 @@ public function __construct($config = null) /** * Initialize preferences * - * @param array|\Config\Email|null $config + * @param array|\Config\Email|null $config * * @return $this */ @@ -554,7 +561,7 @@ public function setReplyTo($replyto, $name = '') } /** - * @param array|string $to + * @param list|string $to * * @return $this */ @@ -933,7 +940,7 @@ protected function getMimeMessage() } /** - * @param array|string $email + * @param list|string $email * * @return bool */ @@ -972,9 +979,9 @@ public function isValidEmail($email) } /** - * @param array|string $email + * @param list|string $email * - * @return array|string + * @return ($email is array ? list : string) */ public function cleanEmail($email) { @@ -2121,8 +2128,8 @@ protected function getHostname() } /** - * @param array|string $include List of raw data chunks to include in the output - * Valid options are: 'headers', 'subject', 'body' + * @param list|string $include List of raw data chunks to include in the output + * Valid options are: 'headers', 'subject', 'body' * * @return string */ @@ -2229,7 +2236,7 @@ protected static function substr($str, $start, $length = null) /** * Determines the values that should be stored in $archive. * - * @return array The updated archive values + * @return array The updated archive values */ protected function setArchiveValues(): array { diff --git a/tests/system/Email/EmailTest.php b/tests/system/Email/EmailTest.php index 319504237f08..fc94686589d1 100644 --- a/tests/system/Email/EmailTest.php +++ b/tests/system/Email/EmailTest.php @@ -58,6 +58,9 @@ public function testEmailSendWithClearance($autoClear): void } } + /** + * @return iterable> + */ public static function provideEmailSendWithClearance(): iterable { return [ diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 56afc2207507..f7a6cc6903c4 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 185 errors +# total 168 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 12b4aba4fafc..49bc456f612b 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 127 errors +# total 110 errors parameters: ignoreErrors: @@ -82,86 +82,6 @@ parameters: count: 1 path: ../../system/DataConverter/DataConverter.php - - - message: '#^Method CodeIgniter\\Email\\Email\:\:__construct\(\) has parameter \$config with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Email/Email.php - - - - message: '#^Method CodeIgniter\\Email\\Email\:\:cleanEmail\(\) has parameter \$email with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Email/Email.php - - - - message: '#^Method CodeIgniter\\Email\\Email\:\:cleanEmail\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Email/Email.php - - - - message: '#^Method CodeIgniter\\Email\\Email\:\:initialize\(\) has parameter \$config with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Email/Email.php - - - - message: '#^Method CodeIgniter\\Email\\Email\:\:printDebugger\(\) has parameter \$include with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Email/Email.php - - - - message: '#^Method CodeIgniter\\Email\\Email\:\:setArchiveValues\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Email/Email.php - - - - message: '#^Method CodeIgniter\\Email\\Email\:\:setTo\(\) has parameter \$to with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Email/Email.php - - - - message: '#^Method CodeIgniter\\Email\\Email\:\:validateEmail\(\) has parameter \$email with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Email/Email.php - - - - message: '#^Property CodeIgniter\\Email\\Email\:\:\$BCCArray type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Email/Email.php - - - - message: '#^Property CodeIgniter\\Email\\Email\:\:\$CCArray type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Email/Email.php - - - - message: '#^Property CodeIgniter\\Email\\Email\:\:\$archive type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Email/Email.php - - - - message: '#^Property CodeIgniter\\Email\\Email\:\:\$attachments type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Email/Email.php - - - - message: '#^Property CodeIgniter\\Email\\Email\:\:\$debugMessage type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Email/Email.php - - - - message: '#^Property CodeIgniter\\Email\\Email\:\:\$headers type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Email/Email.php - - - - message: '#^Property CodeIgniter\\Email\\Email\:\:\$recipients type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Email/Email.php - - - - message: '#^Property CodeIgniter\\Email\\Email\:\:\$tmpArchive type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Email/Email.php - - message: '#^Method CodeIgniter\\Exceptions\\PageNotFoundException\:\:lang\(\) has parameter \$args with no value type specified in iterable type array\.$#' count: 1 @@ -502,11 +422,6 @@ parameters: count: 1 path: ../../tests/system/Database/Live/UpdateTest.php - - - message: '#^Method CodeIgniter\\Email\\EmailTest\:\:provideEmailSendWithClearance\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Email/EmailTest.php - - message: '#^Method CodeIgniter\\I18n\\TimeLegacyTest\:\:provideToStringDoesNotDependOnLocale\(\) return type has no value type specified in iterable type iterable\.$#' count: 1 From f794bc90bf9718791de2ee9826844c4541e85607 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Wed, 26 Aug 2026 04:27:56 +0800 Subject: [PATCH 082/163] refactor: fix iterable types in `Router` tests (#10488) --- .../system/Router/AutoRouterImprovedTest.php | 6 ++ .../Router/DefinedRouteCollectorTest.php | 3 + .../RouteCollectionReverseRouteTest.php | 7 ++ tests/system/Router/RouteCollectionTest.php | 20 +++++ tests/system/Router/RouterTest.php | 3 + utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 77 +------------------ 7 files changed, 41 insertions(+), 77 deletions(-) diff --git a/tests/system/Router/AutoRouterImprovedTest.php b/tests/system/Router/AutoRouterImprovedTest.php index 8a4132a8f297..a974509e94c4 100644 --- a/tests/system/Router/AutoRouterImprovedTest.php +++ b/tests/system/Router/AutoRouterImprovedTest.php @@ -528,6 +528,9 @@ public function testTranslateUriToCamelCase( ], $router->getPos()); } + /** + * @return iterable + */ public static function provideTranslateUriToCamelCase(): iterable { yield from [ @@ -587,6 +590,9 @@ public function testRejectTranslateUriToCamelCase(string $uri, string $expMsg): $router->getRoute($uri, Method::GET); } + /** + * @return iterable + */ public static function provideRejectTranslateUriToCamelCase(): iterable { yield from [ diff --git a/tests/system/Router/DefinedRouteCollectorTest.php b/tests/system/Router/DefinedRouteCollectorTest.php index 329c582991bf..31a535e130c6 100644 --- a/tests/system/Router/DefinedRouteCollectorTest.php +++ b/tests/system/Router/DefinedRouteCollectorTest.php @@ -25,6 +25,9 @@ #[Group('Others')] final class DefinedRouteCollectorTest extends CIUnitTestCase { + /** + * @param array|non-empty-string> $config + */ private function createRouteCollection(array $config = [], ?Modules $moduleConfig = null): RouteCollection { $defaults = [ diff --git a/tests/system/Router/RouteCollectionReverseRouteTest.php b/tests/system/Router/RouteCollectionReverseRouteTest.php index 66428a13de99..36ead775da06 100644 --- a/tests/system/Router/RouteCollectionReverseRouteTest.php +++ b/tests/system/Router/RouteCollectionReverseRouteTest.php @@ -35,6 +35,10 @@ protected function setUp(): void $this->resetFactories(); } + /** + * @param array|non-empty-string> $config + * @param array $files + */ protected function getCollector(array $config = [], array $files = [], ?Modules $moduleConfig = null): RouteCollection { $defaults = [ @@ -147,6 +151,9 @@ public function testReverseRoutingDefaultNamespaceAppController(string $controll $this->assertSame('/users/15/gallery12', $match); } + /** + * @return iterable + */ public static function provideReverseRoutingDefaultNamespaceAppController(): iterable { return yield from [ diff --git a/tests/system/Router/RouteCollectionTest.php b/tests/system/Router/RouteCollectionTest.php index e61f8ec472a9..5e3e7bd8015f 100644 --- a/tests/system/Router/RouteCollectionTest.php +++ b/tests/system/Router/RouteCollectionTest.php @@ -44,6 +44,10 @@ protected function setUp(): void Services::injectMock('superglobals', new Superglobals()); } + /** + * @param array|non-empty-string> $config + * @param array $files + */ protected function getCollector(array $config = [], array $files = [], ?Modules $moduleConfig = null): RouteCollection { $defaults = [ @@ -507,6 +511,9 @@ static function ($routes): void { $this->assertSame($expected, $routes->getRoutes()); } + /** + * @param array $expected + */ #[DataProvider('provideNestedGroupingWorksWithRootPrefix')] public function testNestedGroupingWorksWithRootPrefix( string $group, @@ -531,6 +538,9 @@ static function ($routes): void { $this->assertSame($expected, $routes->getRoutes()); } + /** + * @return iterable}> + */ public static function provideNestedGroupingWorksWithRootPrefix(): iterable { yield from [ @@ -1343,6 +1353,10 @@ static function (): void {}, $this->assertSame($options, ['as' => 'admin', 'foo' => 'baz']); } + /** + * @param array $options1 + * @param array $options2 + */ #[DataProvider('provideRoutesOptionsWithSameFromTwoRoutes')] public function testRoutesOptionsWithSameFromTwoRoutes(array $options1, array $options2): void { @@ -1366,6 +1380,9 @@ static function (): void {}, $this->assertSame($options, $options1); } + /** + * @return iterable, array}> + */ public static function provideRoutesOptionsWithSameFromTwoRoutes(): iterable { yield from [ @@ -1860,6 +1877,9 @@ public function testRoutesControllerNameReturnsFQCN(string $namespace): void $this->assertSame('\\' . Product::class, $router->controllerName()); } + /** + * @return iterable + */ public static function provideRouteDefaultNamespace(): iterable { return [ diff --git a/tests/system/Router/RouterTest.php b/tests/system/Router/RouterTest.php index 7eef0a7569aa..ec06de96e0b3 100644 --- a/tests/system/Router/RouterTest.php +++ b/tests/system/Router/RouterTest.php @@ -963,6 +963,9 @@ public function testRedirectRoute( $router->handle($url); } + /** + * @return iterable + */ public static function provideRedirectRoute(): iterable { // [$route, $redirectFrom, $redirectTo, $url, $expectedPath, $alias] diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index f7a6cc6903c4..a77d7c3f2728 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 168 errors +# total 153 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 49bc456f612b..7a94bec26d15 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 110 errors +# total 95 errors parameters: ignoreErrors: @@ -467,81 +467,6 @@ parameters: count: 1 path: ../../tests/system/RESTful/ResourceControllerTest.php - - - message: '#^Method CodeIgniter\\Router\\AutoRouterImprovedTest\:\:provideRejectTranslateUriToCamelCase\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Router/AutoRouterImprovedTest.php - - - - message: '#^Method CodeIgniter\\Router\\AutoRouterImprovedTest\:\:provideTranslateUriToCamelCase\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Router/AutoRouterImprovedTest.php - - - - message: '#^Method CodeIgniter\\Router\\DefinedRouteCollectorTest\:\:createRouteCollection\(\) has parameter \$config with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Router/DefinedRouteCollectorTest.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollectionReverseRouteTest\:\:getCollector\(\) has parameter \$config with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Router/RouteCollectionReverseRouteTest.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollectionReverseRouteTest\:\:getCollector\(\) has parameter \$files with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Router/RouteCollectionReverseRouteTest.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollectionReverseRouteTest\:\:provideReverseRoutingDefaultNamespaceAppController\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Router/RouteCollectionReverseRouteTest.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollectionTest\:\:getCollector\(\) has parameter \$config with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Router/RouteCollectionTest.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollectionTest\:\:getCollector\(\) has parameter \$files with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Router/RouteCollectionTest.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollectionTest\:\:provideNestedGroupingWorksWithRootPrefix\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Router/RouteCollectionTest.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollectionTest\:\:provideRouteDefaultNamespace\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Router/RouteCollectionTest.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollectionTest\:\:provideRoutesOptionsWithSameFromTwoRoutes\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Router/RouteCollectionTest.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollectionTest\:\:testNestedGroupingWorksWithRootPrefix\(\) has parameter \$expected with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Router/RouteCollectionTest.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollectionTest\:\:testRoutesOptionsWithSameFromTwoRoutes\(\) has parameter \$options1 with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Router/RouteCollectionTest.php - - - - message: '#^Method CodeIgniter\\Router\\RouteCollectionTest\:\:testRoutesOptionsWithSameFromTwoRoutes\(\) has parameter \$options2 with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Router/RouteCollectionTest.php - - - - message: '#^Method CodeIgniter\\Router\\RouterTest\:\:provideRedirectRoute\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Router/RouterTest.php - - message: '#^Method CodeIgniter\\Throttle\\ThrottleTest\:\:provideTokenTimeCalculationUCs\(\) return type has no value type specified in iterable type iterable\.$#' count: 1 From 2cb29b750039845deef0d73742da03e315cc7582 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Fri, 28 Aug 2026 06:05:31 +0200 Subject: [PATCH 083/163] fix: return 403 for Honeypot bot detection (#10490) --- system/Honeypot/Exceptions/HoneypotException.php | 11 ++++++----- tests/system/Debug/ExceptionsTest.php | 2 ++ tests/system/Honeypot/HoneypotTest.php | 3 +++ user_guide_src/source/changelogs/v4.7.5.rst | 1 + user_guide_src/source/libraries/honeypot.rst | 3 ++- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/system/Honeypot/Exceptions/HoneypotException.php b/system/Honeypot/Exceptions/HoneypotException.php index 5185f6d3c843..c8eedeb311f6 100644 --- a/system/Honeypot/Exceptions/HoneypotException.php +++ b/system/Honeypot/Exceptions/HoneypotException.php @@ -14,8 +14,9 @@ namespace CodeIgniter\Honeypot\Exceptions; use CodeIgniter\Exceptions\ConfigException; +use CodeIgniter\Exceptions\HTTPExceptionInterface; -class HoneypotException extends ConfigException +class HoneypotException extends ConfigException implements HTTPExceptionInterface { /** * Thrown when the template value of config is empty. @@ -24,7 +25,7 @@ class HoneypotException extends ConfigException */ public static function forNoTemplate() { - return new static(lang('Honeypot.noTemplate')); + return new static(lang('Honeypot.noTemplate'), 500); } /** @@ -34,7 +35,7 @@ public static function forNoTemplate() */ public static function forNoNameField() { - return new static(lang('Honeypot.noNameField')); + return new static(lang('Honeypot.noNameField'), 500); } /** @@ -46,7 +47,7 @@ public static function forNoNameField() */ public static function forNoHiddenValue() { - return new static(lang('Honeypot.noHiddenValue')); + return new static(lang('Honeypot.noHiddenValue'), 500); } /** @@ -56,6 +57,6 @@ public static function forNoHiddenValue() */ public static function isBot() { - return new static(lang('Honeypot.theClientIsABot')); + return new static(lang('Honeypot.theClientIsABot'), 403); } } diff --git a/tests/system/Debug/ExceptionsTest.php b/tests/system/Debug/ExceptionsTest.php index 919c1ea85180..08f006637bcb 100644 --- a/tests/system/Debug/ExceptionsTest.php +++ b/tests/system/Debug/ExceptionsTest.php @@ -17,6 +17,7 @@ use CodeIgniter\Exceptions\ConfigException; use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Exceptions\RuntimeException; +use CodeIgniter\Honeypot\Exceptions\HoneypotException; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\ReflectionHelper; use Config\Exceptions as ExceptionsConfig; @@ -124,6 +125,7 @@ public function testDetermineCodes(): void $this->assertSame([500, EXIT_CONFIG], $determineCodes(new ConfigException('This.'))); $this->assertSame([500, EXIT_CONFIG], $determineCodes(CastException::forInvalidInterface('This.'))); $this->assertSame([500, EXIT_DATABASE], $determineCodes(new DatabaseException('This.'))); + $this->assertSame([403, EXIT_CONFIG], $determineCodes(HoneypotException::isBot())); } public function testMaskSensitiveData(): void diff --git a/tests/system/Honeypot/HoneypotTest.php b/tests/system/Honeypot/HoneypotTest.php index 12341f645889..8ed9f2756064 100644 --- a/tests/system/Honeypot/HoneypotTest.php +++ b/tests/system/Honeypot/HoneypotTest.php @@ -153,6 +153,7 @@ public function testConfigTemplate(): void { $this->config->template = ''; $this->expectException(HoneypotException::class); + $this->expectExceptionCode(500); $this->honeypot = new Honeypot($this->config); } @@ -160,6 +161,7 @@ public function testConfigName(): void { $this->config->name = ''; $this->expectException(HoneypotException::class); + $this->expectExceptionCode(500); $this->honeypot = new Honeypot($this->config); } @@ -173,6 +175,7 @@ public function testHoneypotFilterBefore(): void $uri = 'admin/foo/bar'; $this->expectException(HoneypotException::class); + $this->expectExceptionCode(403); $filters->run($uri, 'before'); } diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index 787d67e7de9a..47d447dfbbc3 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -37,6 +37,7 @@ Bugs Fixed - **CLIRequest:** Fixed a bug where ``parseCommand()`` could throw a TypeError when ``argv`` is missing. - **Content Security Policy:** Fixed a bug where empty ``Content-Security-Policy``, ``Content-Security-Policy-Report-Only``, and ``Reporting-Endpoints`` response headers were generated when no corresponding values existed. - **Helpers:** Fixed a bug where ``get_dir_file_info()`` returned incomplete entries for subdirectories and missing files instead of omitting them. +- **Honeypot:** Fixed a bug where bot detection returned an HTTP 500 response instead of 403 (Forbidden). - **Logger:** Fixed a bug where interpolating a log message with array or non-stringable context values could raise PHP warnings or errors. See the repo's diff --git a/user_guide_src/source/libraries/honeypot.rst b/user_guide_src/source/libraries/honeypot.rst index c6ebf14af0ce..b6c4bd4c3385 100644 --- a/user_guide_src/source/libraries/honeypot.rst +++ b/user_guide_src/source/libraries/honeypot.rst @@ -5,7 +5,8 @@ Honeypot Class The Honeypot Class makes it possible to determine when a Bot makes a request to a CodeIgniter4 application, if it's enabled in **app/Config/Filters.php** file. This is done by attaching form fields to any form, and this form field is hidden from a human but accessible to a Bot. When data is entered into the field, it's -assumed the request is coming from a Bot, and you can throw a ``HoneypotException``. +assumed the request is coming from a Bot. The bundled Honeypot filter throws a ``HoneypotException`` with +an HTTP status code of 403 (Forbidden). .. contents:: :local: From 0eda3a5ad8160147b06f1c7b0bb6e852a0d340ba Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Fri, 28 Aug 2026 14:58:56 +0800 Subject: [PATCH 084/163] style: fix native type ordering in lambda functions (#10491) --- system/Database/Postgre/Builder.php | 2 +- tests/system/Router/Attributes/CacheTest.php | 2 +- utils/src/Rector/RemoveErrorSuppressInTryCatchStmtsRector.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/system/Database/Postgre/Builder.php b/system/Database/Postgre/Builder.php index 4723a58e7cc8..4e5245bca994 100644 --- a/system/Database/Postgre/Builder.php +++ b/system/Database/Postgre/Builder.php @@ -372,7 +372,7 @@ protected function _updateBatch(string $table, array $keys, array $values): stri $sql .= 'WHERE ' . implode( ' AND ', array_map( - static function ($key, $value) use ($table, $alias, $that): string|RawSql { + static function ($key, $value) use ($table, $alias, $that): RawSql|string { if ($value instanceof RawSql && is_string($key)) { return $table . '.' . $key . ' = ' . $value; } diff --git a/tests/system/Router/Attributes/CacheTest.php b/tests/system/Router/Attributes/CacheTest.php index 1570c9658938..f31045336973 100644 --- a/tests/system/Router/Attributes/CacheTest.php +++ b/tests/system/Router/Attributes/CacheTest.php @@ -221,7 +221,7 @@ private function createMockRequest(string $method, string $path, string $query = ->getMock(); $request->method('isCLI')->willReturn(false); $request->method('withMethod')->willReturnCallback( - static function (string $method) use ($request): MockObject&IncomingRequest { + static function (string $method) use ($request): IncomingRequest&MockObject { $request->method('getMethod')->willReturn($method); return $request; diff --git a/utils/src/Rector/RemoveErrorSuppressInTryCatchStmtsRector.php b/utils/src/Rector/RemoveErrorSuppressInTryCatchStmtsRector.php index db84e7bc4bd7..592718837e2e 100644 --- a/utils/src/Rector/RemoveErrorSuppressInTryCatchStmtsRector.php +++ b/utils/src/Rector/RemoveErrorSuppressInTryCatchStmtsRector.php @@ -61,7 +61,7 @@ public function refactor(Node $node): ?Node $this->traverseNodesWithCallable( $node->stmts, - static function (Node $subNode) use (&$hasChanged): int|Expr|null { + static function (Node $subNode) use (&$hasChanged): Expr|int|null { if ($subNode instanceof Class_ || $subNode instanceof Function_) { return NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } From b7bffb140a77a283225bf606ed122dc6aa4319fd Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Fri, 28 Aug 2026 16:46:45 +0800 Subject: [PATCH 085/163] refactor: fix remaining phpstan errors in `Database` (#10492) --- system/Database/BaseBuilder.php | 6 +- tests/system/Database/BaseConnectionTest.php | 10 +++ tests/system/Database/BaseQueryTest.php | 18 +++-- tests/system/Database/Builder/InsertTest.php | 9 ++- tests/system/Database/Builder/UpdateTest.php | 7 +- tests/system/Database/ConfigTest.php | 18 +++++ tests/system/Database/Live/MetadataTest.php | 6 +- .../Database/Live/SQLite3/AlterTableTest.php | 2 +- tests/system/Database/Live/UpdateTest.php | 6 +- utils/phpstan-baseline/argument.type.neon | 22 +----- utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 67 +------------------ .../phpstan-baseline/property.phpDocType.neon | 12 +--- 13 files changed, 60 insertions(+), 125 deletions(-) diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 5b03b0510b5f..1d2055d9ca24 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -2072,9 +2072,9 @@ private function setAlias(string $alias): BaseBuilder /** * Sets update fields for upsert, update * - * @param list|list|string $set - * @param bool $addToDefault adds update fields to the default ones - * @param list|null $ignore ignores items in set + * @param array|string $set + * @param bool $addToDefault Adds update fields to the default ones + * @param list|null $ignore Ignores items in set * * @return $this */ diff --git a/tests/system/Database/BaseConnectionTest.php b/tests/system/Database/BaseConnectionTest.php index 4d6d1f76173d..1b3a9841f58e 100644 --- a/tests/system/Database/BaseConnectionTest.php +++ b/tests/system/Database/BaseConnectionTest.php @@ -27,6 +27,9 @@ #[Group('Others')] final class BaseConnectionTest extends CIUnitTestCase { + /** + * @var array + */ private array $options = [ 'DSN' => '', 'hostname' => 'localhost', @@ -50,6 +53,10 @@ final class BaseConnectionTest extends CIUnitTestCase 'time' => 'H:i:s', ], ]; + + /** + * @var array + */ private array $failoverOptions = [ 'DSN' => '', 'hostname' => 'localhost', @@ -337,6 +344,9 @@ public function testProtectIdentifiers( $this->assertSame($expected, $return); } + /** + * @return iterable + */ public static function provideProtectIdentifiers(): iterable { yield from [ diff --git a/tests/system/Database/BaseQueryTest.php b/tests/system/Database/BaseQueryTest.php index c5265b85dfa7..b50e41b7ece8 100644 --- a/tests/system/Database/BaseQueryTest.php +++ b/tests/system/Database/BaseQueryTest.php @@ -107,12 +107,8 @@ public function testSwapPrefix(): void $this->assertSame($newSQL, $query->getQuery()); } - /** - * @param mixed $expected - * @param mixed $sql - */ #[DataProvider('provideIsWriteType')] - public function testIsWriteType($expected, $sql): void + public function testIsWriteType(bool $expected, string $sql): void { $query = new Query($this->db); @@ -120,6 +116,9 @@ public function testIsWriteType($expected, $sql): void $this->assertSame($expected, $query->isWriteType()); } + /** + * @return iterable + */ public static function provideIsWriteType(): iterable { return [ @@ -579,12 +578,8 @@ public function testSwapPrefixAfterGetQuery(): void $this->assertSame($expected, $query->getQuery()); } - /** - * @param mixed $expected - * @param mixed $sql - */ #[DataProvider('provideHighlightQueryKeywords')] - public function testHighlightQueryKeywords($expected, $sql): void + public function testHighlightQueryKeywords(string $expected, string $sql): void { $query = new Query($this->db); $query->setQuery($sql); @@ -592,6 +587,9 @@ public function testHighlightQueryKeywords($expected, $sql): void $this->assertSame($expected, $query->debugToolbarDisplay()); } + /** + * @return iterable + */ public static function provideHighlightQueryKeywords(): iterable { return [ diff --git a/tests/system/Database/Builder/InsertTest.php b/tests/system/Database/Builder/InsertTest.php index e8761b9de52b..062cf9786b34 100644 --- a/tests/system/Database/Builder/InsertTest.php +++ b/tests/system/Database/Builder/InsertTest.php @@ -27,11 +27,6 @@ #[Group('Others')] final class InsertTest extends CIUnitTestCase { - /** - * @var MockConnection - */ - protected $db; - protected function setUp(): void { parent::setUp(); @@ -162,6 +157,7 @@ public function testInsertBatch(): void ], ]; + $this->assertInstanceOf(MockConnection::class, $this->db); $this->db->shouldReturn('execute', new class () {}); $builder->insertBatch($insertData, true); @@ -197,6 +193,7 @@ public function testInsertBatchIgnore(): void ], ]; + $this->assertInstanceOf(MockConnection::class, $this->db); $this->db->shouldReturn('execute', new class () {}); $builder->ignore()->insertBatch($insertData, true, 1); @@ -231,6 +228,7 @@ public function testInsertBatchWithoutEscape(): void ], ]; + $this->assertInstanceOf(MockConnection::class, $this->db); $this->db->shouldReturn('execute', new class () {}); $builder->insertBatch($insertData, false); @@ -255,6 +253,7 @@ public function testInsertBatchWithFieldsEndingInNumbers(): void ['ip' => '4.4.4.0', 'ip2' => '4.4.4.2'], ]; + $this->assertInstanceOf(MockConnection::class, $this->db); $this->db->shouldReturn('execute', new class () {}); $builder->insertBatch($data, true); diff --git a/tests/system/Database/Builder/UpdateTest.php b/tests/system/Database/Builder/UpdateTest.php index 5bb93ad2a598..5a4ccd0be9d3 100644 --- a/tests/system/Database/Builder/UpdateTest.php +++ b/tests/system/Database/Builder/UpdateTest.php @@ -26,11 +26,6 @@ #[Group('Others')] final class UpdateTest extends CIUnitTestCase { - /** - * @var MockConnection - */ - protected $db; - protected function setUp(): void { parent::setUp(); @@ -229,6 +224,7 @@ public function testUpdateBatch(): void ], ]; + $this->assertInstanceOf(MockConnection::class, $this->db); $this->db->shouldReturn('execute', new class () {}); $builder->updateBatch($updateData, 'id'); @@ -268,6 +264,7 @@ public function testSetUpdateBatchWithoutEscape(): void ], ], 'id', $escape); + $this->assertInstanceOf(MockConnection::class, $this->db); $this->db->shouldReturn('execute', new class () {}); $builder->updateBatch(null, 'id'); diff --git a/tests/system/Database/ConfigTest.php b/tests/system/Database/ConfigTest.php index 1d2482e0ad87..980404ea9f3d 100644 --- a/tests/system/Database/ConfigTest.php +++ b/tests/system/Database/ConfigTest.php @@ -27,6 +27,9 @@ final class ConfigTest extends CIUnitTestCase { use ReflectionHelper; + /** + * @var array + */ private array $group = [ 'DSN' => '', 'hostname' => 'localhost', @@ -46,6 +49,10 @@ final class ConfigTest extends CIUnitTestCase 'failover' => [], 'port' => 3306, ]; + + /** + * @var array + */ private array $dsnGroup = [ 'DSN' => 'MySQLi://user:pass@localhost:3306/dbname?DBPrefix=test_&pConnect=true&charset=latin1&DBCollat=latin1_swedish_ci', 'hostname' => '', @@ -65,6 +72,10 @@ final class ConfigTest extends CIUnitTestCase 'failover' => [], 'port' => 3306, ]; + + /** + * @var array + */ private array $dsnGroupPostgre = [ 'DSN' => 'Postgre://user:pass@localhost:5432/dbname?DBPrefix=test_&connect_timeout=5&sslmode=1', 'hostname' => '', @@ -84,6 +95,10 @@ final class ConfigTest extends CIUnitTestCase 'failover' => [], 'port' => 5432, ]; + + /** + * @var array + */ private array $dsnGroupPostgreNative = [ 'DSN' => 'pgsql:host=localhost;port=5432;dbname=database_name', 'hostname' => '', @@ -214,6 +229,9 @@ public function testConvertDSN(string $input, string $expected): void $this->assertSame($expected, $this->getPrivateProperty($conn, 'DSN')); } + /** + * @return iterable + */ public static function provideConvertDSN(): iterable { yield from [ diff --git a/tests/system/Database/Live/MetadataTest.php b/tests/system/Database/Live/MetadataTest.php index 5030a6544231..62f96f443acc 100644 --- a/tests/system/Database/Live/MetadataTest.php +++ b/tests/system/Database/Live/MetadataTest.php @@ -27,7 +27,11 @@ final class MetadataTest extends CIUnitTestCase { use DatabaseTestTrait; - protected $seed = CITestSeeder::class; + protected $seed = CITestSeeder::class; + + /** + * @var list + */ private array $expectedTables = []; protected function setUp(): void diff --git a/tests/system/Database/Live/SQLite3/AlterTableTest.php b/tests/system/Database/Live/SQLite3/AlterTableTest.php index 61bf7ad44f6b..a68d42ca1440 100644 --- a/tests/system/Database/Live/SQLite3/AlterTableTest.php +++ b/tests/system/Database/Live/SQLite3/AlterTableTest.php @@ -63,7 +63,7 @@ protected function setUp(): void $this->assertInstanceOf(Forge::class, $forge); $this->forge = $forge; - $this->table = new Table($this->db, $this->forge); + $this->table = new Table($db, $this->forge); $this->dropTables(); } diff --git a/tests/system/Database/Live/UpdateTest.php b/tests/system/Database/Live/UpdateTest.php index 7f9ce90944ae..8961e7b05664 100644 --- a/tests/system/Database/Live/UpdateTest.php +++ b/tests/system/Database/Live/UpdateTest.php @@ -115,7 +115,8 @@ public function testUpdateWithWhereAndLimit(): void } /** - * @param array $expected + * @param list> $data + * @param list> $expected */ #[DataProvider('provideUpdateBatch')] public function testUpdateBatch(string $constraints, array $data, array $expected): void @@ -155,6 +156,9 @@ public function testUpdateBatch(string $constraints, array $data, array $expecte $this->seeInDatabase($table, $expected[1]); } + /** + * @return iterable>, list>}> + */ public static function provideUpdateBatch(): iterable { yield from [ diff --git a/utils/phpstan-baseline/argument.type.neon b/utils/phpstan-baseline/argument.type.neon index f5e14ec6a08f..b9f5906a8ac3 100644 --- a/utils/phpstan-baseline/argument.type.neon +++ b/utils/phpstan-baseline/argument.type.neon @@ -1,4 +1,4 @@ -# total 12 errors +# total 6 errors parameters: ignoreErrors: @@ -22,26 +22,6 @@ parameters: count: 1 path: ../../tests/system/CodeIgniterTest.php - - - message: '#^Parameter \#1 \$db of class CodeIgniter\\Database\\SQLite3\\Table constructor expects CodeIgniter\\Database\\SQLite3\\Connection, CodeIgniter\\Database\\BaseConnection given\.$#' - count: 1 - path: ../../tests/system/Database/Live/SQLite3/AlterTableTest.php - - - - message: '#^Parameter \#1 \$set of method CodeIgniter\\Database\\BaseBuilder\\:\:updateFields\(\) expects list\\|string, array\{0\: ''country'', updated_at\: CodeIgniter\\Database\\RawSql\} given\.$#' - count: 2 - path: ../../tests/system/Database/Live/UpdateTest.php - - - - message: '#^Parameter \#1 \$set of method CodeIgniter\\Database\\BaseBuilder\\:\:updateFields\(\) expects list\\|string, array\{0\: ''name'', updated_at\: CodeIgniter\\Database\\RawSql\} given\.$#' - count: 1 - path: ../../tests/system/Database/Live/UpdateTest.php - - - - message: '#^Parameter \#1 \$set of method CodeIgniter\\Database\\BaseBuilder\\:\:updateFields\(\) expects list\\|string, array\{updated_at\: CodeIgniter\\Database\\RawSql\} given\.$#' - count: 2 - path: ../../tests/system/Database/Live/UpsertTest.php - - message: '#^Parameter \#2 \$message of method CodeIgniter\\Log\\Handlers\\ChromeLoggerHandler\:\:handle\(\) expects string, stdClass given\.$#' count: 1 diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index a77d7c3f2728..37af12f96222 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 153 errors +# total 132 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index 7a94bec26d15..e3ce34dc009d 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 95 errors +# total 82 errors parameters: ignoreErrors: @@ -357,71 +357,6 @@ parameters: count: 1 path: ../../tests/system/DataConverter/DataConverterTest.php - - - message: '#^Method CodeIgniter\\Database\\BaseConnectionTest\:\:provideProtectIdentifiers\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Database/BaseConnectionTest.php - - - - message: '#^Property CodeIgniter\\Database\\BaseConnectionTest\:\:\$failoverOptions type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Database/BaseConnectionTest.php - - - - message: '#^Property CodeIgniter\\Database\\BaseConnectionTest\:\:\$options type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Database/BaseConnectionTest.php - - - - message: '#^Method CodeIgniter\\Database\\BaseQueryTest\:\:provideHighlightQueryKeywords\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Database/BaseQueryTest.php - - - - message: '#^Method CodeIgniter\\Database\\BaseQueryTest\:\:provideIsWriteType\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Database/BaseQueryTest.php - - - - message: '#^Method CodeIgniter\\Database\\ConfigTest\:\:provideConvertDSN\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Database/ConfigTest.php - - - - message: '#^Property CodeIgniter\\Database\\ConfigTest\:\:\$dsnGroup type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Database/ConfigTest.php - - - - message: '#^Property CodeIgniter\\Database\\ConfigTest\:\:\$dsnGroupPostgre type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Database/ConfigTest.php - - - - message: '#^Property CodeIgniter\\Database\\ConfigTest\:\:\$dsnGroupPostgreNative type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Database/ConfigTest.php - - - - message: '#^Property CodeIgniter\\Database\\ConfigTest\:\:\$group type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Database/ConfigTest.php - - - - message: '#^Property CodeIgniter\\Database\\Live\\MetadataTest\:\:\$expectedTables type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Database/Live/MetadataTest.php - - - - message: '#^Method CodeIgniter\\Database\\Live\\UpdateTest\:\:provideUpdateBatch\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Database/Live/UpdateTest.php - - - - message: '#^Method CodeIgniter\\Database\\Live\\UpdateTest\:\:testUpdateBatch\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Database/Live/UpdateTest.php - - message: '#^Method CodeIgniter\\I18n\\TimeLegacyTest\:\:provideToStringDoesNotDependOnLocale\(\) return type has no value type specified in iterable type iterable\.$#' count: 1 diff --git a/utils/phpstan-baseline/property.phpDocType.neon b/utils/phpstan-baseline/property.phpDocType.neon index fbbec53d281e..00ab948073fd 100644 --- a/utils/phpstan-baseline/property.phpDocType.neon +++ b/utils/phpstan-baseline/property.phpDocType.neon @@ -1,4 +1,4 @@ -# total 9 errors +# total 7 errors parameters: ignoreErrors: @@ -22,16 +22,6 @@ parameters: count: 1 path: ../../system/Session/Handlers/FileHandler.php - - - message: '#^PHPDoc type CodeIgniter\\Test\\Mock\\MockConnection of property CodeIgniter\\Database\\Builder\\InsertTest\:\:\$db is not the same as PHPDoc type CodeIgniter\\Database\\BaseConnection of overridden property CodeIgniter\\Test\\CIUnitTestCase\:\:\$db\.$#' - count: 1 - path: ../../tests/system/Database/Builder/InsertTest.php - - - - message: '#^PHPDoc type CodeIgniter\\Test\\Mock\\MockConnection of property CodeIgniter\\Database\\Builder\\UpdateTest\:\:\$db is not the same as PHPDoc type CodeIgniter\\Database\\BaseConnection of overridden property CodeIgniter\\Test\\CIUnitTestCase\:\:\$db\.$#' - count: 1 - path: ../../tests/system/Database/Builder/UpdateTest.php - - message: '#^PHPDoc type Tests\\Support\\Models\\EventModel of property CodeIgniter\\Models\\EventsModelTest\:\:\$model is not the same as PHPDoc type CodeIgniter\\Model of overridden property CodeIgniter\\Models\\LiveModelTestCase\:\:\$model\.$#' count: 1 From 5c2ff009ba415b8b7aaca139fc7f82cfeef982b2 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Fri, 28 Aug 2026 18:32:26 +0800 Subject: [PATCH 086/163] refactor: fix phpstan errors in `I18n` (#10493) --- system/I18n/Time.php | 1 + system/I18n/TimeLegacy.php | 33 ++++++++++--------- tests/system/I18n/TimeLegacyTest.php | 7 ++-- tests/system/I18n/TimeTest.php | 7 ++-- utils/phpstan-baseline/loader.neon | 3 +- .../missingType.iterableValue.neon | 12 +------ utils/phpstan-baseline/property.notFound.neon | 33 ------------------- 7 files changed, 30 insertions(+), 66 deletions(-) delete mode 100644 utils/phpstan-baseline/property.notFound.neon diff --git a/system/I18n/Time.php b/system/I18n/Time.php index 255f879a90ec..cc254dba03ea 100644 --- a/system/I18n/Time.php +++ b/system/I18n/Time.php @@ -34,6 +34,7 @@ * @property-read string $quarter * @property-read string $second * @property-read int $timestamp + * @property-read string $timezoneName * @property-read bool $utc * @property-read string $weekOfMonth * @property-read string $weekOfYear diff --git a/system/I18n/TimeLegacy.php b/system/I18n/TimeLegacy.php index 034cab0dadcf..119fc3f5d050 100644 --- a/system/I18n/TimeLegacy.php +++ b/system/I18n/TimeLegacy.php @@ -23,22 +23,23 @@ * This is not immutable! Some methods are immutable, * but some methods can alter the state. * - * @property int $age read-only - * @property string $day read-only - * @property string $dayOfWeek read-only - * @property string $dayOfYear read-only - * @property bool $dst read-only - * @property string $hour read-only - * @property bool $local read-only - * @property string $minute read-only - * @property string $month read-only - * @property string $quarter read-only - * @property string $second read-only - * @property int $timestamp read-only - * @property bool $utc read-only - * @property string $weekOfMonth read-only - * @property string $weekOfYear read-only - * @property string $year read-only + * @property int $age read-only + * @property string $day read-only + * @property string $dayOfWeek read-only + * @property string $dayOfYear read-only + * @property bool $dst read-only + * @property string $hour read-only + * @property bool $local read-only + * @property string $minute read-only + * @property string $month read-only + * @property string $quarter read-only + * @property string $second read-only + * @property int $timestamp read-only + * @property string $timezoneName read-only + * @property bool $utc read-only + * @property string $weekOfMonth read-only + * @property string $weekOfYear read-only + * @property string $year read-only * * @phpstan-consistent-constructor * diff --git a/tests/system/I18n/TimeLegacyTest.php b/tests/system/I18n/TimeLegacyTest.php index 8fbd359fbfd5..6cdcdb45ff74 100644 --- a/tests/system/I18n/TimeLegacyTest.php +++ b/tests/system/I18n/TimeLegacyTest.php @@ -317,7 +317,7 @@ public function testMagicIssetFalse(): void { $time = TimeLegacy::parse('January 1, 2016'); - $this->assertFalse(isset($time->foobar)); + $this->assertFalse(isset($time->foobar)); // @phpstan-ignore property.notFound } public function testGetYear(): void @@ -1128,7 +1128,7 @@ public function testGetter(): void { $time = TimeLegacy::parse('August 12, 2016 4:15:23pm'); - $this->assertNull($time->weekOfWeek); + $this->assertNull($time->weekOfWeek); // @phpstan-ignore property.notFound } public function testUnserializeTimeObject(): void @@ -1163,6 +1163,9 @@ public function testToStringDoesNotDependOnLocale(string $locale): void $this->assertSame('2017-03-10 12:00:00', (string) $time); } + /** + * @return iterable + */ public static function provideToStringDoesNotDependOnLocale(): iterable { yield from [ diff --git a/tests/system/I18n/TimeTest.php b/tests/system/I18n/TimeTest.php index a7346c181b93..0d08af39f3f2 100644 --- a/tests/system/I18n/TimeTest.php +++ b/tests/system/I18n/TimeTest.php @@ -346,7 +346,7 @@ public function testMagicIssetFalse(): void { $time = Time::parse('January 1, 2016'); - $this->assertFalse(isset($time->foobar)); + $this->assertFalse(isset($time->foobar)); // @phpstan-ignore property.notFound } public function testGetYear(): void @@ -1280,7 +1280,7 @@ public function testGetter(): void { $time = Time::parse('August 12, 2016 4:15:23pm'); - $this->assertNull($time->weekOfWeek); + $this->assertNull($time->weekOfWeek); // @phpstan-ignore property.notFound } public function testUnserializeTimeObject(): void @@ -1324,6 +1324,9 @@ public function testToStringDoesNotDependOnLocale(string $locale): void $this->assertSame('2017-03-10 12:00:00', (string) $time); } + /** + * @return iterable + */ public static function provideToStringDoesNotDependOnLocale(): iterable { yield from [ diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 37af12f96222..d9cf5a7deb6d 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 132 errors +# total 124 errors includes: - argument.type.neon @@ -12,5 +12,4 @@ includes: - missingType.iterableValue.neon - missingType.parameter.neon - property.defaultValue.neon - - property.notFound.neon - property.phpDocType.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index e3ce34dc009d..ff0cd8e18d33 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 82 errors +# total 80 errors parameters: ignoreErrors: @@ -357,16 +357,6 @@ parameters: count: 1 path: ../../tests/system/DataConverter/DataConverterTest.php - - - message: '#^Method CodeIgniter\\I18n\\TimeLegacyTest\:\:provideToStringDoesNotDependOnLocale\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/I18n/TimeLegacyTest.php - - - - message: '#^Method CodeIgniter\\I18n\\TimeTest\:\:provideToStringDoesNotDependOnLocale\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/I18n/TimeTest.php - - message: '#^Method CodeIgniter\\Models\\FindModelTest\:\:provideAggregateAndGroupBy\(\) return type has no value type specified in iterable type iterable\.$#' count: 1 diff --git a/utils/phpstan-baseline/property.notFound.neon b/utils/phpstan-baseline/property.notFound.neon deleted file mode 100644 index 1a8daae7b918..000000000000 --- a/utils/phpstan-baseline/property.notFound.neon +++ /dev/null @@ -1,33 +0,0 @@ -# total 6 errors - -parameters: - ignoreErrors: - - - message: '#^Access to an undefined property CodeIgniter\\I18n\\TimeLegacy\:\:\$foobar\.$#' - count: 1 - path: ../../tests/system/I18n/TimeLegacyTest.php - - - - message: '#^Access to an undefined property CodeIgniter\\I18n\\TimeLegacy\:\:\$timezoneName\.$#' - count: 1 - path: ../../tests/system/I18n/TimeLegacyTest.php - - - - message: '#^Access to an undefined property CodeIgniter\\I18n\\TimeLegacy\:\:\$weekOfWeek\.$#' - count: 1 - path: ../../tests/system/I18n/TimeLegacyTest.php - - - - message: '#^Access to an undefined property CodeIgniter\\I18n\\Time\:\:\$foobar\.$#' - count: 1 - path: ../../tests/system/I18n/TimeTest.php - - - - message: '#^Access to an undefined property CodeIgniter\\I18n\\Time\:\:\$timezoneName\.$#' - count: 1 - path: ../../tests/system/I18n/TimeTest.php - - - - message: '#^Access to an undefined property CodeIgniter\\I18n\\Time\:\:\$weekOfWeek\.$#' - count: 1 - path: ../../tests/system/I18n/TimeTest.php From 0b4e5912fa0f891f6cddc4333b4900f915512363 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Fri, 28 Aug 2026 18:50:31 +0800 Subject: [PATCH 087/163] refactor: fix phpstan errors in `DataConverter` (#10494) --- system/DataConverter/DataConverter.php | 4 ++ .../DataConverter/DataConverterTest.php | 18 ++++++ utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 62 +------------------ 4 files changed, 24 insertions(+), 62 deletions(-) diff --git a/system/DataConverter/DataConverter.php b/system/DataConverter/DataConverter.php index 4895ec4127d2..a1b5db4f0ef9 100644 --- a/system/DataConverter/DataConverter.php +++ b/system/DataConverter/DataConverter.php @@ -71,6 +71,8 @@ public function __construct( * * @param array $data DataSource data * + * @return array + * * @internal */ public function fromDataSource(array $data): array @@ -89,6 +91,8 @@ public function fromDataSource(array $data): array * * @param array $phpData PHP data * + * @return array + * * @internal */ public function toDataSource(array $phpData): array diff --git a/tests/system/DataConverter/DataConverterTest.php b/tests/system/DataConverter/DataConverterTest.php index 6686618a181b..4ee4a8078217 100644 --- a/tests/system/DataConverter/DataConverterTest.php +++ b/tests/system/DataConverter/DataConverterTest.php @@ -44,6 +44,11 @@ public function testInstantiate(): void $this->assertInstanceOf(DataConverter::class, $converter); } + /** + * @param array $types + * @param array $dbData + * @param array $expected + */ #[DataProvider('provideConvertDataFromDB')] public function testConvertDataFromDB(array $types, array $dbData, array $expected): void { @@ -54,6 +59,9 @@ public function testConvertDataFromDB(array $types, array $dbData, array $expect $this->assertSame($expected, $data); } + /** + * @return iterable, array, array}> + */ public static function provideConvertDataFromDB(): iterable { yield from [ @@ -270,6 +278,11 @@ public static function provideConvertDataFromDB(): iterable ]; } + /** + * @param array $types + * @param array $phpData + * @param array $expected + */ #[DataProvider('provideConvertDataToDB')] public function testConvertDataToDB(array $types, array $phpData, array $expected): void { @@ -280,6 +293,9 @@ public function testConvertDataToDB(array $types, array $phpData, array $expecte $this->assertSame($expected, $data); } + /** + * @return iterable, array, array}> + */ public static function provideConvertDataToDB(): iterable { yield from [ @@ -706,6 +722,8 @@ public function testNotNullable(): void } /** + * @param array $types + * @param array $handlers * @param (Closure(array): object)|string|null $reconstructor * @param (Closure(object, bool, bool): array)|string|null $extractor */ diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index d9cf5a7deb6d..8cfa166d48df 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 124 errors +# total 112 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index ff0cd8e18d33..b426389b6a68 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 80 errors +# total 68 errors parameters: ignoreErrors: @@ -72,16 +72,6 @@ parameters: count: 1 path: ../../system/Cookie/CookieStore.php - - - message: '#^Method CodeIgniter\\DataConverter\\DataConverter\:\:fromDataSource\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/DataConverter/DataConverter.php - - - - message: '#^Method CodeIgniter\\DataConverter\\DataConverter\:\:toDataSource\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/DataConverter/DataConverter.php - - message: '#^Method CodeIgniter\\Exceptions\\PageNotFoundException\:\:lang\(\) has parameter \$args with no value type specified in iterable type array\.$#' count: 1 @@ -307,56 +297,6 @@ parameters: count: 1 path: ../../tests/system/Cookie/CookieTest.php - - - message: '#^Method CodeIgniter\\DataConverter\\DataConverterTest\:\:createDataConverter\(\) has parameter \$handlers with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/DataConverter/DataConverterTest.php - - - - message: '#^Method CodeIgniter\\DataConverter\\DataConverterTest\:\:createDataConverter\(\) has parameter \$types with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/DataConverter/DataConverterTest.php - - - - message: '#^Method CodeIgniter\\DataConverter\\DataConverterTest\:\:provideConvertDataFromDB\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/DataConverter/DataConverterTest.php - - - - message: '#^Method CodeIgniter\\DataConverter\\DataConverterTest\:\:provideConvertDataToDB\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/DataConverter/DataConverterTest.php - - - - message: '#^Method CodeIgniter\\DataConverter\\DataConverterTest\:\:testConvertDataFromDB\(\) has parameter \$dbData with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/DataConverter/DataConverterTest.php - - - - message: '#^Method CodeIgniter\\DataConverter\\DataConverterTest\:\:testConvertDataFromDB\(\) has parameter \$expected with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/DataConverter/DataConverterTest.php - - - - message: '#^Method CodeIgniter\\DataConverter\\DataConverterTest\:\:testConvertDataFromDB\(\) has parameter \$types with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/DataConverter/DataConverterTest.php - - - - message: '#^Method CodeIgniter\\DataConverter\\DataConverterTest\:\:testConvertDataToDB\(\) has parameter \$expected with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/DataConverter/DataConverterTest.php - - - - message: '#^Method CodeIgniter\\DataConverter\\DataConverterTest\:\:testConvertDataToDB\(\) has parameter \$phpData with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/DataConverter/DataConverterTest.php - - - - message: '#^Method CodeIgniter\\DataConverter\\DataConverterTest\:\:testConvertDataToDB\(\) has parameter \$types with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/DataConverter/DataConverterTest.php - - message: '#^Method CodeIgniter\\Models\\FindModelTest\:\:provideAggregateAndGroupBy\(\) return type has no value type specified in iterable type iterable\.$#' count: 1 From a10101828d439b4af7bc444f7fae7019b272e334 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Fri, 28 Aug 2026 19:07:46 +0800 Subject: [PATCH 088/163] refactor: fix phpstan errors in `AutoReview` (#10495) --- tests/system/AutoReview/ComposerJsonTest.php | 18 +++++++ tests/system/AutoReview/FrameworkCodeTest.php | 11 ++++ utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 52 +------------------ 4 files changed, 31 insertions(+), 52 deletions(-) diff --git a/tests/system/AutoReview/ComposerJsonTest.php b/tests/system/AutoReview/ComposerJsonTest.php index ab6b98b3bd64..55f22adb892b 100644 --- a/tests/system/AutoReview/ComposerJsonTest.php +++ b/tests/system/AutoReview/ComposerJsonTest.php @@ -26,8 +26,19 @@ #[Group('AutoReview')] final class ComposerJsonTest extends TestCase { + /** + * @var array + */ private array $devComposer; + + /** + * @var array + */ private array $frameworkComposer; + + /** + * @var array + */ private array $starterComposer; protected function setUp(): void @@ -105,6 +116,10 @@ private function checkSection(string $section, string $component): void ); } + /** + * @param array $fromMain + * @param array $fromComponent + */ private function checkConfig(array $fromMain, array $fromComponent, string $component): void { foreach ($fromMain as $key => $expectedValue) { @@ -124,6 +139,9 @@ private function checkConfig(array $fromMain, array $fromComponent, string $comp } } + /** + * @return array + */ private function getComposerJson(string $path): array { try { diff --git a/tests/system/AutoReview/FrameworkCodeTest.php b/tests/system/AutoReview/FrameworkCodeTest.php index 7345b5dfa537..812131f80509 100644 --- a/tests/system/AutoReview/FrameworkCodeTest.php +++ b/tests/system/AutoReview/FrameworkCodeTest.php @@ -31,9 +31,14 @@ final class FrameworkCodeTest extends TestCase { /** * Cache of discovered test class names. + * + * @var list */ private static array $testClasses = []; + /** + * @var list + */ private static array $recognizedGroupAttributeNames = [ 'AutoReview', 'CacheLive', @@ -79,6 +84,9 @@ public function testEachTestClassHasCorrectGroupAttributeName(string $class): vo )); } + /** + * @return iterable + */ public static function provideEachTestClassHasCorrectGroupAttributeName(): iterable { foreach (self::getTestClasses() as $class) { @@ -86,6 +94,9 @@ public static function provideEachTestClassHasCorrectGroupAttributeName(): itera } } + /** + * @return list + */ private static function getTestClasses(): array { if (self::$testClasses !== []) { diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 8cfa166d48df..296f8148b260 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 112 errors +# total 102 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index b426389b6a68..de7951be1665 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 68 errors +# total 58 errors parameters: ignoreErrors: @@ -172,56 +172,6 @@ parameters: count: 1 path: ../../system/View/Cells/Cell.php - - - message: '#^Method CodeIgniter\\AutoReview\\ComposerJsonTest\:\:checkConfig\(\) has parameter \$fromComponent with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/AutoReview/ComposerJsonTest.php - - - - message: '#^Method CodeIgniter\\AutoReview\\ComposerJsonTest\:\:checkConfig\(\) has parameter \$fromMain with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/AutoReview/ComposerJsonTest.php - - - - message: '#^Method CodeIgniter\\AutoReview\\ComposerJsonTest\:\:getComposerJson\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/AutoReview/ComposerJsonTest.php - - - - message: '#^Property CodeIgniter\\AutoReview\\ComposerJsonTest\:\:\$devComposer type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/AutoReview/ComposerJsonTest.php - - - - message: '#^Property CodeIgniter\\AutoReview\\ComposerJsonTest\:\:\$frameworkComposer type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/AutoReview/ComposerJsonTest.php - - - - message: '#^Property CodeIgniter\\AutoReview\\ComposerJsonTest\:\:\$starterComposer type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/AutoReview/ComposerJsonTest.php - - - - message: '#^Method CodeIgniter\\AutoReview\\FrameworkCodeTest\:\:getTestClasses\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/AutoReview/FrameworkCodeTest.php - - - - message: '#^Method CodeIgniter\\AutoReview\\FrameworkCodeTest\:\:provideEachTestClassHasCorrectGroupAttributeName\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/AutoReview/FrameworkCodeTest.php - - - - message: '#^Property CodeIgniter\\AutoReview\\FrameworkCodeTest\:\:\$recognizedGroupAttributeNames type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/AutoReview/FrameworkCodeTest.php - - - - message: '#^Property CodeIgniter\\AutoReview\\FrameworkCodeTest\:\:\$testClasses type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/AutoReview/FrameworkCodeTest.php - - message: '#^Method CodeIgniter\\CLI\\CLITest\:\:provideTable\(\) return type has no value type specified in iterable type iterable\.$#' count: 1 From 07009134115a961d0498b034cf6c2c28681618fa Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Fri, 28 Aug 2026 19:35:32 +0800 Subject: [PATCH 089/163] refactor: fix missing parameter types (#10496) --- tests/system/Entity/EntityTest.php | 17 ++++++++--------- .../SecurityCSRFSessionRandomizeTokenTest.php | 5 ++++- .../Security/SecurityCSRFSessionTest.php | 5 ++++- utils/phpstan-baseline/loader.neon | 3 +-- .../missingType.parameter.neon | 18 ------------------ 5 files changed, 17 insertions(+), 31 deletions(-) delete mode 100644 utils/phpstan-baseline/missingType.parameter.neon diff --git a/tests/system/Entity/EntityTest.php b/tests/system/Entity/EntityTest.php index 5233a28deeaf..82ff14253d17 100644 --- a/tests/system/Entity/EntityTest.php +++ b/tests/system/Entity/EntityTest.php @@ -502,22 +502,18 @@ public function testCastDateTime(): void public function testCastDateTimeWithTimestampTimezone(): void { - // Save the current timezone. $tz = date_default_timezone_get(); - - // Change the timezone other than UTC. - date_default_timezone_set('Asia/Tokyo'); // +09:00 + date_default_timezone_set('Asia/Tokyo'); $entity = $this->getCastEntity(); $entity->eighth = 1722988800; // 2024-08-07 00:00:00 UTC - $this->assertInstanceOf(DateTimeInterface::class, $entity->eighth); + $this->assertInstanceOf(Time::class, $entity->eighth); // The timezone is the default timezone, not UTC. $this->assertSame('2024-08-07 09:00:00', $entity->eighth->format('Y-m-d H:i:s')); $this->assertSame('Asia/Tokyo', $entity->eighth->getTimezoneName()); - // Restore timezone. date_default_timezone_set($tz); } @@ -651,8 +647,8 @@ public function testCastAsJSON(): void $check = $this->getPrivateProperty($entity, 'attributes')['tenth']; $this->assertSame('{"foo":"bar"}', $check); - $this->assertInstanceOf('stdClass', $entity->tenth); - $this->assertSame(['foo' => 'bar'], (array) $entity->tenth); + $this->assertInstanceOf(stdClass::class, $entity->tenth); + $this->assertSame('bar', $entity->tenth->foo); } public function testCastAsJSONArray(): void @@ -1744,7 +1740,10 @@ private function getSimpleSwappedEntity(): object }; } - private function getCastEntity($data = null): object + /** + * @param array|null $data + */ + private function getCastEntity(?array $data = null): Entity { return new class ($data) extends Entity { protected $attributes = [ diff --git a/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php b/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php index f5f1f437c83d..1c242e249519 100644 --- a/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php +++ b/tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php @@ -76,7 +76,10 @@ protected function setUp(): void $this->injectSession($this->hash); } - private function createSession($options = []): Session + /** + * @param array $options + */ + private function createSession(array $options = []): Session { $defaults = [ 'driver' => FileHandler::class, diff --git a/tests/system/Security/SecurityCSRFSessionTest.php b/tests/system/Security/SecurityCSRFSessionTest.php index fb410ac71ac3..fbb28e5baaf1 100644 --- a/tests/system/Security/SecurityCSRFSessionTest.php +++ b/tests/system/Security/SecurityCSRFSessionTest.php @@ -69,7 +69,10 @@ protected function setUp(): void $this->injectSession($this->hash); } - private function createSession($options = []): Session + /** + * @param array $options + */ + private function createSession(array $options = []): Session { $defaults = [ 'driver' => FileHandler::class, diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 296f8148b260..e796bfe1763f 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 102 errors +# total 99 errors includes: - argument.type.neon @@ -10,6 +10,5 @@ includes: - method.childReturnType.neon - method.notFound.neon - missingType.iterableValue.neon - - missingType.parameter.neon - property.defaultValue.neon - property.phpDocType.neon diff --git a/utils/phpstan-baseline/missingType.parameter.neon b/utils/phpstan-baseline/missingType.parameter.neon deleted file mode 100644 index 49f9dffb6507..000000000000 --- a/utils/phpstan-baseline/missingType.parameter.neon +++ /dev/null @@ -1,18 +0,0 @@ -# total 3 errors - -parameters: - ignoreErrors: - - - message: '#^Method CodeIgniter\\Entity\\EntityTest\:\:getCastEntity\(\) has parameter \$data with no type specified\.$#' - count: 1 - path: ../../tests/system/Entity/EntityTest.php - - - - message: '#^Method CodeIgniter\\Security\\SecurityCSRFSessionRandomizeTokenTest\:\:createSession\(\) has parameter \$options with no type specified\.$#' - count: 1 - path: ../../tests/system/Security/SecurityCSRFSessionRandomizeTokenTest.php - - - - message: '#^Method CodeIgniter\\Security\\SecurityCSRFSessionTest\:\:createSession\(\) has parameter \$options with no type specified\.$#' - count: 1 - path: ../../tests/system/Security/SecurityCSRFSessionTest.php From 62d489d3cd7f2ca5c083b00087e3df10d73bb3b3 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Fri, 28 Aug 2026 20:23:11 +0800 Subject: [PATCH 090/163] refactor: fix remaining missing iterable types (#10497) --- system/Common.php | 11 +- system/Controller.php | 15 +- system/Cookie/CookieStore.php | 2 + system/Exceptions/PageNotFoundException.php | 2 + system/HotReloader/DirectoryHasher.php | 2 + system/HotReloader/HotReloader.php | 2 + system/HotReloader/IteratorFilter.php | 3 + system/Images/Handlers/BaseHandler.php | 6 + system/Images/Image.php | 2 +- system/Modules/Modules.php | 3 + system/Pager/Pager.php | 25 +- system/Pager/PagerInterface.php | 14 + system/Pager/PagerRenderer.php | 11 +- system/Publisher/ContentReplacer.php | 2 +- system/Publisher/Publisher.php | 2 +- system/Typography/Typography.php | 4 +- system/View/Cell.php | 18 +- tests/system/CLI/CLITest.php | 10 +- tests/system/CodeIgniterTest.php | 6 +- tests/system/CommonFunctionsTest.php | 3 + tests/system/CommonHelperTest.php | 3 + tests/system/Cookie/CookieStoreTest.php | 12 + tests/system/Cookie/CookieTest.php | 24 ++ tests/system/Models/FindModelTest.php | 6 + tests/system/Models/TimestampModelTest.php | 4 + tests/system/Pager/PagerRendererTest.php | 8 +- tests/system/Pager/PagerTest.php | 4 +- .../Publisher/PublisherRestrictionsTest.php | 6 + .../system/RESTful/ResourceControllerTest.php | 3 + tests/system/Throttle/ThrottleTest.php | 10 + utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 287 +----------------- 32 files changed, 182 insertions(+), 330 deletions(-) diff --git a/system/Common.php b/system/Common.php index 32f66d1803ee..226c0554a6cf 100644 --- a/system/Common.php +++ b/system/Common.php @@ -819,6 +819,8 @@ function lang(string $line, array $args = [], ?string $locale = null) * - notice * - info * - debug + * + * @param array $context */ function log_message(string $level, string $message, array $context = []): void { @@ -861,7 +863,7 @@ function model(string $name, bool $getShared = true, ?ConnectionInterface &$conn * @param string|null $default * @param 'attr'|'css'|'html'|'js'|'raw'|'url'|false $escape * - * @return array|string|null + * @return array|string|null */ function old(string $key, $default = null, $escape = 'html') { @@ -1185,7 +1187,7 @@ function slash_item(string $item): ?string * Helper function used to convert a string, array, or object * of attributes to a string. * - * @param array|object|string $attributes string, array, object that can be cast to array + * @param array|object|string $attributes */ function stringify_attributes($attributes, bool $js = false): string { @@ -1252,7 +1254,8 @@ function timer(?string $name = null, ?callable $callable = null) * NOTE: Does not provide any escaping of the data, so that must * all be handled manually by the developer. * - * @param array $options Options for saveData or third-party extensions. + * @param array $data + * @param array $options Options for saveData or third-party extensions. */ function view(string $name, array $data = [], array $options = []): string { @@ -1275,7 +1278,7 @@ function view(string $name, array $data = [], array $options = []): string * View cells are used within views to insert HTML chunks that are managed * by other classes. * - * @param array|string|null $params + * @param array|string|null $params * * @throws ReflectionException */ diff --git a/system/Controller.php b/system/Controller.php index af74f3a3af10..e4516383c08c 100644 --- a/system/Controller.php +++ b/system/Controller.php @@ -126,8 +126,8 @@ protected function cachePage(int $time) /** * A shortcut to performing validation on Request data. * - * @param array|string $rules - * @param array $messages An array of custom error messages + * @param array|string $rules + * @param array|string> $messages An array of custom error messages */ protected function validate($rules, array $messages = []): bool { @@ -139,10 +139,10 @@ protected function validate($rules, array $messages = []): bool /** * A shortcut to performing validation on any input data. * - * @param array $data The data to validate - * @param array|string $rules - * @param array $messages An array of custom error messages - * @param string|null $dbGroup The database group to use + * @param array $data The data to validate + * @param array|string $rules + * @param array|string> $messages An array of custom error messages + * @param string|null $dbGroup The database group to use */ protected function validateData(array $data, $rules, array $messages = [], ?string $dbGroup = null): bool { @@ -152,7 +152,8 @@ protected function validateData(array $data, $rules, array $messages = [], ?stri } /** - * @param array|string $rules + * @param array|string $rules + * @param array|string> $messages */ private function setValidator($rules, array $messages): void { diff --git a/system/Cookie/CookieStore.php b/system/Cookie/CookieStore.php index c4b2994d9cf3..ece407e98d80 100644 --- a/system/Cookie/CookieStore.php +++ b/system/Cookie/CookieStore.php @@ -196,6 +196,8 @@ public function getIterator(): Traversable /** * Validates all cookies passed to be instances of Cookie. * + * @param array $cookies + * * @throws CookieException */ protected function validateCookies(array $cookies): void diff --git a/system/Exceptions/PageNotFoundException.php b/system/Exceptions/PageNotFoundException.php index f84763ec25ff..f3a69745adc9 100644 --- a/system/Exceptions/PageNotFoundException.php +++ b/system/Exceptions/PageNotFoundException.php @@ -72,6 +72,8 @@ public static function forLocaleNotSupported(string $locale) * have the current locale, so even if users call * `$this->request->setLocale()` in the controller afterwards, * the Language locale will not be changed. + * + * @param array $args */ private static function lang(string $line, array $args = []): string { diff --git a/system/HotReloader/DirectoryHasher.php b/system/HotReloader/DirectoryHasher.php index ed82d98b894a..ab9ab7a017a4 100644 --- a/system/HotReloader/DirectoryHasher.php +++ b/system/HotReloader/DirectoryHasher.php @@ -39,6 +39,8 @@ public function hash(): string /** * Generates an array of md5 hashes for all directories that are * watched by the Hot Reloader, as defined in the Config\Toolbar. + * + * @return array */ public function hashApp(): array { diff --git a/system/HotReloader/HotReloader.php b/system/HotReloader/HotReloader.php index 305e641af04e..e1100e898436 100644 --- a/system/HotReloader/HotReloader.php +++ b/system/HotReloader/HotReloader.php @@ -61,6 +61,8 @@ public function run(): void /** * Send an event to the browser. + * + * @param array $data */ private function sendEvent(string $event, array $data): void { diff --git a/system/HotReloader/IteratorFilter.php b/system/HotReloader/IteratorFilter.php index 983e1297a71b..1134f77665f1 100644 --- a/system/HotReloader/IteratorFilter.php +++ b/system/HotReloader/IteratorFilter.php @@ -24,6 +24,9 @@ */ final class IteratorFilter extends RecursiveFilterIterator implements RecursiveIterator { + /** + * @var list + */ private array $watchedExtensions = []; public function __construct(RecursiveIterator $iterator) diff --git a/system/Images/Handlers/BaseHandler.php b/system/Images/Handlers/BaseHandler.php index bc70517a36a6..83535e1f8ad2 100644 --- a/system/Images/Handlers/BaseHandler.php +++ b/system/Images/Handlers/BaseHandler.php @@ -552,6 +552,8 @@ public function fit(int $width, ?int $height = null, string $position = 'center' * @param float|int|null $height * @param float|int $origWidth * @param float|int $origHeight + * + * @return array{0: float|int, 1: float|int} */ protected function calcAspectRatio($width, $height = null, $origWidth = 0, $origHeight = 0): array { @@ -593,6 +595,8 @@ protected function calcAspectRatio($width, $height = null, $origWidth = 0, $orig * @param float|int $origWidth * @param float|int $origHeight * @param string $position + * + * @return array{0: float|int, 1: float|int} */ protected function calcCropCoords($width, $height, $origWidth, $origHeight, $position): array { @@ -684,6 +688,8 @@ abstract protected function process(string $action); * Provide access to the Image class' methods if they don't exist * on the handler itself. * + * @param list $args + * * @return mixed */ public function __call(string $name, array $args = []) diff --git a/system/Images/Image.php b/system/Images/Image.php index 3e0b0ba6876d..e634b914016a 100644 --- a/system/Images/Image.php +++ b/system/Images/Image.php @@ -97,7 +97,7 @@ public function copy(string $targetPath, ?string $targetName = null, int $perms * * A helper function that gets info about the file * - * @return array|bool + * @return array{width: int, height: int, image_type: int, size_str: string, mime_type: string}|bool */ public function getProperties(bool $return = false) { diff --git a/system/Modules/Modules.php b/system/Modules/Modules.php index ca4fe40ab128..4a7ce3ef942c 100644 --- a/system/Modules/Modules.php +++ b/system/Modules/Modules.php @@ -60,6 +60,9 @@ public function shouldDiscover(string $alias): bool return in_array(strtolower($alias), $this->aliases, true); } + /** + * @param array $array + */ public static function __set_state(array $array) { $obj = new static(); diff --git a/system/Pager/Pager.php b/system/Pager/Pager.php index cb151a603a40..4537f1d43db3 100644 --- a/system/Pager/Pager.php +++ b/system/Pager/Pager.php @@ -34,14 +34,23 @@ class Pager implements PagerInterface /** * The group data. * - * @var array + * @var array */ protected $groups = []; /** * URI segment for groups if provided. * - * @var array + * @var array */ protected $segment = []; @@ -272,7 +281,7 @@ public function getPageURI(?int $page = null, string $group = 'default', bool $r $segment = $this->segment[$group] ?? 0; - if ($segment) { + if ($segment !== 0) { $uri->setSegment($segment, $page); } else { $uri->addQuery($this->groups[$group]['pageSelector'], $page); @@ -281,7 +290,7 @@ public function getPageURI(?int $page = null, string $group = 'default', bool $r if ($this->only !== null) { $query = array_intersect_key(service('superglobals')->getGetArray(), array_flip($this->only)); - if (! $segment) { + if ($segment === 0) { $query[$this->groups[$group]['pageSelector']] = $page; } @@ -357,12 +366,6 @@ public function getPerPage(string $group = 'default'): int return (int) $this->groups[$group]['perPage']; } - /** - * Returns an array with details about the results, including - * total, per_page, current_page, last_page, next_url, prev_url, from, to. - * Does not include the actual data. This data is suitable for adding - * a 'data' object to with the result set and converting to JSON. - */ public function getDetails(string $group = 'default'): array { if (! array_key_exists($group, $this->groups)) { @@ -380,6 +383,8 @@ public function getDetails(string $group = 'default'): array /** * Sets only allowed queries on pagination links. + * + * @param list $queries */ public function only(array $queries): self { diff --git a/system/Pager/PagerInterface.php b/system/Pager/PagerInterface.php index 650b04f48e30..bfffd92411a8 100644 --- a/system/Pager/PagerInterface.php +++ b/system/Pager/PagerInterface.php @@ -115,6 +115,20 @@ public function getPerPage(string $group = 'default'): int; * total, per_page, current_page, last_page, next_url, prev_url, from, to. * Does not include the actual data. This data is suitable for adding * a 'data' object to with the result set and converting to JSON. + * + * @return array{ + * currentUri: URI, + * uri: URI, + * hasMore: bool, + * total: int|null, + * perPage: int, + * pageCount: int, + * pageSelector: string, + * currentPage: int, + * next: string|null, + * previous: string|null, + * segment: int + * } */ public function getDetails(string $group = 'default'): array; } diff --git a/system/Pager/PagerRenderer.php b/system/Pager/PagerRenderer.php index f970ee551f19..70959c820ea4 100644 --- a/system/Pager/PagerRenderer.php +++ b/system/Pager/PagerRenderer.php @@ -101,7 +101,16 @@ class PagerRenderer protected ?int $perPageEnd = null; /** - * Constructor. + * @param array{ + * uri: URI, + * pageCount: int, + * currentPage: int, + * total: int|null, + * segment?: int, + * pageSelector?: string, + * perPage?: int|null, + * ... + * } $details */ public function __construct(array $details) { diff --git a/system/Publisher/ContentReplacer.php b/system/Publisher/ContentReplacer.php index 6e8c01fa2e41..b263b1a18d7f 100644 --- a/system/Publisher/ContentReplacer.php +++ b/system/Publisher/ContentReplacer.php @@ -25,7 +25,7 @@ class ContentReplacer /** * Replace content * - * @param array $replaces [search => replace] + * @param array $replaces [search => replace] */ public function replace(string $content, array $replaces): string { diff --git a/system/Publisher/Publisher.php b/system/Publisher/Publisher.php index 8d9f6b72327a..abf414f45c37 100644 --- a/system/Publisher/Publisher.php +++ b/system/Publisher/Publisher.php @@ -413,7 +413,7 @@ final public function merge(bool $replace = true): bool /** * Replace content * - * @param array $replaces [search => replace] + * @param array $replaces [search => replace] */ public function replace(string $file, array $replaces): bool { diff --git a/system/Typography/Typography.php b/system/Typography/Typography.php index 58bb10b0df5a..e2adc4cd5057 100644 --- a/system/Typography/Typography.php +++ b/system/Typography/Typography.php @@ -46,7 +46,7 @@ class Typography /** * array of block level elements that require inner content to be within another block level element * - * @var array + * @var list */ public $innerBlockRequired = ['blockquote']; @@ -316,6 +316,8 @@ protected function formatNewLines(string $str): string * We don't want quotes converted within tags so we'll temporarily convert them to {@DQ} and {@SQ} * and we don't want double dashes converted to emdash entities, so they are marked with {@DD} * likewise double spaces are converted to {@NBS} to prevent entity conversion + * + * @param array $match */ protected function protectCharacters(array $match): string { diff --git a/system/View/Cell.php b/system/View/Cell.php index 5e0eda26255b..7b0a15853b2f 100644 --- a/system/View/Cell.php +++ b/system/View/Cell.php @@ -67,10 +67,10 @@ public function __construct(CacheInterface $cache) /** * Render a cell, returning its body as a string. * - * @param string $library Cell class and method name. - * @param array|string|null $params Parameters to pass to the method. - * @param int $ttl Number of seconds to cache the cell. - * @param string|null $cacheName Cache item name. + * @param string $library Cell class and method name. + * @param array|string|null $params Parameters to pass to the method. + * @param int $ttl Number of seconds to cache the cell. + * @param string|null $cacheName Cache item name. * * @throws ReflectionException */ @@ -116,9 +116,9 @@ public function render(string $library, $params = null, int $ttl = 0, ?string $c * If a string, it should be in the format "key1=value key2=value". * It will be split and returned as an array. * - * @param array|float|string|null $params + * @param array|float|string|null $params * - * @return array + * @return array */ public function prepareParams($params) { @@ -199,7 +199,7 @@ protected function determineClass(string $library): array /** * Renders a cell that extends the BaseCell class. * - * @param array $params + * @param array $params */ final protected function renderCell(BaseCell $instance, string $method, array $params): string { @@ -237,7 +237,7 @@ final protected function renderCell(BaseCell $instance, string $method, array $p * for a method, in the order they are defined. This allows * them to be passed directly into the method. * - * @param array $params + * @param array $params * * @return list */ @@ -268,7 +268,7 @@ private function getMethodParams(BaseCell $instance, string $method, array $para * * @todo Determine if this can be refactored to use $this-getMethodParams(). * - * @param array $params + * @param array $params */ final protected function renderSimpleClass(object $instance, string $method, array $params, string $class): string { diff --git a/tests/system/CLI/CLITest.php b/tests/system/CLI/CLITest.php index 083f8792e31a..052fbd602aea 100644 --- a/tests/system/CLI/CLITest.php +++ b/tests/system/CLI/CLITest.php @@ -649,18 +649,20 @@ private function captureGenerateDimensionsStderr(?array $env = null): string } /** - * @param array $tbody - * @param array $thead - * @param array $expected + * @param list> $tbody + * @param list $thead */ #[DataProvider('provideTable')] - public function testTable($tbody, $thead, $expected): void + public function testTable(array $tbody, array $thead, string $expected): void { CLI::table($tbody, $thead); $this->assertSame($this->getStreamFilterBuffer(), $expected); } + /** + * @return iterable>, list, string}> + */ public static function provideTable(): iterable { $head = [ diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index 2d621f3b8703..0c7c8d2e64d0 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -834,7 +834,8 @@ public function testPageCacheSendSecureHeaders(): void } /** - * @param array|bool $cacheQueryStringValue + * @param bool|list $cacheQueryStringValue + * @param list $testingUrls * * @see https://github.com/codeigniter4/CodeIgniter4/pull/6410 */ @@ -905,6 +906,9 @@ public function testPageCacheWithCacheQueryString( CITestStreamFilter::removeErrorFilter(); } + /** + * @return iterable, int, list}> + */ public static function providePageCacheWithCacheQueryString(): iterable { $testingUrls = [ diff --git a/tests/system/CommonFunctionsTest.php b/tests/system/CommonFunctionsTest.php index 3f970d72a154..b4901a5e8d93 100644 --- a/tests/system/CommonFunctionsTest.php +++ b/tests/system/CommonFunctionsTest.php @@ -743,6 +743,9 @@ public function testCleanPathActuallyCleaningThePaths($input, $expected): void $this->assertSame($expected, clean_path($input)); } + /** + * @return iterable + */ public static function provideCleanPathActuallyCleaningThePaths(): iterable { $ds = DIRECTORY_SEPARATOR; diff --git a/tests/system/CommonHelperTest.php b/tests/system/CommonHelperTest.php index 71d5bdb8c14e..641f56dc8ec4 100644 --- a/tests/system/CommonHelperTest.php +++ b/tests/system/CommonHelperTest.php @@ -31,6 +31,9 @@ #[Group('Others')] final class CommonHelperTest extends CIUnitTestCase { + /** + * @var list + */ private array $dummyHelpers = [ APPPATH . 'Helpers' . DIRECTORY_SEPARATOR . 'foobarbaz_helper.php', SYSTEMPATH . 'Helpers' . DIRECTORY_SEPARATOR . 'foobarbaz_helper.php', diff --git a/tests/system/Cookie/CookieStoreTest.php b/tests/system/Cookie/CookieStoreTest.php index 60498052ee93..2b8fb9c5032c 100644 --- a/tests/system/Cookie/CookieStoreTest.php +++ b/tests/system/Cookie/CookieStoreTest.php @@ -24,6 +24,18 @@ #[Group('Others')] final class CookieStoreTest extends CIUnitTestCase { + /** + * @var array{ + * prefix: string, + * expires: int, + * path: string, + * domain: string, + * secure: bool, + * httponly: bool, + * samesite: string, + * raw: bool + * } + */ private array $defaults; protected function setUp(): void diff --git a/tests/system/Cookie/CookieTest.php b/tests/system/Cookie/CookieTest.php index 542ff096d0ce..cade48b24f52 100644 --- a/tests/system/Cookie/CookieTest.php +++ b/tests/system/Cookie/CookieTest.php @@ -28,6 +28,18 @@ #[Group('Others')] final class CookieTest extends CIUnitTestCase { + /** + * @var array{ + * prefix: string, + * expires: int, + * path: string, + * domain: string, + * secure: bool, + * httponly: bool, + * samesite: string, + * raw: bool + * } + */ private array $defaults; protected function setUp(): void @@ -99,6 +111,9 @@ public function testConfigPrefix(string $configPrefix, string $optionPrefix, str $this->assertSame($expected, $cookie->getPrefixedName()); } + /** + * @return iterable + */ public static function provideConfigPrefix(): iterable { yield from [ @@ -175,6 +190,9 @@ public function testInvalidExpires($expires): void new Cookie('test', 'value', ['expires' => $expires]); } + /** + * @return iterable + */ public static function provideInvalidExpires(): iterable { $cases = [ @@ -188,6 +206,9 @@ public static function provideInvalidExpires(): iterable } } + /** + * @param array $changed + */ #[DataProvider('provideSetCookieHeaderCreation')] public function testSetCookieHeaderCreation(string $header, array $changed): void { @@ -196,6 +217,9 @@ public function testSetCookieHeaderCreation(string $header, array $changed): voi $this->assertSame(array_merge($cookie, $changed), $cookie); } + /** + * @return iterable}> + */ public static function provideSetCookieHeaderCreation(): iterable { yield 'basic' => [ diff --git a/tests/system/Models/FindModelTest.php b/tests/system/Models/FindModelTest.php index b5efc8a64f64..41937bb22bf7 100644 --- a/tests/system/Models/FindModelTest.php +++ b/tests/system/Models/FindModelTest.php @@ -207,6 +207,9 @@ public function testFirstAggregate($groupBy, $total): void $this->assertSame($total, (int) $user->total); } + /** + * @return iterable + */ public static function provideFirstAggregate(): iterable { return [ @@ -293,6 +296,9 @@ public function testFirstRecoverTempUseSoftDeletes($aggregate, $groupBy): void $this->assertSame(2, (int) $user2->id); } + /** + * @return iterable + */ public static function provideAggregateAndGroupBy(): iterable { return [ diff --git a/tests/system/Models/TimestampModelTest.php b/tests/system/Models/TimestampModelTest.php index 52989cff754a..f87f54772831 100644 --- a/tests/system/Models/TimestampModelTest.php +++ b/tests/system/Models/TimestampModelTest.php @@ -39,6 +39,8 @@ protected function tearDown(): void } /** + * @param array $data + * * @return int|string Insert ID */ private function allowDatesPrepareOneRecord(array $data): int|string @@ -59,6 +61,8 @@ private function allowDatesPrepareOneRecord(array $data): int|string } /** + * @param array $data + * * @return int|string Insert ID */ private function doNotAllowDatesPrepareOneRecord(array $data): int|string diff --git a/tests/system/Pager/PagerRendererTest.php b/tests/system/Pager/PagerRendererTest.php index d06a040a7925..6744c2522ab9 100644 --- a/tests/system/Pager/PagerRendererTest.php +++ b/tests/system/Pager/PagerRendererTest.php @@ -622,7 +622,7 @@ public function testGetNextPageNumberNull(): void } /** - * @param array> $details + * @param array{uri: URI, pageCount: int, total: int, currentPage: int, perPage: int} $details */ #[DataProvider('providePageStartEnd')] public function testPageStartEnd(array $details, int $pageStart, int $pageEnd): void @@ -635,7 +635,11 @@ public function testPageStartEnd(array $details, int $pageStart, int $pageEnd): } /** - * @return array> $details + * @return iterable */ public static function providePageStartEnd(): iterable { diff --git a/tests/system/Pager/PagerTest.php b/tests/system/Pager/PagerTest.php index b1ab5d7dc4cd..875f5c25b320 100644 --- a/tests/system/Pager/PagerTest.php +++ b/tests/system/Pager/PagerTest.php @@ -76,7 +76,9 @@ public function testSetPathRemembersPath(): void $details = $this->pager->getDetails(); - $this->assertSame('foo/bar', $details['uri']->getRoutePath()); + $uri = $details['uri']; + $this->assertInstanceOf(SiteURI::class, $uri); + $this->assertSame('foo/bar', $uri->getRoutePath()); } public function testGetDetailsRecognizesPageQueryVar(): void diff --git a/tests/system/Publisher/PublisherRestrictionsTest.php b/tests/system/Publisher/PublisherRestrictionsTest.php index eaa326ee82eb..93fcb499faa1 100644 --- a/tests/system/Publisher/PublisherRestrictionsTest.php +++ b/tests/system/Publisher/PublisherRestrictionsTest.php @@ -70,6 +70,9 @@ public function testDefaultPublicRestrictions(string $path): void $this->assertSame($expected, $errors[$file]->getMessage()); } + /** + * @return iterable + */ public static function provideDefaultPublicRestrictions(): iterable { yield from [ @@ -98,6 +101,9 @@ public function testDestinations(string $destination, bool $allowed): void $this->assertInstanceOf(Publisher::class, $publisher); } + /** + * @return iterable + */ public static function provideDestinations(): iterable { return [ diff --git a/tests/system/RESTful/ResourceControllerTest.php b/tests/system/RESTful/ResourceControllerTest.php index c51e4ae39174..2fe864736c05 100644 --- a/tests/system/RESTful/ResourceControllerTest.php +++ b/tests/system/RESTful/ResourceControllerTest.php @@ -370,6 +370,9 @@ public function testXMLFormatOutput(): void $this->assertSame($expected, $result); } + /** + * @param list $args + */ private function invoke(object $controller, string $method, array $args = []): object { $method = self::getPrivateMethodInvoker($controller, $method); diff --git a/tests/system/Throttle/ThrottleTest.php b/tests/system/Throttle/ThrottleTest.php index 655cc21dfcfa..98751a86781b 100644 --- a/tests/system/Throttle/ThrottleTest.php +++ b/tests/system/Throttle/ThrottleTest.php @@ -191,6 +191,9 @@ public function testFlooding(): void $this->assertEqualsWithDelta(10.0, round($this->cache->get('throttler_127.0.0.1')), PHP_FLOAT_EPSILON); } + /** + * @param list $checkInputs + */ #[DataProvider('provideTokenTimeCalculationUCs')] public function testTokenTimeCalculationUCs(int $capacity, int $seconds, array $checkInputs): void { @@ -209,6 +212,13 @@ public function testTokenTimeCalculationUCs(int $capacity, int $seconds, array $ } } + /** + * @return iterable + * }> + */ public static function provideTokenTimeCalculationUCs(): iterable { return [ diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index e796bfe1763f..999a75fe8d95 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 99 errors +# total 42 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index de7951be1665..43333c3fa6f6 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,293 +1,8 @@ -# total 58 errors +# total 1 error parameters: ignoreErrors: - - - message: '#^Function log_message\(\) has parameter \$context with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Common.php - - - - message: '#^Function old\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Common.php - - - - message: '#^Function stringify_attributes\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Common.php - - - - message: '#^Function view\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Common.php - - - - message: '#^Function view\(\) has parameter \$options with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Common.php - - - - message: '#^Function view_cell\(\) has parameter \$params with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Common.php - - - - message: '#^Method CodeIgniter\\Controller\:\:setValidator\(\) has parameter \$messages with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Controller.php - - - - message: '#^Method CodeIgniter\\Controller\:\:setValidator\(\) has parameter \$rules with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Controller.php - - - - message: '#^Method CodeIgniter\\Controller\:\:validateData\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Controller.php - - - - message: '#^Method CodeIgniter\\Controller\:\:validateData\(\) has parameter \$messages with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Controller.php - - - - message: '#^Method CodeIgniter\\Controller\:\:validateData\(\) has parameter \$rules with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Controller.php - - - - message: '#^Method CodeIgniter\\Controller\:\:validate\(\) has parameter \$messages with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Controller.php - - - - message: '#^Method CodeIgniter\\Controller\:\:validate\(\) has parameter \$rules with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Controller.php - - - - message: '#^Method CodeIgniter\\Cookie\\CookieStore\:\:validateCookies\(\) has parameter \$cookies with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Cookie/CookieStore.php - - - - message: '#^Method CodeIgniter\\Exceptions\\PageNotFoundException\:\:lang\(\) has parameter \$args with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Exceptions/PageNotFoundException.php - - - - message: '#^Method CodeIgniter\\HotReloader\\DirectoryHasher\:\:hashApp\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HotReloader/DirectoryHasher.php - - - - message: '#^Method CodeIgniter\\HotReloader\\HotReloader\:\:sendEvent\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HotReloader/HotReloader.php - - - - message: '#^Property CodeIgniter\\HotReloader\\IteratorFilter\:\:\$watchedExtensions type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/HotReloader/IteratorFilter.php - - - - message: '#^Method CodeIgniter\\Images\\Handlers\\BaseHandler\:\:__call\(\) has parameter \$args with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Images/Handlers/BaseHandler.php - - - - message: '#^Method CodeIgniter\\Images\\Handlers\\BaseHandler\:\:calcAspectRatio\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Images/Handlers/BaseHandler.php - - - - message: '#^Method CodeIgniter\\Images\\Handlers\\BaseHandler\:\:calcCropCoords\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Images/Handlers/BaseHandler.php - - - - message: '#^Method CodeIgniter\\Images\\Image\:\:getProperties\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Images/Image.php - - - - message: '#^Method CodeIgniter\\Modules\\Modules\:\:__set_state\(\) has parameter \$array with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Modules/Modules.php - - - - message: '#^Method CodeIgniter\\Pager\\Pager\:\:getDetails\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Pager/Pager.php - - - - message: '#^Method CodeIgniter\\Pager\\Pager\:\:only\(\) has parameter \$queries with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Pager/Pager.php - - - - message: '#^Property CodeIgniter\\Pager\\Pager\:\:\$groups type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Pager/Pager.php - - - - message: '#^Property CodeIgniter\\Pager\\Pager\:\:\$segment type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Pager/Pager.php - - - - message: '#^Method CodeIgniter\\Pager\\PagerInterface\:\:getDetails\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Pager/PagerInterface.php - - - - message: '#^Method CodeIgniter\\Pager\\PagerRenderer\:\:__construct\(\) has parameter \$details with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Pager/PagerRenderer.php - - - - message: '#^Method CodeIgniter\\Publisher\\ContentReplacer\:\:replace\(\) has parameter \$replaces with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Publisher/ContentReplacer.php - - - - message: '#^Method CodeIgniter\\Publisher\\Publisher\:\:replace\(\) has parameter \$replaces with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Publisher/Publisher.php - - - - message: '#^Method CodeIgniter\\Typography\\Typography\:\:protectCharacters\(\) has parameter \$match with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Typography/Typography.php - - - - message: '#^Property CodeIgniter\\Typography\\Typography\:\:\$innerBlockRequired type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Typography/Typography.php - - message: '#^Method class@anonymous/system/Traits/PropertiesTrait\.php\:51\:\:getProperties\(\) return type has no value type specified in iterable type array\.$#' count: 1 path: ../../system/View/Cells/Cell.php - - - - message: '#^Method CodeIgniter\\CLI\\CLITest\:\:provideTable\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/CLI/CLITest.php - - - - message: '#^Method CodeIgniter\\CLI\\CLITest\:\:testTable\(\) has parameter \$expected with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/CLI/CLITest.php - - - - message: '#^Method CodeIgniter\\CLI\\CLITest\:\:testTable\(\) has parameter \$tbody with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/CLI/CLITest.php - - - - message: '#^Method CodeIgniter\\CLI\\CLITest\:\:testTable\(\) has parameter \$thead with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/CLI/CLITest.php - - - - message: '#^Method CodeIgniter\\CodeIgniterTest\:\:providePageCacheWithCacheQueryString\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/CodeIgniterTest.php - - - - message: '#^Method CodeIgniter\\CodeIgniterTest\:\:testPageCacheWithCacheQueryString\(\) has parameter \$cacheQueryStringValue with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/CodeIgniterTest.php - - - - message: '#^Method CodeIgniter\\CodeIgniterTest\:\:testPageCacheWithCacheQueryString\(\) has parameter \$testingUrls with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/CodeIgniterTest.php - - - - message: '#^Method CodeIgniter\\CommonFunctionsTest\:\:provideCleanPathActuallyCleaningThePaths\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/CommonFunctionsTest.php - - - - message: '#^Property CodeIgniter\\CommonHelperTest\:\:\$dummyHelpers type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/CommonHelperTest.php - - - - message: '#^Property CodeIgniter\\Cookie\\CookieStoreTest\:\:\$defaults type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Cookie/CookieStoreTest.php - - - - message: '#^Method CodeIgniter\\Cookie\\CookieTest\:\:provideConfigPrefix\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Cookie/CookieTest.php - - - - message: '#^Method CodeIgniter\\Cookie\\CookieTest\:\:provideInvalidExpires\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Cookie/CookieTest.php - - - - message: '#^Method CodeIgniter\\Cookie\\CookieTest\:\:provideSetCookieHeaderCreation\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Cookie/CookieTest.php - - - - message: '#^Method CodeIgniter\\Cookie\\CookieTest\:\:testSetCookieHeaderCreation\(\) has parameter \$changed with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Cookie/CookieTest.php - - - - message: '#^Property CodeIgniter\\Cookie\\CookieTest\:\:\$defaults type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Cookie/CookieTest.php - - - - message: '#^Method CodeIgniter\\Models\\FindModelTest\:\:provideAggregateAndGroupBy\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Models/FindModelTest.php - - - - message: '#^Method CodeIgniter\\Models\\FindModelTest\:\:provideFirstAggregate\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Models/FindModelTest.php - - - - message: '#^Method CodeIgniter\\Models\\TimestampModelTest\:\:allowDatesPrepareOneRecord\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Models/TimestampModelTest.php - - - - message: '#^Method CodeIgniter\\Models\\TimestampModelTest\:\:doNotAllowDatesPrepareOneRecord\(\) has parameter \$data with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Models/TimestampModelTest.php - - - - message: '#^Method CodeIgniter\\Publisher\\PublisherRestrictionsTest\:\:provideDefaultPublicRestrictions\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Publisher/PublisherRestrictionsTest.php - - - - message: '#^Method CodeIgniter\\Publisher\\PublisherRestrictionsTest\:\:provideDestinations\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Publisher/PublisherRestrictionsTest.php - - - - message: '#^Method CodeIgniter\\RESTful\\ResourceControllerTest\:\:invoke\(\) has parameter \$args with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/RESTful/ResourceControllerTest.php - - - - message: '#^Method CodeIgniter\\Throttle\\ThrottleTest\:\:provideTokenTimeCalculationUCs\(\) return type has no value type specified in iterable type iterable\.$#' - count: 1 - path: ../../tests/system/Throttle/ThrottleTest.php - - - - message: '#^Method CodeIgniter\\Throttle\\ThrottleTest\:\:testTokenTimeCalculationUCs\(\) has parameter \$checkInputs with no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/Throttle/ThrottleTest.php From 58fed05076330628240466ca91fbc5a28716385c Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Fri, 28 Aug 2026 21:00:58 +0800 Subject: [PATCH 091/163] refactor: fix remaining not found methods (#10498) --- system/Images/Handlers/BaseHandler.php | 2 ++ tests/system/CodeIgniterTest.php | 6 ++-- tests/system/Images/GDHandlerTest.php | 15 +++++++-- tests/system/Models/FindModelTest.php | 2 +- tests/system/Models/GeneralModelTest.php | 3 +- .../system/RESTful/ResourceControllerTest.php | 1 + utils/phpstan-baseline/loader.neon | 1 - utils/phpstan-baseline/method.notFound.neon | 33 ------------------- 8 files changed, 20 insertions(+), 43 deletions(-) delete mode 100644 utils/phpstan-baseline/method.notFound.neon diff --git a/system/Images/Handlers/BaseHandler.php b/system/Images/Handlers/BaseHandler.php index 83535e1f8ad2..334075701465 100644 --- a/system/Images/Handlers/BaseHandler.php +++ b/system/Images/Handlers/BaseHandler.php @@ -23,6 +23,8 @@ * Base image handling implementation. * * @template T of object + * + * @method string getPathname() */ abstract class BaseHandler implements ImageHandlerInterface { diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index 0c7c8d2e64d0..bf6321d9d1fd 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -63,6 +63,7 @@ protected function setUp(): void $this->codeigniter = new MockCodeIgniter(new App()); $response = service('response'); + $this->assertInstanceOf(Response::class, $response); $response->pretend(); } @@ -985,10 +986,7 @@ public function testStartControllerPermitsInvoke(): void $startController = self::getPrivateMethodInvoker($this->codeigniter, 'startController'); $this->setPrivateProperty($this->codeigniter, 'method', '__invoke'); - $startController(); - - // No PageNotFoundException - $this->expectNotToPerformAssertions(); + $this->assertNull($startController()); } public function testRouteAttributeCacheIntegration(): void diff --git a/tests/system/Images/GDHandlerTest.php b/tests/system/Images/GDHandlerTest.php index 8d634d0912cd..a121b17ec899 100644 --- a/tests/system/Images/GDHandlerTest.php +++ b/tests/system/Images/GDHandlerTest.php @@ -346,9 +346,12 @@ public function testImageCopy(): void $this->handler->save($this->start . 'work/ci-logo.' . $type); $this->assertTrue($this->root->hasChild('work/ci-logo.' . $type)); + /** @var vfsStreamFile $child */ + $child = $this->root->getChild('work/ci-logo.' . $type); + $this->assertNotSame( file_get_contents($this->origin . 'ci-logo.' . $type), - $this->root->getChild('work/ci-logo.' . $type)->getContent(), + $child->getContent(), ); } } @@ -380,9 +383,12 @@ public function testImageCompressionGetResource(): void $this->handler->save($this->start . 'work/ci-logo.' . $type); $this->assertTrue($this->root->hasChild('work/ci-logo.' . $type)); + /** @var vfsStreamFile $child */ + $child = $this->root->getChild('work/ci-logo.' . $type); + $this->assertNotSame( file_get_contents($this->origin . 'ci-logo.' . $type), - $this->root->getChild('work/ci-logo.' . $type)->getContent(), + $child->getContent(), ); } } @@ -401,9 +407,12 @@ public function testImageCompressionWithResource(): void $this->assertTrue($this->root->hasChild('work/ci-logo.' . $type)); + /** @var vfsStreamFile $child */ + $child = $this->root->getChild('work/ci-logo.' . $type); + $this->assertNotSame( file_get_contents($this->origin . 'ci-logo.' . $type), - $this->root->getChild('work/ci-logo.' . $type)->getContent(), + $child->getContent(), ); } } diff --git a/tests/system/Models/FindModelTest.php b/tests/system/Models/FindModelTest.php index 41937bb22bf7..0f8481b78eac 100644 --- a/tests/system/Models/FindModelTest.php +++ b/tests/system/Models/FindModelTest.php @@ -133,7 +133,7 @@ public function testFindClearsBinds(): void $binds = $this->model->builder()->getBinds(); $this->assertCount(0, $binds); - $query = $this->model->getLastQuery(); + $query = $this->model->db->getLastQuery(); $this->assertCount(1, $this->getPrivateProperty($query, 'binds')); } diff --git a/tests/system/Models/GeneralModelTest.php b/tests/system/Models/GeneralModelTest.php index 9bc62c2d3501..bd9f2d14109e 100644 --- a/tests/system/Models/GeneralModelTest.php +++ b/tests/system/Models/GeneralModelTest.php @@ -83,7 +83,8 @@ public function testUndefinedModelMethod(): void { $this->expectException(BadMethodCallException::class); $this->expectExceptionMessage('Call to undefined method Tests\Support\Models\UserModel::undefinedMethodCall'); - $this->createModel(UserModel::class)->undefinedMethodCall(); + + $this->createModel(UserModel::class)->undefinedMethodCall(); // @phpstan-ignore method.notFound (Testing Model::__call() fallback) } public function testSetAllowedFields(): void diff --git a/tests/system/RESTful/ResourceControllerTest.php b/tests/system/RESTful/ResourceControllerTest.php index 2fe864736c05..05c3a3e02ffb 100644 --- a/tests/system/RESTful/ResourceControllerTest.php +++ b/tests/system/RESTful/ResourceControllerTest.php @@ -80,6 +80,7 @@ private function createCodeigniter(): void $this->codeigniter = new MockCodeIgniter($config); $response = service('response'); + $this->assertInstanceOf(Response::class, $response); $response->pretend(); } diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 999a75fe8d95..63afcc1d175c 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -8,7 +8,6 @@ includes: - function.resultUnused.neon - method.childParameterType.neon - method.childReturnType.neon - - method.notFound.neon - missingType.iterableValue.neon - property.defaultValue.neon - property.phpDocType.neon diff --git a/utils/phpstan-baseline/method.notFound.neon b/utils/phpstan-baseline/method.notFound.neon deleted file mode 100644 index 72d050dcc45c..000000000000 --- a/utils/phpstan-baseline/method.notFound.neon +++ /dev/null @@ -1,33 +0,0 @@ -# total 8 errors - -parameters: - ignoreErrors: - - - message: '#^Call to an undefined method CodeIgniter\\HTTP\\ResponseInterface\:\:pretend\(\)\.$#' - count: 1 - path: ../../tests/system/CodeIgniterTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\Images\\Handlers\\BaseHandler\:\:getPathname\(\)\.$#' - count: 1 - path: ../../tests/system/Images/BaseHandlerTest.php - - - - message: '#^Call to an undefined method org\\bovigo\\vfs\\vfsStreamContent\:\:getContent\(\)\.$#' - count: 3 - path: ../../tests/system/Images/GDHandlerTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\Model\:\:getLastQuery\(\)\.$#' - count: 1 - path: ../../tests/system/Models/FindModelTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\Model\:\:undefinedMethodCall\(\)\.$#' - count: 1 - path: ../../tests/system/Models/GeneralModelTest.php - - - - message: '#^Call to an undefined method CodeIgniter\\HTTP\\ResponseInterface\:\:pretend\(\)\.$#' - count: 1 - path: ../../tests/system/RESTful/ResourceControllerTest.php From 9421bd4732ddc0a72d1c0174577d127d6792fb3b Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sat, 29 Aug 2026 05:16:42 +0800 Subject: [PATCH 092/163] refactor: fix the remaining fixable `assign.propertyType` and `phpdoc.propertyType` errors (#10499) --- app/Controllers/BaseController.php | 4 + system/Controller.php | 2 + system/RESTful/BaseResource.php | 2 + system/Session/Handlers/FileHandler.php | 4 +- tests/system/ControllerTest.php | 3 +- tests/system/Honeypot/HoneypotTest.php | 23 +++- tests/system/Models/EventsModelTest.php | 130 +++++++++--------- .../system/RESTful/ResourceControllerTest.php | 32 +++-- .../system/RESTful/ResourcePresenterTest.php | 32 +++-- tests/system/Session/SessionTest.php | 2 +- .../phpstan-baseline/assign.propertyType.neon | 22 +-- utils/phpstan-baseline/loader.neon | 2 +- .../phpstan-baseline/property.phpDocType.neon | 22 +-- 13 files changed, 132 insertions(+), 148 deletions(-) diff --git a/app/Controllers/BaseController.php b/app/Controllers/BaseController.php index ab45a7710be9..6f86f9254ed1 100644 --- a/app/Controllers/BaseController.php +++ b/app/Controllers/BaseController.php @@ -3,6 +3,8 @@ namespace App\Controllers; use CodeIgniter\Controller; +use CodeIgniter\HTTP\CLIRequest; +use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; use Psr\Log\LoggerInterface; @@ -28,6 +30,8 @@ abstract class BaseController extends Controller // protected $session; /** + * @param CLIRequest|IncomingRequest $request + * * @return void */ public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger) diff --git a/system/Controller.php b/system/Controller.php index e4516383c08c..2fdb5d5a59a6 100644 --- a/system/Controller.php +++ b/system/Controller.php @@ -74,6 +74,8 @@ class Controller /** * Constructor. * + * @param CLIRequest|IncomingRequest $request + * * @return void * * @throws HTTPException|RedirectException diff --git a/system/RESTful/BaseResource.php b/system/RESTful/BaseResource.php index 638f46becbbe..e5ff13e1abdb 100644 --- a/system/RESTful/BaseResource.php +++ b/system/RESTful/BaseResource.php @@ -42,6 +42,8 @@ abstract class BaseResource extends Controller /** * Constructor. * + * @param CLIRequest|IncomingRequest $request + * * @return void */ public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger) diff --git a/system/Session/Handlers/FileHandler.php b/system/Session/Handlers/FileHandler.php index f5e9261af0f4..a985b5f89793 100644 --- a/system/Session/Handlers/FileHandler.php +++ b/system/Session/Handlers/FileHandler.php @@ -24,8 +24,6 @@ class FileHandler extends BaseHandler { /** * Where to save the session files to. - * - * @var string */ protected $savePath; @@ -68,7 +66,7 @@ public function __construct(SessionConfig $config, string $ipAddress) { parent::__construct($config, $ipAddress); - if ($this->savePath !== '') { + if (is_string($this->savePath) && $this->savePath !== '') { $this->savePath = rtrim($this->savePath, '/\\'); ini_set('session.save_path', $this->savePath); } else { diff --git a/tests/system/ControllerTest.php b/tests/system/ControllerTest.php index 2ca26f9aa15c..ee6d0604ba99 100644 --- a/tests/system/ControllerTest.php +++ b/tests/system/ControllerTest.php @@ -16,7 +16,6 @@ use CodeIgniter\Config\Factories; use CodeIgniter\HTTP\Exceptions\RedirectException; use CodeIgniter\HTTP\IncomingRequest; -use CodeIgniter\HTTP\Request; use CodeIgniter\HTTP\Response; use CodeIgniter\HTTP\SiteURI; use CodeIgniter\HTTP\UserAgent; @@ -44,7 +43,7 @@ final class ControllerTest extends CIUnitTestCase /** * Current request. */ - private Request $request; + private IncomingRequest $request; /** * Current response. diff --git a/tests/system/Honeypot/HoneypotTest.php b/tests/system/Honeypot/HoneypotTest.php index 8ed9f2756064..0f0ba51f9d52 100644 --- a/tests/system/Honeypot/HoneypotTest.php +++ b/tests/system/Honeypot/HoneypotTest.php @@ -61,8 +61,11 @@ protected function setUp(): void $superglobals->setServer('REQUEST_METHOD', 'POST'); $superglobals->setPost($this->config->name, 'hey'); - $this->request = service('request', null, false); - $this->response = service('response'); + $this->request = service('request', null, false); + + $response = service('response'); + $this->assertInstanceOf(Response::class, $response); + $this->response = $response; } public function testAttachHoneypot(): void @@ -106,7 +109,10 @@ public function testAttachHoneypotAndContainerWithCSP(): void $config = new App(); $config->CSPEnabled = true; Factories::injectMock('config', 'App', $config); - $this->response = service('response', $config, false); + + $response = service('response', $config, false); + $this->assertInstanceOf(Response::class, $response); + $this->response = $response; $this->config = new HoneypotConfig(); $this->honeypot = new Honeypot($this->config); @@ -125,7 +131,10 @@ public function testNotAttachHoneypotWithCSP(): void $config = new App(); $config->CSPEnabled = true; Factories::injectMock('config', 'App', $config); - $this->response = service('response', $config, false); + + $response = service('response', $config, false); + $this->assertInstanceOf(Response::class, $response); + $this->response = $response; $this->config = new HoneypotConfig(); $this->honeypot = new Honeypot($this->config); @@ -189,7 +198,11 @@ public function testHoneypotFilterAfter(): void $uri = 'admin/foo/bar'; $this->response->setBody('
'); - $this->response = $filters->run($uri, 'after'); + + $response = $filters->run($uri, 'after'); + $this->assertInstanceOf(Response::class, $response); + $this->response = $response; + $this->assertStringContainsString($this->config->name, (string) $this->response->getBody()); } diff --git a/tests/system/Models/EventsModelTest.php b/tests/system/Models/EventsModelTest.php index 4c6b2640723a..82e49952c7a2 100644 --- a/tests/system/Models/EventsModelTest.php +++ b/tests/system/Models/EventsModelTest.php @@ -23,17 +23,19 @@ #[Group('DatabaseLive')] final class EventsModelTest extends LiveModelTestCase { - /** - * @var EventModel - */ - protected $model; - protected function setUp(): void { parent::setUp(); $this->createModel(EventModel::class); } + private function model(): EventModel + { + $this->assertInstanceOf(EventModel::class, $this->model); + + return $this->model; + } + public function testInsertEvent(): void { $data = [ @@ -43,9 +45,9 @@ public function testInsertEvent(): void 'deleted' => 0, ]; - $this->model->insert($data); - $this->assertTrue($this->model->hasToken('beforeInsert')); - $this->assertTrue($this->model->hasToken('afterInsert')); + $this->model()->insert($data); + $this->assertTrue($this->model()->hasToken('beforeInsert')); + $this->assertTrue($this->model()->hasToken('afterInsert')); } public function testUpdateEvent(): void @@ -57,106 +59,106 @@ public function testUpdateEvent(): void 'deleted' => 0, ]; - $id = $this->model->insert($data); + $id = $this->model()->insert($data); - $this->model->update($id, $data); - $this->assertTrue($this->model->hasToken('beforeUpdate')); - $this->assertTrue($this->model->hasToken('afterUpdate')); + $this->model()->update($id, $data); + $this->assertTrue($this->model()->hasToken('beforeUpdate')); + $this->assertTrue($this->model()->hasToken('afterUpdate')); } public function testDeleteEvent(): void { - $this->model->delete(1); - $this->assertTrue($this->model->hasToken('beforeDelete')); - $this->assertTrue($this->model->hasToken('afterDelete')); + $this->model()->delete(1); + $this->assertTrue($this->model()->hasToken('beforeDelete')); + $this->assertTrue($this->model()->hasToken('afterDelete')); } public function testFindEvent(): void { - $this->model->find(1); - $this->assertTrue($this->model->hasToken('beforeFind')); - $this->assertTrue($this->model->hasToken('afterFind')); + $this->model()->find(1); + $this->assertTrue($this->model()->hasToken('beforeFind')); + $this->assertTrue($this->model()->hasToken('afterFind')); } public function testBeforeFindReturnsData(): void { - $this->model->beforeFindReturnData = true; + $this->model()->beforeFindReturnData = true; - $result = $this->model->find(1); - $this->assertTrue($this->model->hasToken('beforeFind')); + $result = $this->model()->find(1); + $this->assertTrue($this->model()->hasToken('beforeFind')); $this->assertSame('foobar', $result); } public function testBeforeFindReturnDataPreventsAfterFind(): void { - $this->model->beforeFindReturnData = true; - $this->model->find(1); - $this->assertFalse($this->model->hasToken('afterFind')); + $this->model()->beforeFindReturnData = true; + $this->model()->find(1); + $this->assertFalse($this->model()->hasToken('afterFind')); } public function testFindEventSingletons(): void { // afterFind - $this->model->first(); - $this->assertTrue($this->model->eventData['singleton']); + $this->model()->first(); + $this->assertTrue($this->model()->eventData['singleton']); - $this->model->find(1); - $this->assertTrue($this->model->eventData['singleton']); + $this->model()->find(1); + $this->assertTrue($this->model()->eventData['singleton']); - $this->model->find(); - $this->assertFalse($this->model->eventData['singleton']); + $this->model()->find(); + $this->assertFalse($this->model()->eventData['singleton']); - $this->model->findAll(); - $this->assertFalse($this->model->eventData['singleton']); + $this->model()->findAll(); + $this->assertFalse($this->model()->eventData['singleton']); // beforeFind - $this->model->beforeFindReturnData = true; + $this->model()->beforeFindReturnData = true; - $this->model->first(); - $this->assertTrue($this->model->eventData['singleton']); + $this->model()->first(); + $this->assertTrue($this->model()->eventData['singleton']); - $this->model->find(1); - $this->assertTrue($this->model->eventData['singleton']); + $this->model()->find(1); + $this->assertTrue($this->model()->eventData['singleton']); - $this->model->find(); - $this->assertFalse($this->model->eventData['singleton']); + $this->model()->find(); + $this->assertFalse($this->model()->eventData['singleton']); - $this->model->findAll(); - $this->assertFalse($this->model->eventData['singleton']); + $this->model()->findAll(); + $this->assertFalse($this->model()->eventData['singleton']); } public function testAllowCallbacksFalsePreventsTriggers(): void { - $this->model->allowCallbacks(false)->find(1); - $this->assertFalse($this->model->hasToken('afterFind')); + $this->model()->allowCallbacks(false)->find(1); + $this->assertFalse($this->model()->hasToken('afterFind')); } public function testAllowCallbacksTrueFiresTriggers(): void { - $this->setPrivateProperty($this->model, 'allowCallbacks', false); - $this->model->allowCallbacks(true)->find(1); - $this->assertTrue($this->model->hasToken('afterFind')); + $this->setPrivateProperty($this->model(), 'allowCallbacks', false); + $this->model()->allowCallbacks(true)->find(1); + $this->assertTrue($this->model()->hasToken('afterFind')); } public function testAllowCallbacksResetsAfterTrigger(): void { - $this->model->allowCallbacks(false)->find(1); - $this->model->delete(1); + $this->model()->allowCallbacks(false)->find(1); + $this->model()->delete(1); - $this->assertFalse($this->model->hasToken('afterFind')); - $this->assertTrue($this->model->hasToken('afterDelete')); + $this->assertFalse($this->model()->hasToken('afterFind')); + $this->assertTrue($this->model()->hasToken('afterDelete')); } public function testAllowCallbacksUsesModelProperty(): void { - $this->setPrivateProperty($this->model, 'allowCallbacks', false); - $this->setPrivateProperty($this->model, 'tempAllowCallbacks', false); // Was already set by the constructor + $this->setPrivateProperty($this->model(), 'allowCallbacks', false); + $this->setPrivateProperty($this->model(), 'tempAllowCallbacks', false); // Was already set by the constructor - $this->model->find(1); - $this->model->delete(1); + $this->model()->find(1); + $this->model()->delete(1); - $this->assertFalse($this->model->hasToken('afterFind')); - $this->assertFalse($this->model->hasToken('afterDelete')); + $this->assertFalse($this->model()->hasToken('afterFind')); + $this->assertFalse($this->model()->hasToken('afterDelete')); } public function testInvalidEventException(): void @@ -168,11 +170,11 @@ public function testInvalidEventException(): void 'deleted' => 0, ]; - $this->setPrivateProperty($this->model, 'beforeInsert', ['anotherBeforeInsertMethod']); + $this->setPrivateProperty($this->model(), 'beforeInsert', ['anotherBeforeInsertMethod']); $this->expectException(DataException::class); $this->expectExceptionMessage('"anotherBeforeInsertMethod" is not a valid Model Event callback.'); - $this->model->insert($data); + $this->model()->insert($data); } public function testInsertBatchEvent(): void @@ -192,9 +194,9 @@ public function testInsertBatchEvent(): void ], ]; - $this->model->insertBatch($data); - $this->assertTrue($this->model->hasToken('beforeInsertBatch')); - $this->assertTrue($this->model->hasToken('afterInsertBatch')); + $this->model()->insertBatch($data); + $this->assertTrue($this->model()->hasToken('beforeInsertBatch')); + $this->assertTrue($this->model()->hasToken('afterInsertBatch')); } public function testUpdateBatchEvent(): void @@ -210,8 +212,8 @@ public function testUpdateBatchEvent(): void ], ]; - $this->model->updateBatch($data, 'name'); - $this->assertTrue($this->model->hasToken('beforeUpdateBatch')); - $this->assertTrue($this->model->hasToken('afterUpdateBatch')); + $this->model()->updateBatch($data, 'name'); + $this->assertTrue($this->model()->hasToken('beforeUpdateBatch')); + $this->assertTrue($this->model()->hasToken('afterUpdateBatch')); } } diff --git a/tests/system/RESTful/ResourceControllerTest.php b/tests/system/RESTful/ResourceControllerTest.php index 05c3a3e02ffb..70081b5ee3f9 100644 --- a/tests/system/RESTful/ResourceControllerTest.php +++ b/tests/system/RESTful/ResourceControllerTest.php @@ -51,11 +51,6 @@ final class ResourceControllerTest extends CIUnitTestCase { private CodeIgniter $codeigniter; - /** - * @var RouteCollection - */ - protected $routes; - #[WithoutErrorHandler] protected function setUp(): void { @@ -67,14 +62,21 @@ protected function setUp(): void Services::injectMock('superglobals', new Superglobals()); } + private function routes(): RouteCollection + { + $this->assertInstanceOf(RouteCollection::class, $this->routes); + + return $this->routes; + } + private function createCodeigniter(): void { service('superglobals')->setServer('SERVER_PROTOCOL', 'HTTP/1.1'); // Inject mock router. $this->routes = service('routes'); - $this->routes->resource('work', ['controller' => '\\' . Worker::class]); - Services::injectMock('routes', $this->routes); + $this->routes()->resource('work', ['controller' => '\\' . Worker::class]); + Services::injectMock('routes', $this->routes()); $config = new App(); $this->codeigniter = new MockCodeIgniter($config); @@ -107,7 +109,7 @@ public function testResourceGet(): void $this->createCodeigniter(); ob_start(); - $this->codeigniter->run($this->routes); + $this->codeigniter->run($this->routes()); $output = ob_get_clean(); $error = json_decode($output)->messages->error; @@ -129,7 +131,7 @@ public function testResourceGetNew(): void $this->createCodeigniter(); ob_start(); - $this->codeigniter->run($this->routes); + $this->codeigniter->run($this->routes()); $output = ob_get_clean(); $error = json_decode($output)->messages->error; @@ -152,7 +154,7 @@ public function testResourceGetEdit(): void $this->createCodeigniter(); ob_start(); - $this->codeigniter->run($this->routes); + $this->codeigniter->run($this->routes()); $output = ob_get_clean(); $error = json_decode($output)->messages->error; @@ -174,7 +176,7 @@ public function testResourceGetOne(): void $this->createCodeigniter(); ob_start(); - $this->codeigniter->run($this->routes); + $this->codeigniter->run($this->routes()); $output = ob_get_clean(); $error = json_decode($output)->messages->error; @@ -195,7 +197,7 @@ public function testResourcePost(): void $this->createCodeigniter(); ob_start(); - $this->codeigniter->run($this->routes); + $this->codeigniter->run($this->routes()); $output = ob_get_clean(); $error = json_decode($output)->messages->error; @@ -217,7 +219,7 @@ public function testResourcePatch(): void $this->createCodeigniter(); ob_start(); - $this->codeigniter->run($this->routes); + $this->codeigniter->run($this->routes()); $output = ob_get_clean(); $error = json_decode($output)->messages->error; @@ -239,7 +241,7 @@ public function testResourcePut(): void $this->createCodeigniter(); ob_start(); - $this->codeigniter->run($this->routes); + $this->codeigniter->run($this->routes()); $output = ob_get_clean(); $error = json_decode($output)->messages->error; @@ -261,7 +263,7 @@ public function testResourceDelete(): void $this->createCodeigniter(); ob_start(); - $this->codeigniter->run($this->routes); + $this->codeigniter->run($this->routes()); $output = ob_get_clean(); $error = json_decode($output)->messages->error; diff --git a/tests/system/RESTful/ResourcePresenterTest.php b/tests/system/RESTful/ResourcePresenterTest.php index 7e002d8e48b7..8f691c57296b 100644 --- a/tests/system/RESTful/ResourcePresenterTest.php +++ b/tests/system/RESTful/ResourcePresenterTest.php @@ -45,11 +45,6 @@ final class ResourcePresenterTest extends CIUnitTestCase { private CodeIgniter $codeigniter; - /** - * @var RouteCollection - */ - protected $routes; - #[WithoutErrorHandler] protected function setUp(): void { @@ -61,14 +56,21 @@ protected function setUp(): void Services::injectMock('superglobals', new Superglobals()); } + private function routes(): RouteCollection + { + $this->assertInstanceOf(RouteCollection::class, $this->routes); + + return $this->routes; + } + private function createCodeigniter(): void { service('superglobals')->setServer('SERVER_PROTOCOL', 'HTTP/1.1'); // Inject mock router. $this->routes = service('routes'); - $this->routes->presenter('work', ['controller' => '\\' . Worker2::class]); - Services::injectMock('routes', $this->routes); + $this->routes()->presenter('work', ['controller' => '\\' . Worker2::class]); + Services::injectMock('routes', $this->routes()); $config = new App(); $this->codeigniter = new MockCodeIgniter($config); @@ -97,7 +99,7 @@ public function testResourceGet(): void $this->createCodeigniter(); ob_start(); - $this->codeigniter->run($this->routes); + $this->codeigniter->run($this->routes()); $output = ob_get_clean(); $this->assertSame(lang('RESTful.notImplemented', ['index']), $output); @@ -119,7 +121,7 @@ public function testResourceShow(): void $this->createCodeigniter(); ob_start(); - $this->codeigniter->run($this->routes); + $this->codeigniter->run($this->routes()); $output = ob_get_clean(); $this->assertStringContainsString(lang('RESTful.notImplemented', ['show']), (string) $output); @@ -140,7 +142,7 @@ public function testResourceNew(): void $this->createCodeigniter(); ob_start(); - $this->codeigniter->run($this->routes); + $this->codeigniter->run($this->routes()); $output = ob_get_clean(); $this->assertStringContainsString(lang('RESTful.notImplemented', ['new']), (string) $output); @@ -161,7 +163,7 @@ public function testResourceCreate(): void $this->createCodeigniter(); ob_start(); - $this->codeigniter->run($this->routes); + $this->codeigniter->run($this->routes()); $output = ob_get_clean(); $this->assertStringContainsString(lang('RESTful.notImplemented', ['create']), (string) $output); @@ -183,7 +185,7 @@ public function testResourceRemove(): void $this->createCodeigniter(); ob_start(); - $this->codeigniter->run($this->routes); + $this->codeigniter->run($this->routes()); $output = ob_get_clean(); $this->assertStringContainsString(lang('RESTful.notImplemented', ['remove']), (string) $output); @@ -205,7 +207,7 @@ public function testResourceDelete(): void $this->createCodeigniter(); ob_start(); - $this->codeigniter->run($this->routes); + $this->codeigniter->run($this->routes()); $output = ob_get_clean(); $this->assertStringContainsString(lang('RESTful.notImplemented', ['delete']), (string) $output); @@ -228,7 +230,7 @@ public function testResourceEdit(): void $this->createCodeigniter(); ob_start(); - $this->codeigniter->run($this->routes); + $this->codeigniter->run($this->routes()); $output = ob_get_clean(); $this->assertStringContainsString(lang('RESTful.notImplemented', ['edit']), (string) $output); @@ -250,7 +252,7 @@ public function testResourceUpdate(): void $this->createCodeigniter(); ob_start(); - $this->codeigniter->run($this->routes); + $this->codeigniter->run($this->routes()); $output = ob_get_clean(); $this->assertStringContainsString(lang('RESTful.notImplemented', ['update']), (string) $output); diff --git a/tests/system/Session/SessionTest.php b/tests/system/Session/SessionTest.php index e405325501d5..91ed72173097 100644 --- a/tests/system/Session/SessionTest.php +++ b/tests/system/Session/SessionTest.php @@ -667,7 +667,7 @@ public function testInvalidSameSite(): void $this->expectExceptionMessage(lang('Cookie.invalidSameSite', ['Invalid'])); $config = new CookieConfig(); - $config->samesite = 'Invalid'; + $config->samesite = 'Invalid'; // @phpstan-ignore assign.propertyType (Testing rejection of an invalid SameSite value) Factories::injectMock('config', CookieConfig::class, $config); diff --git a/utils/phpstan-baseline/assign.propertyType.neon b/utils/phpstan-baseline/assign.propertyType.neon index 51c177310b90..9685a21060ff 100644 --- a/utils/phpstan-baseline/assign.propertyType.neon +++ b/utils/phpstan-baseline/assign.propertyType.neon @@ -1,4 +1,4 @@ -# total 7 errors +# total 1 error parameters: ignoreErrors: @@ -6,23 +6,3 @@ parameters: message: '#^Property CodeIgniter\\CodeIgniter\:\:\$request \(CodeIgniter\\HTTP\\CLIRequest\|CodeIgniter\\HTTP\\IncomingRequest\|null\) does not accept CodeIgniter\\HTTP\\RequestInterface\.$#' count: 1 path: ../../system/CodeIgniter.php - - - - message: '#^Property CodeIgniter\\Controller\:\:\$request \(CodeIgniter\\HTTP\\CLIRequest\|CodeIgniter\\HTTP\\IncomingRequest\) does not accept CodeIgniter\\HTTP\\RequestInterface\.$#' - count: 1 - path: ../../system/Controller.php - - - - message: '#^Property CodeIgniter\\Honeypot\\HoneypotTest\:\:\$response \(CodeIgniter\\HTTP\\Response\) does not accept CodeIgniter\\HTTP\\RequestInterface\|CodeIgniter\\HTTP\\ResponseInterface\|string\|null\.$#' - count: 1 - path: ../../tests/system/Honeypot/HoneypotTest.php - - - - message: '#^Property CodeIgniter\\Honeypot\\HoneypotTest\:\:\$response \(CodeIgniter\\HTTP\\Response\) does not accept CodeIgniter\\HTTP\\ResponseInterface\.$#' - count: 3 - path: ../../tests/system/Honeypot/HoneypotTest.php - - - - message: '#^Property Config\\Cookie\:\:\$samesite \(''''\|''Lax''\|''None''\|''Strict''\) does not accept ''Invalid''\.$#' - count: 1 - path: ../../tests/system/Session/SessionTest.php diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 63afcc1d175c..290d2f74e5a8 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 42 errors +# total 24 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/property.phpDocType.neon b/utils/phpstan-baseline/property.phpDocType.neon index 00ab948073fd..8d3e1007949e 100644 --- a/utils/phpstan-baseline/property.phpDocType.neon +++ b/utils/phpstan-baseline/property.phpDocType.neon @@ -1,4 +1,4 @@ -# total 7 errors +# total 3 errors parameters: ignoreErrors: @@ -16,23 +16,3 @@ parameters: message: '#^PHPDoc type CodeIgniter\\HTTP\\URI of property CodeIgniter\\HTTP\\IncomingRequest\:\:\$uri is not the same as PHPDoc type CodeIgniter\\HTTP\\URI\|null of overridden property CodeIgniter\\HTTP\\OutgoingRequest\:\:\$uri\.$#' count: 1 path: ../../system/HTTP/IncomingRequest.php - - - - message: '#^PHPDoc type string of property CodeIgniter\\Session\\Handlers\\FileHandler\:\:\$savePath is not the same as PHPDoc type array\\|string of overridden property CodeIgniter\\Session\\Handlers\\BaseHandler\:\:\$savePath\.$#' - count: 1 - path: ../../system/Session/Handlers/FileHandler.php - - - - message: '#^PHPDoc type Tests\\Support\\Models\\EventModel of property CodeIgniter\\Models\\EventsModelTest\:\:\$model is not the same as PHPDoc type CodeIgniter\\Model of overridden property CodeIgniter\\Models\\LiveModelTestCase\:\:\$model\.$#' - count: 1 - path: ../../tests/system/Models/EventsModelTest.php - - - - message: '#^PHPDoc type CodeIgniter\\Router\\RouteCollection of property CodeIgniter\\RESTful\\ResourceControllerTest\:\:\$routes is not the same as PHPDoc type CodeIgniter\\Router\\RouteCollection\|null of overridden property CodeIgniter\\Test\\CIUnitTestCase\:\:\$routes\.$#' - count: 1 - path: ../../tests/system/RESTful/ResourceControllerTest.php - - - - message: '#^PHPDoc type CodeIgniter\\Router\\RouteCollection of property CodeIgniter\\RESTful\\ResourcePresenterTest\:\:\$routes is not the same as PHPDoc type CodeIgniter\\Router\\RouteCollection\|null of overridden property CodeIgniter\\Test\\CIUnitTestCase\:\:\$routes\.$#' - count: 1 - path: ../../tests/system/RESTful/ResourcePresenterTest.php From 4a2fef450dae7c2ed0798459861d1c3b31095399 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Sat, 29 Aug 2026 19:04:17 +0700 Subject: [PATCH 093/163] chore: set coverage: none on setup-php on structarmed workflow (#10503) --- .github/workflows/test-structarmed.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-structarmed.yml b/.github/workflows/test-structarmed.yml index 1c6d731c9e4b..62a2dbc7d1cb 100644 --- a/.github/workflows/test-structarmed.yml +++ b/.github/workflows/test-structarmed.yml @@ -59,6 +59,7 @@ jobs: php-version: ${{ matrix.php-version }} extensions: intl tools: composer + coverage: none - name: Validate composer.json run: composer validate --strict From 02ef6670ae5d713b050dcaf61ac7977722079842 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sun, 30 Aug 2026 18:26:35 +0800 Subject: [PATCH 094/163] refactor: fix remaining `argument.type` and `method.childParameterType` errors (#10502) --- system/Cookie/Cookie.php | 2 +- system/I18n/TimeTrait.php | 8 +++-- system/Log/Handlers/ChromeLoggerHandler.php | 4 +-- tests/system/CodeIgniterTest.php | 23 ++++++------- .../system/RESTful/ResourceControllerTest.php | 2 +- utils/phpstan-baseline/argument.type.neon | 33 ------------------- utils/phpstan-baseline/loader.neon | 4 +-- .../method.childParameterType.neon | 28 ---------------- 8 files changed, 21 insertions(+), 83 deletions(-) delete mode 100644 utils/phpstan-baseline/argument.type.neon delete mode 100644 utils/phpstan-baseline/method.childParameterType.neon diff --git a/system/Cookie/Cookie.php b/system/Cookie/Cookie.php index 61de7d6ce672..b807e0885df4 100644 --- a/system/Cookie/Cookie.php +++ b/system/Cookie/Cookie.php @@ -619,7 +619,7 @@ public function offsetGet($offset): bool|int|string /** * Offset to set. * - * @param string $offset + * @param string|null $offset * @param bool|int|string $value * * @throws LogicException diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index 588162b6f093..0660724f5019 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -1239,10 +1239,14 @@ public function __isset($name): bool /** * This is called when we unserialize the Time object. * - * @param array{date: string, timezone: string, timezone_type: int} $data + * @param array $data */ public function __unserialize(array $data): void { - parent::__construct($data['date'], new DateTimeZone($data['timezone'])); + $date = $data['date'] ?? null; + $timezone = $data['timezone'] ?? null; + assert(is_string($date) && is_string($timezone)); + + parent::__construct($date, new DateTimeZone($timezone)); } } diff --git a/system/Log/Handlers/ChromeLoggerHandler.php b/system/Log/Handlers/ChromeLoggerHandler.php index 8d763399b513..ce8e97a12750 100644 --- a/system/Log/Handlers/ChromeLoggerHandler.php +++ b/system/Log/Handlers/ChromeLoggerHandler.php @@ -99,8 +99,8 @@ public function __construct(array $config = []) * will stop. Any handlers that have not run, yet, will not * be run. * - * @param string $level - * @param string $message + * @param string $level + * @param object|string $message */ public function handle($level, $message): bool { diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index bf6321d9d1fd..c5b3863ee203 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -119,7 +119,7 @@ public function testRunClosureRoute(): void // Inject mock router. $routes = service('routes'); - $routes->add('pages/(:segment)', static function ($segment): void { + $routes->add('pages/(:segment)', static function (string $segment = ''): void { echo 'You want to see "' . esc($segment) . '" page.'; }); $router = service('router', $routes, service('incomingrequest')); @@ -231,7 +231,7 @@ public function testControllersCanReturnString(): void $routes = service('routes'); $routes->add( 'pages/(:segment)', - static fn ($segment): string => 'You want to see "' . esc($segment) . '" page.', + static fn (string $segment = ''): string => 'You want to see "' . esc($segment) . '" page.', ); $router = service('router', $routes, service('incomingrequest')); Services::injectMock('router', $router); @@ -253,12 +253,10 @@ public function testControllersCanReturnResponseObject(): void // Inject mock router. $routes = service('routes'); - $routes->add('pages/(:segment)', static function ($segment) { - $response = service('response'); - $string = "You want to see 'about' page."; - - return $response->setBody($string); - }); + $routes->add( + 'pages/(:segment)', + static fn () => service('response')->setBody("You want to see 'about' page."), + ); $router = service('router', $routes, service('incomingrequest')); Services::injectMock('router', $router); @@ -282,11 +280,10 @@ public function testControllersCanReturnDownloadResponseObject(): void // Inject mock router. $routes = service('routes'); - $routes->add('pages/(:segment)', static function ($segment) { - $response = service('response'); - - return $response->download('some.txt', 'some text', true); - }); + $routes->add( + 'pages/(:segment)', + static fn () => service('response')->download('some.txt', 'some text', true), + ); $router = service('router', $routes, service('incomingrequest')); Services::injectMock('router', $router); diff --git a/tests/system/RESTful/ResourceControllerTest.php b/tests/system/RESTful/ResourceControllerTest.php index 70081b5ee3f9..d660cb7c3f9c 100644 --- a/tests/system/RESTful/ResourceControllerTest.php +++ b/tests/system/RESTful/ResourceControllerTest.php @@ -310,7 +310,7 @@ public function testFormat(): void $resource = new MockResourceController(); $this->assertSame('json', $resource->getFormat()); - $resource->setFormat('Nonsense'); + $resource->setFormat('Nonsense'); // @phpstan-ignore argument.type (Testing that an invalid format is silently ignored) $this->assertSame('json', $resource->getFormat()); $resource->setFormat('xml'); diff --git a/utils/phpstan-baseline/argument.type.neon b/utils/phpstan-baseline/argument.type.neon deleted file mode 100644 index b9f5906a8ac3..000000000000 --- a/utils/phpstan-baseline/argument.type.neon +++ /dev/null @@ -1,33 +0,0 @@ -# total 6 errors - -parameters: - ignoreErrors: - - - message: '#^Parameter \#2 \$to of method CodeIgniter\\Router\\RouteCollection\:\:add\(\) expects array\\|\(Closure\(mixed \.\.\.\)\: \(CodeIgniter\\HTTP\\ResponseInterface\|string\|void\)\)\|string, Closure\(mixed\)\: CodeIgniter\\HTTP\\ResponseInterface given\.$#' - count: 1 - path: ../../tests/system/CodeIgniterTest.php - - - - message: '#^Parameter \#2 \$to of method CodeIgniter\\Router\\RouteCollection\:\:add\(\) expects array\\|\(Closure\(mixed \.\.\.\)\: \(CodeIgniter\\HTTP\\ResponseInterface\|string\|void\)\)\|string, Closure\(mixed\)\: \(CodeIgniter\\HTTP\\DownloadResponse\|null\) given\.$#' - count: 1 - path: ../../tests/system/CodeIgniterTest.php - - - - message: '#^Parameter \#2 \$to of method CodeIgniter\\Router\\RouteCollection\:\:add\(\) expects array\\|\(Closure\(mixed \.\.\.\)\: \(CodeIgniter\\HTTP\\ResponseInterface\|string\|void\)\)\|string, Closure\(mixed\)\: non\-falsy\-string given\.$#' - count: 1 - path: ../../tests/system/CodeIgniterTest.php - - - - message: '#^Parameter \#2 \$to of method CodeIgniter\\Router\\RouteCollection\:\:add\(\) expects array\\|\(Closure\(mixed \.\.\.\)\: \(CodeIgniter\\HTTP\\ResponseInterface\|string\|void\)\)\|string, Closure\(mixed\)\: void given\.$#' - count: 1 - path: ../../tests/system/CodeIgniterTest.php - - - - message: '#^Parameter \#2 \$message of method CodeIgniter\\Log\\Handlers\\ChromeLoggerHandler\:\:handle\(\) expects string, stdClass given\.$#' - count: 1 - path: ../../tests/system/Log/Handlers/ChromeLoggerHandlerTest.php - - - - message: '#^Parameter \#1 \$format of method CodeIgniter\\RESTful\\ResourceController\:\:setFormat\(\) expects ''json''\|''xml'', ''Nonsense'' given\.$#' - count: 1 - path: ../../tests/system/RESTful/ResourceControllerTest.php diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 290d2f74e5a8..78ffcf540c8a 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,12 +1,10 @@ -# total 24 errors +# total 13 errors includes: - - argument.type.neon - arguments.count.neon - assign.propertyType.neon - deadCode.unreachable.neon - function.resultUnused.neon - - method.childParameterType.neon - method.childReturnType.neon - missingType.iterableValue.neon - property.defaultValue.neon diff --git a/utils/phpstan-baseline/method.childParameterType.neon b/utils/phpstan-baseline/method.childParameterType.neon deleted file mode 100644 index d237e221ebf5..000000000000 --- a/utils/phpstan-baseline/method.childParameterType.neon +++ /dev/null @@ -1,28 +0,0 @@ -# total 5 errors - -parameters: - ignoreErrors: - - - message: '#^Parameter \#1 \$offset \(string\) of method CodeIgniter\\Cookie\\Cookie\:\:offsetSet\(\) should be contravariant with parameter \$offset \(string\|null\) of method ArrayAccess\\:\:offsetSet\(\)$#' - count: 1 - path: ../../system/Cookie/Cookie.php - - - - message: '#^Parameter \#1 \$data \(array\{date\: string, timezone\: string, timezone_type\: int\}\) of method CodeIgniter\\I18n\\Time\:\:__unserialize\(\) should be contravariant with parameter \$data \(array\) of method DateTimeImmutable\:\:__unserialize\(\)$#' - count: 1 - path: ../../system/I18n/Time.php - - - - message: '#^Parameter \#1 \$data \(array\{date\: string, timezone\: string, timezone_type\: int\}\) of method CodeIgniter\\I18n\\Time\:\:__unserialize\(\) should be contravariant with parameter \$data \(array\) of method DateTimeInterface\:\:__unserialize\(\)$#' - count: 1 - path: ../../system/I18n/Time.php - - - - message: '#^Parameter \#1 \$data \(array\{date\: string, timezone\: string, timezone_type\: int\}\) of method CodeIgniter\\I18n\\TimeLegacy\:\:__unserialize\(\) should be contravariant with parameter \$data \(array\) of method DateTimeInterface\:\:__unserialize\(\)$#' - count: 1 - path: ../../system/I18n/TimeLegacy.php - - - - message: '#^Parameter \#1 \$data \(array\{date\: string, timezone\: string, timezone_type\: int\}\) of method CodeIgniter\\I18n\\TimeLegacy\:\:__unserialize\(\) should be contravariant with parameter \$data \(array\) of method DateTime\:\:__unserialize\(\)$#' - count: 1 - path: ../../system/I18n/TimeLegacy.php From a826e69524c36a75590c58c2fed4c5c09c424a2d Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sun, 30 Aug 2026 19:00:07 +0800 Subject: [PATCH 095/163] refactor: replace anonymous class with bound closure in `PropertiesTrait` (#10504) --- system/Traits/PropertiesTrait.php | 15 +++++---------- utils/phpstan-baseline/loader.neon | 3 +-- .../missingType.iterableValue.neon | 8 -------- 3 files changed, 6 insertions(+), 20 deletions(-) delete mode 100644 utils/phpstan-baseline/missingType.iterableValue.neon diff --git a/system/Traits/PropertiesTrait.php b/system/Traits/PropertiesTrait.php index dd14e6c68d3a..26bb4bf322f4 100644 --- a/system/Traits/PropertiesTrait.php +++ b/system/Traits/PropertiesTrait.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Traits; +use Closure; use ReflectionClass; use ReflectionProperty; @@ -48,17 +49,11 @@ final public function fill(array $params): self */ final public function getPublicProperties(): array { - $worker = new class () { - /** - * @return array - */ - public function getProperties(object $obj): array - { - return get_object_vars($obj); - } - }; + $fn = fn (): array => get_object_vars($this); + + $bound = Closure::bind($fn, $this, null); - return $worker->getProperties($this); + return $bound(); } /** diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 78ffcf540c8a..c00a0da66add 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 13 errors +# total 12 errors includes: - arguments.count.neon @@ -6,6 +6,5 @@ includes: - deadCode.unreachable.neon - function.resultUnused.neon - method.childReturnType.neon - - missingType.iterableValue.neon - property.defaultValue.neon - property.phpDocType.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon deleted file mode 100644 index 43333c3fa6f6..000000000000 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ /dev/null @@ -1,8 +0,0 @@ -# total 1 error - -parameters: - ignoreErrors: - - - message: '#^Method class@anonymous/system/Traits/PropertiesTrait\.php\:51\:\:getProperties\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/View/Cells/Cell.php From 9b71bf7cfe11cd54ebf107d1bbaea6af1a3bbc93 Mon Sep 17 00:00:00 2001 From: MD Ali Kadar Date: Tue, 1 Sep 2026 22:00:56 +0600 Subject: [PATCH 096/163] docs: fix UPLOAD_ERR_XXX typo in UploadedFile docblocks (#10511) --- system/HTTP/Files/UploadedFile.php | 4 ++-- system/HTTP/Files/UploadedFileInterface.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/system/HTTP/Files/UploadedFile.php b/system/HTTP/Files/UploadedFile.php index 97c228df8f7c..bf594acddc38 100644 --- a/system/HTTP/Files/UploadedFile.php +++ b/system/HTTP/Files/UploadedFile.php @@ -57,7 +57,7 @@ class UploadedFile extends File implements UploadedFileInterface /** * The error constant of the upload - * (one of PHP's UPLOADERRXXX constants) + * (one of PHP's UPLOAD_ERR_XXX constants) * * @var int|null */ @@ -77,7 +77,7 @@ class UploadedFile extends File implements UploadedFileInterface * @param string $originalName The client-provided filename. * @param string|null $mimeType The type of file as provided by PHP * @param int|null $size The size of the file, in bytes - * @param int|null $error The error constant of the upload (one of PHP's UPLOADERRXXX constants) + * @param int|null $error The error constant of the upload (one of PHP's UPLOAD_ERR_XXX constants) * @param string|null $clientPath The webkit relative path of the uploaded file. */ public function __construct(string $path, string $originalName, ?string $mimeType = null, ?int $size = null, ?int $error = null, ?string $clientPath = null) diff --git a/system/HTTP/Files/UploadedFileInterface.php b/system/HTTP/Files/UploadedFileInterface.php index 6e849d04a76b..edc88e500573 100644 --- a/system/HTTP/Files/UploadedFileInterface.php +++ b/system/HTTP/Files/UploadedFileInterface.php @@ -32,7 +32,7 @@ interface UploadedFileInterface * @param string $originalName The client-provided filename. * @param string|null $mimeType The type of file as provided by PHP * @param int|null $size The size of the file, in bytes - * @param int|null $error The error constant of the upload (one of PHP's UPLOADERRXXX constants) + * @param int|null $error The error constant of the upload (one of PHP's UPLOAD_ERR_XXX constants) * @param string|null $clientPath The webkit relative path of the uploaded file. */ public function __construct(string $path, string $originalName, ?string $mimeType = null, ?int $size = null, ?int $error = null, ?string $clientPath = null); From 70c99ebb3f180d561e7a8cf48b751a63dac5421c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 18:01:51 +0200 Subject: [PATCH 097/163] chore(deps-dev): update rector/rector requirement from 2.6.3 to 2.6.4 in the composer-dependencies group (#10513) * 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.6.4 - [Release notes](https://github.com/rectorphp/rector/releases) - [Commits](https://github.com/rectorphp/rector/compare/2.6.3...2.6.4) --- updated-dependencies: - dependency-name: rector/rector dependency-version: 2.6.4 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] * chore: apply IfToNullCoalescingAssignRector rule * fix: phpstan --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: michalsn --- composer.json | 2 +- system/BaseModel.php | 4 +- system/CLI/SignalTrait.php | 4 +- system/CodeIgniter.php | 4 +- system/Commands/ListCommands.php | 4 +- system/Common.php | 8 +-- system/Config/BaseService.php | 4 +- system/Database/BaseBuilder.php | 4 +- system/Database/BaseConnection.php | 9 +-- system/Database/BaseUtils.php | 4 +- system/Database/Config.php | 4 +- system/Database/Query.php | 4 +- system/Database/SQLSRV/Connection.php | 7 +- system/Database/SQLite3/Builder.php | 2 +- system/Debug/BaseExceptionHandler.php | 4 +- system/Debug/Timer.php | 8 +-- system/Email/Email.php | 9 +-- system/Filters/Filters.php | 8 +-- system/HTTP/IncomingRequest.php | 16 ++--- system/HTTP/RequestTrait.php | 8 +-- system/HTTP/ResponseTrait.php | 4 +- system/HTTP/URI.php | 4 +- system/Helpers/form_helper.php | 12 +--- system/Helpers/html_helper.php | 8 +-- system/Helpers/url_helper.php | 4 +- system/Images/Handlers/BaseHandler.php | 4 +- system/Images/Handlers/GDHandler.php | 10 ++- system/Router/RouteCollection.php | 4 +- system/Session/Handlers/DatabaseHandler.php | 4 +- system/Session/Handlers/FileHandler.php | 4 +- system/Session/Handlers/MemcachedHandler.php | 4 +- system/Session/Handlers/RedisHandler.php | 4 +- system/Test/CIUnitTestCase.php | 4 +- system/Test/Fabricator.php | 5 +- system/Test/FilterTestTrait.php | 4 +- system/Typography/Typography.php | 74 ++++++++++---------- system/Validation/StrictRules/FileRules.php | 28 ++------ system/View/Parser.php | 20 ++---- system/View/Table.php | 4 +- tests/system/Validation/ValidationTest.php | 4 +- tests/system/View/ParserTest.php | 4 +- 41 files changed, 102 insertions(+), 228 deletions(-) diff --git a/composer.json b/composer.json index 046faefcf83a..bb51819c9849 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "phpunit/phpcov": "^9.0.2 || ^10.0", "phpunit/phpunit": "^10.5.16 || ^11.2", "predis/predis": "^3.0", - "rector/rector": "2.6.3", + "rector/rector": "2.6.4", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { diff --git a/system/BaseModel.php b/system/BaseModel.php index 5579bd8f810a..64d083fe4d2b 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -1648,9 +1648,7 @@ public function getValidationRules(array $options = []): array protected function ensureValidation(): void { - if ($this->validation === null) { - $this->validation = service('validation', null, false); - } + $this->validation ??= service('validation', null, false); } /** diff --git a/system/CLI/SignalTrait.php b/system/CLI/SignalTrait.php index 0490e15d9d55..792f7334f865 100644 --- a/system/CLI/SignalTrait.php +++ b/system/CLI/SignalTrait.php @@ -81,9 +81,7 @@ protected function isPcntlAvailable(): bool */ protected function isPosixAvailable(): bool { - if (self::$isPosixAvailable === null) { - self::$isPosixAvailable = is_windows() ? false : extension_loaded('posix'); - } + self::$isPosixAvailable ??= is_windows() ? false : extension_loaded('posix'); return self::$isPosixAvailable; } diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 6fef8cbb6362..177edbd8d753 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -624,9 +624,7 @@ protected function bootstrapEnvironment() */ protected function startBenchmark() { - if ($this->startTime === null) { - $this->startTime = microtime(true); - } + $this->startTime ??= microtime(true); $this->benchmark = Services::timer(); $this->benchmark->start('total_execution', $this->startTime); diff --git a/system/Commands/ListCommands.php b/system/Commands/ListCommands.php index c976733b768d..20ee5305ecaa 100644 --- a/system/Commands/ListCommands.php +++ b/system/Commands/ListCommands.php @@ -101,9 +101,7 @@ protected function listFull(array $commands) $groups = []; foreach ($commands as $title => $command) { - if (! isset($groups[$command['group']])) { - $groups[$command['group']] = []; - } + $groups[$command['group']] ??= []; $groups[$command['group']][$title] = $command; } diff --git a/system/Common.php b/system/Common.php index 226c0554a6cf..72502d0fd5d0 100644 --- a/system/Common.php +++ b/system/Common.php @@ -490,9 +490,7 @@ function esc($data, string $context = 'html', ?string $encoding = null) static $escapers = []; $cacheKey = strtolower($encoding ?? 'utf-8'); - if (! isset($escapers[$cacheKey])) { - $escapers[$cacheKey] = new Escaper($encoding); - } + $escapers[$cacheKey] ??= new Escaper($encoding); $data = $escapers[$cacheKey]->{$method}($data); } @@ -588,9 +586,7 @@ function function_usable(string $functionName): bool static $_suhosin_func_blacklist; if (function_exists($functionName)) { - if (! isset($_suhosin_func_blacklist)) { - $_suhosin_func_blacklist = extension_loaded('suhosin') ? explode(',', trim(ini_get('suhosin.executor.func.blacklist'))) : []; - } + $_suhosin_func_blacklist ??= extension_loaded('suhosin') ? explode(',', trim(ini_get('suhosin.executor.func.blacklist'))) : []; return ! in_array($functionName, $_suhosin_func_blacklist, true); } diff --git a/system/Config/BaseService.php b/system/Config/BaseService.php index e5f46586a305..06cbabd10efd 100644 --- a/system/Config/BaseService.php +++ b/system/Config/BaseService.php @@ -276,9 +276,7 @@ protected static function getSharedInstance(string $key, ...$params) public static function autoloader(bool $getShared = true) { if ($getShared) { - if (! isset(static::$instances['autoloader'])) { - static::$instances['autoloader'] = new Autoloader(); - } + static::$instances['autoloader'] ??= new Autoloader(); return static::$instances['autoloader']; } diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 1d2055d9ca24..fe2c1c371816 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -1488,9 +1488,7 @@ public function orderBy(string $orderBy, string $direction = '', ?bool $escape = $direction = in_array($direction, ['ASC', 'DESC'], true) ? ' ' . $direction : ''; } - if ($escape === null) { - $escape = $this->db->protectIdentifiers; - } + $escape ??= $this->db->protectIdentifiers; if ($escape === false) { $qbOrderBy[] = [ diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index 98ec9df3e2b9..a7af22565cca 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -467,9 +467,7 @@ private function getBuiltinPropertyTypesMap(array $properties): array $className = static::class; $requested = array_fill_keys($properties, true); - if (! isset(self::$propertyBuiltinTypesCache[$className])) { - self::$propertyBuiltinTypesCache[$className] = []; - } + self::$propertyBuiltinTypesCache[$className] ??= []; // Fill only the properties requested by this call that are not cached yet. $missing = array_diff_key($requested, self::$propertyBuiltinTypesCache[$className]); @@ -1867,10 +1865,7 @@ protected function foreignKeyDataToObjects(array $data) foreach ($data as $row) { $name = $row['constraint_name']; - // for sqlite generate name - if ($name === null) { - $name = $row['table_name'] . '_' . implode('_', $row['column_name']) . '_foreign'; - } + $name ??= $row['table_name'] . '_' . implode('_', $row['column_name']) . '_foreign'; $obj = new stdClass(); $obj->constraint_name = $name; diff --git a/system/Database/BaseUtils.php b/system/Database/BaseUtils.php index cf42bb7f7184..442fdd2a766b 100644 --- a/system/Database/BaseUtils.php +++ b/system/Database/BaseUtils.php @@ -233,9 +233,7 @@ public function getCSVFromResult(ResultInterface $query, string $delim = ',', st public function getXMLFromResult(ResultInterface $query, array $params = []): string { foreach (['root' => 'root', 'element' => 'element', 'newline' => "\n", 'tab' => "\t"] as $key => $val) { - if (! isset($params[$key])) { - $params[$key] = $val; - } + $params[$key] ??= $val; } $root = $params['root']; diff --git a/system/Database/Config.php b/system/Database/Config.php index 790c2fb63b12..250af22acd62 100644 --- a/system/Database/Config.php +++ b/system/Database/Config.php @@ -60,9 +60,7 @@ public static function connect($group = null, bool $getShared = true) } else { $dbConfig = config(DbConfig::class); - if ($group === null) { - $group = (ENVIRONMENT === 'testing') ? 'tests' : $dbConfig->defaultGroup; - } + $group ??= (ENVIRONMENT === 'testing') ? 'tests' : $dbConfig->defaultGroup; assert(is_string($group)); diff --git a/system/Database/Query.php b/system/Database/Query.php index 294f143e57e8..41d21c93330d 100644 --- a/system/Database/Query.php +++ b/system/Database/Query.php @@ -161,9 +161,7 @@ public function setDuration(float $start, ?float $end = null): self { $this->startTime = $start; - if ($end === null) { - $end = microtime(true); - } + $end ??= microtime(true); $this->endTime = $end; diff --git a/system/Database/SQLSRV/Connection.php b/system/Database/SQLSRV/Connection.php index 5e82e7b2abf3..e18408c5c34d 100644 --- a/system/Database/SQLSRV/Connection.php +++ b/system/Database/SQLSRV/Connection.php @@ -47,7 +47,7 @@ class Connection extends BaseConnection * FALSE or SQLSRV_CURSOR_FORWARD would increase performance, * but would disable num_rows() (and possibly insert_id()) * - * @var false|string + * @var false|string|null */ public $scrollable; @@ -88,10 +88,7 @@ public function __construct(array $params) { parent::__construct($params); - // This is only supported as of SQLSRV 3.0 - if ($this->scrollable === null) { - $this->scrollable = defined('SQLSRV_CURSOR_CLIENT_BUFFERED') ? SQLSRV_CURSOR_CLIENT_BUFFERED : false; - } + $this->scrollable ??= defined('SQLSRV_CURSOR_CLIENT_BUFFERED') ? SQLSRV_CURSOR_CLIENT_BUFFERED : false; } /** diff --git a/system/Database/SQLite3/Builder.php b/system/Database/SQLite3/Builder.php index d31cfb493967..be6bcbe1a0c5 100644 --- a/system/Database/SQLite3/Builder.php +++ b/system/Database/SQLite3/Builder.php @@ -242,7 +242,7 @@ protected function _deleteBatch(string $table, array $keys, array $values): stri // @codeCoverageIgnore } - if (is_string(current(array_keys($constraints)))) { + if (is_string(array_key_first($constraints))) { $concat1 = implode(' || ', array_keys($constraints)); $concat2 = implode(' || ', array_values($constraints)); } else { diff --git a/system/Debug/BaseExceptionHandler.php b/system/Debug/BaseExceptionHandler.php index 6adaa8eaaca9..48fd5b3404c8 100644 --- a/system/Debug/BaseExceptionHandler.php +++ b/system/Debug/BaseExceptionHandler.php @@ -48,9 +48,7 @@ public function __construct(ExceptionsConfig $config) $this->obLevel = ob_get_level(); - if ($this->viewPath === null) { - $this->viewPath = rtrim($this->config->errorViewPath, '\\/ ') . DIRECTORY_SEPARATOR; - } + $this->viewPath ??= rtrim($this->config->errorViewPath, '\\/ ') . DIRECTORY_SEPARATOR; } /** diff --git a/system/Debug/Timer.php b/system/Debug/Timer.php index be4b56ce7cc8..9b6af4d4bcd0 100644 --- a/system/Debug/Timer.php +++ b/system/Debug/Timer.php @@ -96,9 +96,7 @@ public function getElapsedTime(string $name, int $decimals = 4) $timer = $this->timers[$name]; - if ($timer['end'] === null) { - $timer['end'] = microtime(true); - } + $timer['end'] ??= microtime(true); return (float) number_format($timer['end'] - $timer['start'], $decimals, '.', ''); } @@ -115,9 +113,7 @@ public function getTimers(int $decimals = 4): array $timers = $this->timers; foreach ($timers as &$timer) { - if ($timer['end'] === null) { - $timer['end'] = microtime(true); - } + $timer['end'] ??= microtime(true); $timer['duration'] = (float) number_format($timer['end'] - $timer['start'], $decimals); } diff --git a/system/Email/Email.php b/system/Email/Email.php index a3d948d9edfd..9304734fd179 100644 --- a/system/Email/Email.php +++ b/system/Email/Email.php @@ -418,9 +418,7 @@ public function __construct($config = null) { $this->initialize($config); - if (! isset(static::$func_overload)) { - static::$func_overload = extension_loaded('mbstring') && ini_get('mbstring.func_overload'); - } + static::$func_overload ??= extension_loaded('mbstring') && ini_get('mbstring.func_overload'); } /** @@ -1461,10 +1459,7 @@ protected function prepQEncoding($str) } } - // We might already have this set for UTF-8 - if (! isset($chars)) { - $chars = static::strlen($str); - } + $chars ??= static::strlen($str); $output = '=?' . $this->charset . '?Q?'; diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 973b9ded7979..36725b8198d3 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -556,13 +556,9 @@ public function addFilter(string $class, ?string $alias = null, string $position { $alias ??= md5($class); - if (! isset($this->config->{$section})) { - $this->config->{$section} = []; - } + $this->config->{$section} ??= []; - if (! isset($this->config->{$section}[$position])) { - $this->config->{$section}[$position] = []; - } + $this->config->{$section}[$position] ??= []; $this->config->aliases[$alias] = $class; diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index f630767bcc35..5eb029df8385 100644 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -208,9 +208,7 @@ public function detectLocale($config) */ public function negotiate(string $type, array $supported, bool $strictMatch = false): string { - if ($this->negotiator === null) { - $this->negotiator = Services::negotiator($this, true); - } + $this->negotiator ??= Services::negotiator($this, true); return match (strtolower($type)) { 'media' => $this->negotiator->media($supported, $strictMatch), @@ -750,9 +748,7 @@ public function getOldInput(string $key) */ public function getFiles(): array { - if ($this->files === null) { - $this->files = new FileCollection(); - } + $this->files ??= new FileCollection(); return $this->files->all(); // return all files } @@ -765,9 +761,7 @@ public function getFiles(): array */ public function getFileMultiple(string $fileID) { - if ($this->files === null) { - $this->files = new FileCollection(); - } + $this->files ??= new FileCollection(); return $this->files->getFileMultiple($fileID); } @@ -780,9 +774,7 @@ public function getFileMultiple(string $fileID) */ public function getFile(string $fileID) { - if ($this->files === null) { - $this->files = new FileCollection(); - } + $this->files ??= new FileCollection(); return $this->files->getFile($fileID); } diff --git a/system/HTTP/RequestTrait.php b/system/HTTP/RequestTrait.php index c08d1d0bb808..973757c8559e 100644 --- a/system/HTTP/RequestTrait.php +++ b/system/HTTP/RequestTrait.php @@ -338,9 +338,7 @@ public function fetchGlobal(string $name, $index = null, ?int $filter = null, $f } } - if (! isset($value)) { - $value = $this->globals[$name][$index] ?? null; - } + $value ??= $this->globals[$name][$index] ?? null; if (is_array($value) && ( @@ -379,9 +377,7 @@ public function fetchGlobal(string $name, $index = null, ?int $filter = null, $f */ protected function populateGlobals(string $name) { - if (! isset($this->globals[$name])) { - $this->globals[$name] = []; - } + $this->globals[$name] ??= []; // Get data from Superglobals service instead of direct access $this->globals[$name] = service('superglobals')->getGlobalArray($name); diff --git a/system/HTTP/ResponseTrait.php b/system/HTTP/ResponseTrait.php index 50a8ab8c319e..5b7e02c2a954 100644 --- a/system/HTTP/ResponseTrait.php +++ b/system/HTTP/ResponseTrait.php @@ -475,9 +475,7 @@ public function redirect(string $uri, string $method = 'auto', ?int $code = null } } - if ($code === null) { - $code = 302; - } + $code ??= 302; match ($method) { 'refresh' => $this->setHeader('Refresh', '0;url=' . $uri), diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index 3b91a8e18e8a..4b8b324dc80e 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -697,9 +697,7 @@ public function setAuthority(string $str) { $parts = parse_url($str); - if (! isset($parts['path'])) { - $parts['path'] = $this->getPath(); - } + $parts['path'] ??= $this->getPath(); if (! isset($parts['host']) && $parts['path'] !== '') { $parts['host'] = $parts['path']; diff --git a/system/Helpers/form_helper.php b/system/Helpers/form_helper.php index fc67aac601bf..1cf120acb78a 100644 --- a/system/Helpers/form_helper.php +++ b/system/Helpers/form_helper.php @@ -565,9 +565,7 @@ function set_value(string $field, $default = '', bool $htmlEscape = true) // Try any old input data we may have first $value = $request->getOldInput($field); - if ($value === null) { - $value = $request->getPost($field) ?? $default; - } + $value ??= $request->getPost($field) ?? $default; return ($htmlEscape) ? esc($value) : $value; } @@ -586,9 +584,7 @@ function set_select(string $field, string $value = '', bool $default = false): s // Try any old input data we may have first $input = $request->getOldInput($field); - if ($input === null) { - $input = $request->getPost($field); - } + $input ??= $request->getPost($field); if ($input === null) { return $default ? ' selected="selected"' : ''; @@ -622,9 +618,7 @@ function set_checkbox(string $field, string $value = '', bool $default = false): // Try any old input data we may have first $input = $request->getOldInput($field); - if ($input === null) { - $input = $request->getPost($field); - } + $input ??= $request->getPost($field); if (is_array($input)) { // Note: in_array('', array(0)) returns TRUE, do not use it diff --git a/system/Helpers/html_helper.php b/system/Helpers/html_helper.php index e206cbd0f5e5..9f0b86470aa8 100644 --- a/system/Helpers/html_helper.php +++ b/system/Helpers/html_helper.php @@ -102,12 +102,8 @@ function img($src = '', bool $indexPage = false, $attributes = ''): string if (! is_array($src)) { $src = ['src' => $src]; } - if (! isset($src['src'])) { - $src['src'] = $attributes['src'] ?? ''; - } - if (! isset($src['alt'])) { - $src['alt'] = $attributes['alt'] ?? ''; - } + $src['src'] ??= $attributes['src'] ?? ''; + $src['alt'] ??= $attributes['alt'] ?? ''; $img = 'getUri()->getHost(); - } + $host ??= service('request')->getUri()->getHost(); // Handle localhost and IP addresses - they don't have subdomains if ($host === 'localhost' || filter_var($host, FILTER_VALIDATE_IP)) { diff --git a/system/Images/Handlers/BaseHandler.php b/system/Images/Handlers/BaseHandler.php index 334075701465..906fc9f0b8e3 100644 --- a/system/Images/Handlers/BaseHandler.php +++ b/system/Images/Handlers/BaseHandler.php @@ -538,9 +538,7 @@ public function fit(int $width, ?int $height = null, string $position = 'center' [$cropWidth, $cropHeight] = $this->calcAspectRatio($width, $height, $origWidth, $origHeight); - if ($height === null) { - $height = (int) ceil(($width / $cropWidth) * $cropHeight); - } + $height ??= (int) ceil(($width / $cropWidth) * $cropHeight); [$x, $y] = $this->calcCropCoords($cropWidth, $cropHeight, $origWidth, $origHeight, $position); diff --git a/system/Images/Handlers/GDHandler.php b/system/Images/Handlers/GDHandler.php index 2ba97437edda..f565ee317963 100644 --- a/system/Images/Handlers/GDHandler.php +++ b/system/Images/Handlers/GDHandler.php @@ -301,12 +301,10 @@ protected function createImage(string $path = '', string $imageType = '') */ protected function ensureResource() { - if ($this->resource === null) { - $this->resource = $this->getImageResource( - $this->image()->getPathname(), - $this->image()->imageType, - ); - } + $this->resource ??= $this->getImageResource( + $this->image()->getPathname(), + $this->image()->imageType, + ); } /** diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 198168675fad..6ade80e6f9fc 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -1512,9 +1512,7 @@ private function checkSubdomains($subdomains): bool return false; } - if ($this->currentSubdomain === null) { - $this->currentSubdomain = parse_subdomain($this->httpHost); - } + $this->currentSubdomain ??= parse_subdomain($this->httpHost); if (! is_array($subdomains)) { $subdomains = [$subdomains]; diff --git a/system/Session/Handlers/DatabaseHandler.php b/system/Session/Handlers/DatabaseHandler.php index c4ce2b8222e7..433e85ba3cdc 100644 --- a/system/Session/Handlers/DatabaseHandler.php +++ b/system/Session/Handlers/DatabaseHandler.php @@ -115,9 +115,7 @@ public function read($id): false|string return ''; } - if (! isset($this->sessionID)) { - $this->sessionID = $id; - } + $this->sessionID ??= $id; $builder = $this->db->table($this->table)->where('id', $this->idPrefix . $id); diff --git a/system/Session/Handlers/FileHandler.php b/system/Session/Handlers/FileHandler.php index a985b5f89793..0924aac43904 100644 --- a/system/Session/Handlers/FileHandler.php +++ b/system/Session/Handlers/FileHandler.php @@ -134,9 +134,7 @@ public function read($id): false|string return false; } - if (! isset($this->sessionID)) { - $this->sessionID = $id; - } + $this->sessionID ??= $id; if ($this->fileNew) { chmod($this->filePath . $id, 0600); diff --git a/system/Session/Handlers/MemcachedHandler.php b/system/Session/Handlers/MemcachedHandler.php index 7b7775916019..c64ba65f6454 100644 --- a/system/Session/Handlers/MemcachedHandler.php +++ b/system/Session/Handlers/MemcachedHandler.php @@ -163,9 +163,7 @@ public function open($path, $name): bool public function read($id): false|string { if (isset($this->memcached) && $this->lockSession($id)) { - if (! isset($this->sessionID)) { - $this->sessionID = $id; - } + $this->sessionID ??= $id; $data = (string) $this->memcached->get($this->keyPrefix . $id); diff --git a/system/Session/Handlers/RedisHandler.php b/system/Session/Handlers/RedisHandler.php index d1c3c02e6fe4..a6cce214f6d3 100644 --- a/system/Session/Handlers/RedisHandler.php +++ b/system/Session/Handlers/RedisHandler.php @@ -228,9 +228,7 @@ public function open($path, $name): bool public function read($id): false|string { if (isset($this->redis) && $this->lockSession($id)) { - if (! isset($this->sessionID)) { - $this->sessionID = $id; - } + $this->sessionID ??= $id; $data = $this->redis->get($this->keyPrefix . $id); diff --git a/system/Test/CIUnitTestCase.php b/system/Test/CIUnitTestCase.php index 92b5d8631bf2..d86e543a9a61 100644 --- a/system/Test/CIUnitTestCase.php +++ b/system/Test/CIUnitTestCase.php @@ -283,9 +283,7 @@ protected function tearDown(): void */ private function callTraitMethods(string $stage): void { - if ($this->traits === null) { - $this->traits = class_uses_recursive($this); - } + $this->traits ??= class_uses_recursive($this); foreach ($this->traits as $trait) { $method = $stage . class_basename($trait); diff --git a/system/Test/Fabricator.php b/system/Test/Fabricator.php index c7cf97780fdc..c0b925d3cf66 100644 --- a/system/Test/Fabricator.php +++ b/system/Test/Fabricator.php @@ -129,10 +129,7 @@ public function __construct($model, ?array $formatters = null, ?string $locale = $this->model = $model; - // If no locale was specified then use the App default - if ($locale === null) { - $locale = config(App::class)->defaultLocale; - } + $locale ??= config(App::class)->defaultLocale; // There is no easy way to retrieve the locale from Faker so we will store it $this->locale = $locale; diff --git a/system/Test/FilterTestTrait.php b/system/Test/FilterTestTrait.php index ac97db432133..554c9fd472ef 100644 --- a/system/Test/FilterTestTrait.php +++ b/system/Test/FilterTestTrait.php @@ -102,9 +102,7 @@ protected function setUpFilterTestTrait(): void $this->filtersConfig ??= config(FiltersConfig::class); $this->filters ??= new Filters($this->filtersConfig, $this->request, $this->response); - if ($this->collection === null) { - $this->collection = service('routes')->loadRoutes(); - } + $this->collection ??= service('routes')->loadRoutes(); $this->doneFilterSetUp = true; } diff --git a/system/Typography/Typography.php b/system/Typography/Typography.php index e2adc4cd5057..66a13a0b3f06 100644 --- a/system/Typography/Typography.php +++ b/system/Typography/Typography.php @@ -236,44 +236,42 @@ public function formatCharacters(string $str): string { static $table; - if (! isset($table)) { - $table = [ - // nested smart quotes, opening and closing - // note that rules for grammar (English) allow only for two levels deep - // and that single quotes are _supposed_ to always be on the outside - // but we'll accommodate both - // Note that in all cases, whitespace is the primary determining factor - // on which direction to curl, with non-word characters like punctuation - // being a secondary factor only after whitespace is addressed. - '/\'"(\s|$)/' => '’”$1', - '/(^|\s|

)\'"/' => '$1‘“', - '/\'"(\W)/' => '’”$1', - '/(\W)\'"/' => '$1‘“', - '/"\'(\s|$)/' => '”’$1', - '/(^|\s|

)"\'/' => '$1“‘', - '/"\'(\W)/' => '”’$1', - '/(\W)"\'/' => '$1“‘', - // single quote smart quotes - '/\'(\s|$)/' => '’$1', - '/(^|\s|

)\'/' => '$1‘', - '/\'(\W)/' => '’$1', - '/(\W)\'/' => '$1‘', - // double quote smart quotes - '/"(\s|$)/' => '”$1', - '/(^|\s|

)"/' => '$1“', - '/"(\W)/' => '”$1', - '/(\W)"/' => '$1“', - // apostrophes - '/(\w)\'(\w)/' => '$1’$2', - // Em dash and ellipses dots - '/\s?\-\-\s?/' => '—', - '/(\w)\.{3}/' => '$1…', - // double space after sentences - '/(\W) /' => '$1  ', - // ampersands, if not a character entity - '/&(?!#?[a-zA-Z0-9]{2,};)/' => '&', - ]; - } + $table ??= [ + // nested smart quotes, opening and closing + // note that rules for grammar (English) allow only for two levels deep + // and that single quotes are _supposed_ to always be on the outside + // but we'll accommodate both + // Note that in all cases, whitespace is the primary determining factor + // on which direction to curl, with non-word characters like punctuation + // being a secondary factor only after whitespace is addressed. + '/\'"(\s|$)/' => '’”$1', + '/(^|\s|

)\'"/' => '$1‘“', + '/\'"(\W)/' => '’”$1', + '/(\W)\'"/' => '$1‘“', + '/"\'(\s|$)/' => '”’$1', + '/(^|\s|

)"\'/' => '$1“‘', + '/"\'(\W)/' => '”’$1', + '/(\W)"\'/' => '$1“‘', + // single quote smart quotes + '/\'(\s|$)/' => '’$1', + '/(^|\s|

)\'/' => '$1‘', + '/\'(\W)/' => '’$1', + '/(\W)\'/' => '$1‘', + // double quote smart quotes + '/"(\s|$)/' => '”$1', + '/(^|\s|

)"/' => '$1“', + '/"(\W)/' => '”$1', + '/(\W)"/' => '$1“', + // apostrophes + '/(\w)\'(\w)/' => '$1’$2', + // Em dash and ellipses dots + '/\s?\-\-\s?/' => '—', + '/(\w)\.{3}/' => '$1…', + // double space after sentences + '/(\W) /' => '$1  ', + // ampersands, if not a character entity + '/&(?!#?[a-zA-Z0-9]{2,};)/' => '&', + ]; return preg_replace(array_keys($table), $table, $str); } diff --git a/system/Validation/StrictRules/FileRules.php b/system/Validation/StrictRules/FileRules.php index d381f4f47301..575e02d9c01a 100644 --- a/system/Validation/StrictRules/FileRules.php +++ b/system/Validation/StrictRules/FileRules.php @@ -54,9 +54,7 @@ public function __construct(?RequestInterface $request = null) public function uploaded(?string $blank, string $name): bool { $files = $this->request->getFileMultiple($name); - if ($files === null) { - $files = [$this->request->getFile($name)]; - } + $files ??= [$this->request->getFile($name)]; foreach ($files as $file) { if ($file === null) { @@ -94,9 +92,7 @@ public function max_size(?string $blank, string $params): bool $name = array_shift($paramArray); $files = $this->request->getFileMultiple($name); - if ($files === null) { - $files = [$this->request->getFile($name)]; - } + $files ??= [$this->request->getFile($name)]; foreach ($files as $file) { if ($file === null) { @@ -131,9 +127,7 @@ public function is_image(?string $blank, string $params): bool $name = array_shift($params); $files = $this->request->getFileMultiple($name); - if ($files === null) { - $files = [$this->request->getFile($name)]; - } + $files ??= [$this->request->getFile($name)]; foreach ($files as $file) { if ($file === null) { @@ -171,9 +165,7 @@ public function mime_in(?string $blank, string $params): bool $name = array_shift($params); $files = $this->request->getFileMultiple($name); - if ($files === null) { - $files = [$this->request->getFile($name)]; - } + $files ??= [$this->request->getFile($name)]; foreach ($files as $file) { if ($file === null) { @@ -207,9 +199,7 @@ public function ext_in(?string $blank, string $params): bool $name = array_shift($params); $files = $this->request->getFileMultiple($name); - if ($files === null) { - $files = [$this->request->getFile($name)]; - } + $files ??= [$this->request->getFile($name)]; foreach ($files as $file) { if ($file === null) { @@ -247,9 +237,7 @@ public function max_dims(?string $blank, string $params): bool $name = array_shift($params); $files = $this->request->getFileMultiple($name); - if ($files === null) { - $files = [$this->request->getFile($name)]; - } + $files ??= [$this->request->getFile($name)]; foreach ($files as $file) { if ($file === null) { @@ -295,9 +283,7 @@ public function min_dims(?string $blank, string $params): bool $name = array_shift($params); $files = $this->request->getFileMultiple($name); - if ($files === null) { - $files = [$this->request->getFile($name)]; - } + $files ??= [$this->request->getFile($name)]; foreach ($files as $file) { if ($file === null) { diff --git a/system/View/Parser.php b/system/View/Parser.php index 56c3b093bc5d..2aea776dda39 100644 --- a/system/View/Parser.php +++ b/system/View/Parser.php @@ -103,9 +103,7 @@ public function __construct( public function render(string $view, ?array $options = null, ?bool $saveData = null): string { $start = microtime(true); - if ($saveData === null) { - $saveData = $this->config->saveData; - } + $saveData ??= $this->config->saveData; $fileExt = pathinfo($view, PATHINFO_EXTENSION); $view = ($fileExt === '') ? $view . '.php' : $view; // allow Views as .html, .tpl, etc (from CI3) @@ -135,9 +133,7 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n } } - if ($this->tempData === null) { - $this->tempData = $this->data; - } + $this->tempData ??= $this->data; $template = file_get_contents($file); $output = $this->parse($template, $this->tempData, $options); @@ -171,13 +167,9 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n public function renderString(string $template, ?array $options = null, ?bool $saveData = null): string { $start = microtime(true); - if ($saveData === null) { - $saveData = $this->config->saveData; - } + $saveData ??= $this->config->saveData; - if ($this->tempData === null) { - $this->tempData = $this->data; - } + $this->tempData ??= $this->data; $output = $this->parse($template, $this->tempData, $options); @@ -494,9 +486,7 @@ protected function parseConditionals(string $template): string // Parse the PHP itself, or insert an error so they can debug ob_start(); - if ($this->tempData === null) { - $this->tempData = $this->data; - } + $this->tempData ??= $this->data; extract($this->tempData); diff --git a/system/View/Table.php b/system/View/Table.php index 42ee5402adff..5d4958088b2d 100644 --- a/system/View/Table.php +++ b/system/View/Table.php @@ -503,9 +503,7 @@ protected function _compileTemplate() } foreach ($this->_defaultTemplate() as $field => $template) { - if (! isset($this->template[$field])) { - $this->template[$field] = $template; - } + $this->template[$field] ??= $template; } } diff --git a/tests/system/Validation/ValidationTest.php b/tests/system/Validation/ValidationTest.php index b43bb057b52f..49baeaba0c79 100644 --- a/tests/system/Validation/ValidationTest.php +++ b/tests/system/Validation/ValidationTest.php @@ -1683,9 +1683,7 @@ public static function provideSplittingOfComplexStringRules(): iterable */ protected function placeholderReplacementResultDetermination(string $placeholder = 'id', ?array $data = null): void { - if ($data === null) { - $data = [$placeholder => '12']; - } + $data ??= [$placeholder => '12']; $validationRules = self::getPrivateMethodInvoker($this->validation, 'fillPlaceholders')($this->validation->getRules(), $data); $fieldRules = $validationRules['foo']['rules'] ?? $validationRules['foo']; diff --git a/tests/system/View/ParserTest.php b/tests/system/View/ParserTest.php index 730acb6cf8c1..95d807729c58 100644 --- a/tests/system/View/ParserTest.php +++ b/tests/system/View/ParserTest.php @@ -432,9 +432,7 @@ public function testMismatchedVarPair(): void #[DataProvider('provideEscHandling')] public function testEscHandling($value, $expected = null): void { - if ($expected === null) { - $expected = $value; - } + $expected ??= $value; $this->assertSame($expected, \esc($value)); } From f6533e95ae8d3198452c2cd256e3402e211d7dcf Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Fri, 4 Sep 2026 01:24:54 +0800 Subject: [PATCH 098/163] refactor: bound phpstan's analysed PHP versions (#10505) --- phpstan.dist.neon | 4 +++- system/Session/Handlers/FileHandler.php | 19 ++++++------------- system/Session/Session.php | 20 +++++++------------- 3 files changed, 16 insertions(+), 27 deletions(-) diff --git a/phpstan.dist.neon b/phpstan.dist.neon index 71e4151f6f3d..7d6f2d81a709 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -2,7 +2,9 @@ includes: - utils/phpstan-baseline/loader.neon parameters: - phpVersion: 80200 + phpVersion: + min: 80200 + max: 80599 tmpDir: build/phpstan level: 6 bootstrapFiles: diff --git a/system/Session/Handlers/FileHandler.php b/system/Session/Handlers/FileHandler.php index 0924aac43904..b01b23ce03a9 100644 --- a/system/Session/Handlers/FileHandler.php +++ b/system/Session/Handlers/FileHandler.php @@ -298,9 +298,7 @@ public function gc($max_lifetime): false|int } /** - * Configure Session ID regular expression. - * - * To make life easier, we force the PHP defaults. Because PHP9 forces them. + * Configure Session ID regular expression. To make life easier, we force the PHP defaults because PHP 9 forces them. * * @see https://wiki.php.net/rfc/deprecations_php_8_4#sessionsid_length_and_sessionsid_bits_per_character * @@ -308,17 +306,12 @@ public function gc($max_lifetime): false|int */ protected function configureSessionIDRegex() { - $bitsPerCharacter = (int) ini_get('session.sid_bits_per_character'); - $sidLength = (int) ini_get('session.sid_length'); + if ((int) ini_get('session.sid_bits_per_character') !== 4) { + ini_set('session.sid_bits_per_character', '4'); + } - // We force the PHP defaults. - if (PHP_VERSION_ID < 90000) { - if ($bitsPerCharacter !== 4) { - ini_set('session.sid_bits_per_character', '4'); - } - if ($sidLength !== 32) { - ini_set('session.sid_length', '32'); - } + if ((int) ini_get('session.sid_length') !== 32) { + ini_set('session.sid_length', '32'); } $this->sessionIDRegex = '[0-9a-f]{32}'; diff --git a/system/Session/Session.php b/system/Session/Session.php index 1998a0ea8596..65b1e87558d8 100644 --- a/system/Session/Session.php +++ b/system/Session/Session.php @@ -186,26 +186,20 @@ protected function configure() } /** - * Configure session ID length. + * Configure session ID length. To make life easier, we force the PHP defaults because PHP 9 forces them. * - * To make life easier, we force the PHP defaults. Because PHP9 forces them. - * See https://wiki.php.net/rfc/deprecations_php_8_4#sessionsid_length_and_sessionsid_bits_per_character + * @see https://wiki.php.net/rfc/deprecations_php_8_4#sessionsid_length_and_sessionsid_bits_per_character * * @return void */ protected function configureSidLength() { - $bitsPerCharacter = (int) ini_get('session.sid_bits_per_character'); - $sidLength = (int) ini_get('session.sid_length'); + if ((int) ini_get('session.sid_bits_per_character') !== 4) { + ini_set('session.sid_bits_per_character', '4'); + } - // We force the PHP defaults. - if (PHP_VERSION_ID < 90000) { - if ($bitsPerCharacter !== 4) { - ini_set('session.sid_bits_per_character', '4'); - } - if ($sidLength !== 32) { - ini_set('session.sid_length', '32'); - } + if ((int) ini_get('session.sid_length') !== 32) { + ini_set('session.sid_length', '32'); } $this->sidRegexp = '[0-9a-f]{32}'; From b7321d331bc3ea01c3f5f439b9d7e7fa85c10f27 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Fri, 4 Sep 2026 01:26:48 +0800 Subject: [PATCH 099/163] fix: pass prompt text to readline in `CLI::prompt()` so backspace does not erase it (#10507) --- system/CLI/CLI.php | 8 +- system/CLI/InputOutput.php | 20 +++- tests/system/CLI/CLITest.php | 124 ++++++++++++++++++++ user_guide_src/source/changelogs/v4.7.5.rst | 2 + 4 files changed, 148 insertions(+), 6 deletions(-) diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index 037eaf878ebb..08d5c214582e 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -254,12 +254,12 @@ public static function prompt(string $field, $options = null, $validation = null $default = $options[0]; } - static::fwrite(STDOUT, $field . (trim($field) !== '' ? ' ' : '') . $extraOutput . ': '); static::$lastWrite = 'write'; - // Read the input from keyboard. - $input = trim(static::$io->input()); - $input = ($input === '') ? (string) $default : $input; + // The reader renders the prompt itself, so readline redraws repaint it instead of erasing it. + $prompt = sprintf('%s%s%s: ', $field, trim($field) !== '' ? ' ' : '', $extraOutput); + $input = trim(static::$io->input($prompt)); + $input = $input === '' ? (string) $default : $input; if ($validation !== []) { while (! static::validate('"' . trim($field) . '"', $input, $validation)) { diff --git a/system/CLI/InputOutput.php b/system/CLI/InputOutput.php index 6ed3b8f36138..1d99563dd0ea 100644 --- a/system/CLI/InputOutput.php +++ b/system/CLI/InputOutput.php @@ -43,10 +43,18 @@ public function input(?string $prefix = null): string { // readline() can't be tested. if ($this->readlineSupport && ENVIRONMENT !== 'testing') { - return readline($prefix); // @codeCoverageIgnore + // @codeCoverageIgnoreStart + // Libedit reports "EditLine wrapper" and mangles the markers, so only GNU readline gets them. + if ($prefix !== null && ! str_contains(readline_info('library_version'), 'EditLine')) { + $prefix = $this->markAnsiNonPrinting($prefix); + } + + return readline($prefix); + // @codeCoverageIgnoreEnd } - echo $prefix; + // self:: skips MockInputOutput's fwrite override, whose filter bookkeeping must not nest inside input(). + self::fwrite(STDOUT, $prefix ?? ''); $input = fgets(fopen('php://stdin', 'rb')); @@ -77,4 +85,12 @@ public function fwrite($handle, string $string): void fwrite($handle, $string); } + + /** + * Wraps ANSI escape sequences in readline's non-printing markers so line-redraw column accounting skips them. + */ + private function markAnsiNonPrinting(string $prefix): string + { + return preg_replace('/(\e\[[0-9;]*m)/', "\x01\$1\x02", $prefix); + } } diff --git a/tests/system/CLI/CLITest.php b/tests/system/CLI/CLITest.php index 052fbd602aea..f4db06f17102 100644 --- a/tests/system/CLI/CLITest.php +++ b/tests/system/CLI/CLITest.php @@ -17,6 +17,7 @@ use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Superglobals; use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\Mock\MockInputOutput; use CodeIgniter\Test\PhpStreamWrapper; use CodeIgniter\Test\StreamFilterTrait; use PHPUnit\Framework\Attributes\DataProvider; @@ -143,6 +144,129 @@ public function testPromptInputZero(): void $this->assertSame('0', $output); } + public function testPromptPassesPromptTextToInputReader(): void + { + $io = new class () extends InputOutput { + public ?string $receivedPrefix = null; + + public function input(?string $prefix = null): string + { + $this->receivedPrefix = $prefix; + + return 'red'; + } + }; + CLI::setInputOutput($io); + + $output = CLI::prompt('What is your favorite color?'); + + CLI::resetInputOutput(); + + $this->assertSame('red', $output); + $this->assertSame('What is your favorite color? : ', $io->receivedPrefix); + } + + public function testPromptPassesDefaultOptionInPromptText(): void + { + $io = new class () extends InputOutput { + public ?string $receivedPrefix = null; + + public function input(?string $prefix = null): string + { + $this->receivedPrefix = $prefix; + + return ''; + } + }; + CLI::setInputOutput($io); + + $output = CLI::prompt('What is your favorite color?', 'red'); + + CLI::resetInputOutput(); + + $this->assertSame('red', $output); + $this->assertSame( + sprintf('What is your favorite color? [%s]: ', CLI::color('red', 'green')), + $io->receivedPrefix, + ); + } + + public function testPromptByKeyPassesPromptTextToInputReader(): void + { + $io = new class () extends InputOutput { + public ?string $receivedPrefix = null; + + public function input(?string $prefix = null): string + { + $this->receivedPrefix = $prefix; + + return '1'; + } + }; + CLI::setInputOutput($io); + + $output = CLI::promptByKey('Select your hobbies:', ['Playing game', 'Sleep', 'Badminton']); + + CLI::resetInputOutput(); + + $this->assertSame('1', $output); + $this->assertSame( + PHP_EOL . sprintf('[%s, 1, 2]: ', CLI::color('0', 'green')), + $io->receivedPrefix, + ); + } + + public function testPromptByMultipleKeysPassesPromptTextToInputReader(): void + { + $io = new class () extends InputOutput { + public ?string $receivedPrefix = null; + + public function input(?string $prefix = null): string + { + $this->receivedPrefix = $prefix; + + return '0,1'; + } + }; + CLI::setInputOutput($io); + + $output = CLI::promptByMultipleKeys('Select your hobbies:', ['Playing game', 'Sleep', 'Badminton']); + + CLI::resetInputOutput(); + + $this->assertSame([0 => 'Playing game', 1 => 'Sleep'], $output); + $this->assertSame( + 'You can specify multiple values separated by commas.' . PHP_EOL + . sprintf('[%s, 1, 2] : ', CLI::color('0', 'green')), + $io->receivedPrefix, + ); + } + + public function testInputWritesPrefixToStdout(): void + { + $io = new MockInputOutput(); + $io->setInputs(['blue']); + CLI::setInputOutput($io); + + $output = CLI::input('Name: '); + + CLI::resetInputOutput(); + + $this->assertSame('blue', $output); + $this->assertSame('Name: blue' . PHP_EOL, $io->getOutput()); + } + + public function testMarkAnsiNonPrintingWrapsEscapeSequences(): void + { + $wrap = $this->getPrivateMethodInvoker(new InputOutput(), 'markAnsiNonPrinting'); + + $this->assertSame( + "What is your favorite color? [\x01\e[0;32m\x02red\x01\e[0m\x02]: ", + $wrap(sprintf('What is your favorite color? [%s]: ', CLI::color('red', 'green'))), + ); + $this->assertSame('Name: ', $wrap('Name: ')); + } + public function testPromptByKey(): void { PhpStreamWrapper::register(); diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index 47d447dfbbc3..7cd8169a5d34 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -34,6 +34,8 @@ Deprecations Bugs Fixed ********** +- **CLI:** Fixed a bug where pressing backspace in a ``CLI::prompt()`` erased the prompt text when the ``readline`` extension is enabled. The prompt is now passed to ``readline()`` so line redraws repaint it. + ANSI color codes in the prompt (e.g., option defaults) are wrapped in readline's non-printing markers under GNU readline so cursor positioning stays accurate. - **CLIRequest:** Fixed a bug where ``parseCommand()`` could throw a TypeError when ``argv`` is missing. - **Content Security Policy:** Fixed a bug where empty ``Content-Security-Policy``, ``Content-Security-Policy-Report-Only``, and ``Reporting-Endpoints`` response headers were generated when no corresponding values existed. - **Helpers:** Fixed a bug where ``get_dir_file_info()`` returned incomplete entries for subdirectories and missing files instead of omitting them. From 322c33a9785ac4130bb8fbc45a5dd138d56cffb4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 06:09:43 +0700 Subject: [PATCH 100/163] chore(deps-dev): update rector/rector requirement (#10518) Updates the requirements on [rector/rector](https://github.com/rectorphp/rector) to permit the latest version. Updates `rector/rector` to 2.6.5 - [Release notes](https://github.com/rectorphp/rector/releases) - [Commits](https://github.com/rectorphp/rector/compare/2.6.4...2.6.5) --- updated-dependencies: - dependency-name: rector/rector dependency-version: 2.6.5 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index bb51819c9849..e6edd89efb65 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "phpunit/phpcov": "^9.0.2 || ^10.0", "phpunit/phpunit": "^10.5.16 || ^11.2", "predis/predis": "^3.0", - "rector/rector": "2.6.4", + "rector/rector": "2.6.5", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { From c32a6e5a9d433275397c102f99c879ad2fac5dc3 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Fri, 4 Sep 2026 13:07:14 +0700 Subject: [PATCH 101/163] chore: Bump structarmed to ^0.17 and enable CodeQuality preset (#10520) * chore: Bump structarmed to ^0.17 and enable CodeQuality preset * chore: skip Cell for static closure --- composer.json | 2 +- structarmed.php | 6 +++++- system/Autoloader/Autoloader.php | 2 +- system/CodeIgniter.php | 2 +- tests/system/Entity/EntityTest.php | 2 +- tests/system/I18n/TimeTest.php | 2 +- tests/system/SuperglobalsTest.php | 8 ++++---- 7 files changed, 14 insertions(+), 10 deletions(-) diff --git a/composer.json b/composer.json index e6edd89efb65..b8f61a462bf1 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "^0.16", + "boundwize/structarmed": "^0.17", "codeigniter/phpstan-codeigniter": "^2.1", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", diff --git a/structarmed.php b/structarmed.php index e1027f578ce2..fc5c799b13c3 100644 --- a/structarmed.php +++ b/structarmed.php @@ -11,6 +11,7 @@ * the LICENSE file that was distributed with this source code. */ +use Boundwize\StructArmed\Preset\Presets\CodeQualityPreset; use CodeIgniter\Cache\ResponseCache; use CodeIgniter\HTTP\CLIRequest; use CodeIgniter\HTTP\Header; @@ -45,9 +46,12 @@ __DIR__ . '/tests/system/Config/fixtures', ], __DIR__ . '/system/ThirdParty', + CodeQualityPreset::ANONYMOUS_FUNCTIONS_MUST_BE_STATIC => [ + __DIR__ . '/system/View/Cells/Cell.php', + ], ]) ->cacheDirectory(is_dir('/tmp') ? '/tmp/structarmed' : null) - ->withPreset(Preset::PSR4()) + ->withPresets(Preset::PSR4(), Preset::CODEQUALITY()) // Resolve CodeIgniter layers from class names because several layers share directories. ->layerPattern('API', '/^CodeIgniter\\\\API\\\\.*$/') ->layerPattern('Cache', '/^CodeIgniter\\\\Cache\\\\.*$/') diff --git a/system/Autoloader/Autoloader.php b/system/Autoloader/Autoloader.php index 4666149f27e9..92ff2026bb19 100644 --- a/system/Autoloader/Autoloader.php +++ b/system/Autoloader/Autoloader.php @@ -531,7 +531,7 @@ private function autoloadKint(): void { // If we have KINT_DIR it means it's already loaded via composer if (! defined('KINT_DIR')) { - spl_autoload_register(function ($class): void { + spl_autoload_register(static function ($class): void { $class = explode('\\', $class); if (array_shift($class) !== 'Kint') { diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 177edbd8d753..1654e82399e7 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -261,7 +261,7 @@ private function autoloadKint(): void { // If we have KINT_DIR it means it's already loaded via composer if (! defined('KINT_DIR')) { - spl_autoload_register(function ($class): void { + spl_autoload_register(static function ($class): void { $class = explode('\\', $class); if (array_shift($class) !== 'Kint') { diff --git a/tests/system/Entity/EntityTest.php b/tests/system/Entity/EntityTest.php index 82ff14253d17..be8f64f31775 100644 --- a/tests/system/Entity/EntityTest.php +++ b/tests/system/Entity/EntityTest.php @@ -507,7 +507,7 @@ public function testCastDateTimeWithTimestampTimezone(): void $entity = $this->getCastEntity(); - $entity->eighth = 1722988800; // 2024-08-07 00:00:00 UTC + $entity->eighth = 1_722_988_800; // 2024-08-07 00:00:00 UTC $this->assertInstanceOf(Time::class, $entity->eighth); // The timezone is the default timezone, not UTC. diff --git a/tests/system/I18n/TimeTest.php b/tests/system/I18n/TimeTest.php index 0d08af39f3f2..ef6712f37259 100644 --- a/tests/system/I18n/TimeTest.php +++ b/tests/system/I18n/TimeTest.php @@ -290,7 +290,7 @@ public function testCreateFromTimestamp(): void public function testCreateFromTimestampWithMicroseconds(): void { - $timestamp = 1489762800.654321; + $timestamp = 1_489_762_800.654321; $locale = setlocale(LC_NUMERIC, '0'); setlocale(LC_NUMERIC, 'de_DE.UTF-8', 'de_DE'); diff --git a/tests/system/SuperglobalsTest.php b/tests/system/SuperglobalsTest.php index de1fed832f3a..e4b8b23b2b28 100644 --- a/tests/system/SuperglobalsTest.php +++ b/tests/system/SuperglobalsTest.php @@ -57,16 +57,16 @@ public function testServerSetWithArray(): void public function testServerSetWithInt(): void { - $this->superglobals->setServer('REQUEST_TIME', 1234567890); + $this->superglobals->setServer('REQUEST_TIME', 1_234_567_890); - $this->assertSame(1234567890, $this->superglobals->server('REQUEST_TIME')); + $this->assertSame(1_234_567_890, $this->superglobals->server('REQUEST_TIME')); } public function testServerSetWithFloat(): void { - $this->superglobals->setServer('REQUEST_TIME_FLOAT', 1234567890.123); + $this->superglobals->setServer('REQUEST_TIME_FLOAT', 1_234_567_890.123); - $this->assertEqualsWithDelta(1234567890.123, $this->superglobals->server('REQUEST_TIME_FLOAT'), PHP_FLOAT_EPSILON); + $this->assertEqualsWithDelta(1_234_567_890.123, $this->superglobals->server('REQUEST_TIME_FLOAT'), PHP_FLOAT_EPSILON); } public function testServerUnset(): void From 17b02c43cf2eb441ff3ac0af8a365ea6a35b8cc4 Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Fri, 4 Sep 2026 14:38:39 +0200 Subject: [PATCH 102/163] fix(Cookie): validate raw cookie values (#10516) --- system/Cookie/Cookie.php | 24 +++++ system/Cookie/Exceptions/CookieException.php | 10 ++ system/Language/en/Cookie.php | 1 + tests/system/Cookie/CookieTest.php | 102 +++++++++++++++++++ user_guide_src/source/changelogs/v4.7.5.rst | 3 + user_guide_src/source/libraries/cookies.rst | 7 ++ 6 files changed, 147 insertions(+) diff --git a/system/Cookie/Cookie.php b/system/Cookie/Cookie.php index b807e0885df4..27ffc908cb38 100644 --- a/system/Cookie/Cookie.php +++ b/system/Cookie/Cookie.php @@ -129,6 +129,11 @@ class Cookie implements ArrayAccess, CloneableCookieInterface */ private static string $reservedCharsList = "=,; \t\r\n\v\f()<>@:\\\"/[]?{}"; + /** + * @see https://www.php.net/manual/en/function.setrawcookie.php + */ + private static string $reservedValueCharsList = ",; \t\r\n\v\f\0"; + /** * Set the default attributes to a Cookie instance by injecting * the values from the `CookieConfig` config or an array. @@ -265,6 +270,7 @@ final public function __construct(string $name, string $value = '', array $optio $httponly = $options['httponly']; $this->validateName($name, $raw); + $this->validateValue($value, $raw); $this->validatePrefix($prefix, $secure, $path, $domain); $this->validateSameSite($samesite, $secure); @@ -470,6 +476,8 @@ public function withName(string $name) */ public function withValue(string $value) { + $this->validateValue($value, $this->raw); + $cookie = clone $this; $cookie->value = $value; @@ -578,6 +586,7 @@ public function withSameSite(string $samesite) public function withRaw(bool $raw = true) { $this->validateName($this->name, $raw); + $this->validateValue($this->value, $raw); $cookie = clone $this; @@ -766,6 +775,21 @@ protected function validateName(string $name, bool $raw): void } } + /** + * Validates the cookie value. + * + * If `$raw` is true, values should not contain invalid characters + * as `setrawcookie()` will reject this. + * + * @throws CookieException + */ + protected function validateValue(string $value, bool $raw): void + { + if ($raw && strpbrk($value, self::$reservedValueCharsList) !== false) { + throw CookieException::forInvalidCookieValue(); + } + } + /** * Validates the special prefixes if some attribute requirements are met. * diff --git a/system/Cookie/Exceptions/CookieException.php b/system/Cookie/Exceptions/CookieException.php index 8b6c7575983a..3cc7e32e6eff 100644 --- a/system/Cookie/Exceptions/CookieException.php +++ b/system/Cookie/Exceptions/CookieException.php @@ -60,6 +60,16 @@ public static function forEmptyCookieName() return new static(lang('Cookie.emptyCookieName')); } + /** + * Thrown when the cookie value contains invalid characters. + * + * @return static + */ + public static function forInvalidCookieValue() + { + return new static(lang('Cookie.invalidCookieValue')); + } + /** * Thrown when using the `__Secure-` prefix but the `Secure` attribute * is not set to true. diff --git a/system/Language/en/Cookie.php b/system/Language/en/Cookie.php index c880f032935e..4f4294ff0fc5 100644 --- a/system/Language/en/Cookie.php +++ b/system/Language/en/Cookie.php @@ -16,6 +16,7 @@ 'invalidExpiresTime' => 'Invalid "{0}" type for "Expires" attribute. Expected: string, integer, DateTimeInterface object.', 'invalidExpiresValue' => 'The cookie expiration time is not valid.', 'invalidCookieName' => 'The cookie name "{0}" contains invalid characters.', + 'invalidCookieValue' => 'The cookie value contains invalid characters.', 'emptyCookieName' => 'The cookie name cannot be empty.', 'invalidSecurePrefix' => 'Using the "__Secure-" prefix requires setting the "Secure" attribute.', 'invalidHostPrefix' => 'Using the "__Host-" prefix must be set with the "Secure" flag, must not have a "Domain" attribute, and the "Path" is set to "/".', diff --git a/tests/system/Cookie/CookieTest.php b/tests/system/Cookie/CookieTest.php index cade48b24f52..07991be006f6 100644 --- a/tests/system/Cookie/CookieTest.php +++ b/tests/system/Cookie/CookieTest.php @@ -341,4 +341,106 @@ public function testCannotUnsetPropertyViaArrayAccess(): void $cookie = new Cookie('cookie', 'monster'); unset($cookie['path']); } + + #[DataProvider('provideValidationOfRawCookieValue')] + public function testValidationOfRawCookieValue(string $value): void + { + $this->expectException(CookieException::class); + new Cookie('test', $value, ['raw' => true]); + } + + /** + * @return iterable + */ + public static function provideValidationOfRawCookieValue(): iterable + { + yield 'comma' => ['value,comma']; + + yield 'semicolon' => ['value;semicolon']; + + yield 'space' => ['value with space']; + + yield 'tab' => ["value\twith_tab"]; + + yield 'carriage return' => ["value\rcarriage"]; + + yield 'newline' => ["value\nnewline"]; + + yield 'vertical tab' => ["value\vvertical_tab"]; + + yield 'form feed' => ["value\fform_feed"]; + + yield 'null byte' => ["value\0null_byte"]; + + yield 'CRLF' => ["value\r\nwith_crlf"]; + } + + #[DataProvider('provideFromHeaderStringValidationOfRawCookieValue')] + public function testFromHeaderStringValidationOfRawCookieValue(string $value): void + { + $this->expectException(CookieException::class); + Cookie::fromHeaderString("test={$value}; Path=/", true); + } + + /** + * @return iterable + */ + public static function provideFromHeaderStringValidationOfRawCookieValue(): iterable + { + foreach (self::provideValidationOfRawCookieValue() as $name => $case) { + if ($name === 'semicolon') { + continue; + } + + yield $name => $case; + } + } + + public function testFromHeaderStringWithRawTrue(): void + { + $cookie = Cookie::fromHeaderString('test=valid_raw_value=123; Path=/', true); + + $this->assertTrue($cookie->isRaw()); + $this->assertSame('valid_raw_value=123', $cookie->getValue()); + } + + public function testFromHeaderStringWithRawFalseDecodesValue(): void + { + $cookie = Cookie::fromHeaderString('test=value%20with%20space; Path=/', false); + + $this->assertFalse($cookie->isRaw()); + $this->assertSame('value with space', $cookie->getValue()); + } + + public function testValidationOfRawCookieValueInWithValue(): void + { + $this->expectException(CookieException::class); + $cookie = new Cookie('test', 'valid_value', ['raw' => true]); + $cookie->withValue("injected\r\nvalue"); + } + + public function testValidationOfRawCookieValueInWithRaw(): void + { + $this->expectException(CookieException::class); + $cookie = new Cookie('test', "injected\r\nvalue", ['raw' => false]); + $cookie->withRaw(true); + } + + public function testValidRawCookieRetainsValueWithoutEncoding(): void + { + $cookie = new Cookie('test', 'valid_raw_value=123', ['raw' => true]); + + $this->assertSame('valid_raw_value=123', $cookie->getValue()); + $this->assertStringContainsString('test=valid_raw_value=123', (string) $cookie); + } + + public function testNonRawCookieSafelyEncodesCRLF(): void + { + $cookie = new Cookie('test', "value\r\nwith_crlf", ['raw' => false]); + $result = (string) $cookie; + + $this->assertStringContainsString('%0D%0A', $result); + $this->assertStringNotContainsString("\r", $result); + $this->assertStringNotContainsString("\n", $result); + } } diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index 7cd8169a5d34..9977c5ef7df5 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -18,6 +18,8 @@ BREAKING Message Changes *************** +- Added the ``Cookie.invalidCookieValue`` language string. + ******* Changes ******* @@ -38,6 +40,7 @@ Bugs Fixed ANSI color codes in the prompt (e.g., option defaults) are wrapped in readline's non-printing markers under GNU readline so cursor positioning stays accurate. - **CLIRequest:** Fixed a bug where ``parseCommand()`` could throw a TypeError when ``argv`` is missing. - **Content Security Policy:** Fixed a bug where empty ``Content-Security-Policy``, ``Content-Security-Policy-Report-Only``, and ``Reporting-Endpoints`` response headers were generated when no corresponding values existed. +- **Cookie:** Fixed a bug where ``Cookie`` instances created with ``raw: true`` allowed invalid characters in cookie values rejected by ``setrawcookie()``. - **Helpers:** Fixed a bug where ``get_dir_file_info()`` returned incomplete entries for subdirectories and missing files instead of omitting them. - **Honeypot:** Fixed a bug where bot detection returned an HTTP 500 response instead of 403 (Forbidden). - **Logger:** Fixed a bug where interpolating a log message with array or non-stringable context values could raise PHP warnings or errors. diff --git a/user_guide_src/source/libraries/cookies.rst b/user_guide_src/source/libraries/cookies.rst index 00239b0911f7..99b3103e45de 100644 --- a/user_guide_src/source/libraries/cookies.rst +++ b/user_guide_src/source/libraries/cookies.rst @@ -97,6 +97,13 @@ and `setrawcookie() `_ will reject cookies with invalid names. Additionally, cookie names cannot be an empty string. +Validating the Value Attribute +============================== + +If setting the ``$raw`` parameter to ``true``, the cookie value will also be validated. +It must not contain control characters, spaces, tabs, or separator characters +(``, ;``) as `setrawcookie() `_ will reject them. + Validating the Prefix Attribute =============================== From bc580a7910a4ff3f53d4aaefda132a6ec3ff4632 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Fri, 4 Sep 2026 17:46:44 +0200 Subject: [PATCH 103/163] docs: clarify Entity array casts (#10515) --- user_guide_src/source/models/entities.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/user_guide_src/source/models/entities.rst b/user_guide_src/source/models/entities.rst index 56c17114138e..52fb020612d5 100644 --- a/user_guide_src/source/models/entities.rst +++ b/user_guide_src/source/models/entities.rst @@ -287,6 +287,10 @@ the value whenever the property is set: .. literalinclude:: entities/014.php +.. warning:: The ``array`` cast uses PHP serialization. Only read values previously + written through the same Entity cast. Never store untrusted serialized strings + directly in an ``array``-cast field. + CSV Casting ----------- From e7aa2120f6df4a98a3abcd8ec11f076babb01783 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Fri, 4 Sep 2026 23:59:52 +0700 Subject: [PATCH 104/163] refactor: Enable MustHaveReturnTypeFunctionRule StructArmed rule on system/Helpers (#10521) --- structarmed.php | 8 ++++++-- system/Helpers/array_helper.php | 8 ++------ system/Helpers/cookie_helper.php | 2 +- system/Helpers/filesystem_helper.php | 2 +- system/Helpers/form_helper.php | 4 ++-- system/Helpers/html_helper.php | 4 +--- system/Helpers/kint_helper.php | 12 +++--------- system/Helpers/number_helper.php | 8 ++------ system/Helpers/test_helper.php | 6 ++---- system/Helpers/text_helper.php | 2 +- system/Helpers/url_helper.php | 6 ++---- 11 files changed, 23 insertions(+), 39 deletions(-) diff --git a/structarmed.php b/structarmed.php index fc5c799b13c3..88932d17cdd0 100644 --- a/structarmed.php +++ b/structarmed.php @@ -10,7 +10,7 @@ * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ - +use Boundwize\StructArmed\Rule\Rules\Function_\MustHaveReturnTypeFunctionRule; use Boundwize\StructArmed\Preset\Presets\CodeQualityPreset; use CodeIgniter\Cache\ResponseCache; use CodeIgniter\HTTP\CLIRequest; @@ -52,6 +52,10 @@ ]) ->cacheDirectory(is_dir('/tmp') ? '/tmp/structarmed' : null) ->withPresets(Preset::PSR4(), Preset::CODEQUALITY()) + + ->layer('Helpers', __DIR__ . '/system/Helpers') + ->rule('helpers.functions_must_have_return_type', new MustHaveReturnTypeFunctionRule('Helpers')) + // Resolve CodeIgniter layers from class names because several layers share directories. ->layerPattern('API', '/^CodeIgniter\\\\API\\\\.*$/') ->layerPattern('Cache', '/^CodeIgniter\\\\Cache\\\\.*$/') @@ -110,7 +114,7 @@ 'Security' => ['Cookie', 'HTTP', 'I18n', 'Session'], 'Session' => ['Cookie', 'Database', 'HTTP', 'I18n'], 'Throttle' => ['Cache', 'I18n'], - 'Validation' => ['Database', 'HTTP'], + 'Validation' => ['Database', 'HTTP', 'Helpers'], 'View' => ['Cache'], ]) ->skipPathsForRuleset(['*test*']) diff --git a/system/Helpers/array_helper.php b/system/Helpers/array_helper.php index f0ea0637c545..c5ccde761544 100644 --- a/system/Helpers/array_helper.php +++ b/system/Helpers/array_helper.php @@ -21,10 +21,8 @@ * wildcard searches, like foo.*.bar * * @param array $array - * - * @return mixed */ - function dot_array_search(string $index, array $array) + function dot_array_search(string $index, array $array): mixed { return ArrayHelper::dotSearch($index, $array); } @@ -36,10 +34,8 @@ function dot_array_search(string $index, array $array) * * @param int|string $key * @param array $array - * - * @return mixed */ - function array_deep_search($key, array $array) + function array_deep_search($key, array $array): mixed { if (isset($array[$key])) { return $array[$key]; diff --git a/system/Helpers/cookie_helper.php b/system/Helpers/cookie_helper.php index e3b10a1b60f6..3d3758278b8d 100644 --- a/system/Helpers/cookie_helper.php +++ b/system/Helpers/cookie_helper.php @@ -79,7 +79,7 @@ function set_cookie( * * @see \CodeIgniter\HTTP\IncomingRequest::getCookie() */ - function get_cookie($index, bool $xssClean = false, ?string $prefix = '') + function get_cookie($index, bool $xssClean = false, ?string $prefix = ''): array|string|null { if ($prefix === '') { $cookie = config(CookieConfig::class); diff --git a/system/Helpers/filesystem_helper.php b/system/Helpers/filesystem_helper.php index f9a04dd142df..7c42bd5c65f8 100644 --- a/system/Helpers/filesystem_helper.php +++ b/system/Helpers/filesystem_helper.php @@ -326,7 +326,7 @@ function get_dir_file_info(string $sourceDir, bool $topLevelOnly = true, bool $r * fileperms?: int * }|null */ - function get_file_info(string $file, $returnedValues = ['name', 'server_path', 'size', 'date']) + function get_file_info(string $file, $returnedValues = ['name', 'server_path', 'size', 'date']): ?array { if (! is_file($file)) { return null; diff --git a/system/Helpers/form_helper.php b/system/Helpers/form_helper.php index 1cf120acb78a..dee1f001365b 100644 --- a/system/Helpers/form_helper.php +++ b/system/Helpers/form_helper.php @@ -558,7 +558,7 @@ function form_close(string $extra = ''): string * * @return list|string */ - function set_value(string $field, $default = '', bool $htmlEscape = true) + function set_value(string $field, $default = '', bool $htmlEscape = true): array|string { $request = service('request'); @@ -695,7 +695,7 @@ function set_radio(string $field, string $value = '', bool $default = false): st * * @return array */ - function validation_errors() + function validation_errors(): array { $errors = session('_ci_validation_errors'); diff --git a/system/Helpers/html_helper.php b/system/Helpers/html_helper.php index 9f0b86470aa8..67441f935c11 100644 --- a/system/Helpers/html_helper.php +++ b/system/Helpers/html_helper.php @@ -540,10 +540,8 @@ function embed(string $src, string $type = 'unknown', string $attributes = '', b if (! function_exists('_has_protocol')) { /** * Test the protocol of a URI. - * - * @return false|int */ - function _has_protocol(string $url) + function _has_protocol(string $url): false|int { return preg_match('#^([a-z]+:)?//#i', $url); } diff --git a/system/Helpers/kint_helper.php b/system/Helpers/kint_helper.php index 02d180328e92..e311d9ff7cc8 100644 --- a/system/Helpers/kint_helper.php +++ b/system/Helpers/kint_helper.php @@ -39,10 +39,8 @@ function dd(...$vars): void * dd function * * @param mixed ...$vars - * - * @return int */ - function dd(...$vars) + function dd(...$vars): int { return 0; } @@ -55,10 +53,8 @@ function dd(...$vars) * d function * * @param mixed ...$vars - * - * @return int */ - function d(...$vars) + function d(...$vars): int { return 0; } @@ -77,10 +73,8 @@ function trace(): void } else { /** * Generic trace function in case that Kint is not loaded. - * - * @return int */ - function trace() + function trace(): int { return 0; } diff --git a/system/Helpers/number_helper.php b/system/Helpers/number_helper.php index a737428706bd..2f4f6f9bed74 100644 --- a/system/Helpers/number_helper.php +++ b/system/Helpers/number_helper.php @@ -21,10 +21,8 @@ * * @param float|int|string $num Will be cast as int * @param non-empty-string|null $locale [optional] - * - * @return bool|string */ - function number_to_size($num, int $precision = 1, ?string $locale = null) + function number_to_size($num, int $precision = 1, ?string $locale = null): bool|string { try { // Strip any formatting & ensure numeric input @@ -76,10 +74,8 @@ function number_to_size($num, int $precision = 1, ?string $locale = null) * @param int|string $num Will be cast as int * @param int $precision [optional] The optional number of decimal digits to round to. * @param non-empty-string|null $locale [optional] - * - * @return bool|string */ - function number_to_amount($num, int $precision = 0, ?string $locale = null) + function number_to_amount($num, int $precision = 0, ?string $locale = null): bool|string { // Strip any formatting & ensure numeric input try { diff --git a/system/Helpers/test_helper.php b/system/Helpers/test_helper.php index b455aa301e2f..b8cbe0462350 100644 --- a/system/Helpers/test_helper.php +++ b/system/Helpers/test_helper.php @@ -28,7 +28,7 @@ * * @return array|object */ - function fake($model, ?array $overrides = null, $persist = true) + function fake($model, ?array $overrides = null, $persist = true): array|object { $fabricator = new Fabricator($model); @@ -49,10 +49,8 @@ function fake($model, ?array $overrides = null, $persist = true) * Used within our test suite to mock certain system tools. * * @param string $className Fully qualified class name - * - * @return object */ - function mock(string $className) + function mock(string $className): object { $mockClass = $className::$mockClass; $mockService = $className::$mockServiceName ?? ''; diff --git a/system/Helpers/text_helper.php b/system/Helpers/text_helper.php index f8ea0c15447c..cf9459100dfb 100644 --- a/system/Helpers/text_helper.php +++ b/system/Helpers/text_helper.php @@ -458,7 +458,7 @@ function ellipsize(string $str, int $maxLength, $position = 1, string $ellipsis * * @return array|string string or array */ - function strip_slashes($str) + function strip_slashes($str): array|string { if (! is_array($str)) { return stripslashes($str); diff --git a/system/Helpers/url_helper.php b/system/Helpers/url_helper.php index 884354a1254e..e43954909d65 100644 --- a/system/Helpers/url_helper.php +++ b/system/Helpers/url_helper.php @@ -71,7 +71,7 @@ function base_url($relativePath = '', ?string $scheme = null): string * @return string|URI When returning string, the query and fragment parts are removed. * When returning URI, the query and fragment parts are preserved. */ - function current_url(bool $returnObject = false, ?IncomingRequest $request = null) + function current_url(bool $returnObject = false, ?IncomingRequest $request = null): string|URI { $request ??= service('request'); /** @var CLIRequest|IncomingRequest $request */ @@ -87,10 +87,8 @@ function current_url(bool $returnObject = false, ?IncomingRequest $request = nul * we first check in a saved session variable, if it exists, and use that. * If that's not available, however, we'll use a sanitized url from $_SERVER['HTTP_REFERER'] * which can be set by the user so is untrusted and not set by certain browsers/servers. - * - * @return string|URI */ - function previous_url(bool $returnObject = false) + function previous_url(bool $returnObject = false): string|URI { // Grab from the session first, if we have it, // since it's more reliable and safer. From 745eaa30b0aa51f6924f5e3831fe4fffec71bcb1 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Sat, 5 Sep 2026 18:14:48 +0700 Subject: [PATCH 105/163] chore: Move phpstan.dist.neon order in rector.php config to avoid tmp/schema.php created on run (#10523) --- rector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rector.php b/rector.php index 4cb10eecfa10..e852257d8129 100644 --- a/rector.php +++ b/rector.php @@ -69,10 +69,10 @@ __DIR__ . '/phpstan-bootstrap.php', ]) ->withPHPStanConfigs([ - __DIR__ . '/phpstan.dist.neon', __DIR__ . '/vendor/codeigniter/phpstan-codeigniter/extension.neon', __DIR__ . '/vendor/phpstan/phpstan-strict-rules/rules.neon', __DIR__ . '/vendor/shipmonk/phpstan-baseline-per-identifier/extension.neon', + __DIR__ . '/phpstan.dist.neon', ]) // is there a file you need to skip? ->withSkip([ From a2c1f54dcc5f5869684f4e8d1b4a06c0e8c4514a Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Sat, 5 Sep 2026 15:03:23 +0200 Subject: [PATCH 106/163] fix: prevent gatherOutput from being called twice when controller returns Response (#10369) --- system/CodeIgniter.php | 38 ++++++++++----------- tests/system/CodeIgniterTest.php | 33 ++++++++++++++++++ user_guide_src/source/changelogs/v4.7.5.rst | 1 + 3 files changed, 53 insertions(+), 19 deletions(-) diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 1654e82399e7..bf8700660f2a 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -505,29 +505,29 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache // If startController returned a Response (from an attribute or Closure), use it if ($returned instanceof ResponseInterface) { $this->gatherOutput($cacheConfig, $returned); - } - // Closure controller has run in startController(). - elseif (! is_callable($this->controller)) { - $controller = $this->createController(); + } else { + // Closure controller has run in startController(). + if (! is_callable($this->controller)) { + $controller = $this->createController(); - if (! method_exists($controller, '_remap') && ! is_callable([$controller, $this->method], false)) { - throw PageNotFoundException::forMethodNotFound($this->method); - } + if (! method_exists($controller, '_remap') && ! is_callable([$controller, $this->method], false)) { + throw PageNotFoundException::forMethodNotFound($this->method); + } - // Is there a "post_controller_constructor" event? - Events::trigger('post_controller_constructor'); + // Is there a "post_controller_constructor" event? + Events::trigger('post_controller_constructor'); - $returned = $this->runController($controller); - } else { - $this->benchmark->stop('controller_constructor'); - $this->benchmark->stop('controller'); - } - - // If $returned is a string, then the controller output something, - // probably a view, instead of echoing it directly. Send it along - // so it can be used with the output. - $this->gatherOutput($cacheConfig, $returned); + $returned = $this->runController($controller); + } else { + $this->benchmark->stop('controller_constructor'); + $this->benchmark->stop('controller'); + } + // If $returned is a string, then the controller output something, + // probably a view, instead of echoing it directly. Send it along + // so it can be used with the output. + $this->gatherOutput($cacheConfig, $returned); + } if ($this->enableFilters) { /** @var Filters $filters */ $filters = service('filters'); diff --git a/tests/system/CodeIgniterTest.php b/tests/system/CodeIgniterTest.php index c5b3863ee203..da65ffd4a22d 100644 --- a/tests/system/CodeIgniterTest.php +++ b/tests/system/CodeIgniterTest.php @@ -1308,4 +1308,37 @@ public function testResetForWorkerMode(): void $this->assertSame($csp->getStyleNonce(), RichRenderer::$css_nonce); $this->assertTrue(RichRenderer::$needs_pre_render); } + + public function testGatherOutputCalledOnceWhenControllerReturnsResponse(): void + { + $this->resetServices(); + + $superglobals = service('superglobals'); + $superglobals->setServer('argv', ['index.php', 'pages/test']); + $superglobals->setServer('argc', 2); + $superglobals->setServer('REQUEST_URI', '/pages/test'); + $superglobals->setServer('SCRIPT_NAME', '/index.php'); + + $routes = service('routes'); + $routes->add('pages/test', static fn () => service('response')->setBody('Test Body')); + + $config = new App(); + $codeigniter = new class ($config) extends MockCodeIgniter { + public int $gatherOutputCalls = 0; + + protected function gatherOutput(?Cache $cacheConfig = null, $returned = null): void + { + $this->gatherOutputCalls++; + parent::gatherOutput($cacheConfig, $returned); + } + }; + + ob_start(); + $codeigniter->run($routes); + ob_end_clean(); + + // When startController() returns a ResponseInterface (e.g. from a closure route), + // gatherOutput() must be called exactly once — not twice as in the original bug. + $this->assertSame(1, $codeigniter->gatherOutputCalls); + } } diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index 9977c5ef7df5..c23de3e46061 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -39,6 +39,7 @@ Bugs Fixed - **CLI:** Fixed a bug where pressing backspace in a ``CLI::prompt()`` erased the prompt text when the ``readline`` extension is enabled. The prompt is now passed to ``readline()`` so line redraws repaint it. ANSI color codes in the prompt (e.g., option defaults) are wrapped in readline's non-printing markers under GNU readline so cursor positioning stays accurate. - **CLIRequest:** Fixed a bug where ``parseCommand()`` could throw a TypeError when ``argv`` is missing. +- **CodeIgniter:** Fixed a bug where ``gatherOutput()`` could be called twice when ``startController()`` returned a ``ResponseInterface`` (e.g., from filter attributes or closure routes). - **Content Security Policy:** Fixed a bug where empty ``Content-Security-Policy``, ``Content-Security-Policy-Report-Only``, and ``Reporting-Endpoints`` response headers were generated when no corresponding values existed. - **Cookie:** Fixed a bug where ``Cookie`` instances created with ``raw: true`` allowed invalid characters in cookie values rejected by ``setrawcookie()``. - **Helpers:** Fixed a bug where ``get_dir_file_info()`` returned incomplete entries for subdirectories and missing files instead of omitting them. From b8866f2dc0ebe6e66cbdc19599a4e294c573afc7 Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Sat, 5 Sep 2026 15:08:09 +0200 Subject: [PATCH 107/163] test(Session): add unit tests for FileHandler (#10435) --- .../Session/Handlers/FileHandlerTest.php | 487 ++++++++++++++++++ 1 file changed, 487 insertions(+) create mode 100644 tests/system/Session/Handlers/FileHandlerTest.php diff --git a/tests/system/Session/Handlers/FileHandlerTest.php b/tests/system/Session/Handlers/FileHandlerTest.php new file mode 100644 index 000000000000..3c94aacad693 --- /dev/null +++ b/tests/system/Session/Handlers/FileHandlerTest.php @@ -0,0 +1,487 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Session\Handlers; + +use Closure; +use CodeIgniter\Session\Exceptions\SessionException; +use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\TestLogger; +use Config\Logger as LoggerConfig; +use Config\Session as SessionConfig; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; + +/** + * @internal + */ +#[Group('Others')] +final class FileHandlerTest extends CIUnitTestCase +{ + private string $sessionDriver = FileHandler::class; + private string $sessionName = 'ci_session'; + private string $userIpAddress = '127.0.0.1'; + private string $tempPath; + private string $originalSavePath; + private string $originalSidBits; + private string $originalSidLength; + + protected function setUp(): void + { + parent::setUp(); + + $this->originalSavePath = (string) ini_get('session.save_path'); + $this->originalSidBits = (string) ini_get('session.sid_bits_per_character'); + $this->originalSidLength = (string) ini_get('session.sid_length'); + + $this->tempPath = rtrim(sys_get_temp_dir(), '/\\') . DIRECTORY_SEPARATOR . 'FileHandlerTest_' . bin2hex(random_bytes(6)); + mkdir($this->tempPath, 0700, true); + } + + protected function tearDown(): void + { + parent::tearDown(); + + ini_set('session.save_path', $this->originalSavePath); + ini_set('session.sid_bits_per_character', $this->originalSidBits); + ini_set('session.sid_length', $this->originalSidLength); + + $this->removeDirectory($this->tempPath); + } + + /** + * @param array $options Replace values for `Config\Session`. + */ + protected function getInstance(array $options = []): FileHandler + { + $defaults = [ + 'driver' => $this->sessionDriver, + 'cookieName' => $this->sessionName, + 'expiration' => 7200, + 'savePath' => $this->tempPath, + 'matchIP' => false, + 'timeToUpdate' => 300, + 'regenerateDestroy' => false, + ]; + $sessionConfig = new SessionConfig(); + $config = array_merge($defaults, $options); + + foreach ($config as $key => $value) { + $sessionConfig->{$key} = $value; + } + + $handler = new FileHandler($sessionConfig, $this->userIpAddress); + $handler->setLogger(new TestLogger(new LoggerConfig())); + + return $handler; + } + + /** + * Runs a callback while suppressing any PHP warnings it raises. + * + * @param Closure(): mixed $callback + */ + private function withSuppressedErrors(Closure $callback): mixed + { + set_error_handler(static fn (int $severity, string $message): bool => true); + + try { + return $callback(); + } finally { + restore_error_handler(); + } + } + + private function removeDirectory(string $path): void + { + if (! is_dir($path)) { + return; + } + + $items = scandir($path); + + foreach ($items === false ? [] : $items as $item) { + if ($item === '.' || $item === '..') { + continue; + } + + $target = $path . DIRECTORY_SEPARATOR . $item; + + if (is_dir($target)) { + $this->removeDirectory($target); + } else { + @unlink($target); + } + } + + @chmod($path, 0700); + @rmdir($path); + } + + private static function sessionId(string $char): string + { + return str_repeat($char, 32); + } + + // -------------------------------------------------------------------- + // Constructor & Initialization + // -------------------------------------------------------------------- + + public function testConstructorTrimsAndSetsIniSavePath(): void + { + $this->getInstance(['savePath' => $this->tempPath . '//']); + + $this->assertSame($this->tempPath, ini_get('session.save_path')); + } + + public function testConstructorForcesSessionIDRegexSettings(): void + { + ini_set('session.sid_bits_per_character', '5'); + ini_set('session.sid_length', '26'); + + $this->getInstance(); + + $this->assertSame('4', ini_get('session.sid_bits_per_character')); + $this->assertSame('32', ini_get('session.sid_length')); + } + + // -------------------------------------------------------------------- + // open() + // -------------------------------------------------------------------- + + public function testOpenExistingDirectory(): void + { + $handler = $this->getInstance(); + + $this->assertTrue($handler->open($this->tempPath, $this->sessionName)); + } + + public function testOpenWithMatchIPIsolatesSessions(): void + { + $id = self::sessionId('1'); + $data = 'ip_test_data'; + + $handler1 = $this->getInstance(['matchIP' => true]); + $this->assertTrue($handler1->open($this->tempPath, $this->sessionName)); + $this->assertSame('', $handler1->read($id)); + $this->assertTrue($handler1->write($id, $data)); + $this->assertTrue($handler1->close()); + + // File created with hashed IP in filename + $this->assertFileExists($this->tempPath . '/' . $this->sessionName . md5($this->userIpAddress) . $id); + + // Handler with different IP cannot read the session (isolation) + $sessionConfig = new SessionConfig(); + $sessionConfig->driver = $this->sessionDriver; + $sessionConfig->cookieName = $this->sessionName; + $sessionConfig->savePath = $this->tempPath; + $sessionConfig->matchIP = true; + + $handler2 = new FileHandler($sessionConfig, '192.168.1.1'); + $handler2->setLogger(new TestLogger(new LoggerConfig())); + $this->assertTrue($handler2->open($this->tempPath, $this->sessionName)); + $this->assertSame('', $handler2->read($id)); + $this->assertTrue($handler2->close()); + } + + public function testOpenCreatesMissingDirectory(): void + { + $handler = $this->getInstance(); + + $path = $this->tempPath . '/new/nested'; + + $this->assertTrue($handler->open($path, $this->sessionName)); + $this->assertDirectoryExists($path); + } + + public function testOpenThrowsForInvalidSavePath(): void + { + file_put_contents($this->tempPath . '/blocker', 'data'); + + $handler = $this->getInstance(); + + $this->expectException(SessionException::class); + $this->withSuppressedErrors(fn (): bool => $handler->open($this->tempPath . '/blocker/sub', $this->sessionName)); + } + + #[RequiresOperatingSystem('Linux|Darwin')] + public function testOpenThrowsForWriteProtectedSavePath(): void + { + $dir = $this->tempPath . '/readonly'; + mkdir($dir, 0700); + chmod($dir, 0555); + + if (is_writable($dir)) { + $this->markTestSkipped('Directory permissions cannot be restricted for this user (e.g. running as root).'); + } + + try { + $handler = $this->getInstance(); + + $this->expectException(SessionException::class); + $handler->open($dir, $this->sessionName); + } finally { + chmod($dir, 0755); + } + } + + // -------------------------------------------------------------------- + // read() + // -------------------------------------------------------------------- + + public function testReadNewSession(): void + { + $handler = $this->getInstance(); + $handler->open($this->tempPath, $this->sessionName); + + $id = self::sessionId('1'); + + $this->assertSame('', $handler->read($id)); + + $handler->close(); + } + + public function testReadExistingSession(): void + { + $id = self::sessionId('2'); + $data = '__ci_last_regenerate|i:1664607454;key|s:5:"value";'; + file_put_contents($this->tempPath . '/' . $this->sessionName . $id, $data); + + $handler = $this->getInstance(); + $handler->open($this->tempPath, $this->sessionName); + + $this->assertSame($data, $handler->read($id)); + + $handler->close(); + } + + public function testReadTwiceWithoutCloseRewinds(): void + { + $id = self::sessionId('3'); + $data = '__ci_last_regenerate|i:1664607454;key|s:5:"value";'; + + $handler = $this->getInstance(); + $handler->open($this->tempPath, $this->sessionName); + + $this->assertSame('', $handler->read($id)); + $this->assertTrue($handler->write($id, $data)); + + $this->assertSame($data, $handler->read($id)); + + $handler->close(); + } + + public function testReadFailsWhenFileCannotBeOpened(): void + { + $handler = $this->getInstance(); + $handler->open($this->tempPath, $this->sessionName); + + $id = self::sessionId('4'); + mkdir($this->tempPath . '/' . $this->sessionName . $id, 0700, true); + + $result = $this->withSuppressedErrors(static fn (): false|string => $handler->read($id)); + + $this->assertFalse($result); + $this->assertLogContains('error', 'Session: Unable to open file'); + } + + // -------------------------------------------------------------------- + // write() + // -------------------------------------------------------------------- + + public function testWriteFailsWhenNoFileHandle(): void + { + $handler = $this->getInstance(); + + $this->assertFalse($handler->write(self::sessionId('a'), 'data')); + } + + public function testReadWriteRoundtrip(): void + { + $id = self::sessionId('b'); + $data = '__ci_last_regenerate|i:1664607454;key|s:5:"value";'; + + $handler = $this->getInstance(); + $handler->open($this->tempPath, $this->sessionName); + + $this->assertSame('', $handler->read($id)); + $this->assertTrue($handler->write($id, $data)); + // Identical data on a new file returns true without writing again. + $this->assertTrue($handler->write($id, $data)); + + $handler->close(); + + $this->assertSame($data, file_get_contents($this->tempPath . '/' . $this->sessionName . $id)); + + // A second handler reads the file back; an identical write just touches it. + $handler2 = $this->getInstance(); + $handler2->open($this->tempPath, $this->sessionName); + + $this->assertSame($data, $handler2->read($id)); + $this->assertTrue($handler2->write($id, $data)); + + // Different data truncates and rewrites the existing file. + $data2 = 'changed|i:1664607455;key|s:6:"value2";'; + $this->assertTrue($handler2->write($id, $data2)); + + $handler2->close(); + + $this->assertSame($data2, file_get_contents($this->tempPath . '/' . $this->sessionName . $id)); + } + + public function testWriteEmptyDataClearsSession(): void + { + $id = self::sessionId('c'); + $data = 'some_data'; + + $handler1 = $this->getInstance(); + $handler1->open($this->tempPath, $this->sessionName); + $this->assertSame('', $handler1->read($id)); + $this->assertTrue($handler1->write($id, $data)); + $handler1->close(); + + $this->assertSame($data, file_get_contents($this->tempPath . '/' . $this->sessionName . $id)); + + $handler2 = $this->getInstance(); + $handler2->open($this->tempPath, $this->sessionName); + $this->assertSame($data, $handler2->read($id)); + $this->assertTrue($handler2->write($id, '')); + $handler2->close(); + + $this->assertSame('', file_get_contents($this->tempPath . '/' . $this->sessionName . $id)); + } + + public function testWriteWithDifferentSessionID(): void + { + $handler = $this->getInstance(); + $handler->open($this->tempPath, $this->sessionName); + + $id1 = self::sessionId('d'); + $id2 = self::sessionId('e'); + + $this->assertSame('', $handler->read($id1)); + $this->assertTrue($handler->write($id2, 'data')); + $handler->close(); + } + + // -------------------------------------------------------------------- + // close() + // -------------------------------------------------------------------- + + public function testCloseWithOpenFile(): void + { + $handler = $this->getInstance(); + $handler->open($this->tempPath, $this->sessionName); + $handler->read(self::sessionId('1')); + + $this->assertTrue($handler->close()); + // After closing, write fails + $this->assertFalse($handler->write(self::sessionId('1'), 'data')); + } + + public function testCloseWithoutOpenFile(): void + { + $handler = $this->getInstance(); + + $this->assertTrue($handler->close()); + } + + // -------------------------------------------------------------------- + // destroy() + // -------------------------------------------------------------------- + + public function testDestroyExistingSession(): void + { + $handler = $this->getInstance(); + $handler->open($this->tempPath, $this->sessionName); + + $id = self::sessionId('2'); + $handler->read($id); + $handler->close(); + + $this->assertFileExists($this->tempPath . '/' . $this->sessionName . $id); + $this->assertTrue($handler->destroy($id)); + $this->assertFileDoesNotExist($this->tempPath . '/' . $this->sessionName . $id); + } + + public function testDestroyNonexistentSession(): void + { + $handler = $this->getInstance(); + $handler->open($this->tempPath, $this->sessionName); + $handler->close(); + + $this->assertTrue($handler->destroy(self::sessionId('3'))); + } + + // -------------------------------------------------------------------- + // gc() + // -------------------------------------------------------------------- + + public function testGCWhenSavePathMissing(): void + { + $handler = $this->getInstance(['savePath' => $this->tempPath . '/missing']); + + $this->assertFalse($handler->gc(3600)); + $this->assertLogContains('debug', "Session: Garbage collector couldn't list files under directory"); + } + + public function testGCRemovesExpiredSessionsOnly(): void + { + $handler = $this->getInstance(['savePath' => $this->tempPath]); + + $expired = $this->tempPath . '/' . $this->sessionName . self::sessionId('a'); + file_put_contents($expired, 'data'); + touch($expired, time() - 4000); + + $recent = $this->tempPath . '/' . $this->sessionName . self::sessionId('b'); + file_put_contents($recent, 'data'); + touch($recent); + + $directory = $this->tempPath . '/' . $this->sessionName . self::sessionId('c'); + mkdir($directory, 0700); + + $foreign = $this->tempPath . '/not-a-session-file'; + file_put_contents($foreign, 'data'); + touch($foreign, time() - 4000); + + $nonHex = $this->tempPath . '/' . $this->sessionName . str_repeat('z', 32); + file_put_contents($nonHex, 'data'); + touch($nonHex, time() - 4000); + + $this->assertSame(1, $handler->gc(3600)); + + $this->assertFileDoesNotExist($expired); + $this->assertFileExists($recent); + $this->assertDirectoryExists($directory); + $this->assertFileExists($foreign); + $this->assertFileExists($nonHex); + } + + public function testGCWithMatchIP(): void + { + $handler = $this->getInstance(['savePath' => $this->tempPath, 'matchIP' => true]); + + $expired = $this->tempPath . '/' . $this->sessionName . md5($this->userIpAddress) . self::sessionId('a'); + file_put_contents($expired, 'data'); + touch($expired, time() - 4000); + + $withoutIp = $this->tempPath . '/' . $this->sessionName . self::sessionId('b'); + file_put_contents($withoutIp, 'data'); + touch($withoutIp, time() - 4000); + + $this->assertSame(1, $handler->gc(3600)); + + $this->assertFileDoesNotExist($expired); + $this->assertFileExists($withoutIp); + } +} From 6dd1c5fa5d8828b7829c86337e44e97db37b9d51 Mon Sep 17 00:00:00 2001 From: MD Ali Kadar Date: Sat, 5 Sep 2026 19:12:09 +0600 Subject: [PATCH 108/163] fix: Memcached `decrement()` gives the wrong sign on a missing key (#10512) * fix: MemcachedHandler::decrement() wrong sign on missing key MemcachedHandler::decrement() passed $offset as Memcached::decrement()'s initial_value, which is used as-is (not decremented from) when a key doesn't exist yet. That made a fresh key end up at +$offset instead of the -$offset every other cache handler (File, Redis, Predis) produces. Memcached counters are unsigned, so they can't hold a negative initial value the way the other handlers effectively can. Use 0 instead (also Memcached::decrement()'s own default), so a fresh key at least stops going the wrong direction. Updates the existing MemcachedHandlerTest::testDecrement() expectation and adds a changelog entry. * fix: address review feedback on Memcached decrement() fix * fix: correct expected type in MemcachedHandlerTest::testDecrement --- system/Cache/Handlers/MemcachedHandler.php | 8 +++++--- tests/system/Cache/Handlers/MemcachedHandlerTest.php | 7 ++++++- user_guide_src/source/changelogs/v4.7.5.rst | 1 + user_guide_src/source/libraries/caching.rst | 5 +++++ 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/system/Cache/Handlers/MemcachedHandler.php b/system/Cache/Handlers/MemcachedHandler.php index 6d0c8aec4d97..3633dc8e40da 100644 --- a/system/Cache/Handlers/MemcachedHandler.php +++ b/system/Cache/Handlers/MemcachedHandler.php @@ -180,9 +180,11 @@ public function decrement(string $key, int $offset = 1): false|int $key = static::validateKey($key, $this->prefix); - // FIXME: third parameter isn't other handler actions. - - return $this->memcached->decrement($key, $offset, $offset, 60); + // Memcached counters are unsigned and saturate at 0, so a missing + // key can only ever be initialized to 0, never a negative value. + // Use Memcached::decrement()'s own default of 0 as the initial + // value instead of $offset, so a new key no longer starts positive. + return $this->memcached->decrement($key, $offset, 0, 60); } public function clean(): bool diff --git a/tests/system/Cache/Handlers/MemcachedHandlerTest.php b/tests/system/Cache/Handlers/MemcachedHandlerTest.php index 78412616e5bf..e81c8c9056fe 100644 --- a/tests/system/Cache/Handlers/MemcachedHandlerTest.php +++ b/tests/system/Cache/Handlers/MemcachedHandlerTest.php @@ -151,7 +151,12 @@ public function testDecrement(): void $this->assertSame(9, $memcachedHandler->decrement(self::$key1, 1)); $this->assertFalse($memcachedHandler->decrement(self::$key2, 1)); - $this->assertSame(1, $memcachedHandler->decrement(self::$key3, 1)); + // A key that doesn't exist yet starts at 0, not at the offset + // (Memcached counters are unsigned, so it can't start negative). + $this->assertSame(0, $memcachedHandler->decrement(self::$key3, 5)); + // Memcached stores counter values as decimal strings on the wire, so + // a plain get() on a counter key returns a string, not an int. + $this->assertSame('0', $memcachedHandler->get(self::$key3)); } public function testClean(): void diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index c23de3e46061..6cb29fb846ad 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -45,6 +45,7 @@ Bugs Fixed - **Helpers:** Fixed a bug where ``get_dir_file_info()`` returned incomplete entries for subdirectories and missing files instead of omitting them. - **Honeypot:** Fixed a bug where bot detection returned an HTTP 500 response instead of 403 (Forbidden). - **Logger:** Fixed a bug where interpolating a log message with array or non-stringable context values could raise PHP warnings or errors. +- **Cache:** Fixed ``MemcachedHandler::decrement()`` initializing a non-existent counter to the positive offset. Missing counters are now initialized to ``0``, reflecting Memcached's unsigned, saturating counter semantics. See the repo's `CHANGELOG.md `_ diff --git a/user_guide_src/source/libraries/caching.rst b/user_guide_src/source/libraries/caching.rst index b208d63499df..76a696f570ee 100644 --- a/user_guide_src/source/libraries/caching.rst +++ b/user_guide_src/source/libraries/caching.rst @@ -212,6 +212,11 @@ Class Reference .. literalinclude:: caching/008.php + .. important:: Memcached counters are unsigned and saturate at ``0`` instead of + going negative. Decrementing a key that does not yet exist initializes it + to ``0`` (rather than ``-$offset``, as the File, Redis, and Predis handlers + do), and decrementing an existing counter below ``0`` clamps it at ``0``. + .. php:method:: clean() :returns: ``true`` on success, ``false`` on failure From 33db2c6129f85b8475a2d0c92273f2e6856a6157 Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Sat, 5 Sep 2026 15:13:23 +0200 Subject: [PATCH 109/163] fix: correct file permissions and chmod target in File and UploadedFile move() (#10519) * fix: correct permissions and target in File and UploadedFile move() * docs: add changelog entry for File and UploadedFile move() permissions fix --- system/Files/File.php | 2 +- system/HTTP/Files/UploadedFile.php | 2 +- tests/system/Files/FileWithVfsTest.php | 9 +++++++++ tests/system/HTTP/Files/FileMovingTest.php | 1 + user_guide_src/source/changelogs/v4.7.5.rst | 1 + 5 files changed, 13 insertions(+), 2 deletions(-) diff --git a/system/Files/File.php b/system/Files/File.php index feda0cd5780d..2cf394355185 100644 --- a/system/Files/File.php +++ b/system/Files/File.php @@ -170,7 +170,7 @@ public function move(string $targetPath, ?string $name = null, bool $overwrite = throw FileException::forUnableToMove($this->getBasename(), $targetPath, strip_tags($error['message'])); } - @chmod($destination, 0777 & ~umask()); + @chmod($destination, 0666 & ~umask()); return new self($destination); } diff --git a/system/HTTP/Files/UploadedFile.php b/system/HTTP/Files/UploadedFile.php index bf594acddc38..f469e3e7023a 100644 --- a/system/HTTP/Files/UploadedFile.php +++ b/system/HTTP/Files/UploadedFile.php @@ -159,7 +159,7 @@ public function move(string $targetPath, ?string $name = null, bool $overwrite = throw HTTPException::forMoveFailed(basename($this->path), $targetPath, $message); } - @chmod($targetPath, 0777 & ~umask()); + @chmod($destination, 0666 & ~umask()); // Success, so store our new information $this->path = $targetPath; diff --git a/tests/system/Files/FileWithVfsTest.php b/tests/system/Files/FileWithVfsTest.php index a6c26383d187..e0065591963d 100644 --- a/tests/system/Files/FileWithVfsTest.php +++ b/tests/system/Files/FileWithVfsTest.php @@ -150,4 +150,13 @@ public function testMoveReturnsNewInstance(): void $this->assertInstanceOf(File::class, $file); $this->assertSame($destination . '/apple.php', $file->getPathname()); } + + public function testMovePermissions(): void + { + $destination = $this->start . 'baker'; + $this->file->move($destination); + + $expectedPerms = 0666 & ~umask(); + $this->assertSame($expectedPerms, $this->root->getChild('baker/apple.php')->getPermissions()); + } } diff --git a/tests/system/HTTP/Files/FileMovingTest.php b/tests/system/HTTP/Files/FileMovingTest.php index bb4b637fdc47..fb07d10ed74b 100644 --- a/tests/system/HTTP/Files/FileMovingTest.php +++ b/tests/system/HTTP/Files/FileMovingTest.php @@ -98,6 +98,7 @@ public function testMove(): void $this->assertTrue($this->root->hasChild('destination/' . $finalFilename . '.txt')); $this->assertTrue($this->root->hasChild('destination/' . $finalFilename . '_1.txt')); + $this->assertSame(0666 & ~umask(), $this->root->getChild('destination/' . $finalFilename . '.txt')->getPermissions()); } public function testMoveSanitizesClientNameByDefault(): void diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index 6cb29fb846ad..3403aca9d8b6 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -42,6 +42,7 @@ Bugs Fixed - **CodeIgniter:** Fixed a bug where ``gatherOutput()`` could be called twice when ``startController()`` returned a ``ResponseInterface`` (e.g., from filter attributes or closure routes). - **Content Security Policy:** Fixed a bug where empty ``Content-Security-Policy``, ``Content-Security-Policy-Report-Only``, and ``Reporting-Endpoints`` response headers were generated when no corresponding values existed. - **Cookie:** Fixed a bug where ``Cookie`` instances created with ``raw: true`` allowed invalid characters in cookie values rejected by ``setrawcookie()``. +- **Files:** Fixed a bug where ``File::move()`` and ``UploadedFile::move()`` set executable and overly permissive file permissions (``0777 & ~umask()`` instead of ``0666 & ~umask()``), and ``UploadedFile::move()`` targeted the parent directory instead of the destination file for ``chmod()``. - **Helpers:** Fixed a bug where ``get_dir_file_info()`` returned incomplete entries for subdirectories and missing files instead of omitting them. - **Honeypot:** Fixed a bug where bot detection returned an HTTP 500 response instead of 403 (Forbidden). - **Logger:** Fixed a bug where interpolating a log message with array or non-stringable context values could raise PHP warnings or errors. From 860b5ec4a9b9ec76055c90e24c7b8e53427d60b2 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sat, 5 Sep 2026 21:19:07 +0800 Subject: [PATCH 110/163] refactor: fix minor type inaccuracies found via PHPStan bleeding edge (#10522) --- system/Database/Database.php | 32 +++++++++++++--------------- system/HTTP/Files/FileCollection.php | 2 +- system/HTTP/SiteURIFactory.php | 2 +- system/Helpers/Array/ArrayHelper.php | 2 +- system/Router/RouteCollection.php | 2 +- 5 files changed, 19 insertions(+), 21 deletions(-) diff --git a/system/Database/Database.php b/system/Database/Database.php index 4cb5dfaf1627..a6f0e9cb73ef 100644 --- a/system/Database/Database.php +++ b/system/Database/Database.php @@ -58,7 +58,7 @@ public function load(array $params = [], string $alias = '') throw new InvalidArgumentException('You have not selected a database type to connect to.'); } - assert($this->checkDbExtension($params['DBDriver'])); + $this->checkDbExtension($params['DBDriver']); $this->connections[$alias] = $this->initDriver($params['DBDriver'], 'Connection', $params); @@ -154,15 +154,17 @@ protected function initDriver(string $driver, string $class, $argument): object } /** - * Check the PHP database extension is loaded. + * Check if the PHP database extension is loaded. * * @param string $driver DB driver or FQCN for custom driver + * + * @throws ConfigException if the driver is invalid + * @throws CriticalError if the required PHP extension is not loaded */ - private function checkDbExtension(string $driver): bool + private function checkDbExtension(string $driver): void { if (str_contains($driver, '\\')) { - // Cannot check a fully qualified classname for a custom driver. - return true; + return; // Cannot check a fully qualified classname for a custom driver. } $extensionMap = [ @@ -174,21 +176,17 @@ private function checkDbExtension(string $driver): bool 'OCI8' => 'oci8', ]; - $extension = $extensionMap[$driver] ?? ''; - - if ($extension === '') { - $message = 'Invalid DBDriver name: "' . $driver . '"'; - - throw new ConfigException($message); - } + $extension = $extensionMap[$driver] + ?? throw new ConfigException(sprintf('Invalid DBDriver name: "%s".', $driver)); if (extension_loaded($extension)) { - return true; + return; } - $message = 'The required PHP extension "' . $extension . '" is not loaded.' - . ' Install and enable it to use "' . $driver . '" driver.'; - - throw new CriticalError($message); + throw new CriticalError(sprintf( + 'The required PHP extension "%s" is not loaded. Install and enable it to use "%s" driver.', + $extension, + $driver, + )); } } diff --git a/system/HTTP/Files/FileCollection.php b/system/HTTP/Files/FileCollection.php index 058e86c3a4aa..c1c0582f6ff5 100644 --- a/system/HTTP/Files/FileCollection.php +++ b/system/HTTP/Files/FileCollection.php @@ -189,7 +189,7 @@ protected function createFileObject(array $array) return new UploadedFile( $array['tmp_name'] ?? null, - $array['name'] ?? null, + $array['name'], $array['type'] ?? null, ($array['size'] ?? null) === null ? null : (int) $array['size'], $array['error'] ?? null, diff --git a/system/HTTP/SiteURIFactory.php b/system/HTTP/SiteURIFactory.php index 73cc9fe84ee4..11dccce6c540 100644 --- a/system/HTTP/SiteURIFactory.php +++ b/system/HTTP/SiteURIFactory.php @@ -227,7 +227,7 @@ private function createURIFromRoutePath(string $routePath): SiteURI */ private function getHost(): ?string { - $httpHostPort = $this->superglobals->server('HTTP_HOST') ?? null; + $httpHostPort = $this->superglobals->server('HTTP_HOST'); if ($httpHostPort !== null) { [$httpHost] = explode(':', $httpHostPort, 2); diff --git a/system/Helpers/Array/ArrayHelper.php b/system/Helpers/Array/ArrayHelper.php index f70f6905ca5d..d370e1f1112f 100644 --- a/system/Helpers/Array/ArrayHelper.php +++ b/system/Helpers/Array/ArrayHelper.php @@ -324,7 +324,7 @@ public static function recursiveCount(array $array, int $counter = 0): int * @param list|string> $array * @param int|string|null $sortByIndex */ - public static function sortValuesByNatural(array &$array, $sortByIndex = null): bool + public static function sortValuesByNatural(array &$array, $sortByIndex = null): true { return usort($array, static function ($currentValue, $nextValue) use ($sortByIndex): int { if ($sortByIndex !== null) { diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 6ade80e6f9fc..c08c01530b79 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -1520,7 +1520,7 @@ private function checkSubdomains($subdomains): bool // Routes can be limited to any sub-domain. In that case, though, // it does require a sub-domain to be present. - if (! in_array($this->currentSubdomain, [null, ''], true) && in_array('*', $subdomains, true)) { + if ($this->currentSubdomain !== '' && in_array('*', $subdomains, true)) { return true; } From cc882d626e76dc50bfd687d568b937df20ae850c Mon Sep 17 00:00:00 2001 From: Karl Gray Date: Sat, 5 Sep 2026 15:08:37 +0100 Subject: [PATCH 111/163] fix: strip the table prefix correctly when rebuilding a SQLite3 table (#10509) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SQLite cannot alter or drop a column in place, so `SQLite3\Table` rebuilds the whole table and recreates its foreign keys from the metadata it collected. That metadata holds prefixed table names, and `createTable()` stripped the prefix with `trim($name, $this->db->DBPrefix)`. `trim()`'s second argument is a set of characters, not a prefix. It removes any of those characters from either end of the string, repeatedly, so with the prefix `db_` it turns `db_bandit_fk` into `andit_fk` — the leading `b` of the table's own name is eaten as well, and characters are stripped from the end too. The rebuilt table's foreign keys then reference tables that do not exist. Nothing fails at that point, because foreign key enforcement is off for the duration of the rebuild; the error surfaces at the next write to the referenced table, naming a table that appears nowhere in the schema. Whether a given table is affected depends on which characters its name happens to begin and end with, so most tables come through untouched. Strip the prefix the way `fromTable()` in the same class already does. The existing tests could not catch this: `AlterTableTest` builds its own connection without a `DBPrefix`, and the damage is invisible until the constraint is used. The regression test therefore sets a prefix and names the referenced table so that it begins with a character the prefix also contains, which is what makes the bug reproduce. --- system/Database/SQLite3/Table.php | 9 ++- .../Database/Live/SQLite3/AlterTableTest.php | 62 +++++++++++++++++++ user_guide_src/source/changelogs/v4.7.5.rst | 1 + 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/system/Database/SQLite3/Table.php b/system/Database/SQLite3/Table.php index 1884d8c3ed44..3737822eaeeb 100644 --- a/system/Database/SQLite3/Table.php +++ b/system/Database/SQLite3/Table.php @@ -333,9 +333,16 @@ protected function createTable() } foreach ($this->foreignKeys as $foreignKey) { + $foreignTableName = $foreignKey->foreign_table_name; + $prefix = $this->db->DBPrefix; + + if ($prefix !== '' && str_starts_with($foreignTableName, $prefix)) { + $foreignTableName = substr($foreignTableName, strlen($prefix)); + } + $this->forge->addForeignKey( $foreignKey->column_name, - trim($foreignKey->foreign_table_name, $this->db->DBPrefix), + $foreignTableName, $foreignKey->foreign_column_name, ); } diff --git a/tests/system/Database/Live/SQLite3/AlterTableTest.php b/tests/system/Database/Live/SQLite3/AlterTableTest.php index a68d42ca1440..a7ba8ad13f03 100644 --- a/tests/system/Database/Live/SQLite3/AlterTableTest.php +++ b/tests/system/Database/Live/SQLite3/AlterTableTest.php @@ -271,6 +271,68 @@ public function testProcessCopiesOldData(): void $this->seeInDatabase('foo', ['email' => 'funkalicious@example.com']); } + public function testDropColumnKeepsForeignKeyTableNameWhenPrefixIsSet(): void + { + $config = [ + 'DBDriver' => 'SQLite3', + 'database' => ':memory:', + 'DBDebug' => true, + 'DBPrefix' => 'db_', + ]; + + $db = db_connect($config, false); + $this->assertInstanceOf(Connection::class, $db); + + $forge = Database::forge($db); + $this->assertInstanceOf(Forge::class, $forge); + + // The referenced table must begin with a character that also appears in + // the prefix, or the prefix stripping cannot damage its name. + $forge->addField([ + 'id' => [ + 'type' => 'integer', + 'constraint' => 11, + 'unsigned' => true, + 'auto_increment' => true, + ], + ]); + $forge->addPrimaryKey('id'); + $forge->createTable('bandit_fk'); + + $forge->addField([ + 'id' => [ + 'type' => 'integer', + 'constraint' => 11, + 'unsigned' => true, + 'auto_increment' => true, + ], + 'key_id' => [ + 'type' => 'integer', + 'constraint' => 11, + 'unsigned' => true, + ], + 'name' => [ + 'type' => 'varchar', + 'constraint' => 255, + 'null' => true, + ], + ]); + $forge->addPrimaryKey('id'); + $forge->addForeignKey('key_id', 'bandit_fk', 'id'); + $forge->createTable('bandit'); + + // Dropping a column rebuilds the table, recreating its foreign keys. + $this->assertTrue($forge->dropColumn('bandit', 'name')); + + $keys = array_values($db->getForeignKeyData('bandit')); + + $this->assertCount(1, $keys); + $this->assertSame($db->DBPrefix . 'bandit_fk', $keys[0]->foreign_table_name); + + $forge->dropTable('bandit', true); + $forge->dropTable('bandit_fk', true); + } + protected function createTable(string $tableName = 'foo'): void { // Create support table for foreign keys diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index 3403aca9d8b6..f13a3e4b2b49 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -42,6 +42,7 @@ Bugs Fixed - **CodeIgniter:** Fixed a bug where ``gatherOutput()`` could be called twice when ``startController()`` returned a ``ResponseInterface`` (e.g., from filter attributes or closure routes). - **Content Security Policy:** Fixed a bug where empty ``Content-Security-Policy``, ``Content-Security-Policy-Report-Only``, and ``Reporting-Endpoints`` response headers were generated when no corresponding values existed. - **Cookie:** Fixed a bug where ``Cookie`` instances created with ``raw: true`` allowed invalid characters in cookie values rejected by ``setrawcookie()``. +- **Database:** Fixed a bug where rebuilding a SQLite3 table (e.g., ``Forge::dropColumn()``, ``Forge::modifyColumn()``, ``Forge::dropForeignKey()`` and ``Forge::dropPrimaryKey()``) corrupted the table names referenced by its foreign keys when ``DBPrefix`` was set. - **Files:** Fixed a bug where ``File::move()`` and ``UploadedFile::move()`` set executable and overly permissive file permissions (``0777 & ~umask()`` instead of ``0666 & ~umask()``), and ``UploadedFile::move()`` targeted the parent directory instead of the destination file for ``chmod()``. - **Helpers:** Fixed a bug where ``get_dir_file_info()`` returned incomplete entries for subdirectories and missing files instead of omitting them. - **Honeypot:** Fixed a bug where bot detection returned an HTTP 500 response instead of 403 (Forbidden). From 501706ce6f545d352ce9d0e7c72852d68bfd1939 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 00:52:40 +0700 Subject: [PATCH 112/163] chore(deps-dev): update rector/rector requirement (#10530) Updates the requirements on [rector/rector](https://github.com/rectorphp/rector) to permit the latest version. Updates `rector/rector` to 2.6.6 - [Release notes](https://github.com/rectorphp/rector/releases) - [Commits](https://github.com/rectorphp/rector/compare/2.6.5...2.6.6) --- updated-dependencies: - dependency-name: rector/rector dependency-version: 2.6.6 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b8f61a462bf1..d44bee53acab 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "phpunit/phpcov": "^9.0.2 || ^10.0", "phpunit/phpunit": "^10.5.16 || ^11.2", "predis/predis": "^3.0", - "rector/rector": "2.6.5", + "rector/rector": "2.6.6", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { From 94036f16300c2debe20f94fc3de9cc4456d134ea Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Thu, 10 Sep 2026 08:47:40 +0200 Subject: [PATCH 113/163] fix: write the prompt to STDOUT before readline() on Windows (#10534) --- system/CLI/InputOutput.php | 37 ++++++++++++++++++--- tests/system/CLI/CLITest.php | 37 +++++++++++++++++++++ user_guide_src/source/changelogs/v4.7.5.rst | 1 + 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/system/CLI/InputOutput.php b/system/CLI/InputOutput.php index 1d99563dd0ea..782e32d0371d 100644 --- a/system/CLI/InputOutput.php +++ b/system/CLI/InputOutput.php @@ -44,12 +44,16 @@ public function input(?string $prefix = null): string // readline() can't be tested. if ($this->readlineSupport && ENVIRONMENT !== 'testing') { // @codeCoverageIgnoreStart - // Libedit reports "EditLine wrapper" and mangles the markers, so only GNU readline gets them. - if ($prefix !== null && ! str_contains(readline_info('library_version'), 'EditLine')) { - $prefix = $this->markAnsiNonPrinting($prefix); + $prompt = $this->readlinePrompt($prefix, readline_info('library_version')); + + if ($prompt !== null) { + return readline($prompt); } - return readline($prefix); + // The library cannot render the prompt, so write it ourselves and let readline() only read the line. + self::fwrite(STDOUT, $prefix ?? ''); + + return readline(); // @codeCoverageIgnoreEnd } @@ -86,6 +90,31 @@ public function fwrite($handle, string $string): void fwrite($handle, $string); } + /** + * Builds the prompt handed to readline(), or returns null when the caller has to write + * the prompt to STDOUT itself because the line-editing library cannot render it. + * + * GNU readline gets ANSI sequences wrapped in its non-printing markers so line-redraw + * column accounting skips them. Libedit reports "EditLine wrapper" and mangles the + * markers, so it gets the raw prompt. Windows builds use WinEditLine, which prints ANSI + * sequences literally, and readline_info() omits the version there (php-src guards it + * with #ifndef PHP_WIN32), so they get no prompt. + * + * @param mixed $libraryVersion The value of readline_info('library_version') + */ + private function readlinePrompt(?string $prefix, mixed $libraryVersion): ?string + { + if ($prefix === null || ! is_string($libraryVersion)) { + return null; + } + + if (str_contains($libraryVersion, 'EditLine')) { + return $prefix; + } + + return $this->markAnsiNonPrinting($prefix); + } + /** * Wraps ANSI escape sequences in readline's non-printing markers so line-redraw column accounting skips them. */ diff --git a/tests/system/CLI/CLITest.php b/tests/system/CLI/CLITest.php index f4db06f17102..62903f80ac64 100644 --- a/tests/system/CLI/CLITest.php +++ b/tests/system/CLI/CLITest.php @@ -267,6 +267,43 @@ public function testMarkAnsiNonPrintingWrapsEscapeSequences(): void $this->assertSame('Name: ', $wrap('Name: ')); } + #[DataProvider('provideReadlinePromptDependsOnLibrary')] + public function testReadlinePromptDependsOnLibrary(mixed $libraryVersion, ?string $expected): void + { + $build = $this->getPrivateMethodInvoker(new InputOutput(), 'readlinePrompt'); + + $this->assertSame( + $expected, + $build(sprintf('What is your favorite color? [%s]: ', CLI::color('red', 'green')), $libraryVersion), + ); + } + + /** + * @return iterable + */ + public static function provideReadlinePromptDependsOnLibrary(): iterable + { + yield 'GNU readline gets the prompt with non-printing markers' => [ + '8.2', + "What is your favorite color? [\x01\e[0;32m\x02red\x01\e[0m\x02]: ", + ]; + + yield 'libedit gets the raw prompt' => [ + 'EditLine wrapper', + "What is your favorite color? [\e[0;32mred\e[0m]: ", + ]; + + // Official Windows builds use WinEditLine, which exposes no library version. + yield 'WinEditLine gets no prompt so the caller writes it' => [null, null]; + } + + public function testReadlinePromptWithoutPrefixReturnsNull(): void + { + $build = $this->getPrivateMethodInvoker(new InputOutput(), 'readlinePrompt'); + + $this->assertNull($build(null, '8.2')); + } + public function testPromptByKey(): void { PhpStreamWrapper::register(); diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index f13a3e4b2b49..6dee8ee9aa6d 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -38,6 +38,7 @@ Bugs Fixed - **CLI:** Fixed a bug where pressing backspace in a ``CLI::prompt()`` erased the prompt text when the ``readline`` extension is enabled. The prompt is now passed to ``readline()`` so line redraws repaint it. ANSI color codes in the prompt (e.g., option defaults) are wrapped in readline's non-printing markers under GNU readline so cursor positioning stays accurate. + On Windows, where the ``readline`` extension is built on WinEditLine, the prompt is written to STDOUT first because WinEditLine reports no library version and prints ANSI sequences literally. - **CLIRequest:** Fixed a bug where ``parseCommand()`` could throw a TypeError when ``argv`` is missing. - **CodeIgniter:** Fixed a bug where ``gatherOutput()`` could be called twice when ``startController()`` returned a ``ResponseInterface`` (e.g., from filter attributes or closure routes). - **Content Security Policy:** Fixed a bug where empty ``Content-Security-Policy``, ``Content-Security-Policy-Report-Only``, and ``Reporting-Endpoints`` response headers were generated when no corresponding values existed. From 2155a8903702cf002870b10aa22f1352b4c411c2 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sat, 12 Sep 2026 20:10:03 +0800 Subject: [PATCH 114/163] chore: replace PRLint with Carson's pr-title-linter (#10529) --- .github/carson.yml | 12 ++++++++++++ .github/prlint.json | 8 -------- .github/workflows/run-carson.yml | 20 ++++++++++++++++++++ 3 files changed, 32 insertions(+), 8 deletions(-) create mode 100644 .github/carson.yml delete mode 100644 .github/prlint.json create mode 100644 .github/workflows/run-carson.yml diff --git a/.github/carson.yml b/.github/carson.yml new file mode 100644 index 000000000000..1c62b62dc0c2 --- /dev/null +++ b/.github/carson.yml @@ -0,0 +1,12 @@ +# https://github.com/NexusPHP/carson/blob/1.x/SUBSCRIBERS.md + +version: 1 +subscribers: + - pr-title-linter +settings: + pr-title-linter: + name: 'Carson / pr-title' + rules: + - pattern: '^(\[\d+\.\d+\]\s{1})?(feat|fix|chore|docs|perf|refactor|style|test|config|revert)(\([\-.@:`a-zA-Z0-9]+\))?!?:\s{1}\S.+\S|Prep for \d\.\d\.\d release|\d\.\d\.\d (Ready|Merge) code$' + description: 'PR title must include the type (feat, fix, chore, docs, perf, refactor, style, test, config, revert) of the commit per Conventional Commits specification. See https://www.conventionalcommits.org/en/v1.0.0/ for the discussion.' + level: error diff --git a/.github/prlint.json b/.github/prlint.json deleted file mode 100644 index bb352e554b71..000000000000 --- a/.github/prlint.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "title": [ - { - "pattern": "^(\\[\\d+\\.\\d+\\]\\s{1})?(feat|fix|chore|docs|perf|refactor|style|test|config|revert)(\\([\\-.@:`a-zA-Z0-9]+\\))?!?:\\s{1}\\S.+\\S|Prep for \\d\\.\\d\\.\\d release|\\d\\.\\d\\.\\d (Ready|Merge) code$", - "message": "PR title must include the type (feat, fix, chore, docs, perf, refactor, style, test, config, revert) of the commit per Conventional Commits specification. See https://www.conventionalcommits.org/en/v1.0.0/ for the discussion." - } - ] -} diff --git a/.github/workflows/run-carson.yml b/.github/workflows/run-carson.yml new file mode 100644 index 000000000000..cc673215deb6 --- /dev/null +++ b/.github/workflows/run-carson.yml @@ -0,0 +1,20 @@ +name: Carson + +on: + pull_request_target: + types: + - opened + - edited + +permissions: {} + +jobs: + carson: + runs-on: ubuntu-24.04 + steps: + - name: Run Carson + uses: NexusPHP/carson@62602be15470a18edd31f217b785863e997572d5 # v1.4.0 + with: + app_id: ${{ secrets.CARSON_APP_ID }} + private_key: ${{ secrets.CARSON_PRIVATE_KEY }} + log_level: info From d47c2dcc597ac1f08943f030e2fa7bb40f2b9da1 Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Sat, 12 Sep 2026 14:38:40 +0200 Subject: [PATCH 115/163] fix(Cookie): validate cookie path and domain attributes (#10527) --- system/Cookie/Cookie.php | 56 ++++- system/Cookie/Exceptions/CookieException.php | 20 ++ system/Language/en/Cookie.php | 2 + tests/system/Cookie/CookieTest.php | 235 +++++++++++++++++++ user_guide_src/source/changelogs/v4.7.5.rst | 3 + user_guide_src/source/libraries/cookies.rst | 18 ++ 6 files changed, 326 insertions(+), 8 deletions(-) diff --git a/system/Cookie/Cookie.php b/system/Cookie/Cookie.php index 27ffc908cb38..da95f18805d4 100644 --- a/system/Cookie/Cookie.php +++ b/system/Cookie/Cookie.php @@ -127,7 +127,14 @@ class Cookie implements ArrayAccess, CloneableCookieInterface * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes * @see https://tools.ietf.org/html/rfc2616#section-2.2 */ - private static string $reservedCharsList = "=,; \t\r\n\v\f()<>@:\\\"/[]?{}"; + private static string $reservedCharsList = "=,; \t\r\n\v\f\0()<>@:\\\"/[]?{}"; + + /** + * Prohibited characters in cookie prefix and name per PHP setcookie() constraints. + * + * @see https://www.php.net/manual/en/function.setcookie.php + */ + private static string $reservedPrefixCharsList = "=,; \t\r\n\v\f\0"; /** * @see https://www.php.net/manual/en/function.setrawcookie.php @@ -232,11 +239,11 @@ public static function fromHeaderString(string $cookie, bool $raw = false) * @param string $name The cookie's name * @param string $value The cookie's value * @param array{ - * prefix?: string, + * prefix?: string|null, * max-age?: int|numeric-string, * expires?: DateTimeInterface|int|string, - * path?: string, - * domain?: string, + * path?: string|null, + * domain?: string|null, * secure?: bool, * httponly?: bool, * samesite?: string, @@ -258,9 +265,9 @@ final public function __construct(string $name, string $value = '', array $optio } // to preserve backward compatibility with array-based cookies in previous CI versions - $prefix = ($options['prefix'] === '') ? self::$defaults['prefix'] : $options['prefix']; - $path = ($options['path'] === '') ? self::$defaults['path'] : $options['path']; - $domain = ($options['domain'] === '') ? self::$defaults['domain'] : $options['domain']; + $prefix = in_array($options['prefix'], [null, ''], true) ? self::$defaults['prefix'] : $options['prefix']; + $path = in_array($options['path'], [null, '', '0'], true) ? self::$defaults['path'] : $options['path']; + $domain = in_array($options['domain'], [null, ''], true) ? self::$defaults['domain'] : $options['domain']; // empty string SameSite should use the default for browsers $samesite = ($options['samesite'] === '') ? self::$defaults['samesite'] : $options['samesite']; @@ -271,6 +278,8 @@ final public function __construct(string $name, string $value = '', array $optio $this->validateName($name, $raw); $this->validateValue($value, $raw); + $this->validatePath($path); + $this->validateDomain($domain); $this->validatePrefix($prefix, $secure, $path, $domain); $this->validateSameSite($samesite, $secure); @@ -515,6 +524,7 @@ public function withExpired() public function withPath(?string $path) { $path = in_array($path, [null, '', '0'], true) ? self::$defaults['path'] : $path; + $this->validatePath($path); $this->validatePrefix($this->prefix, $this->secure, $path, $this->domain); $cookie = clone $this; @@ -530,6 +540,7 @@ public function withPath(?string $path) public function withDomain(?string $domain) { $domain ??= self::$defaults['domain']; + $this->validateDomain($domain); $this->validatePrefix($this->prefix, $this->secure, $this->path, $domain); $cookie = clone $this; @@ -791,12 +802,41 @@ protected function validateValue(string $value, bool $raw): void } /** - * Validates the special prefixes if some attribute requirements are met. + * Validates the cookie path per PHP setcookie() constraints. + * + * @throws CookieException + */ + protected function validatePath(string $path): void + { + if (strpbrk($path, self::$reservedValueCharsList) !== false) { + throw CookieException::forInvalidCookiePath(); + } + } + + /** + * Validates the cookie domain per PHP setcookie() constraints. + * + * @throws CookieException + */ + protected function validateDomain(string $domain): void + { + if ($domain !== '' && strpbrk($domain, self::$reservedValueCharsList) !== false) { + throw CookieException::forInvalidCookieDomain(); + } + } + + /** + * Validates the special prefixes if some attribute requirements are met, + * and ensures the prefix contains no PHP-prohibited characters. * * @throws CookieException */ protected function validatePrefix(string $prefix, bool $secure, string $path, string $domain): void { + if (strpbrk($prefix, self::$reservedPrefixCharsList) !== false) { + throw CookieException::forInvalidCookieName($prefix); + } + if (str_starts_with($prefix, '__Secure-') && ! $secure) { throw CookieException::forInvalidSecurePrefix(); } diff --git a/system/Cookie/Exceptions/CookieException.php b/system/Cookie/Exceptions/CookieException.php index 3cc7e32e6eff..b05aa579e61b 100644 --- a/system/Cookie/Exceptions/CookieException.php +++ b/system/Cookie/Exceptions/CookieException.php @@ -70,6 +70,26 @@ public static function forInvalidCookieValue() return new static(lang('Cookie.invalidCookieValue')); } + /** + * Thrown when the cookie path contains invalid characters. + * + * @return static + */ + public static function forInvalidCookiePath() + { + return new static(lang('Cookie.invalidCookiePath')); + } + + /** + * Thrown when the cookie domain contains invalid characters. + * + * @return static + */ + public static function forInvalidCookieDomain() + { + return new static(lang('Cookie.invalidCookieDomain')); + } + /** * Thrown when using the `__Secure-` prefix but the `Secure` attribute * is not set to true. diff --git a/system/Language/en/Cookie.php b/system/Language/en/Cookie.php index 4f4294ff0fc5..7fde16cd8434 100644 --- a/system/Language/en/Cookie.php +++ b/system/Language/en/Cookie.php @@ -17,6 +17,8 @@ 'invalidExpiresValue' => 'The cookie expiration time is not valid.', 'invalidCookieName' => 'The cookie name "{0}" contains invalid characters.', 'invalidCookieValue' => 'The cookie value contains invalid characters.', + 'invalidCookiePath' => 'The cookie path contains invalid characters.', + 'invalidCookieDomain' => 'The cookie domain contains invalid characters.', 'emptyCookieName' => 'The cookie name cannot be empty.', 'invalidSecurePrefix' => 'Using the "__Secure-" prefix requires setting the "Secure" attribute.', 'invalidHostPrefix' => 'Using the "__Host-" prefix must be set with the "Secure" flag, must not have a "Domain" attribute, and the "Path" is set to "/".', diff --git a/tests/system/Cookie/CookieTest.php b/tests/system/Cookie/CookieTest.php index 07991be006f6..694c9cde6392 100644 --- a/tests/system/Cookie/CookieTest.php +++ b/tests/system/Cookie/CookieTest.php @@ -443,4 +443,239 @@ public function testNonRawCookieSafelyEncodesCRLF(): void $this->assertStringNotContainsString("\r", $result); $this->assertStringNotContainsString("\n", $result); } + + #[DataProvider('provideValidationOfCookiePath')] + public function testValidationOfCookiePath(string $path): void + { + $this->expectException(CookieException::class); + $this->expectExceptionMessage(lang('Cookie.invalidCookiePath')); + new Cookie('test', 'value', ['path' => $path]); + } + + #[DataProvider('provideValidationOfCookiePath')] + public function testValidationOfCookiePathInWithPath(string $path): void + { + $this->expectException(CookieException::class); + $this->expectExceptionMessage(lang('Cookie.invalidCookiePath')); + $cookie = new Cookie('test', 'value'); + $cookie->withPath($path); + } + + /** + * @return iterable + */ + public static function provideValidationOfCookiePath(): iterable + { + yield 'comma' => ['/path,comma']; + + yield 'semicolon' => ['/path;semicolon']; + + yield 'space' => ['/path with space']; + + yield 'tab' => ["/path\twith_tab"]; + + yield 'carriage return' => ["/path\rcarriage"]; + + yield 'newline' => ["/path\nnewline"]; + + yield 'vertical tab' => ["/path\vvertical_tab"]; + + yield 'form feed' => ["/path\fform_feed"]; + + yield 'null byte' => ["/path\0null_byte"]; + + yield 'CRLF' => ["/path\r\nwith_crlf"]; + } + + #[DataProvider('provideFromHeaderStringValidationOfCookiePath')] + public function testFromHeaderStringValidationOfCookiePath(string $path): void + { + $this->expectException(CookieException::class); + $this->expectExceptionMessage(lang('Cookie.invalidCookiePath')); + Cookie::fromHeaderString("test=value; Path={$path}"); + } + + /** + * @return iterable + */ + public static function provideFromHeaderStringValidationOfCookiePath(): iterable + { + foreach (self::provideValidationOfCookiePath() as $name => $case) { + if ($name === 'semicolon') { + continue; + } + + yield $name => $case; + } + } + + #[DataProvider('provideValidationOfCookieDomain')] + public function testValidationOfCookieDomain(string $domain): void + { + $this->expectException(CookieException::class); + $this->expectExceptionMessage(lang('Cookie.invalidCookieDomain')); + new Cookie('test', 'value', ['domain' => $domain]); + } + + #[DataProvider('provideValidationOfCookieDomain')] + public function testValidationOfCookieDomainInWithDomain(string $domain): void + { + $this->expectException(CookieException::class); + $this->expectExceptionMessage(lang('Cookie.invalidCookieDomain')); + $cookie = new Cookie('test', 'value'); + $cookie->withDomain($domain); + } + + /** + * @return iterable + */ + public static function provideValidationOfCookieDomain(): iterable + { + yield 'comma' => ['domain,comma.com']; + + yield 'semicolon' => ['domain;semicolon.com']; + + yield 'space' => ['domain with space.com']; + + yield 'tab' => ["domain\twith_tab.com"]; + + yield 'carriage return' => ["domain\rcarriage.com"]; + + yield 'newline' => ["domain\nnewline.com"]; + + yield 'vertical tab' => ["domain\vvertical_tab.com"]; + + yield 'form feed' => ["domain\fform_feed.com"]; + + yield 'null byte' => ["domain\0null_byte.com"]; + + yield 'CRLF' => ["domain\r\nwith_crlf.com"]; + } + + #[DataProvider('provideFromHeaderStringValidationOfCookieDomain')] + public function testFromHeaderStringValidationOfCookieDomain(string $domain): void + { + $this->expectException(CookieException::class); + $this->expectExceptionMessage(lang('Cookie.invalidCookieDomain')); + Cookie::fromHeaderString("test=value; Domain={$domain}"); + } + + /** + * @return iterable + */ + public static function provideFromHeaderStringValidationOfCookieDomain(): iterable + { + foreach (self::provideValidationOfCookieDomain() as $name => $case) { + if ($name === 'semicolon') { + continue; + } + + yield $name => $case; + } + } + + public function testNullPathAndDomainDefaultProperly(): void + { + $cookie = new Cookie('test', 'val', ['path' => null, 'domain' => null, 'prefix' => null]); + + $this->assertSame('/', $cookie->getPath()); + $this->assertSame('', $cookie->getDomain()); + $this->assertSame('', $cookie->getPrefix()); + + $cookie2 = $cookie->withPath(null)->withDomain(null)->withPrefix(''); + $this->assertSame('/', $cookie2->getPath()); + $this->assertSame('', $cookie2->getDomain()); + $this->assertSame('', $cookie2->getPrefix()); + } + + public function testValidCookiePathAndDomain(): void + { + $cookie = new Cookie('test', 'val', ['path' => '/sub/dir/', 'domain' => 'example.com']); + $this->assertSame('/sub/dir/', $cookie->getPath()); + $this->assertSame('example.com', $cookie->getDomain()); + + $cookie2 = $cookie->withPath('/another/path')->withDomain('.example.com'); + $this->assertSame('/another/path', $cookie2->getPath()); + $this->assertSame('.example.com', $cookie2->getDomain()); + + $cookie3 = new Cookie('test', 'val', ['path' => '/', 'domain' => '']); + $this->assertSame('/', $cookie3->getPath()); + $this->assertSame('', $cookie3->getDomain()); + } + + #[DataProvider('provideValidationOfCookiePrefix')] + public function testValidationOfCookiePrefix(string $prefix): void + { + $this->expectException(CookieException::class); + $this->expectExceptionMessage(lang('Cookie.invalidCookieName', [$prefix])); + new Cookie('test', 'val', ['prefix' => $prefix]); + } + + #[DataProvider('provideValidationOfCookiePrefix')] + public function testValidationOfCookiePrefixInWithPrefix(string $prefix): void + { + $this->expectException(CookieException::class); + $this->expectExceptionMessage(lang('Cookie.invalidCookieName', [$prefix])); + $cookie = new Cookie('test', 'val'); + $cookie->withPrefix($prefix); + } + + #[DataProvider('provideValidationOfCookiePrefix')] + public function testValidationOfRawCookiePrefix(string $prefix): void + { + $this->expectException(CookieException::class); + $this->expectExceptionMessage(lang('Cookie.invalidCookieName', [$prefix])); + new Cookie('test', 'val', ['prefix' => $prefix, 'raw' => true]); + } + + #[DataProvider('provideValidationOfCookiePrefix')] + public function testValidationOfRawCookiePrefixInWithPrefix(string $prefix): void + { + $this->expectException(CookieException::class); + $this->expectExceptionMessage(lang('Cookie.invalidCookieName', [$prefix])); + $cookie = new Cookie('test', 'val', ['raw' => true]); + $cookie->withPrefix($prefix); + } + + /** + * @return iterable + */ + public static function provideValidationOfCookiePrefix(): iterable + { + yield 'equals' => ['prefix=']; + + yield 'comma' => ['prefix,']; + + yield 'semicolon' => ['prefix;']; + + yield 'space' => ['prefix ']; + + yield 'tab' => ["prefix\t"]; + + yield 'carriage return' => ["prefix\r"]; + + yield 'newline' => ["prefix\n"]; + + yield 'vertical tab' => ["prefix\v"]; + + yield 'form feed' => ["prefix\f"]; + + yield 'null byte' => ["prefix\0"]; + + yield 'CRLF' => ["prefix\r\n"]; + } + + public function testValidCookiePrefixAllowedSeparators(): void + { + $cookie = new Cookie('test', 'val', ['prefix' => 'ci:session/']); + $this->assertSame('ci:session/', $cookie->getPrefix()); + $this->assertSame('ci:session/test', $cookie->getPrefixedName()); + + $cookie2 = $cookie->withPrefix('my-app:v1/'); + $this->assertSame('my-app:v1/', $cookie2->getPrefix()); + $this->assertSame('my-app:v1/test', $cookie2->getPrefixedName()); + + $cookie3 = new Cookie('test', 'val', ['prefix' => 'ci:session/', 'raw' => false]); + $this->assertSame('ci:session/test=val; Path=/; HttpOnly; SameSite=Lax', $cookie3->toHeaderString()); + } } diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index 6dee8ee9aa6d..e180de7d33f2 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -19,6 +19,8 @@ Message Changes *************** - Added the ``Cookie.invalidCookieValue`` language string. +- Added the ``Cookie.invalidCookiePath`` language string. +- Added the ``Cookie.invalidCookieDomain`` language string. ******* Changes @@ -43,6 +45,7 @@ Bugs Fixed - **CodeIgniter:** Fixed a bug where ``gatherOutput()`` could be called twice when ``startController()`` returned a ``ResponseInterface`` (e.g., from filter attributes or closure routes). - **Content Security Policy:** Fixed a bug where empty ``Content-Security-Policy``, ``Content-Security-Policy-Report-Only``, and ``Reporting-Endpoints`` response headers were generated when no corresponding values existed. - **Cookie:** Fixed a bug where ``Cookie`` instances created with ``raw: true`` allowed invalid characters in cookie values rejected by ``setrawcookie()``. +- **Cookie:** Fixed a bug where ``Cookie`` instances allowed invalid characters in path, domain, and prefix attributes rejected by ``setcookie()`` and ``setrawcookie()``. - **Database:** Fixed a bug where rebuilding a SQLite3 table (e.g., ``Forge::dropColumn()``, ``Forge::modifyColumn()``, ``Forge::dropForeignKey()`` and ``Forge::dropPrimaryKey()``) corrupted the table names referenced by its foreign keys when ``DBPrefix`` was set. - **Files:** Fixed a bug where ``File::move()`` and ``UploadedFile::move()`` set executable and overly permissive file permissions (``0777 & ~umask()`` instead of ``0666 & ~umask()``), and ``UploadedFile::move()`` targeted the parent directory instead of the destination file for ``chmod()``. - **Helpers:** Fixed a bug where ``get_dir_file_info()`` returned incomplete entries for subdirectories and missing files instead of omitting them. diff --git a/user_guide_src/source/libraries/cookies.rst b/user_guide_src/source/libraries/cookies.rst index 99b3103e45de..774240c40d8d 100644 --- a/user_guide_src/source/libraries/cookies.rst +++ b/user_guide_src/source/libraries/cookies.rst @@ -104,9 +104,27 @@ If setting the ``$raw`` parameter to ``true``, the cookie value will also be val It must not contain control characters, spaces, tabs, or separator characters (``, ;``) as `setrawcookie() `_ will reject them. +Validating the Path Attribute +============================= + +The cookie path must not contain control characters, spaces, tabs, or separator characters +(``, ;``) as `setcookie() `_ and +`setrawcookie() `_ will reject them. + +Validating the Domain Attribute +=============================== + +The cookie domain must not contain control characters, spaces, tabs, or separator characters +(``, ;``) as `setcookie() `_ and +`setrawcookie() `_ will reject them. + Validating the Prefix Attribute =============================== +The cookie prefix must not contain control characters, spaces, tabs, or separator characters +(``= , ; \t \r \n \v \f \0``) as `setcookie() `_ and +`setrawcookie() `_ will reject them. + When using the ``__Secure-`` prefix, cookies must be set with the ``$secure`` flag set to ``true``. If using the ``__Host-`` prefix, cookies must exhibit the following: From 538c762f6a4d030903fa3915941f107de3e53cc4 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Sat, 12 Sep 2026 20:23:23 +0700 Subject: [PATCH 116/163] chore: Backport structarmed.php from 4.8 to develop and use baseline config (#10544) * chore: cherry-pick structarmed.php from 4.8 to develop and use baseline config * clean copy * register baseline * register baseline * register structarmed-baseline.php to .gitattributes --- .gitattributes | 1 + structarmed-baseline.php | 20 ++++++++++++ structarmed.php | 69 ++++++++++++++++++++++++---------------- 3 files changed, 62 insertions(+), 28 deletions(-) create mode 100644 structarmed-baseline.php diff --git a/.gitattributes b/.gitattributes index d87cc8aa8cf2..02a23de3322d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -22,6 +22,7 @@ utils/ export-ignore .php-cs-fixer.no-header.php export-ignore .php-cs-fixer.tests.php export-ignore .php-cs-fixer.user-guide.php export-ignore +structarmed-baseline.php export-ignore structarmed.php export-ignore phpmetrics.json export-ignore phpstan-baseline.php export-ignore diff --git a/structarmed-baseline.php b/structarmed-baseline.php new file mode 100644 index 000000000000..eede90ce7048 --- /dev/null +++ b/structarmed-baseline.php @@ -0,0 +1,20 @@ + 'base_classes.must_be_abstract', + 'message' => 'Extended class [CodeIgniter\Config\BaseService] must be declared abstract or instantiated', + 'file' => 'system/Config/BaseService.php', + 'class' => 'CodeIgniter\Config\BaseService', + 'layer' => 'Source', + ], + [ + 'rule' => 'base_classes.must_be_abstract', + 'message' => 'Extended class [CodeIgniter\Debug\Toolbar\Collectors\BaseCollector] must be declared abstract or instantiated', + 'file' => 'system/Debug/Toolbar/Collectors/BaseCollector.php', + 'class' => 'CodeIgniter\Debug\Toolbar\Collectors\BaseCollector', + 'layer' => 'Source', + ], +]; diff --git a/structarmed.php b/structarmed.php index 88932d17cdd0..990f039601bc 100644 --- a/structarmed.php +++ b/structarmed.php @@ -10,35 +10,39 @@ * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ -use Boundwize\StructArmed\Rule\Rules\Function_\MustHaveReturnTypeFunctionRule; + +use Boundwize\StructArmed\Architecture; +use Boundwize\StructArmed\Preset\Preset; use Boundwize\StructArmed\Preset\Presets\CodeQualityPreset; +use Boundwize\StructArmed\Preset\Presets\Psr4Preset; +use Boundwize\StructArmed\Rule\Rules\Class_\ExtendedClassMustBeAbstractOrInstantiatedRule; +use Boundwize\StructArmed\Rule\Rules\Function_\MustHaveReturnTypeFunctionRule; use CodeIgniter\Cache\ResponseCache; +use CodeIgniter\Database\BaseResult; +use CodeIgniter\DataCaster\DataCaster; +use CodeIgniter\DataConverter\DataConverter; +use CodeIgniter\Entity\Cast\CastInterface; +use CodeIgniter\Entity\Cast\URICast; +use CodeIgniter\Entity\Entity; +use CodeIgniter\Entity\Exceptions\CastException; use CodeIgniter\HTTP\CLIRequest; +use CodeIgniter\HTTP\DownloadResponse; use CodeIgniter\HTTP\Header; use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\RedirectResponse; +use CodeIgniter\HTTP\Response; use CodeIgniter\HTTP\ResponseInterface; -use CodeIgniter\DataCaster\DataCaster; -use CodeIgniter\Entity\Cast\CastInterface; -use CodeIgniter\Entity\Exceptions\CastException; -use CodeIgniter\DataConverter\DataConverter; -use CodeIgniter\Entity\Entity; -use CodeIgniter\Entity\Cast\URICast; +use CodeIgniter\HTTP\ResponseTrait; +use CodeIgniter\HTTP\SSEResponse; +use CodeIgniter\HTTP\StreamResponse; use CodeIgniter\HTTP\URI; use CodeIgniter\Log\Handlers\ChromeLoggerHandler; -use CodeIgniter\Security\CheckPhpIni; -use CodeIgniter\View\Table; -use CodeIgniter\Database\BaseResult; -use CodeIgniter\View\Plugins; -use CodeIgniter\HTTP\ResponseTrait; use CodeIgniter\Pager\PagerInterface; -use CodeIgniter\HTTP\Response; -use CodeIgniter\HTTP\RedirectResponse; -use CodeIgniter\HTTP\DownloadResponse; +use CodeIgniter\Security\CheckPhpIni; use CodeIgniter\Validation\Validation; +use CodeIgniter\View\Plugins; use CodeIgniter\View\RendererInterface; -use Boundwize\StructArmed\Architecture; -use Boundwize\StructArmed\Preset\Preset; -use Boundwize\StructArmed\Preset\Presets\Psr4Preset; +use CodeIgniter\View\Table; return Architecture::define() ->skip([ @@ -50,12 +54,15 @@ __DIR__ . '/system/View/Cells/Cell.php', ], ]) + ->baseline(__DIR__ . '/structarmed-baseline.php') ->cacheDirectory(is_dir('/tmp') ? '/tmp/structarmed' : null) ->withPresets(Preset::PSR4(), Preset::CODEQUALITY()) ->layer('Helpers', __DIR__ . '/system/Helpers') ->rule('helpers.functions_must_have_return_type', new MustHaveReturnTypeFunctionRule('Helpers')) + ->rule('base_classes.must_be_abstract', new ExtendedClassMustBeAbstractOrInstantiatedRule('Source', '/^CodeIgniter\\\\.*Base.*$/')) + // Resolve CodeIgniter layers from class names because several layers share directories. ->layerPattern('API', '/^CodeIgniter\\\\API\\\\.*$/') ->layerPattern('Cache', '/^CodeIgniter\\\\Cache\\\\.*$/') @@ -72,11 +79,13 @@ ->layerPattern('Filters', '/^CodeIgniter\\\\Filters\\\\Filter.*$/') ->layerPattern('Format', '/^CodeIgniter\\\\Format\\\\.*$/') ->layerPattern('Honeypot', '/^CodeIgniter\\\\.*Honeypot.*$/') + ->layerPattern('Input', '/^CodeIgniter\\\\Input\\\\.*$/') ->layerPattern('URI', '/^CodeIgniter\\\\HTTP\\\\URI$/') ->layerPattern('HTTP', '/^CodeIgniter\\\\HTTP\\\\.*$/', '/(Exception|URI)/') ->layerPattern('I18n', '/^CodeIgniter\\\\I18n\\\\.*$/') ->layerPattern('Images', '/^CodeIgniter\\\\Images\\\\.*$/') ->layerPattern('Language', '/^CodeIgniter\\\\Language\\\\.*$/') + ->layerPattern('Lock', '/^CodeIgniter\\\\Lock\\\\.*$/') ->layerPattern('Log', '/^CodeIgniter\\\\Log\\\\.*$/') ->layerPattern('Model', '/^CodeIgniter\\\\.*Model$/') ->layerPattern('Modules', '/^CodeIgniter\\\\Modules\\\\.*$/') @@ -96,26 +105,28 @@ 'Controller' => ['HTTP', 'Validation'], 'Cookie' => ['I18n'], 'Database' => ['Entity', 'Events', 'I18n'], - 'DataCaster' => ['I18n', 'URI', 'Database'], + 'DataCaster' => ['I18n', 'URI', 'Database', 'Encryption'], 'DataConverter' => ['DataCaster'], 'Email' => ['I18n', 'Events'], 'Entity' => ['DataCaster', 'I18n'], 'Files' => ['I18n'], 'Filters' => ['HTTP'], 'Honeypot' => ['Filters', 'HTTP'], - 'HTTP' => ['Cookie', 'Files', 'I18n', 'Security', 'URI'], + 'HTTP' => ['Cookie', 'Files', 'I18n', 'Input', 'Security', 'URI'], + 'Input' => ['I18n'], 'Images' => ['Files', 'I18n'], + 'Lock' => ['Cache'], 'Model' => ['Database', 'DataCaster', 'DataConverter', 'Entity', 'I18n', 'Pager', 'Validation'], 'Pager' => ['URI', 'View'], 'Publisher' => ['Files', 'URI'], // +API = API + its allowed layers; +Controller = Controller + its allowed layers - 'RESTful' => ['+API', '+Controller'], - 'Router' => ['HTTP', 'I18n'], - 'Security' => ['Cookie', 'HTTP', 'I18n', 'Session'], - 'Session' => ['Cookie', 'Database', 'HTTP', 'I18n'], - 'Throttle' => ['Cache', 'I18n'], - 'Validation' => ['Database', 'HTTP', 'Helpers'], - 'View' => ['Cache'], + 'RESTful' => ['+API', '+Controller'], + 'Router' => ['HTTP', 'I18n'], + 'Security' => ['Cookie', 'HTTP', 'I18n', 'Session'], + 'Session' => ['Cookie', 'Database', 'HTTP', 'I18n'], + 'Throttle' => ['Cache', 'I18n'], + 'Validation' => ['Database', 'HTTP', 'Helpers', 'I18n', 'Input'], + 'View' => ['Cache'], ]) ->skipPathsForRuleset(['*test*']) // Skip violations for class-specific dependencies. @@ -129,7 +140,7 @@ CastInterface::class, CastException::class, ]) - ->skipClassViolation(\CodeIgniter\DataCaster\Exceptions\CastException::class, [ + ->skipClassViolation(CodeIgniter\DataCaster\Exceptions\CastException::class, [ CastException::class, ]) ->skipClassViolation(DataConverter::class, [ @@ -157,4 +168,6 @@ ->skipClassViolation(Response::class, [PagerInterface::class]) ->skipClassViolation(RedirectResponse::class, [PagerInterface::class]) ->skipClassViolation(DownloadResponse::class, [PagerInterface::class]) + ->skipClassViolation(SSEResponse::class, [PagerInterface::class]) + ->skipClassViolation(StreamResponse::class, [PagerInterface::class]) ->skipClassViolation(Validation::class, [RendererInterface::class]); From 43e3adffae9288c9491e133a8166eb349c63be70 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Sat, 12 Sep 2026 21:02:07 +0700 Subject: [PATCH 117/163] chore: Skip structarmed-baseline.php on rector config (#10548) --- rector.php | 1 + 1 file changed, 1 insertion(+) diff --git a/rector.php b/rector.php index e852257d8129..5b4c50db1c66 100644 --- a/rector.php +++ b/rector.php @@ -76,6 +76,7 @@ ]) // is there a file you need to skip? ->withSkip([ + __DIR__ . '/structarmed-baseline.php', __DIR__ . '/system/Debug/Toolbar/Views/toolbar.tpl.php', __DIR__ . '/system/ThirdParty', __DIR__ . '/tests/system/Config/fixtures', From 6ab6c650e09f784bbe52eff4f7ca777a7f87ee2d Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sun, 13 Sep 2026 17:08:31 +0800 Subject: [PATCH 118/163] chore: bump Carson to v1.4.2 and re-check PR titles on push (#10553) --- .github/carson.yml | 2 +- .github/workflows/run-carson.yml | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/carson.yml b/.github/carson.yml index 1c62b62dc0c2..8b7b3df521c2 100644 --- a/.github/carson.yml +++ b/.github/carson.yml @@ -5,7 +5,7 @@ subscribers: - pr-title-linter settings: pr-title-linter: - name: 'Carson / pr-title' + name: 'Carson / pr-title-linter' rules: - pattern: '^(\[\d+\.\d+\]\s{1})?(feat|fix|chore|docs|perf|refactor|style|test|config|revert)(\([\-.@:`a-zA-Z0-9]+\))?!?:\s{1}\S.+\S|Prep for \d\.\d\.\d release|\d\.\d\.\d (Ready|Merge) code$' description: 'PR title must include the type (feat, fix, chore, docs, perf, refactor, style, test, config, revert) of the commit per Conventional Commits specification. See https://www.conventionalcommits.org/en/v1.0.0/ for the discussion.' diff --git a/.github/workflows/run-carson.yml b/.github/workflows/run-carson.yml index cc673215deb6..44a30afa7c99 100644 --- a/.github/workflows/run-carson.yml +++ b/.github/workflows/run-carson.yml @@ -5,6 +5,8 @@ on: types: - opened - edited + - synchronize + - reopened permissions: {} @@ -13,7 +15,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Run Carson - uses: NexusPHP/carson@62602be15470a18edd31f217b785863e997572d5 # v1.4.0 + uses: NexusPHP/carson@3973bbb86351a78861b75bbf504f53a83aede4dc # v1.4.2 with: app_id: ${{ secrets.CARSON_APP_ID }} private_key: ${{ secrets.CARSON_PRIVATE_KEY }} From 959cc32fe8db432dcfa1a83221c69f0ba9a2d529 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sun, 13 Sep 2026 17:20:00 +0800 Subject: [PATCH 119/163] chore: replace labeler workflows with Carson's `auto-labeler` and `conflicts-notifier` (#10547) --- .github/carson.yml | 24 +++++++++ .github/labeler.yml | 23 --------- .../workflows/label-add-conflict-all-pr.yml | 51 ------------------- .github/workflows/label-pr.yml | 47 ----------------- .github/workflows/run-carson.yml | 10 ++++ 5 files changed, 34 insertions(+), 121 deletions(-) delete mode 100644 .github/labeler.yml delete mode 100644 .github/workflows/label-add-conflict-all-pr.yml delete mode 100644 .github/workflows/label-pr.yml diff --git a/.github/carson.yml b/.github/carson.yml index 8b7b3df521c2..169ec94a8477 100644 --- a/.github/carson.yml +++ b/.github/carson.yml @@ -2,8 +2,32 @@ version: 1 subscribers: + - auto-labeler + - conflicts-notifier - pr-title-linter settings: + auto-labeler: + sync_labels: true + rules: + - label: '4.8' # @todo change value whenever the next minor version is changed + base_branch: ['^4\.8$'] + - label: github_actions + files: ['.github/workflows/*'] + - label: documentation + files: + all: ['user_guide_src/source/**'] + - label: testing + files: + all: ['tests/**'] + conflicts-notifier: + label: stale + message: |- + :wave: Hi, @{{user}}! + + We detected conflicts in your PR against the base branch :speak_no_evil: + You may want to sync :arrows_counterclockwise: your branch with upstream! + + Ref: [Syncing Your Branch](https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/workflow.md#updating-your-branch) pr-title-linter: name: 'Carson / pr-title-linter' rules: diff --git a/.github/labeler.yml b/.github/labeler.yml deleted file mode 100644 index 69cb0c51e123..000000000000 --- a/.github/labeler.yml +++ /dev/null @@ -1,23 +0,0 @@ -# https://github.com/actions/labeler?tab=readme-ov-file#usage - -# Add the `4.8` label to PRs that target the `4.8` branch. -'4.8': # @todo change value whenever the next minor version is changed -- base-branch: '4.8' - -# Add the `github_actions` label to PRs that change any file in the `.github/workflows/` directory. -'github_actions': -- changed-files: - - any-glob-to-any-file: - - '.github/workflows/*' - -# Add the `documentation` label to PRs for documentation only. -'documentation': -- changed-files: - - any-glob-to-all-files: - - 'user_guide_src/source/**' - -# Add the `testing` label to PRs that changes tests only. -'testing': -- changed-files: - - any-glob-to-all-files: - - 'tests/**' diff --git a/.github/workflows/label-add-conflict-all-pr.yml b/.github/workflows/label-add-conflict-all-pr.yml deleted file mode 100644 index f9adde8207de..000000000000 --- a/.github/workflows/label-add-conflict-all-pr.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Auto Label "stale" for All PRs - -on: - push: - branches: - - develop - - '4.*' - -jobs: - build: - name: Check Conflicts - - permissions: - contents: read - pull-requests: write - - runs-on: ubuntu-24.04 - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Get PR List - id: PR-list - run: echo "pr_list=$(gh pr list -L 100 --json mergeable,url,labels,author)" >> $GITHUB_OUTPUT - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: 'Add label "stale" and comment' - env: - PR_LIST: ${{ steps.PR-list.outputs.pr_list }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - - IFS=$'\n' # Set Internal Field Separator to newline to handle array elements - - # Iterate through the PRs in PR_LIST - for pr in $(echo "$PR_LIST" | jq -c '.[]'); do - mergeable=$(echo "$pr" | jq -r '.mergeable') - author=$(echo "$pr" | jq -r '.author.login') - labels=$(echo "$pr" | jq -c '.labels[].name' | tr -d '[]"') - url=$(echo "$pr" | jq -r '.url') - - # CONFLICTING and no 'stale' label - if [ "$mergeable" == "CONFLICTING" ] && [[ ! "$labels" == *"stale"* ]]; then - # Add "stale" label - gh pr edit $url --add-label "stale" - - # Add a comment - gh pr comment $url --body ":wave: Hi, @$author!

We detected conflicts in your PR against the base branch :speak_no_evil:
You may want to sync :arrows_counterclockwise: your branch with upstream!

Ref: [Syncing Your Branch](https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/workflow.md#updating-your-branch)" - fi - done diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml deleted file mode 100644 index bb84e2ad3504..000000000000 --- a/.github/workflows/label-pr.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Add Labels to PRs - -# NOTE: When updating this workflow, you should first change the event to `pull_request` to test the changes -# in a PR, and then change it back to `pull_request_target` before merging. -# @see https://github.com/actions/labeler?tab=readme-ov-file#updating-major-version-of-the-labeler -on: - - pull_request_target - -jobs: - add-labels: - permissions: - contents: read - pull-requests: write - runs-on: ubuntu-24.04 - - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Verify PR source for workflow file changes - 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@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0 - with: - sync-labels: true # Remove labels when matching files are reverted diff --git a/.github/workflows/run-carson.yml b/.github/workflows/run-carson.yml index 44a30afa7c99..3a5a3e845d29 100644 --- a/.github/workflows/run-carson.yml +++ b/.github/workflows/run-carson.yml @@ -7,6 +7,16 @@ on: - edited - synchronize - reopened + - labeled + issues: + types: + - opened + - edited + - labeled + push: + branches: + - develop + - '4.*' permissions: {} From 64375f1b7eb86641b164caba7bc14536293fe0ee Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sun, 13 Sep 2026 17:24:17 +0800 Subject: [PATCH 120/163] docs: correct what `make:migration --session` reads from `Config\Session` (#10549) --- user_guide_src/source/libraries/sessions.rst | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/user_guide_src/source/libraries/sessions.rst b/user_guide_src/source/libraries/sessions.rst index 45b3c56e112c..7aba78d269d3 100644 --- a/user_guide_src/source/libraries/sessions.rst +++ b/user_guide_src/source/libraries/sessions.rst @@ -653,8 +653,16 @@ from the cli to generate a migration file for you: php spark make:migration --session php spark migrate -This command will take the ``$savePath`` and ``$matchIP`` settings into account -when it generates the code. +This command reads the ``$matchIP`` setting to decide the primary key. The table +name and database group are not read from **app/Config/Session.php**, so pass +``--table`` and ``--dbgroup`` when they differ from ``ci_sessions`` and ``default``: + +.. code-block:: console + + php spark make:migration --session --table my_sessions --dbgroup sessions + +The ``--table`` value should match ``$savePath``, and the ``--dbgroup`` value +should match ``$DBGroup``. .. _sessions-redishandler-driver: From 55c9ec08a3d69555f9ab84c83927a263d97762bc Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sun, 13 Sep 2026 21:05:38 +0800 Subject: [PATCH 121/163] chore: replace signed-commit check with Carson's `signed-commits` (#10556) --- .github/carson.yml | 4 ++++ .github/workflows/label-signing.yml | 32 ----------------------------- 2 files changed, 4 insertions(+), 32 deletions(-) delete mode 100644 .github/workflows/label-signing.yml diff --git a/.github/carson.yml b/.github/carson.yml index 169ec94a8477..b423b95b0515 100644 --- a/.github/carson.yml +++ b/.github/carson.yml @@ -5,6 +5,7 @@ subscribers: - auto-labeler - conflicts-notifier - pr-title-linter + - signed-commits settings: auto-labeler: sync_labels: true @@ -34,3 +35,6 @@ settings: - pattern: '^(\[\d+\.\d+\]\s{1})?(feat|fix|chore|docs|perf|refactor|style|test|config|revert)(\([\-.@:`a-zA-Z0-9]+\))?!?:\s{1}\S.+\S|Prep for \d\.\d\.\d release|\d\.\d\.\d (Ready|Merge) code$' description: 'PR title must include the type (feat, fix, chore, docs, perf, refactor, style, test, config, revert) of the commit per Conventional Commits specification. See https://www.conventionalcommits.org/en/v1.0.0/ for the discussion.' level: error + signed-commits: + name: 'Carson / signed-commits' + treat_unsigned_as: failure diff --git a/.github/workflows/label-signing.yml b/.github/workflows/label-signing.yml deleted file mode 100644 index 609e84a0a23e..000000000000 --- a/.github/workflows/label-signing.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Check Signed PR -on: - pull_request: - branches: - - 'develop' - - '4.*' - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -permissions: - contents: read - pull-requests: write - -jobs: - build: - name: Check Signed Commit - runs-on: ubuntu-24.04 - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Check signed commits in PR - uses: 1Password/check-signed-commits-action@ed2885f3ed2577a4f5d3c3fe895432a557d23d52 # v1.2.0 - with: - comment: | - You must GPG-sign your work, certifying that you either wrote the work or otherwise have the right to pass it on to an open-source project. See Developer's Certificate of Origin. See [signing][1]. - - **Note that all your commits must be signed.** If you have an unsigned commit, you can sign the previous commits by referring to [gpg-signing-old-commits][2]. - [1]: https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/pull_request.md#signing - [2]: https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/workflow.md#gpg-signing-old-commits From de66f3f9db122f405dac67f12da778ea2c5485d1 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sun, 13 Sep 2026 21:06:36 +0800 Subject: [PATCH 122/163] chore: label PRs by title type with Carson's `auto-labeler` (#10557) --- .github/carson.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/carson.yml b/.github/carson.yml index b423b95b0515..7a4dd7fa2689 100644 --- a/.github/carson.yml +++ b/.github/carson.yml @@ -20,6 +20,12 @@ settings: - label: testing files: all: ['tests/**'] + - label: bug + title: ['^fix(\([^)]*\))?: '] + - label: enhancement + title: ['^perf(\([^)]*\))?: '] + - label: refactor + title: ['^refactor(\([^)]*\))?: '] conflicts-notifier: label: stale message: |- From 48bdc648795654fbe97e401314d202a46dca4be5 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Mon, 14 Sep 2026 12:57:10 +0800 Subject: [PATCH 123/163] refactor: fix return type covariance of `FileCollection::getIterator()` (#10558) --- system/Files/FileCollection.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/system/Files/FileCollection.php b/system/Files/FileCollection.php index b293b4d3c08d..b69cfb1d266b 100644 --- a/system/Files/FileCollection.php +++ b/system/Files/FileCollection.php @@ -17,8 +17,8 @@ use CodeIgniter\Files\Exceptions\FileException; use CodeIgniter\Files\Exceptions\FileNotFoundException; use Countable; -use Generator; use IteratorAggregate; +use Traversable; /** * File Collection Class @@ -393,13 +393,12 @@ public function count(): int /** * Yields as an Iterator for the current files. - * Fulfills IteratorAggregate. * - * @return Generator + * @return Traversable * * @throws FileNotFoundException */ - public function getIterator(): Generator + public function getIterator(): Traversable { foreach ($this->get() as $file) { yield new File($file, true); From 01fd6ee4735772e6ac55647505634a473360afd2 Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Mon, 14 Sep 2026 06:59:17 +0200 Subject: [PATCH 124/163] fix(I18n): compute correct date in `Time::today()`, `yesterday()`, and `tomorrow()` across timezones (#10532) * fix(I18n): fix timezone and testNow calculation in Time::today(), yesterday(), and tomorrow() - Compute start of day relative to the requested timezone instead of the server default timezone - Respect Time::setTestNow() mock instance in today(), yesterday(), and tomorrow() - Add unit tests covering date boundary transitions across different timezones - Add changelog entry in v4.7.5.rst * Update user_guide_src/source/changelogs/v4.7.5.rst Co-authored-by: neznaika0 * Fix multiple bugs in Files, Helpers, Honeypot, I18n, Logger, and Cache Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix(I18n): address review suggestions for Time::today/yesterday/tomorrow Use new static(null) instead of static::now() to avoid coupling overridable extension points, and freeze time in testTodayLocalized to prevent intermittent failure across timezones. * Update system/I18n/TimeTrait.php Co-authored-by: Michal Sniatala * Update tests/system/I18n/TimeTest.php Co-authored-by: Michal Sniatala * Update system/I18n/TimeTrait.php Co-authored-by: Michal Sniatala * Update tests/system/I18n/TimeTest.php Co-authored-by: Michal Sniatala * style: correct indentation in TimeTest --------- Co-authored-by: neznaika0 Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Michal Sniatala --- system/I18n/TimeTrait.php | 6 +- tests/system/I18n/TimeLegacyTest.php | 7 ++- tests/system/I18n/TimeTest.php | 66 ++++++++++++++++++++- user_guide_src/source/changelogs/v4.7.5.rst | 1 + 4 files changed, 75 insertions(+), 5 deletions(-) diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index 0660724f5019..16454c5f57b4 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -144,7 +144,7 @@ public static function parse(string $datetime, $timezone = null, ?string $locale */ public static function today($timezone = null, ?string $locale = null) { - return new static(date('Y-m-d 00:00:00'), $timezone, $locale); + return (new static(null, $timezone, $locale))->setTime(0, 0, 0, 0); } /** @@ -158,7 +158,7 @@ public static function today($timezone = null, ?string $locale = null) */ public static function yesterday($timezone = null, ?string $locale = null) { - return new static(date('Y-m-d 00:00:00', strtotime('-1 day')), $timezone, $locale); + return (new static(null, $timezone, $locale))->modify('yesterday'); } /** @@ -172,7 +172,7 @@ public static function yesterday($timezone = null, ?string $locale = null) */ public static function tomorrow($timezone = null, ?string $locale = null) { - return new static(date('Y-m-d 00:00:00', strtotime('+1 day')), $timezone, $locale); + return (new static(null, $timezone, $locale))->modify('tomorrow'); } /** diff --git a/tests/system/I18n/TimeLegacyTest.php b/tests/system/I18n/TimeLegacyTest.php index 6cdcdb45ff74..ec6cdb042640 100644 --- a/tests/system/I18n/TimeLegacyTest.php +++ b/tests/system/I18n/TimeLegacyTest.php @@ -170,9 +170,14 @@ public function testToday(): void public function testTodayLocalized(): void { + // Freeze time to avoid flakiness when server and London are on different calendar days + TimeLegacy::setTestNow('2026-06-15 12:00:00', 'UTC'); + $time = TimeLegacy::today('Europe/London'); - $this->assertSame(date('Y-m-d 00:00:00'), $time->toDateTimeString()); + $this->assertSame('2026-06-15 00:00:00', $time->toDateTimeString()); + + TimeLegacy::setTestNow(); } public function testYesterday(): void diff --git a/tests/system/I18n/TimeTest.php b/tests/system/I18n/TimeTest.php index ef6712f37259..3368e5802d54 100644 --- a/tests/system/I18n/TimeTest.php +++ b/tests/system/I18n/TimeTest.php @@ -171,9 +171,14 @@ public function testToday(): void public function testTodayLocalized(): void { + // Freeze time to avoid flakiness when server and London are on different calendar days + Time::setTestNow('2026-06-15 12:00:00', 'UTC'); + $time = Time::today('Europe/London'); - $this->assertSame(date('Y-m-d 00:00:00'), $time->toDateTimeString()); + $this->assertSame('2026-06-15 00:00:00', $time->toDateTimeString()); + + Time::setTestNow(); } public function testYesterday(): void @@ -190,6 +195,65 @@ public function testTomorrow(): void $this->assertSame(date('Y-m-d 00:00:00', strtotime('+1 day')), $time->toDateTimeString()); } + public function testTodayWithTimezoneAcrossDateBoundary(): void + { + // When UTC is 2026-09-07 23:30:00, in Asia/Tokyo (+09:00) it is already 2026-09-08 08:30:00 + Time::setTestNow('2026-09-07 23:30:00', 'UTC'); + + $tokyoToday = Time::today('Asia/Tokyo'); + $this->assertSame('2026-09-08 00:00:00', $tokyoToday->toDateTimeString()); + + // When UTC is 2026-09-08 02:00:00, in America/New_York (-04:00 EDT) it is still 2026-09-07 22:00:00 + Time::setTestNow('2026-09-08 02:00:00', 'UTC'); + + $nyToday = Time::today('America/New_York'); + $this->assertSame('2026-09-07 00:00:00', $nyToday->toDateTimeString()); + + Time::setTestNow(); + } + + public function testYesterdayWithTimezoneAcrossDateBoundary(): void + { + // When UTC is 2026-09-07 23:30:00, in Tokyo it is 2026-09-08, so Tokyo yesterday is 2026-09-07 + Time::setTestNow('2026-09-07 23:30:00', 'UTC'); + + $tokyoYesterday = Time::yesterday('Asia/Tokyo'); + $this->assertSame('2026-09-07 00:00:00', $tokyoYesterday->toDateTimeString()); + + // When UTC is 2026-09-08 02:00:00, in NY it is 2026-09-07, so NY yesterday is 2026-09-06 + Time::setTestNow('2026-09-08 02:00:00', 'UTC'); + + $nyYesterday = Time::yesterday('America/New_York'); + $this->assertSame('2026-09-06 00:00:00', $nyYesterday->toDateTimeString()); + // March 28 at 23:30 does not exist in Nuuk due to the DST transition. + Time::setTestNow('2026-03-29 23:30:00', 'America/Nuuk'); + + $nuukYesterday = Time::yesterday('America/Nuuk'); + $this->assertSame('2026-03-28 00:00:00', $nuukYesterday->toDateTimeString()); + Time::setTestNow(); + } + + public function testTomorrowWithTimezoneAcrossDateBoundary(): void + { + // When UTC is 2026-09-07 23:30:00, in Tokyo it is 2026-09-08, so Tokyo tomorrow is 2026-09-09 + Time::setTestNow('2026-09-07 23:30:00', 'UTC'); + + $tokyoTomorrow = Time::tomorrow('Asia/Tokyo'); + $this->assertSame('2026-09-09 00:00:00', $tokyoTomorrow->toDateTimeString()); + + // When UTC is 2026-09-08 02:00:00, in NY it is 2026-09-07, so NY tomorrow is 2026-09-08 + Time::setTestNow('2026-09-08 02:00:00', 'UTC'); + + $nyTomorrow = Time::tomorrow('America/New_York'); + $this->assertSame('2026-09-08 00:00:00', $nyTomorrow->toDateTimeString()); + // Moving forward must select March 28 without normalizing into March 29. + Time::setTestNow('2026-03-27 23:30:00', 'America/Nuuk'); + + $nuukTomorrow = Time::tomorrow('America/Nuuk'); + $this->assertSame('2026-03-28 00:00:00', $nuukTomorrow->toDateTimeString()); + Time::setTestNow(); + } + public function testCreateFromDate(): void { $time = Time::createFromDate(2017, 03, 05, 'America/Chicago'); diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index e180de7d33f2..847a8d751a81 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -50,6 +50,7 @@ Bugs Fixed - **Files:** Fixed a bug where ``File::move()`` and ``UploadedFile::move()`` set executable and overly permissive file permissions (``0777 & ~umask()`` instead of ``0666 & ~umask()``), and ``UploadedFile::move()`` targeted the parent directory instead of the destination file for ``chmod()``. - **Helpers:** Fixed a bug where ``get_dir_file_info()`` returned incomplete entries for subdirectories and missing files instead of omitting them. - **Honeypot:** Fixed a bug where bot detection returned an HTTP 500 response instead of 403 (Forbidden). +- **I18n:** Fixed a bug where ``Time::today()``, ``Time::yesterday()``, and ``Time::tomorrow()`` ignored the specified ``$timezone`` and ``setTestNow()`` when calculating the day. - **Logger:** Fixed a bug where interpolating a log message with array or non-stringable context values could raise PHP warnings or errors. - **Cache:** Fixed ``MemcachedHandler::decrement()`` initializing a non-existent counter to the positive offset. Missing counters are now initialized to ``0``, reflecting Memcached's unsigned, saturating counter semantics. From 39a0d2fc714901c7ab5fb79a82ead56a181fd1d9 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Tue, 15 Sep 2026 14:49:09 +0800 Subject: [PATCH 125/163] fix: return an empty string when `readline()` reaches end-of-file (#10542) --- system/CLI/InputOutput.php | 10 +++++----- user_guide_src/source/changelogs/v4.7.5.rst | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/system/CLI/InputOutput.php b/system/CLI/InputOutput.php index 782e32d0371d..893b98a24595 100644 --- a/system/CLI/InputOutput.php +++ b/system/CLI/InputOutput.php @@ -46,14 +46,14 @@ public function input(?string $prefix = null): string // @codeCoverageIgnoreStart $prompt = $this->readlinePrompt($prefix, readline_info('library_version')); - if ($prompt !== null) { - return readline($prompt); + if ($prompt === null) { + // The library cannot render the prompt, so write it ourselves and let readline() only read the line. + self::fwrite(STDOUT, $prefix ?? ''); } - // The library cannot render the prompt, so write it ourselves and let readline() only read the line. - self::fwrite(STDOUT, $prefix ?? ''); + $input = readline($prompt); - return readline(); + return $input === false ? '' : $input; // @codeCoverageIgnoreEnd } diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index 847a8d751a81..dc1739a0041b 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -41,6 +41,7 @@ Bugs Fixed - **CLI:** Fixed a bug where pressing backspace in a ``CLI::prompt()`` erased the prompt text when the ``readline`` extension is enabled. The prompt is now passed to ``readline()`` so line redraws repaint it. ANSI color codes in the prompt (e.g., option defaults) are wrapped in readline's non-printing markers under GNU readline so cursor positioning stays accurate. On Windows, where the ``readline`` extension is built on WinEditLine, the prompt is written to STDOUT first because WinEditLine reports no library version and prints ANSI sequences literally. +- **CLI:** Fixed a bug where ``CLI::input()`` and ``CLI::prompt()`` threw a ``TypeError`` when STDIN reached end-of-file (e.g., Ctrl+D) with the ``readline`` extension enabled. An empty string is now returned, matching the behavior without ``readline``. - **CLIRequest:** Fixed a bug where ``parseCommand()`` could throw a TypeError when ``argv`` is missing. - **CodeIgniter:** Fixed a bug where ``gatherOutput()`` could be called twice when ``startController()`` returned a ``ResponseInterface`` (e.g., from filter attributes or closure routes). - **Content Security Policy:** Fixed a bug where empty ``Content-Security-Policy``, ``Content-Security-Policy-Report-Only``, and ``Reporting-Endpoints`` response headers were generated when no corresponding values existed. From 896c7b66dc179c734c10953648205eb223e31366 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Tue, 15 Sep 2026 14:50:01 +0800 Subject: [PATCH 126/163] fix: reject unsupported database drivers in `make:migration --session` (#10546) --- .../Generators/MigrationGenerator.php | 18 +++++++++++++ system/Language/en/CLI.php | 24 +++++++++-------- .../Generators/MigrationGeneratorTest.php | 27 +++++++++++++++++++ user_guide_src/source/changelogs/v4.7.5.rst | 2 ++ 4 files changed, 60 insertions(+), 11 deletions(-) diff --git a/system/Commands/Generators/MigrationGenerator.php b/system/Commands/Generators/MigrationGenerator.php index b5e64ec1bce1..5967c12c30af 100644 --- a/system/Commands/Generators/MigrationGenerator.php +++ b/system/Commands/Generators/MigrationGenerator.php @@ -89,10 +89,28 @@ public function run(array $params) if (array_key_exists('session', $params) || CLI::getOption('session')) { $table = $params['table'] ?? CLI::getOption('table') ?? 'ci_sessions'; $params[0] = "_create_{$table}_table"; + + $group = $params['dbgroup'] ?? CLI::getOption('dbgroup'); + $group = is_string($group) ? $group : 'default'; + $driver = config(Database::class)->{$group}['DBDriver'] ?? null; + + if ($driver === null) { + CLI::error(lang('CLI.generator.undefinedDatabaseGroup', [$group])); + + return EXIT_ERROR; + } + + if ($driver !== 'MySQLi' && $driver !== 'Postgre') { + CLI::error(lang('CLI.generator.unsupportedSessionDriver', [$group, $driver])); + + return EXIT_ERROR; + } } $this->classNameLang = 'CLI.generator.className.migration'; $this->generateClass($params); + + return EXIT_SUCCESS; } /** diff --git a/system/Language/en/CLI.php b/system/Language/en/CLI.php index 01e60c402955..822186dba926 100644 --- a/system/Language/en/CLI.php +++ b/system/Language/en/CLI.php @@ -33,17 +33,19 @@ 'transformer' => 'Transformer class name', 'validation' => 'Validation class name', ], - 'commandType' => 'Command type', - 'databaseGroup' => 'Database group', - 'fileCreate' => 'File created: {0}', - 'fileError' => 'Error while creating file: "{0}"', - 'fileExist' => 'File exists: "{0}"', - 'fileOverwrite' => 'File overwritten: "{0}"', - 'parentClass' => 'Parent class', - 'returnType' => 'Return type', - 'tableName' => 'Table name', - 'usingCINamespace' => 'Warning: Using the "CodeIgniter" namespace will generate the file in the system directory.', - 'viewName' => [ + 'commandType' => 'Command type', + 'databaseGroup' => 'Database group', + 'fileCreate' => 'File created: {0}', + 'fileError' => 'Error while creating file: "{0}"', + 'fileExist' => 'File exists: "{0}"', + 'fileOverwrite' => 'File overwritten: "{0}"', + 'parentClass' => 'Parent class', + 'returnType' => 'Return type', + 'tableName' => 'Table name', + 'undefinedDatabaseGroup' => 'The "{0}" database group is not defined.', + 'unsupportedSessionDriver' => 'Database sessions are only supported on MySQLi and Postgre. The "{0}" database group uses the "{1}" driver.', + 'usingCINamespace' => 'Warning: Using the "CodeIgniter" namespace will generate the file in the system directory.', + 'viewName' => [ 'cell' => 'Cell view name', ], ], diff --git a/tests/system/Commands/Generators/MigrationGeneratorTest.php b/tests/system/Commands/Generators/MigrationGeneratorTest.php index c4670019a422..7750a2ed35d7 100644 --- a/tests/system/Commands/Generators/MigrationGeneratorTest.php +++ b/tests/system/Commands/Generators/MigrationGeneratorTest.php @@ -13,8 +13,10 @@ namespace CodeIgniter\Commands\Generators; +use CodeIgniter\Config\Factories; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; +use Config\Database; use PHPUnit\Framework\Attributes\Group; /** @@ -27,6 +29,8 @@ final class MigrationGeneratorTest extends CIUnitTestCase protected function tearDown(): void { + Factories::reset('config'); + $result = str_replace(["\033[0;32m", "\033[0m", "\n"], '', $this->getStreamFilterBuffer()); $file = str_replace('APPPATH' . DIRECTORY_SEPARATOR, APPPATH, trim(substr($result, 14))); if (is_file($file)) { @@ -52,6 +56,29 @@ public function testGenerateMigrationWithOptionTable(): void $this->assertStringContainsString('_CreateLoggerTable.php', $this->getStreamFilterBuffer()); } + public function testSessionRejectsUnsupportedDriver(): void + { + $config = new Database(); + $config->default['DBDriver'] = 'SQLite3'; + Factories::injectMock('config', 'Database', $config); + + command('make:migration -session'); + + $this->assertStringContainsString( + 'Database sessions are only supported on MySQLi and Postgre. The "default" database group uses the "SQLite3" driver.', + $this->getStreamFilterBuffer(), + ); + $this->assertSame([], glob(APPPATH . 'Database/Migrations/*_CreateCiSessionsTable.php')); + } + + public function testSessionRejectsUndefinedGroup(): void + { + command('make:migration -session -dbgroup bogus'); + + $this->assertStringContainsString('The "bogus" database group is not defined.', $this->getStreamFilterBuffer()); + $this->assertSame([], glob(APPPATH . 'Database/Migrations/*_CreateCiSessionsTable.php')); + } + public function testGenerateMigrationWithOptionSuffix(): void { command('make:migration database -suffix'); diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index dc1739a0041b..88a73f1a5e74 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -18,6 +18,7 @@ BREAKING Message Changes *************** +- Added the ``CLI.generator.undefinedDatabaseGroup`` and ``CLI.generator.unsupportedSessionDriver`` language strings. - Added the ``Cookie.invalidCookieValue`` language string. - Added the ``Cookie.invalidCookiePath`` language string. - Added the ``Cookie.invalidCookieDomain`` language string. @@ -44,6 +45,7 @@ Bugs Fixed - **CLI:** Fixed a bug where ``CLI::input()`` and ``CLI::prompt()`` threw a ``TypeError`` when STDIN reached end-of-file (e.g., Ctrl+D) with the ``readline`` extension enabled. An empty string is now returned, matching the behavior without ``readline``. - **CLIRequest:** Fixed a bug where ``parseCommand()`` could throw a TypeError when ``argv`` is missing. - **CodeIgniter:** Fixed a bug where ``gatherOutput()`` could be called twice when ``startController()`` returned a ``ResponseInterface`` (e.g., from filter attributes or closure routes). +- **Commands:** Fixed a bug where ``make:migration --session`` silently generated a broken migration when the database group's driver is neither ``MySQLi`` nor ``Postgre``. The command now reports an error and returns ``EXIT_ERROR``. - **Content Security Policy:** Fixed a bug where empty ``Content-Security-Policy``, ``Content-Security-Policy-Report-Only``, and ``Reporting-Endpoints`` response headers were generated when no corresponding values existed. - **Cookie:** Fixed a bug where ``Cookie`` instances created with ``raw: true`` allowed invalid characters in cookie values rejected by ``setrawcookie()``. - **Cookie:** Fixed a bug where ``Cookie`` instances allowed invalid characters in path, domain, and prefix attributes rejected by ``setcookie()`` and ``setrawcookie()``. From 192887feb1a9b351ddc445d4e499168e7f56cd0d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 00:27:08 +0800 Subject: [PATCH 127/163] chore(deps): bump NexusPHP/carson in / (#10565) Bumps [NexusPHP/carson](https://github.com/nexusphp/carson) in `/` from 1.4.2 to 1.4.3. Updates `NexusPHP/carson` from 1.4.2 to 1.4.3 - [Release notes](https://github.com/nexusphp/carson/releases) - [Changelog](https://github.com/NexusPHP/carson/blob/1.x/CHANGELOG.md) - [Commits](https://github.com/nexusphp/carson/compare/3973bbb86351a78861b75bbf504f53a83aede4dc...44db1c01ff55b596cabec251fec6307dabd0852e) --- updated-dependencies: - dependency-name: NexusPHP/carson dependency-version: 1.4.3 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/run-carson.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-carson.yml b/.github/workflows/run-carson.yml index 3a5a3e845d29..13f997730bfc 100644 --- a/.github/workflows/run-carson.yml +++ b/.github/workflows/run-carson.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Run Carson - uses: NexusPHP/carson@3973bbb86351a78861b75bbf504f53a83aede4dc # v1.4.2 + uses: NexusPHP/carson@44db1c01ff55b596cabec251fec6307dabd0852e # v1.4.3 with: app_id: ${{ secrets.CARSON_APP_ID }} private_key: ${{ secrets.CARSON_PRIVATE_KEY }} From 167259b306cb8a7dd279bc1ccaf9784aa7c8d290 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 22:48:21 +0700 Subject: [PATCH 128/163] chore(deps-dev): update rector/rector requirement (#10566) Updates the requirements on [rector/rector](https://github.com/rectorphp/rector) to permit the latest version. Updates `rector/rector` to 2.6.7 - [Release notes](https://github.com/rectorphp/rector/releases) - [Commits](https://github.com/rectorphp/rector/compare/2.6.6...2.6.7) --- updated-dependencies: - dependency-name: rector/rector dependency-version: 2.6.7 dependency-type: direct:development dependency-group: composer-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d44bee53acab..74ac3a05677a 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "phpunit/phpcov": "^9.0.2 || ^10.0", "phpunit/phpunit": "^10.5.16 || ^11.2", "predis/predis": "^3.0", - "rector/rector": "2.6.6", + "rector/rector": "2.6.7", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { From 13a35f769227d965b335eafb441dae8f02290b5f Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Fri, 18 Sep 2026 01:14:39 +0800 Subject: [PATCH 129/163] chore: print random-test failure details as collapsible groups on CI (#10563) --- .github/scripts/run-random-tests.sh | 63 ++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/.github/scripts/run-random-tests.sh b/.github/scripts/run-random-tests.sh index fe321bef0492..a0a962efbba5 100755 --- a/.github/scripts/run-random-tests.sh +++ b/.github/scripts/run-random-tests.sh @@ -41,8 +41,9 @@ readonly BOLD='\033[1m' readonly RESET='\033[0m' # Script paths -readonly script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -readonly project_root="$( cd "$script_dir/../.." && pwd )" +script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +project_root="$( cd "$script_dir/../.." && pwd )" +readonly script_dir project_root readonly config_file="$script_dir/random-tests-config.txt" readonly results_dir="$project_root/build/random-tests" @@ -166,7 +167,8 @@ inflect() { } generate_phpunit_random_seed() { - local seed=$(date +%s) + local seed + seed=$(date +%s) if [[ ! "$seed" =~ ^[0-9]+$ ]]; then echo 1 @@ -233,7 +235,8 @@ get_failed_test_predecessor() { print_result() { local type=$1 completed=$2 total=$3 component=$4 elapsed_str=$5 - local padded=$(printf "%${#total}d" "$completed") + local padded + padded=$(printf "%${#total}d" "$completed") local color symbol case "$type" in @@ -462,7 +465,8 @@ run_component_tests() { fi local test_dir="tests/system/$component" - local start_time=$(date +%s%N) + local start_time + start_time=$(date +%s%N) print_debug "Running tests for: $component" @@ -527,7 +531,8 @@ run_component_tests() { if kill -0 "$test_pid" 2>/dev/null; then touch "$timeout_marker" - local pids_to_kill=$(pgrep -P "$test_pid" 2>/dev/null) + local pids_to_kill + pids_to_kill=$(pgrep -P "$test_pid" 2>/dev/null) kill -TERM "$test_pid" 2>/dev/null || true if [[ -n "$pids_to_kill" ]]; then @@ -607,7 +612,8 @@ run_component_tests() { if [[ $exit_code -eq 124 ]]; then predecessor_info+=$'\nFailed test: (timeout before PHPUnit emitted failure event)' if [[ -f "$events_file" ]]; then - local last_prepared_test=$(extract_test_order "$events_file" | tail -n 1) + local last_prepared_test + last_prepared_test=$(extract_test_order "$events_file" | tail -n 1) if [[ -n "$last_prepared_test" ]]; then predecessor_info+=$'\nLast prepared test before timeout: '"${last_prepared_test}" else @@ -619,7 +625,8 @@ run_component_tests() { predecessor_info+=$'\nPrevious test: (unavailable due to timeout)' else if [[ -f "$events_file" ]]; then - local predecessor_result=$(get_failed_test_predecessor "$events_file") + local predecessor_result + predecessor_result=$(get_failed_test_predecessor "$events_file") if [[ -n "$predecessor_result" ]]; then local previous_test=${predecessor_result#*|} predecessor_info+=$'\nFailed test: '"${predecessor_result%%|*}" @@ -639,7 +646,7 @@ run_component_tests() { fi { - echo "> ${phpunit_args[@]:0:7}" + echo "> ${phpunit_args[*]:0:7}" echo "" echo "$output" echo "$predecessor_info" @@ -691,8 +698,9 @@ process_result() { first_result=false fi - local status=$(grep "^Exit code:" "$result_file" | sed 's/Exit code: //') - local elapsed_str=$(format_elapsed_time "$elapsed") + local status elapsed_str + status=$(grep "^Exit code:" "$result_file" | sed 's/Exit code: //') + elapsed_str=$(format_elapsed_time "$elapsed") case "$status" in 0) @@ -728,7 +736,8 @@ get_completed_components() { while IFS= read -r file_path; do # Remove prefix: random_test_result_ - local temp=$(basename "$file_path") + local temp + temp=$(basename "$file_path") temp=${temp#random_test_result_} # Extract elapsed time (everything before first underscore after number) @@ -749,13 +758,35 @@ get_completed_components() { # Sort entries by elapsed time numerically printf '%s\n' "${entries[@]}" | sort -t'|' -k1,1n | while IFS='|' read -r elapsed listed_component; do - if [[ ! " ${displayed_components[*]:-} " =~ " ${listed_component} " ]]; then + if [[ " ${displayed_components[*]:-} " != *" ${listed_component} "* ]]; then echo "$listed_component|$elapsed" fi done } +print_failed_component_details() { + local component=$1 + local result_file=$2 + + # GitHub Actions renders ::group:: lines as collapsible log sections. + if [[ -z "${GITHUB_ACTIONS:-}" || ! -f "$result_file" ]]; then + return + fi + + echo "::group::${component}: test result" + cat "$result_file" + echo "::endgroup::" + + local order_file="${result_file/random_test_result_/random_test_order_}" + + if [[ -f "$order_file" ]]; then + echo "::group::${component}: execution order" + cat "$order_file" + echo "::endgroup::" + fi +} + print_summary() { local run_number=$1 local pass_percent=0.00 @@ -779,13 +810,15 @@ print_summary() { if [[ $failed -gt 0 ]]; then echo -e "\n${BOLD_RED}Failed $(inflect "$failed" "Component" "Components"):${RESET}" while IFS= read -r failed_component; do - local result_file=$(find "$results_dir" -name "random_test_result_*_${failed_component}.txt" 2>/dev/null | head -n 1) + local result_file + result_file=$(find "$results_dir" -name "random_test_result_*_${failed_component}.txt" 2>/dev/null | head -n 1) if [[ -z "$result_file" ]]; then result_file="$results_dir/random_test_result_*_${failed_component}.txt" fi echo -e " ${RED}✗${RESET} ${BOLD}$failed_component${RESET} ($result_file)" + print_failed_component_details "$failed_component" "$result_file" done < <(printf '%s\n' "${failed_components[@]}" | sort) fi @@ -830,7 +863,7 @@ main() { # Read components from config file verify_config print_success "Configuration file: $config_file\n" - components_array=($(read_components)) + read -ra components_array <<< "$(read_components)" fi total=${#components_array[@]} From edf43801a91d5f807537342f5bbe23ab1c72de3a Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Fri, 18 Sep 2026 10:20:23 +0200 Subject: [PATCH 130/163] fix: reset `is_windows()` mock state in `CIUnitTestCase::tearDown()` (#10554) --- system/Test/CIUnitTestCase.php | 10 ++++++++++ tests/system/CommonFunctionsTest.php | 2 +- tests/system/Test/TestCaseTest.php | 25 +++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/system/Test/CIUnitTestCase.php b/system/Test/CIUnitTestCase.php index d86e543a9a61..05e4c647f0ed 100644 --- a/system/Test/CIUnitTestCase.php +++ b/system/Test/CIUnitTestCase.php @@ -273,6 +273,8 @@ protected function tearDown(): void // Check for other trait methods $this->callTraitMethods('tearDown'); + + $this->resetIsWindowsMock(); } /** @@ -318,6 +320,14 @@ protected function resetServices(bool $initAutoloader = true) Services::reset($initAutoloader); } + /** + * Resets the mocked is_windows() function back to default state. + */ + protected function resetIsWindowsMock(): void + { + is_windows(null); + } + /** * Injects the mock Cache driver to prevent filesystem collisions. * diff --git a/tests/system/CommonFunctionsTest.php b/tests/system/CommonFunctionsTest.php index b4901a5e8d93..f1ce25c04aa1 100644 --- a/tests/system/CommonFunctionsTest.php +++ b/tests/system/CommonFunctionsTest.php @@ -863,7 +863,7 @@ public function testIsWindowsUsingMock(): void $this->assertFalse(is_windows()); $this->assertNotTrue(is_windows()); - is_windows(); + is_windows(null); $this->assertSame(str_contains(php_uname(), 'Windows'), is_windows()); $this->assertSame(defined('PHP_WINDOWS_VERSION_MAJOR'), is_windows()); } diff --git a/tests/system/Test/TestCaseTest.php b/tests/system/Test/TestCaseTest.php index 6905d825533a..611c3fe0ebd4 100644 --- a/tests/system/Test/TestCaseTest.php +++ b/tests/system/Test/TestCaseTest.php @@ -93,4 +93,29 @@ public function testCloseEnoughStringBadLength(): void $result = $this->assertCloseEnoughString('apples & oranges', 'apples'); $this->assertFalse($result, 'Different string lengths should have returned false'); } + + public function testTearDownResetsIsWindowsMock(): void + { + $testCase = new class ('test') extends CIUnitTestCase { + protected $tearDownMethods = ['customTearDown']; + + protected function customTearDown(): void + { + } + + public function triggerTearDown(): void + { + $this->tearDown(); + } + }; + + $default = DIRECTORY_SEPARATOR === '\\'; + + is_windows(! $default); + $this->assertSame(! $default, is_windows()); + + $testCase->triggerTearDown(); + + $this->assertSame($default, is_windows()); + } } From 014c45c3b5960b7bf631cd2a84ced61cadea3d23 Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Fri, 18 Sep 2026 10:21:33 +0200 Subject: [PATCH 131/163] fix(Cache): set secure directory (0755) and file (0644) permissions in `FileVarExportHandler` (#10555) --- .../FactoriesCache/FileVarExportHandler.php | 4 +- ...FactoriesCacheFileVarExportHandlerTest.php | 37 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/system/Cache/FactoriesCache/FileVarExportHandler.php b/system/Cache/FactoriesCache/FileVarExportHandler.php index 023910c4b17d..241399a12035 100644 --- a/system/Cache/FactoriesCache/FileVarExportHandler.php +++ b/system/Cache/FactoriesCache/FileVarExportHandler.php @@ -23,7 +23,7 @@ public function save(string $key, mixed $val): void // Two processes may try to create the directory at the same time. // is_dir() confirms it exists, so suppressing the warning is safe. - if (! is_dir($this->path) && ! @mkdir($this->path, 0777, true) && ! is_dir($this->path)) { + if (! is_dir($this->path) && ! @mkdir($this->path, 0755, true) && ! is_dir($this->path)) { log_message('error', 'FactoriesCache: cannot create cache directory: ' . $this->path); return; @@ -37,6 +37,8 @@ public function save(string $key, mixed $val): void return; } + @chmod($tmp, 0644); + // Another process may have wiped the directory. Clean up on failure. if (! @rename($tmp, $this->path . "/{$key}")) { log_message('warning', 'FactoriesCache: failed to commit cache file for key: ' . $key); diff --git a/tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php b/tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php index c71c756f5cdd..e09816dba2fa 100644 --- a/tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php +++ b/tests/system/Cache/FactoriesCacheFileVarExportHandlerTest.php @@ -22,9 +22,46 @@ #[Group('Others')] final class FactoriesCacheFileVarExportHandlerTest extends AbstractFactoriesCacheHandlerTestCase { + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + + helper('filesystem'); + } + protected function createFactoriesCache(): void { $this->handler = new FileVarExportHandler(); $this->cache = new FactoriesCache($this->handler); } + + public function testSaveCreatesDirectoryAndFileWithCorrectPermissions(): void + { + $dir = WRITEPATH . 'cache_test_dir_' . uniqid('', true); + $oldUmask = umask(0000); + + try { + $handler = new FileVarExportHandler(); + $this->setPrivateProperty($handler, 'path', $dir); + + $handler->save('test_key', ['data']); + + $this->assertDirectoryExists($dir); + + if (! is_windows()) { + $dirPerms = fileperms($dir) & 0777; + $this->assertSame(0755, $dirPerms); + + $filePerms = fileperms($dir . '/test_key') & 0777; + $this->assertSame(0644, $filePerms); + } + } finally { + umask($oldUmask); + + if (is_dir($dir)) { + delete_files($dir); + rmdir($dir); + } + } + } } From 404dcf561789c2e5fb569c57687a81a700629d02 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Fri, 18 Sep 2026 16:22:32 +0800 Subject: [PATCH 132/163] fix: stop `FileLocatorCached` from restoring a deleted cache on shutdown (#10559) --- system/Autoloader/FileLocatorCached.php | 1 + system/Commands/Cache/ClearCache.php | 7 ++ system/Commands/Utilities/Optimize.php | 7 +- .../Autoloader/FileLocatorCachedTest.php | 19 +++- .../system/Commands/Cache/ClearCacheTest.php | 26 ++++++ .../Commands/Utilities/OptimizeTest.php | 88 +++++++++++++++++++ user_guide_src/source/changelogs/v4.7.5.rst | 1 + 7 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 tests/system/Commands/Utilities/OptimizeTest.php diff --git a/system/Autoloader/FileLocatorCached.php b/system/Autoloader/FileLocatorCached.php index 3e31a33d9e84..ee1bb1577433 100644 --- a/system/Autoloader/FileLocatorCached.php +++ b/system/Autoloader/FileLocatorCached.php @@ -84,6 +84,7 @@ private function saveCache(): void */ public function deleteCache(): void { + $this->cache = []; $this->cacheUpdated = false; $this->cacheHandler->delete($this->cacheKey); } diff --git a/system/Commands/Cache/ClearCache.php b/system/Commands/Cache/ClearCache.php index 32f9466a4939..a5c60d315ac5 100644 --- a/system/Commands/Cache/ClearCache.php +++ b/system/Commands/Cache/ClearCache.php @@ -13,6 +13,7 @@ namespace CodeIgniter\Commands\Cache; +use CodeIgniter\Autoloader\FileLocatorCached; use CodeIgniter\CLI\BaseCommand; use CodeIgniter\CLI\CLI; use Config\Cache; @@ -81,6 +82,12 @@ public function run(array $params) return EXIT_ERROR; } + $locator = service('locator'); + + if ($handler === 'file' && $locator instanceof FileLocatorCached) { + $locator->deleteCache(); + } + CLI::write(CLI::color('Cache cleared.', 'green')); return EXIT_SUCCESS; diff --git a/system/Commands/Utilities/Optimize.php b/system/Commands/Utilities/Optimize.php index 1a103a1b9c52..9d3964ddda1d 100644 --- a/system/Commands/Utilities/Optimize.php +++ b/system/Commands/Utilities/Optimize.php @@ -74,7 +74,12 @@ public function run(array $params) private function clearCache(): void { - $locator = new FileLocatorCached(new FileLocator(service('autoloader'))); + $locator = service('locator'); + + if (! $locator instanceof FileLocatorCached) { + $locator = new FileLocatorCached(new FileLocator(service('autoloader'))); + } + $locator->deleteCache(); CLI::write('Removed FileLocatorCache.', 'green'); diff --git a/tests/system/Autoloader/FileLocatorCachedTest.php b/tests/system/Autoloader/FileLocatorCachedTest.php index f8b5b386818c..9700f88bdb33 100644 --- a/tests/system/Autoloader/FileLocatorCachedTest.php +++ b/tests/system/Autoloader/FileLocatorCachedTest.php @@ -75,10 +75,27 @@ protected function tearDown(): void public function testDeleteCache(): void { - $this->assertNotSame([], $this->handler->get('FileLocatorCache')); + $this->locator->search('Config/App'); + $this->locator->__destruct(); + $this->assertIsArray($this->handler->get('FileLocatorCache')); $this->locator->deleteCache(); $this->assertFalse($this->handler->get('FileLocatorCache')); } + + public function testDeleteCacheDiscardsDataHeldInMemory(): void + { + $this->locator->getClassname(SYSTEMPATH . 'CodeIgniter.php'); + + $this->locator->deleteCache(); + + $this->locator->search('Config/App'); + $this->locator->__destruct(); + + $cached = $this->handler->get('FileLocatorCache'); + + $this->assertArrayHasKey('search', $cached); + $this->assertArrayNotHasKey('getClassname', $cached); + } } diff --git a/tests/system/Commands/Cache/ClearCacheTest.php b/tests/system/Commands/Cache/ClearCacheTest.php index e64a24916e15..6674fff5a2c8 100644 --- a/tests/system/Commands/Cache/ClearCacheTest.php +++ b/tests/system/Commands/Cache/ClearCacheTest.php @@ -13,7 +13,10 @@ namespace CodeIgniter\Commands\Cache; +use CodeIgniter\Autoloader\FileLocator; +use CodeIgniter\Autoloader\FileLocatorCached; use CodeIgniter\Cache\CacheFactory; +use CodeIgniter\Cache\FactoriesCache\FileVarExportHandler; use CodeIgniter\Cache\Handlers\FileHandler; use CodeIgniter\CLI\CLI; use CodeIgniter\Config\Factories; @@ -93,6 +96,29 @@ public function testClearCacheWorks(): void $this->assertStringContainsString('Cache cleared.', $this->getStreamFilterBuffer()); } + public function testClearCacheDiscardsSharedLocatorCache(): void + { + $handler = new FileVarExportHandler(); + $handler->delete('FileLocatorCache'); + + $locator = new FileLocatorCached(new FileLocator(service('autoloader')), $handler); + $locator->search('Config/App'); + Services::injectMock('locator', $locator); + + command('cache:clear'); + + $locator->search('Config/Cache'); + $locator->__destruct(); + Services::resetSingle('locator'); + + $cached = $handler->get('FileLocatorCache'); + + $this->assertArrayNotHasKey('Config/App', $cached['search']); + $this->assertArrayHasKey('Config/Cache', $cached['search']); + + $handler->delete('FileLocatorCache'); + } + public function testClearCacheFails(): void { $cache = $this->getMockBuilder(FileHandler::class) diff --git a/tests/system/Commands/Utilities/OptimizeTest.php b/tests/system/Commands/Utilities/OptimizeTest.php new file mode 100644 index 000000000000..ccaf9403c613 --- /dev/null +++ b/tests/system/Commands/Utilities/OptimizeTest.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Commands\Utilities; + +use Closure; +use CodeIgniter\Autoloader\FileLocator; +use CodeIgniter\Autoloader\FileLocatorCached; +use CodeIgniter\Cache\FactoriesCache\FileVarExportHandler; +use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\ReflectionHelper; +use CodeIgniter\Test\StreamFilterTrait; +use Config\Services; +use PHPUnit\Framework\Attributes\Group; + +/** + * @internal + */ +#[Group('Others')] +final class OptimizeTest extends CIUnitTestCase +{ + use ReflectionHelper; + use StreamFilterTrait; + + private FileVarExportHandler $handler; + + protected function setUp(): void + { + parent::setUp(); + + $this->handler = new FileVarExportHandler(); + $this->handler->delete('FileLocatorCache'); + } + + protected function tearDown(): void + { + parent::tearDown(); + + $this->handler->delete('FileLocatorCache'); + Services::resetSingle('locator'); + } + + /** + * @return Closure(): void + */ + private function getClearCache(): Closure + { + return self::getPrivateMethodInvoker(new Optimize(service('logger'), service('commands')), 'clearCache'); + } + + public function testClearCacheDiscardsSharedLocatorCache(): void + { + $locator = new FileLocatorCached(new FileLocator(service('autoloader')), $this->handler); + $locator->search('Config/App'); + Services::injectMock('locator', $locator); + + ($this->getClearCache())(); + + $locator->search('Config/Cache'); + $locator->__destruct(); + + $cached = $this->handler->get('FileLocatorCache'); + + $this->assertArrayNotHasKey('Config/App', $cached['search']); + $this->assertArrayHasKey('Config/Cache', $cached['search']); + $this->assertStringContainsString('Removed FileLocatorCache.', $this->getStreamFilterBuffer()); + } + + public function testClearCacheDeletesTheFileWithoutSharedLocatorCache(): void + { + $this->handler->save('FileLocatorCache', ['search' => []]); + + ($this->getClearCache())(); + + $this->assertFalse($this->handler->get('FileLocatorCache')); + $this->assertStringContainsString('Removed FileLocatorCache.', $this->getStreamFilterBuffer()); + } +} diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index 88a73f1a5e74..1f10e03cdf21 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -39,6 +39,7 @@ Deprecations Bugs Fixed ********** +- **Autoloader:** Fixed a bug where ``FileLocatorCached::deleteCache()`` left the deleted data in memory, so it could be written back to the cache file on shutdown. ``spark optimize`` and ``spark cache:clear`` now clear the shared locator's cache instead of a separate instance. - **CLI:** Fixed a bug where pressing backspace in a ``CLI::prompt()`` erased the prompt text when the ``readline`` extension is enabled. The prompt is now passed to ``readline()`` so line redraws repaint it. ANSI color codes in the prompt (e.g., option defaults) are wrapped in readline's non-printing markers under GNU readline so cursor positioning stays accurate. On Windows, where the ``readline`` extension is built on WinEditLine, the prompt is written to STDOUT first because WinEditLine reports no library version and prints ANSI sequences literally. From 605624db204be7b333b0685bfa7899deb8432de9 Mon Sep 17 00:00:00 2001 From: Waqas Ahmed Date: Sat, 19 Sep 2026 14:48:50 +0500 Subject: [PATCH 133/163] fix: preserve sub-second precision in BaseModel datetime timestamps (#10561) When a model's $dateFormat is 'datetime' and the connection's dateFormat['datetime'] mask includes .v/.u (millisecond/microsecond precision), auto-generated created_at/updated_at timestamps always rendered .000000 instead of the real sub-second value. setDate() converted the current Time instance to a Unix timestamp integer before formatting it, which discards anything below whole-second precision. timeToDate() already formats a Time object directly and retains sub-second precision, but nothing routed the no-explicit-date case through it. Fixes #10540 --- system/BaseModel.php | 6 +++-- tests/system/Models/GeneralModelTest.php | 26 +++++++++++++++++++++ user_guide_src/source/changelogs/v4.7.5.rst | 1 + 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/system/BaseModel.php b/system/BaseModel.php index 64d083fe4d2b..ad79f6f42ec6 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -1425,9 +1425,11 @@ protected function doProtectFieldsForInsert(array $row): array */ protected function setDate(?int $userDate = null) { - $currentDate = $userDate ?? Time::now()->getTimestamp(); + if ($userDate === null && $this->dateFormat === 'datetime') { + return $this->timeToDate(Time::now()); + } - return $this->intToDate($currentDate); + return $this->intToDate($userDate ?? Time::now()->getTimestamp()); } /** diff --git a/tests/system/Models/GeneralModelTest.php b/tests/system/Models/GeneralModelTest.php index bd9f2d14109e..eb72f6aec5eb 100644 --- a/tests/system/Models/GeneralModelTest.php +++ b/tests/system/Models/GeneralModelTest.php @@ -15,8 +15,11 @@ use CodeIgniter\Database\BaseConnection; use CodeIgniter\Exceptions\BadMethodCallException; +use CodeIgniter\I18n\Time; use CodeIgniter\Model; use CodeIgniter\Test\CIUnitTestCase; +use CodeIgniter\Test\Mock\MockConnection; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use Tests\Support\Models\JobModel; use Tests\Support\Models\UserModel; @@ -39,6 +42,7 @@ protected function tearDown(): void { parent::tearDown(); $this->resetServices(); + Time::setTestNow(); } /** @@ -111,6 +115,28 @@ public function testSetAllowedFields(): void $this->assertSame($allowed2, $this->getPrivateProperty($model, 'allowedFields')); } + #[DataProvider('provideCurrentTimestampPreservesSubseconds')] + public function testCurrentTimestampPreservesSubseconds(string $format, string $expected): void + { + Time::setTestNow('2024-07-09 09:13:34.654321'); + + $db = new MockConnection(['dateFormat' => ['datetime' => $format]]); + $model = $this->createModel(UserModel::class, $db); + $date = self::getPrivateMethodInvoker($model, 'setDate'); + + $this->assertSame($expected, $date()); + } + + /** + * @return iterable + */ + public static function provideCurrentTimestampPreservesSubseconds(): iterable + { + yield 'milliseconds' => ['Y-m-d H:i:s.v', '2024-07-09 09:13:34.654']; + + yield 'microseconds' => ['Y-m-d H:i:s.u', '2024-07-09 09:13:34.654321']; + } + public function testBuilderUsesModelTable(): void { $builder = $this->createModel(UserModel::class)->builder(); diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index 1f10e03cdf21..83e0bf47742a 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -40,6 +40,7 @@ Bugs Fixed ********** - **Autoloader:** Fixed a bug where ``FileLocatorCached::deleteCache()`` left the deleted data in memory, so it could be written back to the cache file on shutdown. ``spark optimize`` and ``spark cache:clear`` now clear the shared locator's cache instead of a separate instance. +- **BaseModel:** Fixed a bug where auto-generated ``created_at``/``updated_at`` timestamps always rendered ``.000000`` for a ``'datetime'`` ``$dateFormat`` whose connection ``dateFormat`` mask includes ``.v``/``.u``, instead of the real sub-second value. - **CLI:** Fixed a bug where pressing backspace in a ``CLI::prompt()`` erased the prompt text when the ``readline`` extension is enabled. The prompt is now passed to ``readline()`` so line redraws repaint it. ANSI color codes in the prompt (e.g., option defaults) are wrapped in readline's non-printing markers under GNU readline so cursor positioning stays accurate. On Windows, where the ``readline`` extension is built on WinEditLine, the prompt is written to STDOUT first because WinEditLine reports no library version and prints ANSI sequences literally. From 744e717e5cfc993b5a95bf71f4d8c7983d5e798d Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sat, 19 Sep 2026 18:33:19 +0800 Subject: [PATCH 134/163] test: keep CommandsTest out of app/Commands so parallel runs do not race (#10564) --- tests/system/CLI/CommandsTest.php | 35 +++++++++++++------------------ 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/tests/system/CLI/CommandsTest.php b/tests/system/CLI/CommandsTest.php index 34bb0601da65..cd284960909d 100644 --- a/tests/system/CLI/CommandsTest.php +++ b/tests/system/CLI/CommandsTest.php @@ -13,7 +13,9 @@ namespace CodeIgniter\CLI; +use App\Commands\ListCommands as AppListCommands; use CodeIgniter\Autoloader\FileLocatorInterface; +use CodeIgniter\Commands\ListCommands; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\StreamFilterTrait; use Config\Services; @@ -42,22 +44,6 @@ protected function resetAll(): void 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(); @@ -166,13 +152,22 @@ public function testDiscoverCommandsWithNoFiles(): void public function testDiscoveredCommandsCanBeOverridden(): void { - $this->copyAppListCommands(); + // The fixture sits outside any PSR-4 root, so the autoloader cannot load it. + require_once SUPPORTPATH . '_command/ListCommands.php'; + + $files = [ + SUPPORTPATH . '_command/ListCommands.php' => AppListCommands::class, + SYSTEMPATH . 'Commands/ListCommands.php' => ListCommands::class, + ]; + + $locator = $this->createMock(FileLocatorInterface::class); + $locator->method('listFiles')->with('Commands/')->willReturn(array_keys($files)); + $locator->method('findQualifiedNameFromPath')->willReturnCallback(static fn (string $file): string => $files[$file]); + Services::injectMock('locator', $locator); - command('list'); + (new Commands())->run('list', []); $this->assertStringContainsString('This is App\Commands\ListCommands', $this->getStreamFilterBuffer()); $this->assertStringNotContainsString('Displays basic usage information.', $this->getStreamFilterBuffer()); - - $this->deleteAppListCommands(); } } From 42d75a9fca74175a346af9bb41b798b76a74e60d Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Mon, 21 Sep 2026 00:09:34 +0800 Subject: [PATCH 135/163] chore: replace Mergeable with Carson's welcome, template-enforcer, and no-response-closer (#10572) --- .github/carson.yml | 55 +++++++++++++++++++++++++ .github/mergeable.yml | 69 -------------------------------- .github/workflows/run-carson.yml | 5 ++- 3 files changed, 59 insertions(+), 70 deletions(-) delete mode 100644 .github/mergeable.yml diff --git a/.github/carson.yml b/.github/carson.yml index 7a4dd7fa2689..37cf003c5e9c 100644 --- a/.github/carson.yml +++ b/.github/carson.yml @@ -4,8 +4,11 @@ version: 1 subscribers: - auto-labeler - conflicts-notifier + - no-response-closer - pr-title-linter - signed-commits + - template-enforcer + - welcome settings: auto-labeler: sync_labels: true @@ -35,6 +38,15 @@ settings: You may want to sync :arrows_counterclockwise: your branch with upstream! Ref: [Syncing Your Branch](https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/workflow.md#updating-your-branch) + no-response-closer: + rules: + - label: needs template + only: issues + days_until_close: 2 + close_message: |- + Closing this issue: the description still does not follow the bug report template after {{days_until_close}} days. + + Please open a new issue using the [bug report form](https://github.com/codeigniter4/CodeIgniter4/issues/new?assignees=&labels=bug&projects=&template=bug_report.yml&title=Bug%3A+). For feature requests or support questions, please use the [forums](https://forum.codeigniter.com/forum-30.html). pr-title-linter: name: 'Carson / pr-title-linter' rules: @@ -44,3 +56,46 @@ settings: signed-commits: name: 'Carson / signed-commits' treat_unsigned_as: failure + template-enforcer: + label: needs template + exempt_roles: [admin, maintain, write, triage] + message: |- + Hi there, @{{user}}! :wave: + + It looks like you opened an issue without following the bug report template: + + {{violations}} + + * Bug report ([open an issue](https://github.com/codeigniter4/CodeIgniter4/issues/new?assignees=&labels=bug&projects=&template=bug_report.yml&title=Bug%3A+)) + * For feature request or support question, please use the [forums](https://forum.codeigniter.com/forum-30.html). + + Please update the description to follow the template. The `{{label}}` label will be removed automatically once it does. + issues: + required_sections: + - '### PHP Version' + - '### CodeIgniter4 Version' + welcome: + first_time: + pull_request: |- + Hi there, @{{user}}! :wave: + + Thank you for sending this PR! + + We expect the following in all Pull Requests (PRs). + - PRs must be sent to the [appropriate branch](https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/pull_request.md#branching) + - All git commits must be [GPG-signed](https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/pull_request.md#signing) + - Must follow our [style guide](https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/pull_request.md#php-style) + - Be [commented](https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/pull_request.md#comments) in the PHP source file + - Be documented in the [user guide](https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/pull_request.md#user-guide) + - Be [unit tested](https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/pull_request.md#unit-testing) + - Pass all checks in GitHub Actions + + > [!IMPORTANT] + > We expect all code changes or bug-fixes to be accompanied by one or more tests added to our test suite to prove the code works. + + If pull requests do not comply with the above, they will likely be closed. Since we are a team of volunteers, we don't have any more time to work + on the framework than you do. Please make it as painless for your contributions to be included as possible. + + See https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/pull_request.md + issue: false + returning: false diff --git a/.github/mergeable.yml b/.github/mergeable.yml deleted file mode 100644 index 2e58526188aa..000000000000 --- a/.github/mergeable.yml +++ /dev/null @@ -1,69 +0,0 @@ -# https://mergeable.readthedocs.io/en/latest/configuration.html - -version: 2 -mergeable: - - when: issues.opened, issues.reopened - validate: - - do: or - validate: - - do: and - validate: - - do: description - must_include: - regex: '### PHP Version' - - do: description - must_include: - regex: '### CodeIgniter4 Version' - - do: author - must_include: - regex: ^kenjis|lonnieezell|MGatner|michalsn|paulbalandan|samsonasik|ddevsr$ - fail: - - do: comment - payload: - body: | - Hi there, @@author! :wave: - - It looks like you opened an issue without following the bug report template: - - * Bug report ([open an issue](https://github.com/codeigniter4/CodeIgniter4/issues/new?assignees=&labels=bug&projects=&template=bug_report.yml&title=Bug%3A+)) - * For feature request or support question, please use the [forums](https://forum.codeigniter.com/forum-30.html). - - The current issue will be closed. This is a precaution to save maintainers' time, I hope you'll understand. - - Sincerely, the mergeable bot 🤖 - - do: close - - - when: pull_request.opened, pull_request.ready_for_review - filter: - - do: payload - pull_request: - author_association: - must_include: - regex: ^NONE|FIRST_TIME_CONTRIBUTOR|FIRST_TIMER$ - validate: [] - pass: - - do: comment - payload: - body: | - Hi there, @@author! :wave: - - Thank you for sending this PR! - - We expect the following in all Pull Requests (PRs). - - PRs must be sent to the [appropriate branch](https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/pull_request.md#branching) - - All git commits must be [GPG-signed](https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/pull_request.md#signing) - - Must follow our [style guide](https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/pull_request.md#php-style) - - Be [commented](https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/pull_request.md#comments) in the PHP source file - - Be documented in the [user guide](https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/pull_request.md#user-guide) - - Be [unit tested](https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/pull_request.md#unit-testing) - - Pass all checks in GitHub Actions - - > [!IMPORTANT] - > We expect all code changes or bug-fixes to be accompanied by one or more tests added to our test suite to prove the code works. - - If pull requests do not comply with the above, they will likely be closed. Since we are a team of volunteers, we don't have any more time to work - on the framework than you do. Please make it as painless for your contributions to be included as possible. - - See https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/pull_request.md - - Sincerely, the mergeable bot 🤖 diff --git a/.github/workflows/run-carson.yml b/.github/workflows/run-carson.yml index 13f997730bfc..1f404d1d2a33 100644 --- a/.github/workflows/run-carson.yml +++ b/.github/workflows/run-carson.yml @@ -8,6 +8,7 @@ on: - synchronize - reopened - labeled + - ready_for_review issues: types: - opened @@ -17,6 +18,8 @@ on: branches: - develop - '4.*' + schedule: + - cron: '0 0 * * *' permissions: {} @@ -25,7 +28,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Run Carson - uses: NexusPHP/carson@44db1c01ff55b596cabec251fec6307dabd0852e # v1.4.3 + uses: NexusPHP/carson@e4b2fde33f32c47455df218cf1765702c15c3b8b # v1.6.0 with: app_id: ${{ secrets.CARSON_APP_ID }} private_key: ${{ secrets.CARSON_PRIVATE_KEY }} From c21d575dce91eb3c322e3651c36f32109667c959 Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Sun, 20 Sep 2026 18:10:45 +0200 Subject: [PATCH 136/163] fix(Debug): treat zero time as valid timer start (#10569) * fix(Debug): treat zero time as valid timer start * test: use assertEqualsWithDelta for timer start value in TimerTest * docs: sort v4.7.5 changelog alphabetically --- system/Debug/Timer.php | 2 +- tests/system/Debug/TimerTest.php | 10 ++++++++++ user_guide_src/source/changelogs/v4.7.5.rst | 7 ++++--- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/system/Debug/Timer.php b/system/Debug/Timer.php index 9b6af4d4bcd0..4c383a743413 100644 --- a/system/Debug/Timer.php +++ b/system/Debug/Timer.php @@ -46,7 +46,7 @@ class Timer public function start(string $name, ?float $time = null) { $this->timers[strtolower($name)] = [ - 'start' => $time === null || $time === 0.0 ? microtime(true) : $time, + 'start' => $time ?? microtime(true), 'end' => null, ]; diff --git a/tests/system/Debug/TimerTest.php b/tests/system/Debug/TimerTest.php index 0f28e7dba69b..8ec9a3602c2b 100644 --- a/tests/system/Debug/TimerTest.php +++ b/tests/system/Debug/TimerTest.php @@ -86,6 +86,16 @@ public function testElapsedTimeGivesSameResultAsTimersArray(): void $this->assertSame($expected, $timer->getElapsedTime('test1')); } + public function testStartWithZeroTime(): void + { + $timer = new Timer(); + $timer->start('test1', 0.0); + + $timers = $timer->getTimers(); + + $this->assertEqualsWithDelta(0.0, $timers['test1']['start'], PHP_FLOAT_EPSILON); + } + public function testThrowsExceptionStoppingNonTimer(): void { $this->expectException('RunTimeException'); diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index 83e0bf47742a..5fc0cfce5578 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -19,9 +19,9 @@ Message Changes *************** - Added the ``CLI.generator.undefinedDatabaseGroup`` and ``CLI.generator.unsupportedSessionDriver`` language strings. -- Added the ``Cookie.invalidCookieValue`` language string. -- Added the ``Cookie.invalidCookiePath`` language string. - Added the ``Cookie.invalidCookieDomain`` language string. +- Added the ``Cookie.invalidCookiePath`` language string. +- Added the ``Cookie.invalidCookieValue`` language string. ******* Changes @@ -41,6 +41,7 @@ Bugs Fixed - **Autoloader:** Fixed a bug where ``FileLocatorCached::deleteCache()`` left the deleted data in memory, so it could be written back to the cache file on shutdown. ``spark optimize`` and ``spark cache:clear`` now clear the shared locator's cache instead of a separate instance. - **BaseModel:** Fixed a bug where auto-generated ``created_at``/``updated_at`` timestamps always rendered ``.000000`` for a ``'datetime'`` ``$dateFormat`` whose connection ``dateFormat`` mask includes ``.v``/``.u``, instead of the real sub-second value. +- **Cache:** Fixed ``MemcachedHandler::decrement()`` initializing a non-existent counter to the positive offset. Missing counters are now initialized to ``0``, reflecting Memcached's unsigned, saturating counter semantics. - **CLI:** Fixed a bug where pressing backspace in a ``CLI::prompt()`` erased the prompt text when the ``readline`` extension is enabled. The prompt is now passed to ``readline()`` so line redraws repaint it. ANSI color codes in the prompt (e.g., option defaults) are wrapped in readline's non-printing markers under GNU readline so cursor positioning stays accurate. On Windows, where the ``readline`` extension is built on WinEditLine, the prompt is written to STDOUT first because WinEditLine reports no library version and prints ANSI sequences literally. @@ -52,12 +53,12 @@ Bugs Fixed - **Cookie:** Fixed a bug where ``Cookie`` instances created with ``raw: true`` allowed invalid characters in cookie values rejected by ``setrawcookie()``. - **Cookie:** Fixed a bug where ``Cookie`` instances allowed invalid characters in path, domain, and prefix attributes rejected by ``setcookie()`` and ``setrawcookie()``. - **Database:** Fixed a bug where rebuilding a SQLite3 table (e.g., ``Forge::dropColumn()``, ``Forge::modifyColumn()``, ``Forge::dropForeignKey()`` and ``Forge::dropPrimaryKey()``) corrupted the table names referenced by its foreign keys when ``DBPrefix`` was set. +- **Debug:** Fixed a bug where ``Timer::start()`` treated ``0.0`` as an empty value and substituted the current time. - **Files:** Fixed a bug where ``File::move()`` and ``UploadedFile::move()`` set executable and overly permissive file permissions (``0777 & ~umask()`` instead of ``0666 & ~umask()``), and ``UploadedFile::move()`` targeted the parent directory instead of the destination file for ``chmod()``. - **Helpers:** Fixed a bug where ``get_dir_file_info()`` returned incomplete entries for subdirectories and missing files instead of omitting them. - **Honeypot:** Fixed a bug where bot detection returned an HTTP 500 response instead of 403 (Forbidden). - **I18n:** Fixed a bug where ``Time::today()``, ``Time::yesterday()``, and ``Time::tomorrow()`` ignored the specified ``$timezone`` and ``setTestNow()`` when calculating the day. - **Logger:** Fixed a bug where interpolating a log message with array or non-stringable context values could raise PHP warnings or errors. -- **Cache:** Fixed ``MemcachedHandler::decrement()`` initializing a non-existent counter to the positive offset. Missing counters are now initialized to ``0``, reflecting Memcached's unsigned, saturating counter semantics. See the repo's `CHANGELOG.md `_ From 81af068cd5a7f743fe04af7e62be88545b338925 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Tue, 22 Sep 2026 12:22:49 +0800 Subject: [PATCH 137/163] chore: enforce no merge commits in PRs (#10576) --- .github/carson.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/carson.yml b/.github/carson.yml index 37cf003c5e9c..663c6553047c 100644 --- a/.github/carson.yml +++ b/.github/carson.yml @@ -4,6 +4,7 @@ version: 1 subscribers: - auto-labeler - conflicts-notifier + - no-merge-commits - no-response-closer - pr-title-linter - signed-commits @@ -38,6 +39,14 @@ settings: You may want to sync :arrows_counterclockwise: your branch with upstream! Ref: [Syncing Your Branch](https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/workflow.md#updating-your-branch) + no-merge-commits: + name: 'Carson / no-merge-commits' + treat_merge_commits_as: failure + exempt_branches: + - head: '^develop$' + base: '^master$' + - head: '^4\.\d+$' + base: '^develop$' no-response-closer: rules: - label: needs template From 514b9f46342dad2ed14f27c25c4636f213c6ae7d Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Tue, 22 Sep 2026 12:27:52 +0800 Subject: [PATCH 138/163] chore: enforce PR bodies follow the PR template (#10577) --- .github/carson.yml | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/carson.yml b/.github/carson.yml index 663c6553047c..85bdc33aeca5 100644 --- a/.github/carson.yml +++ b/.github/carson.yml @@ -71,18 +71,24 @@ settings: message: |- Hi there, @{{user}}! :wave: - It looks like you opened an issue without following the bug report template: + It looks like this {{type}} does not follow our template: {{violations}} - * Bug report ([open an issue](https://github.com/codeigniter4/CodeIgniter4/issues/new?assignees=&labels=bug&projects=&template=bug_report.yml&title=Bug%3A+)) - * For feature request or support question, please use the [forums](https://forum.codeigniter.com/forum-30.html). - Please update the description to follow the template. The `{{label}}` label will be removed automatically once it does. issues: - required_sections: - - '### PHP Version' - - '### CodeIgniter4 Version' + rules: + - pattern: '### PHP Version[\s\S]*### CodeIgniter4 Version' + description: 'Use the [bug report form](https://github.com/codeigniter4/CodeIgniter4/issues/new?assignees=&labels=bug&projects=&template=bug_report.yml&title=Bug%3A+), which asks for your PHP and CodeIgniter4 versions. For a feature request or support question, please use the [forums](https://forum.codeigniter.com/forum-30.html).' + pull_requests: + rules: + - pattern: '\*\*Description\*\*' + description: 'Keep the **Description** heading from the [pull request template](https://github.com/codeigniter4/CodeIgniter4/blob/develop/.github/PULL_REQUEST_TEMPLATE.md) and explain what you have changed, and why.' + - pattern: '\*\*Checklist:?\*\*' + description: 'Keep the **Checklist** from the pull request template and tick the items that apply. See [what we expect in a pull request](https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/pull_request.md).' + - pattern: 'Explain what you have changed, and why\.' + description: 'Replace the placeholder text under **Description** with your own explanation.' + mode: forbid welcome: first_time: pull_request: |- From c9efbbc013a2a39a88ed8c3e9a4302fe1ac71c7a Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Thu, 24 Sep 2026 15:01:20 +0700 Subject: [PATCH 139/163] chore: bump structarmed to ^0.18 (#10582) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 74ac3a05677a..e703580fd4b9 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "^0.17", + "boundwize/structarmed": "^0.18", "codeigniter/phpstan-codeigniter": "^2.1", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", From 3bb0f3b990da190701a10521207135aed24d6787 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Fri, 25 Sep 2026 15:37:12 +0700 Subject: [PATCH 140/163] chore: fix phpstan notice on latest 2.2.15 (#10584) * chore: fix phpstan notice on latest 2.2.15 * chore: regenerate baseline for on purpose test * final touch: avoid regenerate baseline, fix instead --- composer.json | 2 +- tests/system/Config/ServicesTest.php | 3 ++- tests/system/Email/EmailTest.php | 3 ++- tests/system/Encryption/Handlers/OpenSSLHandlerTest.php | 3 ++- tests/system/Encryption/Handlers/SodiumHandlerTest.php | 3 ++- tests/system/Entity/EntityTest.php | 5 +++-- tests/system/Events/EventsTest.php | 3 ++- tests/system/HTTP/IncomingRequestTest.php | 3 ++- tests/system/HTTP/RequestTest.php | 3 ++- tests/system/Helpers/FormHelperTest.php | 9 ++++++--- tests/system/Helpers/TextHelperTest.php | 3 ++- tests/system/Language/LanguageTest.php | 6 ++++-- .../Session/Handlers/Database/RedisHandlerTest.php | 3 ++- tests/system/Throttle/ThrottleTest.php | 3 ++- tests/system/View/ParserTest.php | 3 ++- 15 files changed, 36 insertions(+), 19 deletions(-) diff --git a/composer.json b/composer.json index e703580fd4b9..b291ea49c21c 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "mikey179/vfsstream": "^1.6.12", "nexusphp/tachycardia": "^2.0", "phpstan/extension-installer": "^1.4", - "phpstan/phpstan": "^2.2.1", + "phpstan/phpstan": "^2.2.15", "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpcov": "^9.0.2 || ^10.0", "phpunit/phpunit": "^10.5.16 || ^11.2", diff --git a/tests/system/Config/ServicesTest.php b/tests/system/Config/ServicesTest.php index 66c9158e67ab..f9b8d8158c39 100644 --- a/tests/system/Config/ServicesTest.php +++ b/tests/system/Config/ServicesTest.php @@ -197,7 +197,8 @@ public function testNewUnsharedLanguage(): void { $actual = Services::language(null, false); $this->assertInstanceOf(Language::class, $actual); - $this->assertSame('en', $actual->getLocale()); + $locale = $actual->getLocale(); + $this->assertSame('en', $locale); Services::language('la', false); $this->assertSame('en', $actual->getLocale()); diff --git a/tests/system/Email/EmailTest.php b/tests/system/Email/EmailTest.php index fc94686589d1..a54bf8d90ce5 100644 --- a/tests/system/Email/EmailTest.php +++ b/tests/system/Email/EmailTest.php @@ -713,7 +713,8 @@ public function testSetPriorityFallback(): void // Priority 6 is invalid, falls back to 3 $email->setPriority(6); - $this->assertSame(3, $this->getPrivateProperty($email, 'priority')); + $priority = $this->getPrivateProperty($email, 'priority'); + $this->assertSame(3, $priority); // Priority 0 is invalid, falls back to 3 $email->setPriority(0); diff --git a/tests/system/Encryption/Handlers/OpenSSLHandlerTest.php b/tests/system/Encryption/Handlers/OpenSSLHandlerTest.php index bac22a881f0a..01254f31b810 100644 --- a/tests/system/Encryption/Handlers/OpenSSLHandlerTest.php +++ b/tests/system/Encryption/Handlers/OpenSSLHandlerTest.php @@ -149,7 +149,8 @@ public function testInternalKeyNotModifiedByParams(): void $encrypter = $this->encryption->initialize($params); $this->assertInstanceOf(OpenSSLHandler::class, $encrypter); - $this->assertSame('original-key-value', $encrypter->key); + $key = $encrypter->key; + $this->assertSame('original-key-value', $key); $message = 'This is a plain-text message.'; $differentKey = 'temporary-param-key'; diff --git a/tests/system/Encryption/Handlers/SodiumHandlerTest.php b/tests/system/Encryption/Handlers/SodiumHandlerTest.php index 47406cef00d0..fe63cec36fb9 100644 --- a/tests/system/Encryption/Handlers/SodiumHandlerTest.php +++ b/tests/system/Encryption/Handlers/SodiumHandlerTest.php @@ -139,7 +139,8 @@ public function testInternalKeyNotModifiedByParams(): void $encrypter = $this->encryption->initialize($this->config); $this->assertInstanceOf(SodiumHandler::class, $encrypter); - $this->assertSame($originalKey, $encrypter->key); + $key = $encrypter->key; + $this->assertSame($originalKey, $key); $message = 'This is a plain-text message.'; $differentKey = sodium_crypto_secretbox_keygen(); diff --git a/tests/system/Entity/EntityTest.php b/tests/system/Entity/EntityTest.php index be8f64f31775..3aff3fac3a10 100644 --- a/tests/system/Entity/EntityTest.php +++ b/tests/system/Entity/EntityTest.php @@ -373,8 +373,9 @@ public function testCastInteger(): void $entity->first = 3.1; - $this->assertIsInt($entity->first); - $this->assertSame(3, $entity->first); + $first = $entity->first; + $this->assertIsInt($first); + $this->assertSame(3, $first); $entity->first = 3.6; diff --git a/tests/system/Events/EventsTest.php b/tests/system/Events/EventsTest.php index bfdef1e2bbe9..39be06b1235b 100644 --- a/tests/system/Events/EventsTest.php +++ b/tests/system/Events/EventsTest.php @@ -69,7 +69,8 @@ public function testInitialize(): void // but we should be able to change it through the backdoor MockEvents::setFiles(['/peanuts']); - $this->assertSame(['/peanuts'], Events::getFiles()); + $files = Events::getFiles(); + $this->assertSame(['/peanuts'], $files); // re-initializing should have no effect MockEvents::initialize(); diff --git a/tests/system/HTTP/IncomingRequestTest.php b/tests/system/HTTP/IncomingRequestTest.php index 90e6c7d57e90..464b46e2ce2c 100644 --- a/tests/system/HTTP/IncomingRequestTest.php +++ b/tests/system/HTTP/IncomingRequestTest.php @@ -1057,7 +1057,8 @@ public function testGetIPAddressNormal(): void $request = new Request(new App()); $request->populateHeaders(); - $this->assertSame($expected, $request->getIPAddress()); + $ipAddress = $request->getIPAddress(); + $this->assertSame($expected, $ipAddress); // call a second time to exercise the initial conditional block in getIPAddress() $this->assertSame($expected, $request->getIPAddress()); } diff --git a/tests/system/HTTP/RequestTest.php b/tests/system/HTTP/RequestTest.php index e1f70bdd272f..2dafdc4fd792 100644 --- a/tests/system/HTTP/RequestTest.php +++ b/tests/system/HTTP/RequestTest.php @@ -562,7 +562,8 @@ public function testGetIPAddressNormal(): void $expected = '123.123.123.123'; service('superglobals')->setServer('REMOTE_ADDR', $expected); $this->request = new Request(new App()); - $this->assertSame($expected, $this->request->getIPAddress()); + $ipAddress = $this->request->getIPAddress(); + $this->assertSame($expected, $ipAddress); // call a second time to exercise the initial conditional block in getIPAddress() $this->assertSame($expected, $this->request->getIPAddress()); } diff --git a/tests/system/Helpers/FormHelperTest.php b/tests/system/Helpers/FormHelperTest.php index 73cbe2430a5f..c24d34b04a3e 100644 --- a/tests/system/Helpers/FormHelperTest.php +++ b/tests/system/Helpers/FormHelperTest.php @@ -870,7 +870,8 @@ public function testSetValue(): void public function testSetSelect(): void { $_SESSION['_ci_old_input']['post']['foo'] = 'bar'; - $this->assertSame(' selected="selected"', set_select('foo', 'bar')); + $selected = set_select('foo', 'bar'); + $this->assertSame(' selected="selected"', $selected); $_SESSION['_ci_old_input']['post']['foo'] = ['foo' => 'bar']; $this->assertSame(' selected="selected"', set_select('foo', 'bar')); @@ -890,7 +891,8 @@ public function testSetCheckbox(): void ], ]; - $this->assertSame(' checked="checked"', set_checkbox('foo', 'bar')); + $checked = set_checkbox('foo', 'bar'); + $this->assertSame(' checked="checked"', $checked); $_SESSION = [ '_ci_old_input' => [ @@ -919,7 +921,8 @@ public function testSetCheckboxWithValueZero(): void ], ]; - $this->assertSame(' checked="checked"', set_checkbox('foo', '0')); + $checked = set_checkbox('foo', '0'); + $this->assertSame(' checked="checked"', $checked); $_SESSION = [ '_ci_old_input' => [ diff --git a/tests/system/Helpers/TextHelperTest.php b/tests/system/Helpers/TextHelperTest.php index 212f2797bacf..a4702db4b604 100644 --- a/tests/system/Helpers/TextHelperTest.php +++ b/tests/system/Helpers/TextHelperTest.php @@ -161,7 +161,8 @@ public function testIncrementString(): void $this->assertSame('my-test_1', increment_string('my-test')); $this->assertSame('my-test-1', increment_string('my-test', '-')); $this->assertSame('file_5', increment_string('file_4')); - $this->assertSame('file-5', increment_string('file-4', '-')); + $incremented = increment_string('file-4', '-'); + $this->assertSame('file-5', $incremented); $this->assertSame('file-5', increment_string('file-4', '-')); $this->assertSame('file-1', increment_string('file', '-', 1)); $this->assertSame('124', increment_string('123', '')); diff --git a/tests/system/Language/LanguageTest.php b/tests/system/Language/LanguageTest.php index 6233941e7d75..b67292da2236 100644 --- a/tests/system/Language/LanguageTest.php +++ b/tests/system/Language/LanguageTest.php @@ -338,10 +338,12 @@ public function testLangKeepLocale(): void $lang = service('language', 'en', true); lang('Language.languageGetLineInvalidArgumentException'); - $this->assertSame('en', $lang->getLocale()); + $locale = $lang->getLocale(); + $this->assertSame('en', $locale); lang('Language.languageGetLineInvalidArgumentException', [], 'ru'); - $this->assertSame('en', $lang->getLocale()); + $locale = $lang->getLocale(); + $this->assertSame('en', $locale); lang('Language.languageGetLineInvalidArgumentException'); $this->assertSame('en', $lang->getLocale()); diff --git a/tests/system/Session/Handlers/Database/RedisHandlerTest.php b/tests/system/Session/Handlers/Database/RedisHandlerTest.php index 637132669aec..c2f549e31659 100644 --- a/tests/system/Session/Handlers/Database/RedisHandlerTest.php +++ b/tests/system/Session/Handlers/Database/RedisHandlerTest.php @@ -142,7 +142,8 @@ public function testSecondaryReadAfterClose(): void $expected = <<<'DATA' __ci_last_regenerate|i:1664607454;_ci_previous_url|s:32:"http://localhost:8080/index.php/";key|s:5:"value"; DATA; - $this->assertSame($expected, $handler->read('555556b43phsnnf8if6bo33b635e4447')); + $data = $handler->read('555556b43phsnnf8if6bo33b635e4447'); + $this->assertSame($expected, $data); $handler->close(); diff --git a/tests/system/Throttle/ThrottleTest.php b/tests/system/Throttle/ThrottleTest.php index 98751a86781b..b16606341ab7 100644 --- a/tests/system/Throttle/ThrottleTest.php +++ b/tests/system/Throttle/ThrottleTest.php @@ -39,7 +39,8 @@ public function testTokenTime(): void $throttler = new Throttler($this->cache); // tokenTime should be 0 to start - $this->assertSame(0, $throttler->getTokenTime()); + $tokenTime = $throttler->getTokenTime(); + $this->assertSame(0, $tokenTime); // set $rate $rate = 1; // allow 1 request per minute diff --git a/tests/system/View/ParserTest.php b/tests/system/View/ParserTest.php index 95d807729c58..36e8707430d6 100644 --- a/tests/system/View/ParserTest.php +++ b/tests/system/View/ParserTest.php @@ -962,7 +962,8 @@ public function testCachedRender(): void $this->parser->setVar('teststring', 'Hello World'); $expected = "

Hello World

\n"; - $this->assertSame($expected, $this->parser->render('template1', ['cache' => 10, 'cache_name' => 'HelloWorld'])); + $output = $this->parser->render('template1', ['cache' => 10, 'cache_name' => 'HelloWorld']); + $this->assertSame($expected, $output); // this second renderings should go thru the cache $this->assertSame($expected, $this->parser->render('template1', ['cache' => 10, 'cache_name' => 'HelloWorld'])); } From d734e61868c9225468debb509f31be7a3e4c9e7d Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Sun, 27 Sep 2026 09:30:13 +0200 Subject: [PATCH 141/163] fix: reject non-digit characters in `valid_cc_number()` (#10580) --- system/Validation/CreditCardRules.php | 3 +-- .../Validation/StrictRules/CreditCardRulesTest.php | 10 ++++++++++ user_guide_src/source/changelogs/v4.7.5.rst | 1 + 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/system/Validation/CreditCardRules.php b/system/Validation/CreditCardRules.php index a31b303fcb86..0bc20f082123 100644 --- a/system/Validation/CreditCardRules.php +++ b/system/Validation/CreditCardRules.php @@ -199,8 +199,7 @@ public function valid_cc_number(?string $ccNumber, string $type): bool // Remove any spaces and dashes $ccNumber = str_replace([' ', '-'], '', $ccNumber); - // Non-numeric values cannot be a number...duh - if (! is_numeric($ccNumber)) { + if (! ctype_digit($ccNumber)) { return false; } diff --git a/tests/system/Validation/StrictRules/CreditCardRulesTest.php b/tests/system/Validation/StrictRules/CreditCardRulesTest.php index deda7d76f984..55a880839662 100644 --- a/tests/system/Validation/StrictRules/CreditCardRulesTest.php +++ b/tests/system/Validation/StrictRules/CreditCardRulesTest.php @@ -100,6 +100,16 @@ public static function provideValidCCNumber(): iterable 'abcd efgh ijkl mnop', false, ], + 'decimal_point_visa' => [ + 'visa', + '41.1111111111111', + false, + ], + 'decimal_point_mastercard' => [ + 'mastercard', + '5351367.37861108', + false, + ], 'bad_length' => [ 'amex', '3782 8224 6310 0051', diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index 5fc0cfce5578..8da98205bf39 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -59,6 +59,7 @@ Bugs Fixed - **Honeypot:** Fixed a bug where bot detection returned an HTTP 500 response instead of 403 (Forbidden). - **I18n:** Fixed a bug where ``Time::today()``, ``Time::yesterday()``, and ``Time::tomorrow()`` ignored the specified ``$timezone`` and ``setTestNow()`` when calculating the day. - **Logger:** Fixed a bug where interpolating a log message with array or non-stringable context values could raise PHP warnings or errors. +- **Validation:** Fixed a bug where ``valid_cc_number`` accepted non-digit characters (e.g., a decimal point) in the card number. Such values could pass the Luhn check and triggered an ``Undefined array key`` warning inside it; the number is now checked with ``ctype_digit()``. See the repo's `CHANGELOG.md `_ From 0785bf50e99c308041ab3f80744c7b307be5cfa0 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sun, 27 Sep 2026 19:15:34 +0800 Subject: [PATCH 142/163] chore: auto-close stale "waiting for info" items with Carson's `no-response-closer` (#10585) --- .github/carson.yml | 7 +++++++ .github/workflows/run-carson.yml | 5 ++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/carson.yml b/.github/carson.yml index 85bdc33aeca5..dcab95964d48 100644 --- a/.github/carson.yml +++ b/.github/carson.yml @@ -56,6 +56,13 @@ settings: Closing this issue: the description still does not follow the bug report template after {{days_until_close}} days. Please open a new issue using the [bug report form](https://github.com/codeigniter4/CodeIgniter4/issues/new?assignees=&labels=bug&projects=&template=bug_report.yml&title=Bug%3A+). For feature requests or support questions, please use the [forums](https://forum.codeigniter.com/forum-30.html). + - label: waiting for info + days_until_close: 14 + unlabel_on_response: true + close_message: |- + Closing this {{type}}: there has been no response for {{days_until_close}} days after more information was requested. + + If you can provide the requested details, leave a comment and a maintainer can reopen it. pr-title-linter: name: 'Carson / pr-title-linter' rules: diff --git a/.github/workflows/run-carson.yml b/.github/workflows/run-carson.yml index 1f404d1d2a33..2e8c46ba4a8e 100644 --- a/.github/workflows/run-carson.yml +++ b/.github/workflows/run-carson.yml @@ -14,6 +14,9 @@ on: - opened - edited - labeled + issue_comment: + types: + - created push: branches: - develop @@ -28,7 +31,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Run Carson - uses: NexusPHP/carson@e4b2fde33f32c47455df218cf1765702c15c3b8b # v1.6.0 + uses: NexusPHP/carson@cb35604ab330c2e44dba673fcef712c78e75f858 # v1.7.0 with: app_id: ${{ secrets.CARSON_APP_ID }} private_key: ${{ secrets.CARSON_PRIVATE_KEY }} From 28bb14c923df4d83d24072eacd51cb85f9b2cea8 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Sun, 27 Sep 2026 19:18:00 +0800 Subject: [PATCH 143/163] refactor: tighten docblock return types in Result, CLIRequest, url_helper and IncomingRequest (#10588) --- system/Database/ResultInterface.php | 14 +++++++++----- system/HTTP/CLIRequest.php | 12 ++++++------ system/HTTP/IncomingRequest.php | 4 ++-- system/Helpers/url_helper.php | 6 ++++-- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/system/Database/ResultInterface.php b/system/Database/ResultInterface.php index 48de9f01c3d4..afe6ede83390 100644 --- a/system/Database/ResultInterface.php +++ b/system/Database/ResultInterface.php @@ -26,18 +26,22 @@ interface ResultInterface * individual data rows, which can be either an 'array', an * 'object', or a custom class name. * - * @param string $type The row type. Either 'array', 'object', or a class name to use + * @template T of object + * + * @param 'array'|'object'|class-string $type The row type. Either 'array', 'object', or a class name to use * - * @return ($type is 'array' ? list> : ($type is 'object' ? list : list)) + * @return ($type is 'array' ? list> : ($type is 'object' ? list : list)) */ public function getResult(string $type = 'object'): array; /** * Returns the results as an array of custom objects. * - * @param string $className The name of the class to use. + * @template T of object + * + * @param class-string $className The name of the class to use. * - * @return list + * @return list */ public function getCustomResultObject(string $className); @@ -102,7 +106,7 @@ public function getRowArray(int $n = 0); * * If row doesn't exist, returns null. * - * @return object|stdClass|null + * @return stdClass|null */ public function getRowObject(int $n = 0); diff --git a/system/HTTP/CLIRequest.php b/system/HTTP/CLIRequest.php index f1e98c50bf79..55c8b797b094 100644 --- a/system/HTTP/CLIRequest.php +++ b/system/HTTP/CLIRequest.php @@ -236,7 +236,7 @@ public function isCLI(): bool * @param int|null $filter A filter name to apply. * @param array|int|null $flags * - * @return array{}|null + * @return ($index is string ? null : array{}) */ public function getGet($index = null, $filter = null, $flags = null) { @@ -250,7 +250,7 @@ public function getGet($index = null, $filter = null, $flags = null) * @param int|null $filter A filter name to apply * @param array|int|null $flags * - * @return array{}|null + * @return ($index is string ? null : array{}) */ public function getPost($index = null, $filter = null, $flags = null) { @@ -264,7 +264,7 @@ public function getPost($index = null, $filter = null, $flags = null) * @param int|null $filter A filter name to apply * @param array|int|null $flags * - * @return array{}|null + * @return ($index is string ? null : array{}) */ public function getPostGet($index = null, $filter = null, $flags = null) { @@ -278,7 +278,7 @@ public function getPostGet($index = null, $filter = null, $flags = null) * @param int|null $filter A filter name to apply * @param array|int|null $flags * - * @return array{}|null + * @return ($index is string ? null : array{}) */ public function getGetPost($index = null, $filter = null, $flags = null) { @@ -292,7 +292,7 @@ public function getGetPost($index = null, $filter = null, $flags = null) * @param int|null $filter A filter name to be applied * @param mixed $flags * - * @return array{}|null + * @return ($index is string ? null : array{}) */ public function getCookie($index = null, $filter = null, $flags = null) { @@ -302,7 +302,7 @@ public function getCookie($index = null, $filter = null, $flags = null) /** * @param list|string|null $index * - * @return array{}|null + * @return ($index is string ? null : array{}) */ private function returnNullOrEmptyArray($index) { diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 5eb029df8385..cc66e35f4ddc 100644 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -403,7 +403,7 @@ public function getVar($index = null, $filter = null, $flags = null) * * @see http://php.net/manual/en/function.json-decode.php * - * @return array|bool|float|int|stdClass|null + * @return ($assoc is true ? array|bool|float|int|string|null : array|bool|float|int|stdClass|string|null) * * @throws HTTPException When the body is invalid as JSON. */ @@ -430,7 +430,7 @@ public function getJSON(bool $assoc = false, int $depth = 512, int $options = 0) * @param int|null $filter Filter Constant * @param array|int|null $flags Option * - * @return array|bool|float|int|stdClass|string|null + * @return ($assoc is true ? array|bool|float|int|string|null : array|bool|float|int|stdClass|string|null) */ public function getJsonVar($index = null, bool $assoc = false, ?int $filter = null, $flags = null) { diff --git a/system/Helpers/url_helper.php b/system/Helpers/url_helper.php index e43954909d65..c08187c6c40b 100644 --- a/system/Helpers/url_helper.php +++ b/system/Helpers/url_helper.php @@ -68,8 +68,8 @@ function base_url($relativePath = '', ?string $scheme = null): string * @param bool $returnObject True to return an object instead of a string * @param IncomingRequest|null $request A request to use when retrieving the path * - * @return string|URI When returning string, the query and fragment parts are removed. - * When returning URI, the query and fragment parts are preserved. + * @return ($returnObject is true ? URI : string) When returning string, the query and fragment parts are removed. + * When returning URI, the query and fragment parts are preserved. */ function current_url(bool $returnObject = false, ?IncomingRequest $request = null): string|URI { @@ -87,6 +87,8 @@ function current_url(bool $returnObject = false, ?IncomingRequest $request = nul * we first check in a saved session variable, if it exists, and use that. * If that's not available, however, we'll use a sanitized url from $_SERVER['HTTP_REFERER'] * which can be set by the user so is untrusted and not set by certain browsers/servers. + * + * @return ($returnObject is true ? URI : string) */ function previous_url(bool $returnObject = false): string|URI { From fba436206823db34659adb1e41811b726fc065db Mon Sep 17 00:00:00 2001 From: Waqas Ahmed Date: Sun, 27 Sep 2026 16:56:30 +0500 Subject: [PATCH 144/163] fix: throw Postgre query errors with warnings disabled (#10573) * fix: throw Postgre query errors with warnings disabled (#6913) * style: satisfy Rector for Postgre errors (#6913) * docs: add changelog entry for Postgre query errors (#6913) --- system/Database/Postgre/Connection.php | 8 +++++++- tests/system/Database/Live/BadQueryTest.php | 18 ++++++++++++++++++ user_guide_src/source/changelogs/v4.7.5.rst | 1 + 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php index a152b90aa539..6f6233d14b74 100644 --- a/system/Database/Postgre/Connection.php +++ b/system/Database/Postgre/Connection.php @@ -207,7 +207,13 @@ public function getVersion(): string protected function execute(string $sql) { try { - return pg_query($this->connID, $sql); + $result = pg_query($this->connID, $sql); + + if ($result === false && $this->DBDebug) { + throw new DatabaseException(pg_last_error($this->connID)); + } + + return $result; } catch (ErrorException $e) { $trace = array_slice($e->getTrace(), 2); // remove the call to error handler diff --git a/tests/system/Database/Live/BadQueryTest.php b/tests/system/Database/Live/BadQueryTest.php index 8b842b21b930..587cabc3bdec 100644 --- a/tests/system/Database/Live/BadQueryTest.php +++ b/tests/system/Database/Live/BadQueryTest.php @@ -53,4 +53,22 @@ public function testBadQueryDebugFalse(): void $this->enableDBDebug(); } + + public function testPostgreBadQueryDebugTrueWithWarningsDisabled(): void + { + if ($this->db->DBDriver !== 'Postgre') { + $this->markTestSkipped('This test is only for Postgre.'); + } + + $this->enableDBDebug(); + $errorReporting = error_reporting(E_ALL & ~E_WARNING); + + try { + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('table_does_not_exist'); + $this->db->query('SELECT * FROM table_does_not_exist'); + } finally { + error_reporting($errorReporting); + } + } } diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index 8da98205bf39..deabaf4b7ab4 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -53,6 +53,7 @@ Bugs Fixed - **Cookie:** Fixed a bug where ``Cookie`` instances created with ``raw: true`` allowed invalid characters in cookie values rejected by ``setrawcookie()``. - **Cookie:** Fixed a bug where ``Cookie`` instances allowed invalid characters in path, domain, and prefix attributes rejected by ``setcookie()`` and ``setrawcookie()``. - **Database:** Fixed a bug where rebuilding a SQLite3 table (e.g., ``Forge::dropColumn()``, ``Forge::modifyColumn()``, ``Forge::dropForeignKey()`` and ``Forge::dropPrimaryKey()``) corrupted the table names referenced by its foreign keys when ``DBPrefix`` was set. +- **Database:** Fixed a bug where Postgre query failures were silently ignored when ``DBDebug`` was enabled and PHP warnings were disabled. A ``DatabaseException`` is now thrown. - **Debug:** Fixed a bug where ``Timer::start()`` treated ``0.0`` as an empty value and substituted the current time. - **Files:** Fixed a bug where ``File::move()`` and ``UploadedFile::move()`` set executable and overly permissive file permissions (``0777 & ~umask()`` instead of ``0666 & ~umask()``), and ``UploadedFile::move()`` targeted the parent directory instead of the destination file for ``chmod()``. - **Helpers:** Fixed a bug where ``get_dir_file_info()`` returned incomplete entries for subdirectories and missing files instead of omitting them. From 4a5f2facd7c90eaf79af161ecb7457b81dd0e805 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Thu, 1 Oct 2026 15:39:47 +0200 Subject: [PATCH 145/163] chore: run Carson only in the main repository (#10595) --- .github/workflows/run-carson.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/run-carson.yml b/.github/workflows/run-carson.yml index 2e8c46ba4a8e..a0a160b0930d 100644 --- a/.github/workflows/run-carson.yml +++ b/.github/workflows/run-carson.yml @@ -28,6 +28,7 @@ permissions: {} jobs: carson: + if: github.repository == 'codeigniter4/CodeIgniter4' runs-on: ubuntu-24.04 steps: - name: Run Carson From 6274001a50cb1b09bb6d6945009d9b895b1e8a7e Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Thu, 1 Oct 2026 15:58:17 +0200 Subject: [PATCH 146/163] docs: clarify escaping with Parser filters (#10594) --- .../source/outgoing/view_parser.rst | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/user_guide_src/source/outgoing/view_parser.rst b/user_guide_src/source/outgoing/view_parser.rst index ad3e6bf5103c..fbc45140e6bc 100644 --- a/user_guide_src/source/outgoing/view_parser.rst +++ b/user_guide_src/source/outgoing/view_parser.rst @@ -320,16 +320,25 @@ In this case, you will write code in your template:: Escaping Data ============= -By default, all variable substitution is escaped to help prevent XSS attacks on your pages. CodeIgniter's ``esc()`` method -supports several different contexts, like general ``html``, when it's in an HTML ``attr``, in ``css``, etc. If nothing -else is specified, the data will be assumed to be in an HTML context. You can specify the context used by using the ``esc()`` -filter:: +Variable substitutions without filters are escaped by default to help prevent XSS attacks on your pages. +CodeIgniter's ``esc()`` method supports several contexts, such as ``html``, ``attr``, and ``css``. +If no context is specified, the Parser uses ``html``. You can specify the context with the ``esc`` filter:: { user_styles | esc(css) } { title } -There will be times when you absolutely need something to used and NOT escaped. You can do this by adding exclamation -marks to the opening and closing braces:: +When a substitution uses one or more filters, the Parser does not add an ``esc`` filter automatically. +This also applies when a context is specified with ``setData()`` or ``setVar()``. +Add ``esc`` explicitly when filtering untrusted data. Filters run from left to right, so its position matters:: + + { title|capitalize|esc } + { body|esc|nl2br } + +In the second example, ``esc`` escapes the input before ``nl2br`` adds HTML ``
`` tags. +Escaping after ``nl2br`` would display those tags as text. +Unknown filter names are ignored, but still prevent automatic escaping; check filter names carefully. + +To disable automatic escaping for a substitution without filters, add exclamation marks to its delimiters:: {! unescaped_var !} From 64c4fec5d0acd07ab93fb207b3afc199757f5afd Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Thu, 1 Oct 2026 16:01:40 +0200 Subject: [PATCH 147/163] docs: clarify shared page cache behavior (#10593) --- user_guide_src/source/general/caching.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/user_guide_src/source/general/caching.rst b/user_guide_src/source/general/caching.rst index a5a92e29fe6d..2091cda9d0a7 100644 --- a/user_guide_src/source/general/caching.rst +++ b/user_guide_src/source/general/caching.rst @@ -26,6 +26,12 @@ of time that a page should remain cached before being refreshed. request is also taken into account. This means that the same URI will be cached separately if the HTTP method is different. +.. warning:: Web Page Caching is shared between requests with the same HTTP + method and URI. Its cache key does not vary by authenticated user, cookies, + authorization headers, ``Cache-Control``, or ``Vary``. Do not use it for + protected or personalized responses, as cached responses may be returned + before authentication and authorization filters run. + When a page is loaded for the first time, the page will be cached using the currently configured cache engine. On subsequent page loads, the cache will be retrieved and sent to the requesting user's browser. From 94e9a166e80ec28cc35764b5248978d9f36b5c50 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Thu, 1 Oct 2026 23:54:52 +0800 Subject: [PATCH 148/163] chore: keep manual enhancement labels and accept two-digit versions in PR titles (#10591) --- .github/carson.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/carson.yml b/.github/carson.yml index dcab95964d48..07c841b7a021 100644 --- a/.github/carson.yml +++ b/.github/carson.yml @@ -13,6 +13,8 @@ subscribers: settings: auto-labeler: sync_labels: true + sync_exempt: + - enhancement rules: - label: '4.8' # @todo change value whenever the next minor version is changed base_branch: ['^4\.8$'] @@ -66,7 +68,7 @@ settings: pr-title-linter: name: 'Carson / pr-title-linter' rules: - - pattern: '^(\[\d+\.\d+\]\s{1})?(feat|fix|chore|docs|perf|refactor|style|test|config|revert)(\([\-.@:`a-zA-Z0-9]+\))?!?:\s{1}\S.+\S|Prep for \d\.\d\.\d release|\d\.\d\.\d (Ready|Merge) code$' + - pattern: '^(?:(?:\[\d+\.\d+\] )?(?:feat|fix|chore|docs|perf|refactor|style|test|config|revert)(?:\([\-.@:`a-zA-Z0-9]+\))?!?: \S.+\S|Prep for \d+\.\d+\.\d+ release|\d+\.\d+\.\d+ (?:Ready|Merge) code)$' description: 'PR title must include the type (feat, fix, chore, docs, perf, refactor, style, test, config, revert) of the commit per Conventional Commits specification. See https://www.conventionalcommits.org/en/v1.0.0/ for the discussion.' level: error signed-commits: From 9d671be278e8c50a8ee2fc8448446c36f909be96 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Tue, 6 Oct 2026 19:19:09 +0800 Subject: [PATCH 149/163] refactor: pass the session TTL to `Redis::set()` as an options array (#10600) --- system/Session/Handlers/RedisHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Session/Handlers/RedisHandler.php b/system/Session/Handlers/RedisHandler.php index a6cce214f6d3..0ce94ce80791 100644 --- a/system/Session/Handlers/RedisHandler.php +++ b/system/Session/Handlers/RedisHandler.php @@ -273,7 +273,7 @@ public function write($id, $data): bool $this->redis->expire($this->lockKey, 300); if ($this->fingerprint !== ($fingerprint = md5($data)) || $this->keyExists === false) { - if ($this->redis->set($this->keyPrefix . $id, $data, $this->sessionExpiration)) { + if ($this->redis->set($this->keyPrefix . $id, $data, ['ex' => $this->sessionExpiration])) { $this->fingerprint = $fingerprint; $this->keyExists = true; From 092c8f3d914afe4a7567695d55ee368ea9267e95 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Tue, 6 Oct 2026 18:32:42 +0700 Subject: [PATCH 150/163] chore: bump Rector to 2.7.0 and clean up duplicated registered rules (#10603) * Bump Rector to 2.7.0 and clean up duplicated registered rules * re-run rector * flip whole block test assert --- composer.json | 2 +- rector.php | 2 - tests/system/Database/Live/ForgeTest.php | 38 +++++++++---------- .../Live/OCI8/CallStoredProcedureTest.php | 2 +- tests/system/HTTP/CLIRequestTest.php | 2 +- tests/system/Router/RouteCollectionTest.php | 8 ++-- tests/system/Router/RouterTest.php | 2 +- tests/system/View/ParserFilterTest.php | 6 --- 8 files changed, 27 insertions(+), 35 deletions(-) diff --git a/composer.json b/composer.json index b291ea49c21c..9aa9641b8e6d 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "phpunit/phpcov": "^9.0.2 || ^10.0", "phpunit/phpunit": "^10.5.16 || ^11.2", "predis/predis": "^3.0", - "rector/rector": "2.6.7", + "rector/rector": "2.7.0", "shipmonk/phpstan-baseline-per-identifier": "^2.0" }, "replace": { diff --git a/rector.php b/rector.php index 5b4c50db1c66..2b3c36597dae 100644 --- a/rector.php +++ b/rector.php @@ -15,7 +15,6 @@ use Rector\CodeQuality\Rector\BooleanNot\NegatedAndsToPositiveOrsRector; use Rector\CodeQuality\Rector\ClassMethod\LocallyCalledStaticMethodToNonStaticRector; use Rector\CodeQuality\Rector\FuncCall\CompactToVariablesRector; -use Rector\CodeQuality\Rector\FunctionLike\SimplifyUselessVariableRector; use Rector\CodeQuality\Rector\Isset_\IssetOnPropertyObjectToPropertyExistsRector; use Rector\CodingStyle\Rector\ClassMethod\FuncGetArgsToVariadicParamRector; use Rector\CodingStyle\Rector\ClassMethod\MakeInheritedMethodVisibilitySameAsParentRector; @@ -174,7 +173,6 @@ ->withRules([ DeclareStrictTypesRector::class, UnderscoreToCamelCaseVariableNameRector::class, - SimplifyUselessVariableRector::class, PassStrictParameterToFunctionParameterRector::class, RemoveErrorSuppressInTryCatchStmtsRector::class, FuncGetArgsToVariadicParamRector::class, diff --git a/tests/system/Database/Live/ForgeTest.php b/tests/system/Database/Live/ForgeTest.php index 39433abde857..8c5b80e1c8d7 100644 --- a/tests/system/Database/Live/ForgeTest.php +++ b/tests/system/Database/Live/ForgeTest.php @@ -544,10 +544,10 @@ public function testForeignKey(): void } $this->assertSame($foreignKeyData[$foreignKeyName]->constraint_name, $foreignKeyName); - $this->assertSame($foreignKeyData[$foreignKeyName]->column_name, ['users_id']); - $this->assertSame($foreignKeyData[$foreignKeyName]->foreign_column_name, ['id']); - $this->assertSame($foreignKeyData[$foreignKeyName]->table_name, $this->db->DBPrefix . $tableName); - $this->assertSame($foreignKeyData[$foreignKeyName]->foreign_table_name, $this->db->DBPrefix . 'forge_test_users'); + $this->assertSame(['users_id'], $foreignKeyData[$foreignKeyName]->column_name); + $this->assertSame(['id'], $foreignKeyData[$foreignKeyName]->foreign_column_name); + $this->assertSame($this->db->DBPrefix . $tableName, $foreignKeyData[$foreignKeyName]->table_name); + $this->assertSame($this->db->DBPrefix . 'forge_test_users', $foreignKeyData[$foreignKeyName]->foreign_table_name); $this->forge->dropTable($tableName, true); $this->forge->dropTable('forge_test_users', true); @@ -1170,63 +1170,63 @@ public function testCompositeKey(): void if ($this->db->DBDriver === 'MySQLi') { $this->assertSame('PRIMARY', $keys['PRIMARY']->name); - $this->assertSame($keys['PRIMARY']->fields, ['id']); + $this->assertSame(['id'], $keys['PRIMARY']->fields); $this->assertSame('PRIMARY', $keys['PRIMARY']->type); $this->assertSame('code_company', $keys['code_company']->name); - $this->assertSame($keys['code_company']->fields, ['code', 'company']); + $this->assertSame(['code', 'company'], $keys['code_company']->fields); $this->assertSame('INDEX', $keys['code_company']->type); $this->assertSame('code_active', $keys['code_active']->name); - $this->assertSame($keys['code_active']->fields, ['code', 'active']); + $this->assertSame(['code', 'active'], $keys['code_active']->fields); $this->assertSame('UNIQUE', $keys['code_active']->type); } elseif ($this->db->DBDriver === 'Postgre') { $this->assertSame('pk_db_forge_test_1', $keys['pk_db_forge_test_1']->name); - $this->assertSame($keys['pk_db_forge_test_1']->fields, ['id']); + $this->assertSame(['id'], $keys['pk_db_forge_test_1']->fields); $this->assertSame('PRIMARY', $keys['pk_db_forge_test_1']->type); $this->assertSame('db_forge_test_1_code_company', $keys['db_forge_test_1_code_company']->name); - $this->assertSame($keys['db_forge_test_1_code_company']->fields, ['code', 'company']); + $this->assertSame(['code', 'company'], $keys['db_forge_test_1_code_company']->fields); $this->assertSame('INDEX', $keys['db_forge_test_1_code_company']->type); $this->assertSame('db_forge_test_1_code_active', $keys['db_forge_test_1_code_active']->name); - $this->assertSame($keys['db_forge_test_1_code_active']->fields, ['code', 'active']); + $this->assertSame(['code', 'active'], $keys['db_forge_test_1_code_active']->fields); $this->assertSame('UNIQUE', $keys['db_forge_test_1_code_active']->type); } elseif ($this->db->DBDriver === 'SQLite3') { $this->assertSame('PRIMARY', $keys['PRIMARY']->name); - $this->assertSame($keys['PRIMARY']->fields, ['id']); + $this->assertSame(['id'], $keys['PRIMARY']->fields); $this->assertSame('PRIMARY', $keys['PRIMARY']->type); $this->assertSame('db_forge_test_1_code_company', $keys['db_forge_test_1_code_company']->name); - $this->assertSame($keys['db_forge_test_1_code_company']->fields, ['code', 'company']); + $this->assertSame(['code', 'company'], $keys['db_forge_test_1_code_company']->fields); $this->assertSame('INDEX', $keys['db_forge_test_1_code_company']->type); $this->assertSame('db_forge_test_1_code_active', $keys['db_forge_test_1_code_active']->name); - $this->assertSame($keys['db_forge_test_1_code_active']->fields, ['code', 'active']); + $this->assertSame(['code', 'active'], $keys['db_forge_test_1_code_active']->fields); $this->assertSame('UNIQUE', $keys['db_forge_test_1_code_active']->type); } elseif ($this->db->DBDriver === 'SQLSRV') { $this->assertSame('pk_db_forge_test_1', $keys['pk_db_forge_test_1']->name); - $this->assertSame($keys['pk_db_forge_test_1']->fields, ['id']); + $this->assertSame(['id'], $keys['pk_db_forge_test_1']->fields); $this->assertSame('PRIMARY', $keys['pk_db_forge_test_1']->type); $this->assertSame('db_forge_test_1_code_company', $keys['db_forge_test_1_code_company']->name); - $this->assertSame($keys['db_forge_test_1_code_company']->fields, ['code', 'company']); + $this->assertSame(['code', 'company'], $keys['db_forge_test_1_code_company']->fields); $this->assertSame('INDEX', $keys['db_forge_test_1_code_company']->type); $this->assertSame('db_forge_test_1_code_active', $keys['db_forge_test_1_code_active']->name); - $this->assertSame($keys['db_forge_test_1_code_active']->fields, ['code', 'active']); + $this->assertSame(['code', 'active'], $keys['db_forge_test_1_code_active']->fields); $this->assertSame('UNIQUE', $keys['db_forge_test_1_code_active']->type); } elseif ($this->db->DBDriver === 'OCI8') { $this->assertSame('pk_db_forge_test_1', $keys['pk_db_forge_test_1']->name); - $this->assertSame($keys['pk_db_forge_test_1']->fields, ['id']); + $this->assertSame(['id'], $keys['pk_db_forge_test_1']->fields); $this->assertSame('PRIMARY', $keys['pk_db_forge_test_1']->type); $this->assertSame('db_forge_test_1_code_company', $keys['db_forge_test_1_code_company']->name); - $this->assertSame($keys['db_forge_test_1_code_company']->fields, ['code', 'company']); + $this->assertSame(['code', 'company'], $keys['db_forge_test_1_code_company']->fields); $this->assertSame('INDEX', $keys['db_forge_test_1_code_company']->type); $this->assertSame('db_forge_test_1_code_active', $keys['db_forge_test_1_code_active']->name); - $this->assertSame($keys['db_forge_test_1_code_active']->fields, ['code', 'active']); + $this->assertSame(['code', 'active'], $keys['db_forge_test_1_code_active']->fields); $this->assertSame('UNIQUE', $keys['db_forge_test_1_code_active']->type); } diff --git a/tests/system/Database/Live/OCI8/CallStoredProcedureTest.php b/tests/system/Database/Live/OCI8/CallStoredProcedureTest.php index 8404e2378ae2..50a034e36dc2 100644 --- a/tests/system/Database/Live/OCI8/CallStoredProcedureTest.php +++ b/tests/system/Database/Live/OCI8/CallStoredProcedureTest.php @@ -102,6 +102,6 @@ public function testCallStoredProcedureForCursor(): void oci_execute($result); $row = oci_fetch_array($result, OCI_ASSOC + OCI_RETURN_NULLS); - $this->assertSame($row, ['ONE' => '1']); + $this->assertSame(['ONE' => '1'], $row); } } diff --git a/tests/system/HTTP/CLIRequestTest.php b/tests/system/HTTP/CLIRequestTest.php index 91155e445f86..b9ce3dc5f7ce 100644 --- a/tests/system/HTTP/CLIRequestTest.php +++ b/tests/system/HTTP/CLIRequestTest.php @@ -566,7 +566,7 @@ public function testGetCookie(): void { $this->assertNull($this->request->getCookie('TESTY')); - $this->assertSame($this->request->getCookie(), []); + $this->assertSame([], $this->request->getCookie()); } public function testIs(): void diff --git a/tests/system/Router/RouteCollectionTest.php b/tests/system/Router/RouteCollectionTest.php index 5e3e7bd8015f..9ceec705cf6b 100644 --- a/tests/system/Router/RouteCollectionTest.php +++ b/tests/system/Router/RouteCollectionTest.php @@ -1350,7 +1350,7 @@ static function (): void {}, $options = $routes->getRoutesOptions('administrator'); - $this->assertSame($options, ['as' => 'admin', 'foo' => 'baz']); + $this->assertSame(['as' => 'admin', 'foo' => 'baz'], $options); } /** @@ -1459,15 +1459,15 @@ static function (): void {}, $options = $routes->getRoutesOptions('administrator'); - $this->assertSame($options, ['as' => 'admin1', 'foo' => 'baz1', 'bar' => 'baz']); + $this->assertSame(['as' => 'admin1', 'foo' => 'baz1', 'bar' => 'baz'], $options); $options = $routes->setHTTPVerb(Method::POST)->getRoutesOptions('administrator'); - $this->assertSame($options, ['as' => 'admin2', 'foo' => 'baz2', 'bar' => 'baz']); + $this->assertSame(['as' => 'admin2', 'foo' => 'baz2', 'bar' => 'baz'], $options); $options = $routes->setHTTPVerb(Method::GET)->getRoutesOptions('administrator', Method::POST); - $this->assertSame($options, ['as' => 'admin2', 'foo' => 'baz2', 'bar' => 'baz']); + $this->assertSame(['as' => 'admin2', 'foo' => 'baz2', 'bar' => 'baz'], $options); } public function testRouteGroupWithFilterSimple(): void diff --git a/tests/system/Router/RouterTest.php b/tests/system/Router/RouterTest.php index ec06de96e0b3..6e22c95ed4f9 100644 --- a/tests/system/Router/RouterTest.php +++ b/tests/system/Router/RouterTest.php @@ -550,7 +550,7 @@ public function testMatchedRouteOptions(): void $router->handle('foo'); - $this->assertSame($router->getMatchedRouteOptions(), ['as' => 'login', 'foo' => 'baz']); + $this->assertSame(['as' => 'login', 'foo' => 'baz'], $router->getMatchedRouteOptions()); } public function testRouteWorksWithFilters(): void diff --git a/tests/system/View/ParserFilterTest.php b/tests/system/View/ParserFilterTest.php index 5761d0fbb74b..3e4c4d40d9b6 100644 --- a/tests/system/View/ParserFilterTest.php +++ b/tests/system/View/ParserFilterTest.php @@ -181,12 +181,6 @@ public function testHighlightCode(): void $parser->setData($data); $template = '{ value1|highlight_code }'; - $expected = <<<'EOF' - - Sincerely  - - - EOF; // PHP 8.3 changes the output. if (PHP_VERSION_ID >= 80300) { From 99c3f2442b10ca6a383ec4384a6e8f33e793e6e4 Mon Sep 17 00:00:00 2001 From: MD Ali Kadar Date: Fri, 9 Oct 2026 01:15:18 +0600 Subject: [PATCH 151/163] fix: prevent infinite loop in `word_wrap()` with a character limit below 2 (#10596) * fix: prevent infinite loop in word_wrap() with a character limit below 2 * test: add regression tests for word_wrap() with small character limits * fix: wrap word_wrap() at one character when charlim is 1 * fix: keep long words whole in word_wrap() when charlim is 1 * refactor: drop redundant charlim clamp in word_wrap() * docs: add changelog entry for word_wrap() small charlim fix --- system/Helpers/text_helper.php | 4 +++- tests/system/Helpers/TextHelperTest.php | 21 +++++++++++++++++++++ user_guide_src/source/changelogs/v4.7.5.rst | 1 + 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/system/Helpers/text_helper.php b/system/Helpers/text_helper.php index cf9459100dfb..09d14abbe2ee 100644 --- a/system/Helpers/text_helper.php +++ b/system/Helpers/text_helper.php @@ -383,7 +383,9 @@ function word_wrap(string $str, int $charlim = 76): string $temp = ''; - while (mb_strlen($line) > $charlim) { + // A limit of 1 can't be used to split a word (each piece needs room for + // the continuation), so the word is kept whole, like a long URL. + while ($charlim > 1 && mb_strlen($line) > $charlim) { // If the over-length word is a URL we won't wrap it if (preg_match('!\[url.+\]|://|www\.!', $line)) { break; diff --git a/tests/system/Helpers/TextHelperTest.php b/tests/system/Helpers/TextHelperTest.php index a4702db4b604..88878bcf8294 100644 --- a/tests/system/Helpers/TextHelperTest.php +++ b/tests/system/Helpers/TextHelperTest.php @@ -358,6 +358,27 @@ public function testEllipsize(): void } } + /** + * A character limit below 2 used to make word_wrap() loop forever. + */ + #[DataProvider('provideWordWrapSmallCharLimit')] + public function testWordWrapSmallCharLimit(int $charlim): void + { + $this->assertSame("aaaa\nbbbb", word_wrap('aaaa bbbb', $charlim)); + } + + /** + * @return iterable + */ + public static function provideWordWrapSmallCharLimit(): iterable + { + yield 'one' => [1]; + + yield 'zero' => [0]; + + yield 'negative' => [-5]; + } + public function testWordWrap(): void { $string = 'Here is a simple string of text that will help us demonstrate this function.'; diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index deabaf4b7ab4..492db76c20c7 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -57,6 +57,7 @@ Bugs Fixed - **Debug:** Fixed a bug where ``Timer::start()`` treated ``0.0`` as an empty value and substituted the current time. - **Files:** Fixed a bug where ``File::move()`` and ``UploadedFile::move()`` set executable and overly permissive file permissions (``0777 & ~umask()`` instead of ``0666 & ~umask()``), and ``UploadedFile::move()`` targeted the parent directory instead of the destination file for ``chmod()``. - **Helpers:** Fixed a bug where ``get_dir_file_info()`` returned incomplete entries for subdirectories and missing files instead of omitting them. +- **Helpers:** Fixed a bug where ``word_wrap()`` entered an infinite loop when the character limit was below 2. Over-length words are now kept whole in that case. - **Honeypot:** Fixed a bug where bot detection returned an HTTP 500 response instead of 403 (Forbidden). - **I18n:** Fixed a bug where ``Time::today()``, ``Time::yesterday()``, and ``Time::tomorrow()`` ignored the specified ``$timezone`` and ``setTestNow()`` when calculating the day. - **Logger:** Fixed a bug where interpolating a log message with array or non-stringable context values could raise PHP warnings or errors. From b86f337225f9ac5f6f4a33571e5cd115c7a773c8 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Fri, 9 Oct 2026 19:10:08 +0800 Subject: [PATCH 152/163] fix: resolve schema-qualified table names in `getFieldData()` and `protectIdentifiers()` (#10604) * fix: resolve schema-qualified table names in `getFieldData()` and `protectIdentifiers()` * address reviews --- system/Database/BaseConnection.php | 10 +++++-- system/Database/Postgre/Connection.php | 13 ++++++++-- system/Database/SQLSRV/Connection.php | 8 ++++++ system/Database/SQLite3/Connection.php | 7 ++++- tests/system/Database/BaseConnectionTest.php | 26 +++++++++++++++++++ .../Live/AbstractGetFieldDataTestCase.php | 24 +++++++++++++++++ user_guide_src/source/changelogs/v4.7.5.rst | 1 + 7 files changed, 84 insertions(+), 5 deletions(-) diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index a7af22565cca..1db7618e2971 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -1315,7 +1315,7 @@ public function protectIdentifiers($item, bool $prefixSingle = false, ?bool $pro // In some cases, especially 'from', we end up running through // protect_identifiers twice. This algorithm won't work when // it contains the escapeChar so strip it out. - $item = trim($item, $this->escapeChar); + $item = $this->trimEscapeChar($item); // Is there a table prefix? If not, no need to insert it if ($this->DBPrefix !== '') { @@ -1336,9 +1336,15 @@ public function protectIdentifiers($item, bool $prefixSingle = false, ?bool $pro return $item . $alias; } + private function trimEscapeChar(string $item): string + { + return trim($item, is_array($this->escapeChar) ? implode('', $this->escapeChar) : $this->escapeChar); + } + private function protectDotItem(string $item, string $alias, bool $protectIdentifiers, bool $fieldExists): string { - $parts = explode('.', $item); + $parts = array_map($this->trimEscapeChar(...), explode('.', $item)); + $item = implode('.', $parts); // Does the first segment of the exploded item match // one of the aliases previously identified? If so, diff --git a/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php index 6f6233d14b74..900f8f716c38 100644 --- a/system/Database/Postgre/Connection.php +++ b/system/Database/Postgre/Connection.php @@ -325,11 +325,20 @@ protected function _listColumns($table = ''): string */ protected function _fieldData(string $table): array { + $parts = explode('.', $table); + $table = array_pop($parts); + $schema = array_pop($parts); + $sql = 'SELECT "column_name", "data_type", "character_maximum_length", "numeric_precision", "column_default", "is_nullable" FROM "information_schema"."columns" WHERE LOWER("table_name") = ' - . $this->escape(strtolower($table)) - . ' ORDER BY "ordinal_position"'; + . $this->escape(strtolower($table)); + + if ($schema !== null) { + $sql .= ' AND LOWER("table_schema") = ' . $this->escape(strtolower($schema)); + } + + $sql .= ' ORDER BY "ordinal_position"'; if (($query = $this->query($sql)) === false) { throw new DatabaseException(lang('Database.failGetFieldData')); diff --git a/system/Database/SQLSRV/Connection.php b/system/Database/SQLSRV/Connection.php index e18408c5c34d..2bfbdabc57e3 100644 --- a/system/Database/SQLSRV/Connection.php +++ b/system/Database/SQLSRV/Connection.php @@ -314,12 +314,20 @@ protected function _enableForeignKeyChecks() */ protected function _fieldData(string $table): array { + $parts = explode('.', $table); + $table = array_pop($parts); + $schema = array_pop($parts); + $sql = 'SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, COLUMN_DEFAULT, IS_NULLABLE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME= ' . $this->escape(($table)); + if ($schema !== null) { + $sql .= ' AND TABLE_SCHEMA = ' . $this->escape($schema); + } + if (($query = $this->query($sql)) === false) { throw new DatabaseException(lang('Database.failGetFieldData')); } diff --git a/system/Database/SQLite3/Connection.php b/system/Database/SQLite3/Connection.php index 23ca0eaa5c35..582ab2b4b2b1 100644 --- a/system/Database/SQLite3/Connection.php +++ b/system/Database/SQLite3/Connection.php @@ -270,7 +270,12 @@ public function getFieldNames($tableName) */ protected function _fieldData(string $table): array { - if (false === $query = $this->query('PRAGMA TABLE_INFO(' . $this->protectIdentifiers($table, true, null, false) . ')')) { + $parts = explode('.', $table); + $table = array_pop($parts); + $schema = array_pop($parts); + $pragma = ($schema === null ? '' : $this->escapeIdentifiers($schema) . '.') . 'TABLE_INFO(' . $this->protectIdentifiers($table, true, null, false) . ')'; + + if (false === $query = $this->query('PRAGMA ' . $pragma)) { throw new DatabaseException(lang('Database.failGetFieldData')); } diff --git a/tests/system/Database/BaseConnectionTest.php b/tests/system/Database/BaseConnectionTest.php index 1b3a9841f58e..7052250eab8c 100644 --- a/tests/system/Database/BaseConnectionTest.php +++ b/tests/system/Database/BaseConnectionTest.php @@ -372,6 +372,11 @@ public static function provideProtectIdentifiers(): iterable 'quoted table alias' => [false, true, false, '"jobs" "j"', '"jobs" "j"'], 'quoted table alias prefix' => [true, true, false, '"jobs" "j"', '"test_jobs" "j"'], + 'schema.table' => [false, true, false, 'tenant.jobs', '"tenant"."test_jobs"'], + 'schema.table prefix' => [true, true, false, 'tenant.jobs', '"tenant"."test_jobs"'], + 'quoted schema.table prefix' => [true, true, false, '"tenant"."test_jobs"', '"tenant"."test_jobs"'], + 'quoted schema.table no-protect' => [true, false, false, '"tenant"."test_jobs"', 'tenant.test_jobs'], + 'table.*' => [false, true, true, 'jobs.*', '"test_jobs".*'], // Prefixed because it has segments 'table.* prefix' => [true, true, true, 'jobs.*', '"test_jobs".*'], 'table.column' => [false, true, true, 'users.id', '"test_users"."id"'], // Prefixed because it has segments @@ -434,6 +439,27 @@ public static function provideProtectIdentifiers(): iterable ]; } + public function testProtectIdentifiersWithBracketEscapeChar(): void + { + $db = new MockConnection($this->options); + $db->escapeChar = ['[', ']']; + + $this->assertSame('[test_jobs]', $db->protectIdentifiers('jobs', true, true, false)); + $this->assertSame('[test_jobs]', $db->protectIdentifiers('[test_jobs]', true, true, false)); + $this->assertSame('test_jobs', $db->protectIdentifiers('[test_jobs]', true, false, false)); + $this->assertSame('[test_jobs].[id]', $db->protectIdentifiers('jobs.id')); + $this->assertSame('[test_jobs].[id]', $db->protectIdentifiers('[test_jobs].[id]')); + $this->assertSame('test_jobs.id', $db->protectIdentifiers('[test_jobs].[id]', true, false)); + } + + public function testProtectIdentifiersStripsQuotesFromDottedItemWithoutPrefix(): void + { + $db = new MockConnection([...$this->options, 'DBPrefix' => '']); + + $this->assertSame('public.jobs', $db->protectIdentifiers('"public"."jobs"', true, false, false)); + $this->assertSame('"public"."jobs"', $db->protectIdentifiers('"public"."jobs"', true, true, false)); + } + /** * These tests are intended to confirm the current behavior. */ diff --git a/tests/system/Database/Live/AbstractGetFieldDataTestCase.php b/tests/system/Database/Live/AbstractGetFieldDataTestCase.php index 4749fabd9de7..533e1089c3d6 100644 --- a/tests/system/Database/Live/AbstractGetFieldDataTestCase.php +++ b/tests/system/Database/Live/AbstractGetFieldDataTestCase.php @@ -57,6 +57,30 @@ protected function tearDown(): void $this->forge->dropTable($this->table, true); } + protected function qualifiedTableName(): string + { + return match ($this->db->DBDriver) { + 'MySQLi' => $this->db->getDatabase() . '.' . $this->table, + 'OCI8' => $this->db->username . '.' . $this->table, + 'Postgre' => 'public.' . $this->table, + 'SQLite3' => 'main.' . $this->table, + 'SQLSRV' => 'dbo.' . $this->table, + default => $this->markTestSkipped('No qualified table name is defined for ' . $this->db->DBDriver . '.'), + }; + } + + public function testGetFieldDataWithQualifiedTableName(): void + { + $this->createTableForDefault(); + + $toArray = static fn (stdClass $field): array => (array) $field; + + $this->assertSame( + array_map($toArray, $this->db->getFieldData($this->table)), + array_map($toArray, $this->db->getFieldData($this->qualifiedTableName())), + ); + } + protected function createTableForDefault(): void { $this->forge->dropTable($this->table, true); diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index 492db76c20c7..34dc4dcf41eb 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -54,6 +54,7 @@ Bugs Fixed - **Cookie:** Fixed a bug where ``Cookie`` instances allowed invalid characters in path, domain, and prefix attributes rejected by ``setcookie()`` and ``setrawcookie()``. - **Database:** Fixed a bug where rebuilding a SQLite3 table (e.g., ``Forge::dropColumn()``, ``Forge::modifyColumn()``, ``Forge::dropForeignKey()`` and ``Forge::dropPrimaryKey()``) corrupted the table names referenced by its foreign keys when ``DBPrefix`` was set. - **Database:** Fixed a bug where Postgre query failures were silently ignored when ``DBDebug`` was enabled and PHP warnings were disabled. A ``DatabaseException`` is now thrown. +- **Database:** Fixed a bug where ``getFieldData()`` failed for a schema-qualified table name on Postgre, SQLSRV and SQLite3, and where ``protectIdentifiers()`` prefixed an already protected segment of a dotted identifier a second time. - **Debug:** Fixed a bug where ``Timer::start()`` treated ``0.0`` as an empty value and substituted the current time. - **Files:** Fixed a bug where ``File::move()`` and ``UploadedFile::move()`` set executable and overly permissive file permissions (``0777 & ~umask()`` instead of ``0666 & ~umask()``), and ``UploadedFile::move()`` targeted the parent directory instead of the destination file for ``chmod()``. - **Helpers:** Fixed a bug where ``get_dir_file_info()`` returned incomplete entries for subdirectories and missing files instead of omitting them. From 483c57f36daf63237bc7af75208d3cd45d28b5f7 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Fri, 9 Oct 2026 20:12:57 +0800 Subject: [PATCH 153/163] fix: cast Postgre batch subquery values so `updateBatch()` and `deleteBatch()` accept mixed PHP types (#10601) --- system/Database/Postgre/Builder.php | 70 +++++++-- tests/system/Database/Builder/DeleteTest.php | 47 ++++++ tests/system/Database/Builder/UpdateTest.php | 69 +++++++++ tests/system/Database/Live/DeleteTest.php | 75 ++++++++++ tests/system/Database/Live/UpdateTest.php | 150 +++++++++++++++++++ user_guide_src/source/changelogs/v4.7.5.rst | 1 + 6 files changed, 401 insertions(+), 11 deletions(-) diff --git a/system/Database/Postgre/Builder.php b/system/Database/Postgre/Builder.php index 4e5245bca994..5367ae2a0fc3 100644 --- a/system/Database/Postgre/Builder.php +++ b/system/Database/Postgre/Builder.php @@ -395,14 +395,20 @@ static function ($key, $value) use ($table, $alias, $that): RawSql|string { if (isset($this->QBOptions['setQueryAsData'])) { $data = $this->QBOptions['setQueryAsData']; } else { + $updateFields = $this->QBOptions['updateFields'] ?? []; + $data = implode( " UNION ALL\n", array_map( - static fn ($value): string => 'SELECT ' . implode(', ', array_map( - static fn ($key, $index): string => $index . ' ' . $key, + fn (array $value): string => sprintf('SELECT %s', implode(', ', array_map( + fn (string $key, float|int|string $index): string => sprintf( + '%s %s', + $this->castValue((string) $index, $this->getSourceType($table, $key, $updateFields)), + $key, + ), $keys, $value, - )), + ))), $values, ), ) . "\n"; @@ -411,6 +417,38 @@ static function ($key, $value) use ($table, $alias, $that): RawSql|string { return str_replace('{:_table_:}', $data, $sql); } + /** + * Returns the type shared by every destination column fed from the source key, or null when they differ. + * + * @param array $map Destination column to source key. + */ + private function getSourceType(string $table, string $key, array $map): ?string + { + $columns = array_filter(array_keys($map, $key, true), is_string(...)); + + if ($columns === []) { + return $this->getFieldType($table, $key); + } + + $types = array_unique(array_map(fn (string $column): ?string => $this->getFieldType($table, $column), $columns)); + + return count($types) === 1 ? reset($types) : null; + } + + /** + * Returns the literal cast to the column type, except a numeric literal for a numeric column, which stays as is so a non-integral value never rounds into a match. + */ + private function castValue(string $value, ?string $type): string + { + $isNumericType = in_array(strtolower((string) $type), ['smallint', 'integer', 'bigint', 'numeric', 'real', 'double precision'], true); + + if ($isNumericType && is_numeric($value)) { + return $value; + } + + return $this->cast($value, $type); + } + /** * Returns cast expression. * @@ -437,11 +475,15 @@ private function getFieldType(string $table, string $fieldName): ?string foreach ($this->db->getFieldData($table) as $field) { $type = $field->type; - // If `character` (or `char`) lacks a specifier, it is equivalent - // to `character(1)`. - // See https://www.postgresql.org/docs/current/datatype-character.html - if ($field->type === 'character') { - $type = $field->type . '(' . $field->max_length . ')'; + // information_schema reports enum, domain and array columns as `USER-DEFINED` or `ARRAY`, which are not castable names. + if ($type === 'USER-DEFINED' || $type === 'ARRAY') { + continue; + } + + // `character` without a length is `character(1)` and `character(n)` truncates on cast, + // while `bpchar` keeps the full value. + if ($type === 'character') { + $type = 'bpchar'; } $this->QBOptions['fieldTypes'][$table][$field->name] = $type; @@ -613,14 +655,20 @@ static function ($key, $value) use ($table, $alias, $that): RawSql|string { if (isset($this->QBOptions['setQueryAsData'])) { $data = $this->QBOptions['setQueryAsData']; } else { + $constraints = $this->QBOptions['constraints'] ?? []; + $data = implode( " UNION ALL\n", array_map( - static fn ($value): string => 'SELECT ' . implode(', ', array_map( - static fn ($key, $index): string => $index . ' ' . $key, + fn (array $value): string => sprintf('SELECT %s', implode(', ', array_map( + fn (string $key, float|int|string $index): string => sprintf( + '%s %s', + $this->castValue((string) $index, $this->getSourceType($table, $key, $constraints)), + $key, + ), $keys, $value, - )), + ))), $values, ), ) . "\n"; diff --git a/tests/system/Database/Builder/DeleteTest.php b/tests/system/Database/Builder/DeleteTest.php index 200eb9ca5b4a..0866cac724a4 100644 --- a/tests/system/Database/Builder/DeleteTest.php +++ b/tests/system/Database/Builder/DeleteTest.php @@ -14,6 +14,7 @@ namespace CodeIgniter\Database\Builder; use CodeIgniter\Database\BaseBuilder; +use CodeIgniter\Database\Postgre\Builder as PostgreBuilder; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockConnection; use CodeIgniter\Test\Mock\MockQuery; @@ -169,4 +170,50 @@ public function testDeleteBatchEscapesWhereInBinds(): void $this->assertStringContainsString("IN ('anything'' OR ''1''=''1','Ahmadinejad')", $query->getQuery()); $this->assertStringNotContainsString("IN anything' OR '1'='1", $query->getQuery()); } + + public function testDeleteBatchPostgreCastsPositionalAndMappedConstraints(): void + { + $db = new class (['DBDriver' => 'Postgre']) extends MockConnection { + protected function _fieldData(string $table): array + { + return [ + (object) ['name' => 'id', 'type' => 'integer', 'max_length' => 32], + (object) ['name' => 'name', 'type' => 'text', 'max_length' => null], + ]; + } + }; + + $positional = (new PostgreBuilder('jobs', $db))->testMode() + ->setData([['id' => 1], ['id' => '2'], ['id' => 1.4]], null, 'data') + ->onConstraint('id') + ->deleteBatch(); + + $expected = <<<'EOF' + DELETE FROM "jobs" + USING ( + SELECT 1 "id" UNION ALL + SELECT CAST('2' AS INTEGER) "id" UNION ALL + SELECT 1.4 "id" + ) "data" + WHERE "jobs"."id" = "data"."id" + EOF; + + $this->assertSame($expected, rtrim($positional[0])); + + $mapped = (new PostgreBuilder('jobs', $db))->testMode() + ->setData([['value' => 'x'], ['value' => 5]], null, 'data') + ->onConstraint(['name' => 'value']) + ->deleteBatch(); + + $expected = <<<'EOF' + DELETE FROM "jobs" + USING ( + SELECT CAST('x' AS TEXT) "value" UNION ALL + SELECT CAST(5 AS TEXT) "value" + ) "data" + WHERE "jobs"."name" = CAST("data"."value" AS TEXT) + EOF; + + $this->assertSame($expected, rtrim($mapped[0])); + } } diff --git a/tests/system/Database/Builder/UpdateTest.php b/tests/system/Database/Builder/UpdateTest.php index 5a4ccd0be9d3..91b220b0ba1b 100644 --- a/tests/system/Database/Builder/UpdateTest.php +++ b/tests/system/Database/Builder/UpdateTest.php @@ -15,6 +15,7 @@ use CodeIgniter\Database\BaseBuilder; use CodeIgniter\Database\Exceptions\DatabaseException; +use CodeIgniter\Database\Postgre\Builder as PostgreBuilder; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockConnection; use CodeIgniter\Test\Mock\MockQuery; @@ -469,4 +470,72 @@ public function testSetWithAndWithoutEscape(): void $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledUpdate())); $this->assertSame($expectedBinds, $builder->getBinds()); } + + public function testUpdateBatchPostgreDoesNotCastEnumAndArrayColumns(): void + { + $db = new class (['DBDriver' => 'Postgre']) extends MockConnection { + protected function _fieldData(string $table): array + { + return [ + (object) ['name' => 'id', 'type' => 'integer', 'max_length' => 32], + (object) ['name' => 'mood', 'type' => 'USER-DEFINED', 'max_length' => null], + (object) ['name' => 'tags', 'type' => 'ARRAY', 'max_length' => null], + ]; + } + }; + + $builder = new PostgreBuilder('jobs', $db); + $sql = $builder->testMode()->updateBatch([ + ['id' => 1, 'mood' => 'happy', 'tags' => '{a}'], + ['id' => 2, 'mood' => 'sad', 'tags' => '{b}'], + ], 'id'); + + $expected = <<<'EOF' + UPDATE "jobs" + SET + "mood" = _u."mood", + "tags" = _u."tags" + FROM ( + SELECT 1 "id", 'happy' "mood", '{a}' "tags" UNION ALL + SELECT 2 "id", 'sad' "mood", '{b}' "tags" + ) _u + WHERE "jobs"."id" = CAST(_u."id" AS INTEGER) + EOF; + + $this->assertSame([$expected], $sql); + } + + public function testUpdateBatchPostgreDoesNotCastSourceSharedByColumnsOfDifferentTypes(): void + { + $db = new class (['DBDriver' => 'Postgre']) extends MockConnection { + protected function _fieldData(string $table): array + { + return [ + (object) ['name' => 'id', 'type' => 'integer', 'max_length' => 32], + (object) ['name' => 'number', 'type' => 'integer', 'max_length' => 32], + (object) ['name' => 'label', 'type' => 'text', 'max_length' => null], + ]; + } + }; + + $builder = new PostgreBuilder('jobs', $db); + $sql = $builder->testMode() + ->setData([['id' => 1, 'value' => 1.4]], null, 'data') + ->updateFields(['number' => 'value', 'label' => 'value']) + ->onConstraint('id') + ->updateBatch(); + + $expected = <<<'EOF' + UPDATE "jobs" + SET + "number" = CAST("data"."value" AS INTEGER), + "label" = CAST("data"."value" AS TEXT) + FROM ( + SELECT 1 "id", 1.4 "value" + ) "data" + WHERE "jobs"."id" = CAST("data"."id" AS INTEGER) + EOF; + + $this->assertSame([$expected], $sql); + } } diff --git a/tests/system/Database/Live/DeleteTest.php b/tests/system/Database/Live/DeleteTest.php index 651dff251a7f..daef35068386 100644 --- a/tests/system/Database/Live/DeleteTest.php +++ b/tests/system/Database/Live/DeleteTest.php @@ -119,6 +119,81 @@ public function testDeleteBatchPreventsSQLInjectionInWhere(): void $this->seeInDatabase('user', ['email' => 'derek@world.com', 'name' => 'Derek Jones']); } + public function testDeleteBatchWithMixedConstraintValueTypesInTextColumn(): void + { + if ($this->db->DBDriver === 'SQLSRV') { + $this->markTestSkipped('SQL Server cannot compare `text` columns with `=`.'); + } + + if ($this->db->DBDriver === 'OCI8') { + $this->markTestSkipped('TODO: the OCI8 builder does not cast mixed `UNION ALL` values yet. Remove this skip once it does.'); + } + + $table = 'type_test'; + + $builder = $this->db->table($table); + $builder->truncate(); + + foreach (['Example', '587', 'kept'] as $i => $text) { + $builder->insert([ + 'type_varchar' => 'test' . $i, + 'type_char' => 'char', + 'type_text' => $text, + 'type_smallint' => 32767, + 'type_integer' => 2_147_483_647, + 'type_bigint' => 9_223_372_036_854_775_807, + 'type_float' => 10.1, + 'type_numeric' => 123.23, + 'type_date' => '2023-12-0' . ($i + 1), + 'type_datetime' => '2023-12-21 12:00:00', + ]); + } + + $this->db->table($table) + ->setData([['text' => 'Example'], ['text' => 587]], null, 'data') + ->onConstraint(['type_text' => 'text']) + ->deleteBatch(); + + $this->dontSeeInDatabase($table, ['type_varchar' => 'test0']); + $this->dontSeeInDatabase($table, ['type_varchar' => 'test1']); + $this->seeInDatabase($table, ['type_varchar' => 'test2']); + } + + public function testDeleteBatchDoesNotTruncateConstraintValueForCharColumn(): void + { + if ($this->db->DBDriver === 'OCI8') { + $this->markTestSkipped('TODO: Oracle resolves a `UNION ALL` of `CHAR` literals with different lengths to `VARCHAR2`, so the OCI8 builder must cast them. Remove this skip once it does.'); + } + + $table = 'type_test'; + + $builder = $this->db->table($table); + $builder->truncate(); + + for ($i = 0; $i < 2; $i++) { + $builder->insert([ + 'type_varchar' => 'test' . $i, + 'type_char' => 'char' . $i, + 'type_text' => 'text', + 'type_smallint' => 32767, + 'type_integer' => 2_147_483_647, + 'type_bigint' => 9_223_372_036_854_775_807, + 'type_float' => 10.1, + 'type_numeric' => 123.23, + 'type_date' => '2023-12-0' . ($i + 1), + 'type_datetime' => '2023-12-21 12:00:00', + ]); + } + + $this->db->table($table) + ->setData([['char' => 'char0 X'], ['char' => 'char1']], null, 'data') + ->onConstraint(['type_char' => 'char']) + ->deleteBatch(); + + $this->seeInDatabase($table, ['type_varchar' => 'test0']); + $this->dontSeeInDatabase($table, ['type_varchar' => 'test1']); + } + public function testDeleteBatchConstraintsDate(): void { $table = 'type_test'; diff --git a/tests/system/Database/Live/UpdateTest.php b/tests/system/Database/Live/UpdateTest.php index 8961e7b05664..4e3944a6c6c2 100644 --- a/tests/system/Database/Live/UpdateTest.php +++ b/tests/system/Database/Live/UpdateTest.php @@ -278,6 +278,156 @@ public static function provideUpdateBatch(): iterable ]; } + public function testUpdateBatchWithMixedValueTypesInTextColumn(): void + { + if ($this->db->DBDriver === 'SQLSRV') { + $this->markTestSkipped('SQL Server cannot compare `text` columns with `=`.'); + } + + if ($this->db->DBDriver === 'OCI8') { + $this->markTestSkipped('TODO: the OCI8 builder does not cast mixed `UNION ALL` values yet. Remove this skip once it does.'); + } + + $table = 'type_test'; + + $builder = $this->db->table($table); + $builder->truncate(); + + for ($i = 1; $i < 3; $i++) { + $builder->insert([ + 'type_varchar' => 'test' . $i, + 'type_char' => 'char' . $i, + 'type_text' => 'text', + 'type_smallint' => 32767, + 'type_integer' => 2_147_483_647, + 'type_bigint' => 9_223_372_036_854_775_807, + 'type_float' => 10.1, + 'type_numeric' => 123.23, + 'type_date' => '2023-12-0' . $i, + 'type_datetime' => '2023-12-21 12:00:00', + ]); + } + + $this->db->table($table)->updateBatch([ + ['type_varchar' => 'test1', 'type_text' => 'Example'], + ['type_varchar' => 'test2', 'type_text' => 587], + ], 'type_varchar'); + + $this->seeInDatabase($table, ['type_varchar' => 'test1', 'type_text' => 'Example']); + $this->seeInDatabase($table, ['type_varchar' => 'test2', 'type_text' => '587']); + } + + public function testUpdateBatchDoesNotTruncateConstraintValueForCharColumn(): void + { + if ($this->db->DBDriver === 'OCI8') { + $this->markTestSkipped('TODO: Oracle resolves a `UNION ALL` of `CHAR` literals with different lengths to `VARCHAR2`, so the OCI8 builder must cast them. Remove this skip once it does.'); + } + + $table = 'type_test'; + + $builder = $this->db->table($table); + $builder->truncate(); + + for ($i = 1; $i < 3; $i++) { + $builder->insert([ + 'type_varchar' => 'test' . $i, + 'type_char' => 'char' . $i, + 'type_text' => 'text', + 'type_smallint' => 32767, + 'type_integer' => 2_147_483_647, + 'type_bigint' => 9_223_372_036_854_775_807, + 'type_float' => 10.1, + 'type_numeric' => 123.23, + 'type_date' => '2023-12-0' . $i, + 'type_datetime' => '2023-12-21 12:00:00', + ]); + } + + $this->db->table($table)->updateBatch([ + ['type_char' => 'char1 X', 'type_text' => 'changed'], + ['type_char' => 'char2', 'type_text' => 'changed'], + ], 'type_char'); + + $this->seeInDatabase($table, ['type_varchar' => 'test1', 'type_text' => 'text']); + $this->seeInDatabase($table, ['type_varchar' => 'test2', 'type_text' => 'changed']); + } + + public function testUpdateBatchWithMappedUpdateFieldsAndMixedValueTypes(): void + { + if ($this->db->DBDriver === 'SQLSRV') { + $this->markTestSkipped('SQL Server cannot compare `text` columns with `=`.'); + } + + if ($this->db->DBDriver === 'OCI8') { + $this->markTestSkipped('TODO: the OCI8 builder does not cast mixed `UNION ALL` values yet. Remove this skip once it does.'); + } + + $table = 'type_test'; + + $builder = $this->db->table($table); + $builder->truncate(); + + for ($i = 1; $i < 3; $i++) { + $builder->insert([ + 'type_varchar' => 'test' . $i, + 'type_char' => 'char' . $i, + 'type_text' => 'text', + 'type_smallint' => 32767, + 'type_integer' => 2_147_483_647, + 'type_bigint' => 9_223_372_036_854_775_807, + 'type_float' => 10.1, + 'type_numeric' => 123.23, + 'type_date' => '2023-12-0' . $i, + 'type_datetime' => '2023-12-21 12:00:00', + ]); + } + + $this->db->table($table) + ->setData([ + ['type_varchar' => 'test1', 'text' => 'Example'], + ['type_varchar' => 'test2', 'text' => 587], + ], null, 'data') + ->updateFields(['type_text' => 'text']) + ->onConstraint('type_varchar') + ->updateBatch(); + + $this->seeInDatabase($table, ['type_varchar' => 'test1', 'type_text' => 'Example']); + $this->seeInDatabase($table, ['type_varchar' => 'test2', 'type_text' => '587']); + } + + public function testUpdateBatchKeepsSourceValueForColumnsOfDifferentTypes(): void + { + if ($this->db->DBDriver === 'SQLSRV') { + $this->markTestSkipped('SQL Server cannot compare `text` columns with `=`.'); + } + + $table = 'type_test'; + + $builder = $this->db->table($table); + $builder->truncate(); + + $builder->insert([ + 'type_varchar' => 'test1', + 'type_char' => 'char1', + 'type_text' => 'text', + 'type_smallint' => 32767, + 'type_integer' => 2_147_483_647, + 'type_bigint' => 9_223_372_036_854_775_807, + 'type_float' => 10.1, + 'type_numeric' => 123.23, + 'type_date' => '2023-12-01', + 'type_datetime' => '2023-12-21 12:00:00', + ]); + + $this->db->table($table) + ->setData([['type_varchar' => 'test1', 'value' => 1.4]], null, 'data') + ->updateFields(['type_integer' => 'value', 'type_text' => 'value']) + ->onConstraint('type_varchar') + ->updateBatch(); + + $this->seeInDatabase($table, ['type_varchar' => 'test1', 'type_text' => '1.4']); + } + public function testUpdateWithWhereSameColumn(): void { $this->db->table('user')->update(['country' => 'CA'], ['country' => 'US']); diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index 34dc4dcf41eb..45416eef17da 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -55,6 +55,7 @@ Bugs Fixed - **Database:** Fixed a bug where rebuilding a SQLite3 table (e.g., ``Forge::dropColumn()``, ``Forge::modifyColumn()``, ``Forge::dropForeignKey()`` and ``Forge::dropPrimaryKey()``) corrupted the table names referenced by its foreign keys when ``DBPrefix`` was set. - **Database:** Fixed a bug where Postgre query failures were silently ignored when ``DBDebug`` was enabled and PHP warnings were disabled. A ``DatabaseException`` is now thrown. - **Database:** Fixed a bug where ``getFieldData()`` failed for a schema-qualified table name on Postgre, SQLSRV and SQLite3, and where ``protectIdentifiers()`` prefixed an already protected segment of a dotted identifier a second time. +- **Database:** Fixed a bug where ``updateBatch()`` and ``deleteBatch()`` failed on PostgreSQL when the rows supplied different PHP types (e.g., a string and an integer) for the same column, because the ``UNION ALL`` subquery values were not cast to the column type. - **Debug:** Fixed a bug where ``Timer::start()`` treated ``0.0`` as an empty value and substituted the current time. - **Files:** Fixed a bug where ``File::move()`` and ``UploadedFile::move()`` set executable and overly permissive file permissions (``0777 & ~umask()`` instead of ``0666 & ~umask()``), and ``UploadedFile::move()`` targeted the parent directory instead of the destination file for ``chmod()``. - **Helpers:** Fixed a bug where ``get_dir_file_info()`` returned incomplete entries for subdirectories and missing files instead of omitting them. From d5de58f80a19bc8a55e4807428a1bca16acd1f45 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Fri, 9 Oct 2026 20:36:09 +0800 Subject: [PATCH 154/163] fix: cast OCI8 batch subquery values so `updateBatch()` and `deleteBatch()` accept mixed PHP types (#10602) --- system/Database/OCI8/Builder.php | 105 ++++++++++++++++++-- tests/system/Database/Live/DeleteTest.php | 8 -- tests/system/Database/Live/UpdateTest.php | 12 --- user_guide_src/source/changelogs/v4.7.5.rst | 2 +- 4 files changed, 100 insertions(+), 27 deletions(-) diff --git a/system/Database/OCI8/Builder.php b/system/Database/OCI8/Builder.php index 118f237f597e..abdaf10ae4a1 100644 --- a/system/Database/OCI8/Builder.php +++ b/system/Database/OCI8/Builder.php @@ -16,6 +16,7 @@ use CodeIgniter\Database\BaseBuilder; use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\RawSql; +use stdClass; /** * Builder for OCI8 @@ -282,14 +283,21 @@ protected function _updateBatch(string $table, array $keys, array $values): stri if (isset($this->QBOptions['setQueryAsData'])) { $data = $this->QBOptions['setQueryAsData']; } else { + $updateFields = $this->QBOptions['updateFields'] ?? []; + $lengths = $this->longestValues($keys, $values); + $data = implode( " UNION ALL\n", array_map( - static fn ($value): string => 'SELECT ' . implode(', ', array_map( - static fn ($key, $index): string => $index . ' ' . $key, + fn (array $value): string => sprintf('SELECT %s FROM DUAL', implode(', ', array_map( + fn (string $key, float|int|string $index): string => sprintf( + '%s %s', + $this->cast((string) $index, $this->getSourceField($table, $key, $updateFields), $lengths[$key]), + $key, + ), $keys, $value, - )) . ' FROM DUAL', + ))), $values, ), ) . "\n"; @@ -466,14 +474,21 @@ protected function _deleteBatch(string $table, array $keys, array $values): stri if (isset($this->QBOptions['setQueryAsData'])) { $data = $this->QBOptions['setQueryAsData']; } else { + $constraints = $this->QBOptions['constraints'] ?? []; + $lengths = $this->longestValues($keys, $values); + $data = implode( " FROM DUAL UNION ALL\n", array_map( - static fn ($value): string => 'SELECT ' . implode(', ', array_map( - static fn ($key, $index): string => $index . ' ' . $key, + fn (array $value): string => sprintf('SELECT %s', implode(', ', array_map( + fn (string $key, float|int|string $index): string => sprintf( + '%s %s', + $this->cast((string) $index, $this->getSourceField($table, $key, $constraints), $lengths[$key]), + $key, + ), $keys, $value, - )), + ))), $values, ), ) . " FROM DUAL\n"; @@ -489,4 +504,82 @@ protected function fieldsFromQuery(string $sql): array { return $this->db->query('SELECT * FROM (' . $sql . ') "_u_" WHERE ROWNUM = 1')->getFieldNames(); } + + /** + * Returns the longest escaped value per key across the batch. + * + * @param list $keys + * @param list> $values + * + * @return array + */ + private function longestValues(array $keys, array $values): array + { + $lengths = []; + + foreach ($keys as $i => $key) { + $lengths[$key] = max(array_map(static fn (array $row): int => mb_strlen((string) $row[$i]), $values)); + } + + return $lengths; + } + + /** + * Returns the field shared by every destination column fed from the source key, or null when their types differ. + * + * @param array $map Destination column to source key. + */ + private function getSourceField(string $table, string $key, array $map): ?stdClass + { + $columns = array_filter(array_keys($map, $key, true), is_string(...)); + + if ($columns === []) { + return $this->getField($table, $key); + } + + $fields = array_map(fn (string $column): ?stdClass => $this->getField($table, $column), $columns); + $types = array_unique(array_map(static fn (?stdClass $field): string => $field instanceof stdClass ? $field->type . '(' . $field->max_length . ')' : '', $fields)); + + return count($types) === 1 ? reset($fields) : null; + } + + /** + * Returns the literal converted to the field type, or unchanged when the field is unknown or a LOB. + * + * @param int $length Longest value for the key in the batch. + */ + private function cast(string $value, ?stdClass $field, int $length): string + { + if (! $field instanceof stdClass) { + return $value; + } + + return match ($field->type) { + // Every row gets the same fixed width, wide enough for the longest value, so the UNION ALL stays CHAR and nothing is truncated. + 'CHAR', 'NCHAR' => sprintf('CAST(%s AS %s(%d))', $value, $field->type, max((int) $field->max_length, $length)), + 'VARCHAR2' => sprintf('TO_CHAR(%s)', $value), + 'NVARCHAR2' => sprintf('TO_NCHAR(%s)', $value), + 'RAW', 'LONG RAW', 'CLOB', 'NCLOB', 'BLOB', 'BFILE', 'LONG' => $value, + default => sprintf('CAST(%s AS %s)', $value, $field->type), + }; + } + + /** + * @param string $table Protected table name. + * @param string $fieldName Field name. May be protected. + */ + private function getField(string $table, string $fieldName): ?stdClass + { + $fieldName = trim($fieldName, $this->db->escapeChar); + + if (! isset($this->QBOptions['fields'][$table])) { + $this->QBOptions['fields'][$table] = []; + + foreach ($this->db->getFieldData($table) as $field) { + $this->QBOptions['fields'][$table][$field->name] = $field; + } + } + + return $this->QBOptions['fields'][$table][$fieldName] ?? null; + } } diff --git a/tests/system/Database/Live/DeleteTest.php b/tests/system/Database/Live/DeleteTest.php index daef35068386..8ef937fb00f5 100644 --- a/tests/system/Database/Live/DeleteTest.php +++ b/tests/system/Database/Live/DeleteTest.php @@ -125,10 +125,6 @@ public function testDeleteBatchWithMixedConstraintValueTypesInTextColumn(): void $this->markTestSkipped('SQL Server cannot compare `text` columns with `=`.'); } - if ($this->db->DBDriver === 'OCI8') { - $this->markTestSkipped('TODO: the OCI8 builder does not cast mixed `UNION ALL` values yet. Remove this skip once it does.'); - } - $table = 'type_test'; $builder = $this->db->table($table); @@ -161,10 +157,6 @@ public function testDeleteBatchWithMixedConstraintValueTypesInTextColumn(): void public function testDeleteBatchDoesNotTruncateConstraintValueForCharColumn(): void { - if ($this->db->DBDriver === 'OCI8') { - $this->markTestSkipped('TODO: Oracle resolves a `UNION ALL` of `CHAR` literals with different lengths to `VARCHAR2`, so the OCI8 builder must cast them. Remove this skip once it does.'); - } - $table = 'type_test'; $builder = $this->db->table($table); diff --git a/tests/system/Database/Live/UpdateTest.php b/tests/system/Database/Live/UpdateTest.php index 4e3944a6c6c2..41efa9b86c17 100644 --- a/tests/system/Database/Live/UpdateTest.php +++ b/tests/system/Database/Live/UpdateTest.php @@ -284,10 +284,6 @@ public function testUpdateBatchWithMixedValueTypesInTextColumn(): void $this->markTestSkipped('SQL Server cannot compare `text` columns with `=`.'); } - if ($this->db->DBDriver === 'OCI8') { - $this->markTestSkipped('TODO: the OCI8 builder does not cast mixed `UNION ALL` values yet. Remove this skip once it does.'); - } - $table = 'type_test'; $builder = $this->db->table($table); @@ -319,10 +315,6 @@ public function testUpdateBatchWithMixedValueTypesInTextColumn(): void public function testUpdateBatchDoesNotTruncateConstraintValueForCharColumn(): void { - if ($this->db->DBDriver === 'OCI8') { - $this->markTestSkipped('TODO: Oracle resolves a `UNION ALL` of `CHAR` literals with different lengths to `VARCHAR2`, so the OCI8 builder must cast them. Remove this skip once it does.'); - } - $table = 'type_test'; $builder = $this->db->table($table); @@ -358,10 +350,6 @@ public function testUpdateBatchWithMappedUpdateFieldsAndMixedValueTypes(): void $this->markTestSkipped('SQL Server cannot compare `text` columns with `=`.'); } - if ($this->db->DBDriver === 'OCI8') { - $this->markTestSkipped('TODO: the OCI8 builder does not cast mixed `UNION ALL` values yet. Remove this skip once it does.'); - } - $table = 'type_test'; $builder = $this->db->table($table); diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index 45416eef17da..600a9a7892b4 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -55,7 +55,7 @@ Bugs Fixed - **Database:** Fixed a bug where rebuilding a SQLite3 table (e.g., ``Forge::dropColumn()``, ``Forge::modifyColumn()``, ``Forge::dropForeignKey()`` and ``Forge::dropPrimaryKey()``) corrupted the table names referenced by its foreign keys when ``DBPrefix`` was set. - **Database:** Fixed a bug where Postgre query failures were silently ignored when ``DBDebug`` was enabled and PHP warnings were disabled. A ``DatabaseException`` is now thrown. - **Database:** Fixed a bug where ``getFieldData()`` failed for a schema-qualified table name on Postgre, SQLSRV and SQLite3, and where ``protectIdentifiers()`` prefixed an already protected segment of a dotted identifier a second time. -- **Database:** Fixed a bug where ``updateBatch()`` and ``deleteBatch()`` failed on PostgreSQL when the rows supplied different PHP types (e.g., a string and an integer) for the same column, because the ``UNION ALL`` subquery values were not cast to the column type. +- **Database:** Fixed a bug where ``updateBatch()`` and ``deleteBatch()`` failed on PostgreSQL and Oracle when the rows supplied different PHP types (e.g., a string and an integer) for the same column, because the ``UNION ALL`` subquery values were not cast to the column type. - **Debug:** Fixed a bug where ``Timer::start()`` treated ``0.0`` as an empty value and substituted the current time. - **Files:** Fixed a bug where ``File::move()`` and ``UploadedFile::move()`` set executable and overly permissive file permissions (``0777 & ~umask()`` instead of ``0666 & ~umask()``), and ``UploadedFile::move()`` targeted the parent directory instead of the destination file for ``chmod()``. - **Helpers:** Fixed a bug where ``get_dir_file_info()`` returned incomplete entries for subdirectories and missing files instead of omitting them. From 12a9bb5a4934b843d29c13c50d9371e3b2682b02 Mon Sep 17 00:00:00 2001 From: John Paul E Balandan Date: Fri, 9 Oct 2026 20:42:17 +0800 Subject: [PATCH 155/163] fix: avoid undefined `STDERR` and `STDIN` outside the CLI (#10605) --- rector.php | 1 + system/CLI/CLI.php | 6 ++--- system/CLI/SignalTrait.php | 2 +- tests/system/CLI/CLITest.php | 26 +++++++++++++++++++++ user_guide_src/source/changelogs/v4.7.5.rst | 2 ++ utils/phpstan-baseline/arguments.count.neon | 7 +++++- utils/phpstan-baseline/loader.neon | 2 +- 7 files changed, 40 insertions(+), 6 deletions(-) diff --git a/rector.php b/rector.php index 2b3c36597dae..2698aec9f098 100644 --- a/rector.php +++ b/rector.php @@ -100,6 +100,7 @@ // Exclude test file because `is_cli()` is mocked and Rector might remove needed parameters. RemoveExtraParametersRector::class => [ + __DIR__ . '/tests/system/CLI/CLITest.php', __DIR__ . '/tests/system/Debug/ToolbarTest.php', ], diff --git a/system/CLI/CLI.php b/system/CLI/CLI.php index 08d5c214582e..314b6a934ee1 100644 --- a/system/CLI/CLI.php +++ b/system/CLI/CLI.php @@ -481,7 +481,7 @@ public static function error(string $text, string $foreground = 'light_red', ?st // Check color support for STDERR $stdout = static::$isColored; - static::$isColored = static::hasColorSupport(STDERR); + static::$isColored = is_cli() && static::hasColorSupport(STDERR); if ($foreground !== '' || (string) $background !== '') { $text = static::color($text, $foreground, $background); @@ -493,7 +493,7 @@ public static function error(string $text, string $foreground = 'light_red', ?st static::$lastWrite = 'write'; } - static::fwrite(STDERR, $text . PHP_EOL); + static::fwrite(is_cli() ? STDERR : STDOUT, $text . PHP_EOL); // return STDOUT color support static::$isColored = $stdout; @@ -572,7 +572,7 @@ public static function clearScreen() { // Unix systems, and Windows with VT100 Terminal support (i.e. Win10) // can handle CSI sequences. For lower than Win10 we just shove in 40 new lines. - is_windows() && ! static::streamSupports('sapi_windows_vt100_support', STDOUT) + is_windows() && is_cli() && ! static::streamSupports('sapi_windows_vt100_support', STDOUT) ? static::newLine(40) : static::fwrite(STDOUT, "\033[H\033[2J"); } diff --git a/system/CLI/SignalTrait.php b/system/CLI/SignalTrait.php index 792f7334f865..2bc69d51e93c 100644 --- a/system/CLI/SignalTrait.php +++ b/system/CLI/SignalTrait.php @@ -394,7 +394,7 @@ protected function getProcessState(): array if ($this->isPosixAvailable()) { $state['session_id'] = posix_getsid($pid); $state['process_group'] = posix_getpgid($pid); - $state['has_controlling_terminal'] = posix_isatty(STDIN); + $state['has_controlling_terminal'] = is_cli() && posix_isatty(STDIN); } return $state; diff --git a/tests/system/CLI/CLITest.php b/tests/system/CLI/CLITest.php index 62903f80ac64..9c697b25afb9 100644 --- a/tests/system/CLI/CLITest.php +++ b/tests/system/CLI/CLITest.php @@ -563,6 +563,32 @@ public function testError(): void $this->assertSame($expected, $this->getStreamFilterBuffer()); } + public function testErrorOutsideCliWritesToStdoutWithoutColor(): void + { + $io = new class () extends InputOutput { + /** + * @var list + */ + public array $writes = []; + + public function fwrite($handle, string $string): void + { + $this->writes[] = [$handle === STDERR ? 'STDERR' : 'STDOUT', $string]; + } + }; + CLI::setInputOutput($io); + + is_cli(false); + + try { + CLI::error('test'); + } finally { + is_cli(true); + } + + $this->assertSame([['STDOUT', "\ntest\n"]], $io->writes); + } + public function testMixedWriteError(): void { CLI::write('test 1'); diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index 600a9a7892b4..0897a791ba7c 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -46,6 +46,8 @@ Bugs Fixed ANSI color codes in the prompt (e.g., option defaults) are wrapped in readline's non-printing markers under GNU readline so cursor positioning stays accurate. On Windows, where the ``readline`` extension is built on WinEditLine, the prompt is written to STDOUT first because WinEditLine reports no library version and prints ANSI sequences literally. - **CLI:** Fixed a bug where ``CLI::input()`` and ``CLI::prompt()`` threw a ``TypeError`` when STDIN reached end-of-file (e.g., Ctrl+D) with the ``readline`` extension enabled. An empty string is now returned, matching the behavior without ``readline``. +- **CLI:** Fixed a bug where ``CLI::error()`` threw an ``Error`` for the undefined ``STDERR`` constant when a command ran outside the CLI (e.g., through ``command()`` in a controller). The message is now written like other output. + ``CLI::clearScreen()`` on Windows and ``SignalTrait::getProcessState()`` no longer probe the standard streams outside the CLI either. - **CLIRequest:** Fixed a bug where ``parseCommand()`` could throw a TypeError when ``argv`` is missing. - **CodeIgniter:** Fixed a bug where ``gatherOutput()`` could be called twice when ``startController()`` returned a ``ResponseInterface`` (e.g., from filter attributes or closure routes). - **Commands:** Fixed a bug where ``make:migration --session`` silently generated a broken migration when the database group's driver is neither ``MySQLi`` nor ``Postgre``. The command now reports an error and returns ``EXIT_ERROR``. diff --git a/utils/phpstan-baseline/arguments.count.neon b/utils/phpstan-baseline/arguments.count.neon index 1df396472752..999a1d99310f 100644 --- a/utils/phpstan-baseline/arguments.count.neon +++ b/utils/phpstan-baseline/arguments.count.neon @@ -1,7 +1,12 @@ -# total 2 errors +# total 4 errors parameters: ignoreErrors: + - + message: '#^Function is_cli invoked with 1 parameter, 0 required\.$#' + count: 2 + path: ../../tests/system/CLI/CLITest.php + - message: '#^Function is_cli invoked with 1 parameter, 0 required\.$#' count: 2 diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index c00a0da66add..5223437222e3 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 12 errors +# total 14 errors includes: - arguments.count.neon From 641ee5b6d7b1fb11453b3a3c7d7b83fa198de281 Mon Sep 17 00:00:00 2001 From: Michal Sniatala Date: Fri, 9 Oct 2026 15:02:40 +0200 Subject: [PATCH 156/163] Merge commit from fork * fix: prevent View Parser from interpreting substituted values as template syntax * fix: handle all repeated nested pairs in View Parser * fix: prevent Parser syntax injection through built-in plugins * docs: clarify existing support for closures in Parser filters --- app/Config/View.php | 2 +- system/Config/View.php | 2 +- system/View/Parser.php | 86 +++++- tests/system/View/ParserTest.php | 288 ++++++++++++++++++ user_guide_src/source/changelogs/v4.7.5.rst | 13 + .../source/installation/upgrade_475.rst | 44 +++ .../source/outgoing/view_parser.rst | 13 + 7 files changed, 439 insertions(+), 9 deletions(-) diff --git a/app/Config/View.php b/app/Config/View.php index b52d980dc39f..d98dd6e4dea3 100644 --- a/app/Config/View.php +++ b/app/Config/View.php @@ -30,7 +30,7 @@ class View extends BaseView * { title|esc(js) } * { created_on|date(Y-m-d)|esc(attr) } * - * @var array + * @var array */ public $filters = []; diff --git a/system/Config/View.php b/system/Config/View.php index fcfe32f40f1c..5b7176d51183 100644 --- a/system/Config/View.php +++ b/system/Config/View.php @@ -37,7 +37,7 @@ class View extends BaseConfig * * @psalm-suppress UndefinedDocblockClass * - * @var array + * @var array */ public $filters = []; diff --git a/system/View/Parser.php b/system/View/Parser.php index 2aea776dda39..498cd65d6b5e 100644 --- a/system/View/Parser.php +++ b/system/View/Parser.php @@ -72,6 +72,16 @@ class Parser extends View */ protected $dataContexts = []; + /** + * Substitution values are temporarily replaced with opaque tokens so they + * cannot be interpreted as Parser syntax by a later substitution pass. + * + * @var array + */ + private array $replacementTokens = []; + + private ?string $replacementTokenPrefix = null; + /** * Constructor * @@ -231,6 +241,29 @@ protected function parse(string $template, array $data = [], ?array $options = n return ''; } + $replacementState = [ + $this->replacementTokens, + $this->replacementTokenPrefix, + ]; + + $this->replacementTokens = []; + $this->replacementTokenPrefix = bin2hex(random_bytes(16)); + + try { + return $this->parseTemplate($template, $data); + } finally { + [ + $this->replacementTokens, + $this->replacementTokenPrefix, + ] = $replacementState; + } + } + + /** + * @param array $data + */ + private function parseTemplate(string $template, array $data): string + { // Remove any possible PHP tags since we don't support it // and parseConditionals needs it clean anyway... $template = str_replace([''], ['<?', '?>'], $template); @@ -281,7 +314,9 @@ protected function parse(string $template, array $data = [], ?array $options = n } } - return $this->insertNoparse($template); + $template = $this->insertNoparse($template); + + return strtr($template, $this->replacementTokens); } /** @@ -355,7 +390,9 @@ protected function parsePair(string $variable, array $data, string $template): a $pair = $this->parsePair($key, $val, $match[1]); if ($pair !== []) { - $pairs[array_keys($pair)[0]] = true; + foreach (array_keys($pair) as $pattern) { + $pairs[$pattern] = true; + } $temp = array_merge($temp, $pair); } @@ -541,8 +578,12 @@ protected function replaceSingle($pattern, $content, $template, bool $escape = f { $content = (string) $content; + // Callers enable escaping for scalar data, but not assembled pairs. + // Protect scalar data even when {! !} disables escaping below. + $protectReplacement = $escape; + // Replace the content in the template - return preg_replace_callback($pattern, function ($matches) use ($content, $escape): string { + return preg_replace_callback($pattern, function ($matches) use ($content, $escape, $protectReplacement): string { // Check for {! !} syntax to not escape this one. if ( str_starts_with($matches[0], $this->leftDelimiter . '!') @@ -551,10 +592,31 @@ protected function replaceSingle($pattern, $content, $template, bool $escape = f $escape = false; } - return $this->prepareReplacement($matches, $content, $escape); + $replacement = $this->prepareReplacement($matches, $content, $escape); + + if (! $protectReplacement || $this->replacementTokenPrefix === null) { + return $replacement; + } + + return $this->protectReplacement($replacement); }, (string) $template); } + /** + * Keeps rendered data opaque until all template processing is complete. + */ + private function protectReplacement(string $replacement): string + { + if ($this->replacementTokenPrefix === null) { + return $replacement; + } + + $token = "\x1A{$this->replacementTokenPrefix}:" . count($this->replacementTokens) . "\x1A"; + $this->replacementTokens[$token] = $replacement; + + return $token; + } + /** * Callback used during parse() to apply any filters to the value. * @@ -699,9 +761,19 @@ protected function parsePlugins(string $template) } } - $template = $isPair - ? str_replace($match[0], $callable($match[2], $params), $template) - : str_replace($match[0], $callable($params), $template); + $replacement = $isPair ? $callable($match[2], $params) : $callable($params); + + // Built-in plugins return rendered data. Custom plugins may + // intentionally return template code for subsequent processing. + // Identify the implementation, not the alias applications can override. + if ( + is_string($callable) + && str_starts_with(strtolower(ltrim($callable, '\\')), strtolower(Plugins::class) . '::') + ) { + $replacement = $this->protectReplacement((string) $replacement); + } + + $template = str_replace($match[0], $replacement, $template); } } diff --git a/tests/system/View/ParserTest.php b/tests/system/View/ParserTest.php index 36e8707430d6..d3a53f3fc480 100644 --- a/tests/system/View/ParserTest.php +++ b/tests/system/View/ParserTest.php @@ -17,9 +17,11 @@ use CodeIgniter\Entity\Entity; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\View\Exceptions\ViewException; +use Config\Services; use Config\View as ViewConfig; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use RuntimeException; use stdClass; /** @@ -612,6 +614,207 @@ public function testParserNoEscape(): void $this->assertSame('', $this->parser->renderString($template)); } + /** + * @param array $data + */ + #[DataProvider('provideParserDoesNotInterpretSyntaxInSubstitutionValues')] + public function testParserDoesNotInterpretSyntaxInSubstitutionValues( + string $template, + array $data, + string $expected, + ): void { + $this->parser->setData($data, 'html'); + + $this->assertSame($expected, $this->parser->renderString($template)); + } + + /** + * @return iterable, string}> + */ + public static function provideParserDoesNotInterpretSyntaxInSubstitutionValues(): iterable + { + $escapedScript = '<script>alert(document.domain)</script>'; + + yield 'top-level no-escape delimiters' => [ + '{first}|{second}', + [ + 'first' => '{!second!}', + 'second' => '', + ], + '{!second!}|' . $escapedScript, + ]; + + yield 'top-level filter' => [ + '{first}|{second}', + [ + 'first' => '{second|upper}', + 'second' => '', + ], + '{second|upper}|' . $escapedScript, + ]; + + yield 'loop no-escape delimiters' => [ + '{comments}{author}: {text}{/comments}', + [ + 'comments' => [[ + 'author' => '{!text!}', + 'text' => '', + ]], + ], + '{!text!}: ' . $escapedScript, + ]; + + yield 'loop unknown filter' => [ + '{comments}{author}: {text}{/comments}', + [ + 'comments' => [[ + 'author' => '{text|raw}', + 'text' => '', + ]], + ], + '{text|raw}: ' . $escapedScript, + ]; + + yield 'loop to top-level no-escape delimiters' => [ + '{comments}{author}{/comments}|{text}', + [ + 'comments' => [['author' => '{!text!}']], + 'text' => '', + ], + '{!text!}|' . $escapedScript, + ]; + + yield 'nested loop to outer row' => [ + '{comments}{replies}[{reply}]{/replies} by {author}{/comments}', + [ + 'comments' => [[ + 'replies' => [['reply' => '{author|raw}']], + 'author' => '', + ]], + ], + '[{author|raw}] by ' . $escapedScript, + ]; + + yield 'repeated nested pairs with different templates' => [ + '{comments}{replies}[{reply}] by {author}{/replies} again {replies}{reply} by {author}{/replies}{/comments}', + [ + 'comments' => [[ + 'replies' => [['reply' => '{author|raw}'], ['reply' => 'Second']], + 'author' => '', + ]], + ], + '[{author|raw}] by ' . $escapedScript . '[Second] by ' . $escapedScript + . ' again {author|raw} by ' . $escapedScript . 'Second by ' . $escapedScript, + ]; + + yield 'value resembling an internal token' => [ + '{first}|{second}', + [ + 'first' => "\x1A0123456789abcdef0123456789abcdef:0\x1A", + 'second' => '', + ], + "\x1A0123456789abcdef0123456789abcdef:0\x1A|" . $escapedScript, + ]; + + yield 'unescaped substitution' => [ + '{!first!}|{second}', + ['first' => '{!second!}', 'second' => ''], + '{!second!}|' . $escapedScript, + ]; + + yield 'syntax assembled across a substitution boundary' => [ + '{first}second!}|{second}', + ['first' => '{!', 'second' => ''], + '{!second!}|' . $escapedScript, + ]; + + yield 'repeated substitutions with different escaping' => [ + '{first}|{!first!}|{second}', + ['first' => '{!second!}', 'second' => ''], + '<b>{!second!}</b>|{!second!}|' . $escapedScript, + ]; + + yield 'noparse marker in data' => [ + '{first}|{noparse}literal{/noparse}', + ['first' => 'noparse_' . md5('literal')], + 'noparse_' . md5('literal') . '|literal', + ]; + } + + public function testParserDoesNotInterpretSyntaxInFilteredValues(): void + { + $this->config->filters['syntax'] = static fn (mixed $value): string => '{!' . $value . '!}'; + $this->parser->setData(['first' => 'second', 'second' => 'unsafe']); + + $this->assertSame( + '{!second!}|<b>unsafe</b>', + $this->parser->renderString('{first|syntax}|{second}'), + ); + } + + public function testParserDoesNotInterpretSyntaxWithCustomDelimiters(): void + { + $this->parser->setDelimiters('{{', '}}'); + $this->parser->setData(['first' => '{{!second!}}', 'second' => 'unsafe']); + + $this->assertSame( + '{{!second!}}|<b>unsafe</b>', + $this->parser->renderString('{{first}}|{{second}}'), + ); + } + + public function testParserDoesNotInterpretSyntaxInRawContext(): void + { + $this->parser->setData(['first' => '{!second!}'], 'raw'); + $this->parser->setData(['second' => 'unsafe'], 'html'); + + $this->assertSame( + '{!second!}|<b>unsafe</b>', + $this->parser->renderString('{first}|{second}'), + ); + } + + public function testParserRestoresReplacementStateAfterNestedRender(): void + { + $this->config->filters['nested'] = fn (mixed $value): string => $this->parser->renderString((string) $value, saveData: false); + $this->parser->setData([ + 'first' => 'before', + 'second' => '{inner}', + 'inner' => '{!last!}', + 'last' => 'unsafe', + ]); + + $this->assertSame( + 'before|{!last!}|<b>unsafe</b>', + $this->parser->renderString('{first}|{second|nested}|{last}', saveData: false), + ); + $this->assertSame([], $this->getPrivateProperty($this->parser, 'replacementTokens')); + $this->assertNull($this->getPrivateProperty($this->parser, 'replacementTokenPrefix')); + } + + public function testParserRestoresReplacementStateAfterFilterException(): void + { + $exception = new RuntimeException('Filter failed.'); + $this->config->filters['fail'] = static fn (mixed $value): string => throw $exception; + $this->parser->setData(['first' => 'before', 'second' => 'failure']); + + try { + $this->parser->renderString('{first}|{second|fail}'); + $this->fail('The filter exception was not propagated.'); + } catch (RuntimeException $caught) { + $this->assertSame($exception, $caught); + } + + $this->assertSame([], $this->getPrivateProperty($this->parser, 'replacementTokens')); + $this->assertNull($this->getPrivateProperty($this->parser, 'replacementTokenPrefix')); + + $this->parser->setData(['first' => '{!second!}', 'second' => 'unsafe']); + $this->assertSame( + '{!second!}|<b>unsafe</b>', + $this->parser->renderString('{first}|{second}'), + ); + } + public function testParserNoEscapeAndDelimiterChange(): void { $this->parser->setDelimiters('{{', '}}'); @@ -779,6 +982,91 @@ public function testCanAddAndRemovePlugins(): void $this->assertArrayNotHasKey('first', $setParsers); } + #[DataProvider('providePreviousURLDoesNotInterpretSyntaxFromReferer')] + public function testPreviousURLDoesNotInterpretSyntaxFromReferer(string $syntax): void + { + $server = service('superglobals')->getServerArray(); + $hadSession = isset($_SESSION); + $session = $_SESSION ?? []; + + try { + unset($_SESSION); + service('superglobals')->setServer('HTTP_REFERER', 'https://example.com/page?x=' . $syntax); + Services::resetSingle('request'); + + $this->parser->addPlugin('probe', static fn (): string => ''); + $this->parser->setData(['bio' => '']); + + $this->assertSame( + 'back|<script>alert(document.domain)</script>', + $this->parser->renderString('back|{bio}'), + ); + } finally { + service('superglobals')->setServerArray($server); + if ($hadSession) { + $_SESSION = $session; + } else { + unset($_SESSION); + } + + Services::resetSingle('request'); + } + } + + /** + * @return iterable + */ + public static function providePreviousURLDoesNotInterpretSyntaxFromReferer(): iterable + { + yield 'variable' => ['{!bio!}']; + + yield 'later plugin' => ['{+probe+}']; + } + + public function testValidationErrorsDoesNotInterpretSyntaxInMessage(): void + { + service('validation')->setError('email', 'Bad value: {!bio!}'); + $this->parser->setData(['bio' => '']); + + $output = $this->parser->renderString('{+ validation_errors field=email +}|{bio}'); + + $this->assertStringContainsString('Bad value: {!bio!}', $output); + $this->assertStringContainsString('|<script>alert(document.domain)</script>', $output); + $this->assertStringNotContainsString('']); + + $output = $this->parser->renderString('{+ errors field=email +}'); + + $this->assertStringContainsString('Bad value: {!bio!}', $output); + $this->assertStringNotContainsString('